Skip to content

Conversation

@minwoox
Copy link
Contributor

@minwoox minwoox commented Dec 24, 2025

Motivation:
For compatiblity with #1228

Modifications:

  • Introduced a new AppIdentity interface to serve as a common abstraction for application identities.
    • Created two concrete implementations: Token and CertificateAppIdentity, representing the different types of app identities.
  • Replaced the JsonPatch mechanism in MetadataService with a transformer.
    • This transformer is designed to handle different versions of the metadata schema, allowing for seamless updates regardless of whether the stored data is in the old or new format.

Result:

  • The system's app identity model is now extensible and can easily support new types of application identities in the future.

Motivation:
For compatiblity with line#1228

Modifications:
- Introduced a new `AppIdentity` interface to serve as a common abstraction for application identities.
  - Created two concrete implementations: `Token` and `CertificateAppIdentity`, representing the different types of app identities.
- Replaced the `JsonPatch` mechanism in `MetadataService` with a transformer.
  - This transformer is designed to handle different versions of the metadata schema, allowing for seamless updates regardless of whether the stored data is in the old or new format.

Result:
- The system's app identity model is now extensible and can easily support new types of application identities in the future.
@minwoox minwoox added this to the 0.79.0 milestone Dec 24, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 24, 2025

📝 Walkthrough

Walkthrough

Unifies application identities by replacing legacy Token-only models with an AppIdentity abstraction (Token and CertificateAppIdentity), migrates metadata from tokens → appIds, removes LegacyToken, and converts MetadataService mutations to transformer-based updates; updates callers and tests to the new types and JSON shapes.

Changes

Cohort / File(s) Summary
Legacy removal
server/src/main/java/com/linecorp/centraldogma/server/internal/admin/auth/LegacyToken.java
Deleted deprecated LegacyToken class and all its constructors, fields, accessors and utility methods.
AppIdentity core
server/src/main/java/.../metadata/AppIdentity.java, AppIdentityType.java, AbstractAppIdentity.java, AppIdentityDeserializer.java
Added AppIdentity interface, AppIdentityType enum, AbstractAppIdentity base class, and a Jackson deserializer supporting legacy and typed JSON.
Token & certificate types
server/src/main/java/.../metadata/Token.java, CertificateAppIdentity.java
Token now extends AbstractAppIdentity (new constructors, withoutSecret(), withSystemAdmin(), addProperties()); new CertificateAppIdentity class with certificateId and JSON ctor.
Metadata model (appIds migration)
server/src/main/java/.../metadata/ProjectMetadata.java, Roles.java, Tokens.java, RepositoryMetadata.java
Replaced tokens → appIds across models; Roles and ProjectMetadata constructors accept nullable tokens + appIds; Tokens.appIds holds AppIdentity; added certificateIds; updated equality/weight/toString.
Metadata service refactor
server/src/main/java/.../metadata/MetadataService.java, RepositoryMetadataTransformer.java
Switched from JSON-patch-based edits to transformer-based metadata mutations; updated call sites to supply null/two-arg tokens/appIds shape.
Service callers & flows
server/src/main/java/.../internal/api/ProjectServiceV1.java, RepositoryServiceUtil.java, TokenService.java, PurgeSchedulingService.java, DefaultProject.java
Updated logic to read/apply appIds (TokenRegistration via appIds), TokenService.listTokens uses AppIdentityType filter, purge/filtering switched to AppIdentity, adjusted Roles construction calls.
Tests & serialization
server/src/test/java/.../SerializationTest.java, ProjectServiceV1Test.java, RepositoryServiceV1Test.java, TokenServiceTest.java, *Metadata* tests, new tests under server/src/test/java/.../metadata/
Updated tests to use appIds, adjusted constructors and JSON expectations; added AppIdentityDeserializerTest, ProjectMetadataTest, RolesTest, TokensTest, and other serialization/round-trip tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

breaking change

Suggested reviewers

  • trustin
  • jrhee17
  • ikhoon

Poem

🐰 I hopped through code, found tokens and seeds,
I stitched them as AppIdentity deeds,
Certificates and tokens now play,
In appIds fields they hop and stay,
A bunny cheers for tidy metadata steeds!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.94% 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 directly related to the changeset, explaining the motivation, modifications (AppIdentity interface with Token and CertificateAppIdentity implementations, JsonPatch replacement), and expected results.
Title check ✅ Passed The PR title accurately summarizes the primary changes: introducing an AppIdentity abstraction and replacing JSON patch operations with transformers in MetadataService.
✨ 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: 0

🧹 Nitpick comments (6)
server/src/main/java/com/linecorp/centraldogma/server/internal/api/sysadmin/TokenService.java (1)

91-110: LGTM!

The filtering logic correctly handles the new AppIdentity abstraction by:

  • Filtering for AppIdentityType.TOKEN to exclude certificate-based identities
  • Safely casting to Token after the type filter
  • Properly masking secrets for non-admin users

Consider extracting the common filtering logic to reduce duplication:

🔎 Optional refactor to reduce duplication
+    private Stream<Token> tokenStream() {
+        return mds.getTokens()
+                  .appIds()
+                  .values()
+                  .stream()
+                  .filter(appIdentity -> appIdentity.type() == AppIdentityType.TOKEN)
+                  .map(appIdentity -> (Token) appIdentity);
+    }
+
     @Get("/tokens")
     public Collection<Token> listTokens(User loginUser) {
         if (loginUser.isSystemAdmin()) {
-            return mds.getTokens()
-                      .appIds()
-                      .values()
-                      .stream()
-                      .filter(appIdentity -> appIdentity.type() == AppIdentityType.TOKEN)
-                      .map(appIdentity -> (Token) appIdentity)
-                      .collect(toImmutableList());
+            return tokenStream().collect(toImmutableList());
         } else {
-            return mds.getTokens()
-                      .appIds()
-                      .values()
-                      .stream()
-                      .filter(appIdentity -> appIdentity.type() == AppIdentityType.TOKEN)
-                      .map(appIdentity -> (Token) appIdentity)
-                      .map(Token::withoutSecret)
-                      .collect(toImmutableList());
+            return tokenStream()
+                      .map(Token::withoutSecret)
+                      .collect(toImmutableList());
         }
     }
server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityDeserializer.java (1)

44-56: Consider handling unknown type values more gracefully.

The AppIdentityType.valueOf(typeValue) on line 47 will throw IllegalArgumentException if the type value is not a valid enum constant. The default case on lines 54-55 is unreachable since valueOf throws before reaching it.

For better error handling and forward compatibility (e.g., if new identity types are added in the future but not yet supported in this deserializer), consider catching the exception:

🔎 Suggested improvement
         if (typeNode != null) {
             // Type field exists, deserialize based on type
             final String typeValue = typeNode.asText();
-            final AppIdentityType type = AppIdentityType.valueOf(typeValue);
-
+            final AppIdentityType type;
+            try {
+                type = AppIdentityType.valueOf(typeValue);
+            } catch (IllegalArgumentException e) {
+                throw new IllegalArgumentException("Unknown AppIdentityType: " + typeValue, e);
+            }
             switch (type) {
                 case TOKEN:
                     return deserializeToken(node);
                 case CERTIFICATE:
                     return deserializeCertificate(node);
-                default:
-                    throw new IllegalArgumentException("Unknown AppIdentityType: " + type);
             }
+            // Unreachable, but required by compiler
+            throw new AssertionError("Unhandled AppIdentityType: " + type);
server/src/test/java/com/linecorp/centraldogma/server/metadata/AppIdentityDeserializerTest.java (1)

105-120: Consider adding a test for unknown type values.

The current tests cover missing type and valid types, but don't test behavior when an unknown type value (e.g., "type": "UNKNOWN") is provided. This would exercise the error handling path for future-proofing.

🔎 Example additional test
@Test
void deserializeUnknownTypeShouldFail() {
    final String unknownTypeJson = '{' +
                               "  \"type\": \"UNKNOWN\"," +
                               "  \"appId\": \"app\"," +
                               "  \"creation\": {" +
                               "    \"user\": \"[email protected]\"," +
                               "    \"timestamp\": \"2025-01-01T00:00:00Z\"" +
                               "  }" +
                               '}';

    assertThatThrownBy(() -> Jackson.readValue(unknownTypeJson, AppIdentity.class))
            .hasCauseInstanceOf(IllegalArgumentException.class);
}
server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentity.java (1)

43-44: Consider tracking the TODO for future cleanup.

The TODO indicates a planned refactor to use JsonTypeInfo instead of the explicit @JsonProperty("type") annotation. This would be a cleaner polymorphic deserialization approach.

Would you like me to open an issue to track this technical debt?

server/src/main/java/com/linecorp/centraldogma/server/metadata/Token.java (1)

104-113: Consider using requireNonNull instead of assert for secret validation.

The assert at line 110 can be disabled at runtime with -da. If this invariant is critical, consider using requireNonNull(secret, "secret") or a similar explicit check that throws an exception.

🔎 Proposed fix
     @Override
     public Token withSystemAdmin(boolean isSystemAdmin) {
         if (isSystemAdmin == isSystemAdmin()) {
             return this;
         }
         final String secret = secret();
-        assert secret != null;
+        if (secret == null) {
+            throw new IllegalStateException("withSystemAdmin() must be called on a Token with a secret");
+        }
         return new Token(id(), secret, isSystemAdmin, allowGuestAccess(), creation(),
                          deactivation(), deletion());
     }
server/src/main/java/com/linecorp/centraldogma/server/metadata/Tokens.java (1)

179-188: Consider including certificateIds in weight calculation.

The weight() method accounts for secrets but not certificateIds. If certificateIds contributes to memory/storage overhead, it should be included for consistency.

🔎 Proposed fix
     @Override
     public int weight() {
         int weight = 0;
         weight += secrets.size();
         for (Entry<String, String> entry : secrets.entrySet()) {
             weight += entry.getKey().length();
             weight += entry.getValue().length();
         }
+        weight += certificateIds.size();
+        for (Entry<String, String> entry : certificateIds.entrySet()) {
+            weight += entry.getKey().length();
+            weight += entry.getValue().length();
+        }
         return weight;
     }
📜 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 f89447c and 6c0f7ab.

📒 Files selected for processing (31)
  • server/src/main/java/com/linecorp/centraldogma/server/internal/admin/auth/LegacyToken.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/ProjectServiceV1.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/RepositoryServiceUtil.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/sysadmin/TokenService.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/storage/PurgeSchedulingService.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/storage/project/DefaultProject.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/AbstractAppIdentity.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentity.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityDeserializer.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityType.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/CertificateAppIdentity.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/ProjectMetadata.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadata.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadataTransformer.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/Roles.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/Token.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/Tokens.java
  • server/src/test/java/com/linecorp/centraldogma/server/internal/admin/model/SerializationTest.java
  • server/src/test/java/com/linecorp/centraldogma/server/internal/api/ProjectServiceV1Test.java
  • server/src/test/java/com/linecorp/centraldogma/server/internal/api/RepositoryServiceV1Test.java
  • server/src/test/java/com/linecorp/centraldogma/server/internal/api/TokenServiceTest.java
  • server/src/test/java/com/linecorp/centraldogma/server/metadata/AppIdentityDeserializerTest.java
  • server/src/test/java/com/linecorp/centraldogma/server/metadata/MetadataApiServiceTest.java
  • server/src/test/java/com/linecorp/centraldogma/server/metadata/MissingRepositoryMetadataTest.java
  • server/src/test/java/com/linecorp/centraldogma/server/metadata/ProjectMetadataTest.java
  • server/src/test/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadataDeserializerTest.java
  • server/src/test/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadataTest.java
  • server/src/test/java/com/linecorp/centraldogma/server/metadata/RolesTest.java
  • server/src/test/java/com/linecorp/centraldogma/server/metadata/TokenTest.java
  • server/src/test/java/com/linecorp/centraldogma/server/metadata/TokensTest.java
💤 Files with no reviewable changes (1)
  • server/src/main/java/com/linecorp/centraldogma/server/internal/admin/auth/LegacyToken.java
🧰 Additional context used
🧬 Code graph analysis (6)
server/src/test/java/com/linecorp/centraldogma/server/metadata/RolesTest.java (3)
webapp/src/dogma/features/auth/RepositoryRole.tsx (1)
  • RepositoryRole (8-8)
common/src/main/java/com/linecorp/centraldogma/internal/Jackson.java (1)
  • Jackson (68-390)
server/src/main/java/com/linecorp/centraldogma/server/metadata/ProjectRoles.java (1)
  • ProjectRoles (29-106)
server/src/test/java/com/linecorp/centraldogma/server/metadata/ProjectMetadataTest.java (2)
common/src/main/java/com/linecorp/centraldogma/internal/Jackson.java (1)
  • Jackson (68-390)
server/src/main/java/com/linecorp/centraldogma/server/metadata/ProjectRoles.java (1)
  • ProjectRoles (29-106)
server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java (2)
server/src/main/java/com/linecorp/centraldogma/server/metadata/UserAndTimestamp.java (1)
  • UserAndTimestamp (35-123)
common/src/main/java/com/linecorp/centraldogma/common/RepositoryExistsException.java (1)
  • RepositoryExistsException (25-55)
server/src/test/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadataDeserializerTest.java (1)
common/src/main/java/com/linecorp/centraldogma/internal/Jackson.java (1)
  • Jackson (68-390)
server/src/main/java/com/linecorp/centraldogma/server/internal/api/RepositoryServiceUtil.java (1)
webapp/src/dogma/features/auth/RepositoryRole.tsx (1)
  • RepositoryRole (8-8)
server/src/main/java/com/linecorp/centraldogma/server/metadata/AbstractAppIdentity.java (1)
common/src/main/java/com/linecorp/centraldogma/internal/Util.java (1)
  • Util (38-513)
⏰ 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). (10)
  • GitHub Check: build-ubuntu-latest-jdk-25-snapshot
  • GitHub Check: build-ubuntu-latest-jdk-11
  • GitHub Check: build-ubuntu-latest-jdk-21
  • GitHub Check: build-ubuntu-latest-jdk-17-min-java-11
  • GitHub Check: build-ubuntu-latest-jdk-17-coverage
  • GitHub Check: build-windows-latest-jdk-25
  • GitHub Check: build-macos-latest-jdk-25
  • GitHub Check: lint
  • GitHub Check: flaky-tests
  • GitHub Check: docker-build
🔇 Additional comments (51)
server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityType.java (1)

22-32: LGTM!

Clean and well-documented enum for distinguishing application identity types. The Javadoc appropriately describes both constants.

server/src/test/java/com/linecorp/centraldogma/server/internal/admin/model/SerializationTest.java (2)

70-85: LGTM!

The test updates correctly reflect the API changes:

  • Token constructor signature updated to the new form
  • ProjectMetadata constructor includes the additional null parameter for the deprecated tokens field
  • Token registrations are now keyed under appIds in the expected JSON output

The tests properly validate the new serialization contract.


192-202: Deserialization assertions properly updated.

The assertions now correctly verify obj.appIds() instead of the former tokens accessor, maintaining round-trip serialization test coverage.

server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java (5)

185-199: LGTM!

The transformer-based approach for removeProject is clean. The precondition check for already-removed projects is now encapsulated within the transformer callback, and the new ProjectMetadata construction correctly propagates appIds() while setting the removal timestamp.


251-272: LGTM!

The addMember transformer correctly:

  • Validates that the member doesn't already exist
  • Uses builderWithExpectedSize for efficient map construction
  • Preserves all existing metadata fields while adding the new member

313-316: Roles constructor usage is consistent.

The Roles constructor is uniformly called with null for the deprecated tokens parameter (3rd position) and uses appIds for the active role mappings (4th position). This pattern is applied consistently across all repository role mutations.


1121-1147: LGTM!

The createToken transformer properly:

  • Validates uniqueness of both the appId and secret before creation
  • Efficiently constructs new immutable maps with pre-sized builders
  • Preserves certificateIds from the existing tokens structure

1203-1213: LGTM!

The purgeToken method correctly removes the token from both appIds and secrets maps while preserving certificateIds. The blocking join is documented in the Javadoc.

server/src/main/java/com/linecorp/centraldogma/server/internal/storage/PurgeSchedulingService.java (1)

152-164: LGTM!

The change correctly uses the AppIdentity interface methods (isDeleted() and appId()) instead of Token-specific methods. This aligns with the new abstraction where tokens.appIds() returns a collection of AppIdentity instances (which may include both tokens and certificates in the future).

server/src/test/java/com/linecorp/centraldogma/server/metadata/MetadataApiServiceTest.java (1)

220-232: LGTM!

Test assertions correctly updated to use roles().appIds() instead of the deprecated roles().tokens(), maintaining proper test coverage for token-based repository role management.

server/src/main/java/com/linecorp/centraldogma/server/internal/api/ProjectServiceV1.java (1)

112-118: LGTM!

The token lookup correctly uses metadata.appIds() to retrieve the TokenRegistration for the authenticated user's token, consistent with the broader tokensappIds migration.

server/src/main/java/com/linecorp/centraldogma/server/internal/storage/project/DefaultProject.java (1)

220-234: LGTM!

The ProjectMetadata constructor call is correctly updated to include the additional null parameter for the deprecated tokens field. The tokens variable (containing the owner's token registration) is passed as the appIds parameter in the new schema.

server/src/test/java/com/linecorp/centraldogma/server/internal/api/TokenServiceTest.java (1)

148-149: LGTM! Test assertions correctly updated to reflect appIds API.

The test assertions have been properly migrated from tokens() to appIds(), maintaining the same validation logic while aligning with the new public API surface.

Also applies to: 167-167

server/src/main/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadata.java (1)

105-106: LGTM! Roles constructor updated to 4-argument form.

The constructor call correctly uses the new 4-argument signature: Roles(projectRoles, users, tokens, appIds), passing null for the tokens parameter and an empty appIds map, which aligns with the broader migration to appIds-based identity management.

server/src/test/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadataDeserializerTest.java (3)

30-53: LGTM! Backward compatibility test properly validates tokens→appIds migration.

The renamed test method now explicitly validates that the legacy tokens field is correctly mapped to appIds during deserialization, ensuring backward compatibility with existing metadata.


55-79: LGTM! New format test validates appIds deserialization.

The new test method validates that the appIds field deserializes correctly, ensuring the new metadata format works as expected. Both legacy and new formats converge to the same internal representation.


90-91: LGTM! Assertion updated to use appIds API.

The validation helper now correctly asserts on appIds() instead of tokens(), which is used by both backward-compatible and new format tests.

server/src/test/java/com/linecorp/centraldogma/server/metadata/MissingRepositoryMetadataTest.java (1)

80-80: LGTM! Assertion correctly updated to appIds API.

The test now validates that auto-generated repository metadata contains an empty appIds map, consistent with the API migration.

server/src/test/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadataTest.java (1)

66-67: LGTM! Validates backward compatibility of tokens→appIds migration.

The test deserializes JSON containing the legacy tokens field (line 47) and correctly validates it via the appIds() accessor, confirming backward compatibility of the migration.

server/src/test/java/com/linecorp/centraldogma/server/metadata/ProjectMetadataTest.java (4)

31-45: LGTM! Backward compatibility test validates tokens→appIds migration.

This test confirms that ProjectMetadata with the legacy tokens field deserializes correctly and maps to the appIds() accessor, ensuring existing metadata files continue to work.


47-61: LGTM! New format test validates appIds deserialization.

This test validates that ProjectMetadata with the new appIds field deserializes correctly, confirming the forward path of the migration.


63-93: LGTM! Serialization test ensures appIds field is written.

This test constructs ProjectMetadata programmatically and validates that serialization produces JSON with the appIds field, and that round-trip deserialization preserves equality.


95-109: LGTM! Round-trip test validates serialization stability.

This test ensures that deserializing legacy tokens format, serializing to new format, and deserializing again maintains object equality, confirming the migration path is stable.

server/src/test/java/com/linecorp/centraldogma/server/metadata/TokenTest.java (1)

83-83: LGTM! Legacy token format removed in favor of standardized format.

The test now uses a single non-legacy token format with the systemAdmin field, removing the conditional legacy admin field handling. This simplification aligns with the PR's goal of removing LegacyToken class.

Also applies to: 92-93, 98-106

server/src/test/java/com/linecorp/centraldogma/server/internal/api/RepositoryServiceV1Test.java (1)

156-166: LGTM! Roles constructor updated and test validates correct identity segregation.

The test correctly uses the 4-argument Roles constructor and validates that:

  • User-created repositories populate the users map with the creator
  • Token-created repositories populate the appIds map with the token ID

This confirms proper segregation of user and application identities in repository metadata.

server/src/test/java/com/linecorp/centraldogma/server/metadata/TokensTest.java (5)

28-45: LGTM! Comprehensive null handling test.

The test correctly validates that when certificateIds is null, the Tokens model returns an empty (not null) map via tokens.certificateIds(), which is important for null-safety guarantees.


47-63: LGTM! Good coverage for mixed identity types.

This test validates that Tokens correctly handles a mix of Token and CertificateAppIdentity entries in the appIds map, ensuring both identity types can coexist.


65-92: LGTM! Validates backward compatibility with legacy format.

This test ensures that JSON without the certificateIds field (legacy format) deserializes correctly, with certificateIds defaulting to an empty map.


94-136: LGTM! Validates new format deserialization with both identity types.

This test correctly validates deserialization of the new JSON format containing both TOKEN and CERTIFICATE type entries with explicit type discriminators.


157-177: LGTM! Round-trip serialization ensures data integrity.

Good practice to verify that serialization followed by deserialization preserves all fields, including the new certificateIds mapping.

server/src/main/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadataTransformer.java (1)

51-57: LGTM! Correct migration to appIds-based metadata construction.

The ProjectMetadata construction now correctly passes null for the deprecated tokens parameter and uses projectMetadata.appIds() for the new appIds parameter, aligning with the PR's migration from tokens to appIds.

server/src/test/java/com/linecorp/centraldogma/server/internal/api/ProjectServiceV1Test.java (1)

117-129: LGTM! Test correctly updated for the tokens() → appIds() API migration.

The assertions now use appIds() instead of tokens(), and correctly verify that:

  • User-created projects have empty appIds and populated members
  • Token-created projects have populated appIds (with TokenRegistration) and empty members
server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityDeserializer.java (2)

70-93: LGTM! Clean extraction of deserialization logic.

The helper methods deserializeToken and deserializeCertificate correctly extract and validate all required fields, with appropriate handling for optional fields like deactivation and deletion.


118-150: LGTM! Robust field extraction with clear error messages.

The helper methods provide strong validation with informative error messages, distinguishing between missing fields and type mismatches.

server/src/test/java/com/linecorp/centraldogma/server/metadata/RolesTest.java (2)

29-56: LGTM! Validates backward compatibility for legacy tokens field.

This test correctly verifies that JSON containing the legacy tokens field is properly deserialized and accessible via the new appIds() accessor, ensuring smooth migration for existing data.


85-103: LGTM! Validates one-way migration to appIds.

The test confirms that:

  1. Serialization outputs appIds (not tokens)
  2. Round-trip preserves data integrity
  3. The new format is forward-compatible
server/src/main/java/com/linecorp/centraldogma/server/internal/api/RepositoryServiceUtil.java (1)

46-57: LGTM! Correct migration to appIds-based Roles construction.

The repository creation logic now correctly:

  1. Populates appIds (instead of tokens) for token-based authors
  2. Uses the 4-argument Roles constructor with null for the deprecated tokens parameter
  3. Maintains the same access control semantics (ADMIN role for the creator)
server/src/main/java/com/linecorp/centraldogma/server/metadata/AbstractAppIdentity.java (3)

44-54: LGTM! Well-designed constructor with appropriate validation.

The constructor:

  • Validates appId using Util.validateFileName for consistency with file-based storage
  • Requires non-null type and creation fields
  • Properly accepts nullable deactivation and deletion for lifecycle management

98-121: LGTM! Correct equality implementation for abstract base class.

Using getClass() != o.getClass() is the right choice for an abstract class, ensuring that Token.equals(CertificateAppIdentity) returns false even if they share the same base fields.


123-137: LGTM! Extensible toString() pattern.

The final modifier on toString() with the abstract addProperties(ToStringHelper) hook is a good template method pattern, allowing subclasses to add type-specific fields without duplicating the base field formatting.

server/src/test/java/com/linecorp/centraldogma/server/metadata/AppIdentityDeserializerTest.java (1)

27-53: LGTM! Excellent test for backward compatibility.

This test validates the critical migration path where legacy tokens without a type field are correctly deserialized as Token instances. The additional assertion that re-serialization includes "type":"TOKEN" ensures data will be upgraded on next write.

server/src/main/java/com/linecorp/centraldogma/server/metadata/Roles.java (1)

39-40: LGTM! Backward-compatible migration from tokens to appIds.

The constructor pattern correctly handles both old JSON (with tokens key) and new JSON (with appIds key), prioritizing appIds when present. The validation at lines 58-60 ensures data integrity.

Also applies to: 51-67

server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentity.java (1)

26-96: Well-designed interface for the AppIdentity abstraction.

The interface provides a clean contract for both token and certificate-based identities. The default methods isActive() and isDeleted() encapsulate the state logic nicely, and the JSON annotations ensure proper serialization behavior.

server/src/main/java/com/linecorp/centraldogma/server/metadata/ProjectMetadata.java (2)

91-113: LGTM! Constructor correctly handles backward-compatible migration.

The constructor properly validates that at least one of tokens or appIds is non-null, and prioritizes appIds when present. This pattern is consistent with Roles.java.


147-150: Verify JSON property name for serialization.

The accessor uses @JsonProperty without an explicit name, which will serialize as "appIds". Old clients expecting "tokens" may need to be updated. Confirm this breaking change in the serialization output is acceptable.

server/src/main/java/com/linecorp/centraldogma/server/metadata/CertificateAppIdentity.java (1)

37-104: LGTM! Clean implementation of certificate-based identity.

The class properly extends AbstractAppIdentity with the certificateId field. Notable design choices:

  • allowGuestAccess defaults to false for certificates (vs true for tokens) - appropriate for stricter certificate-based access.
  • withSystemAdmin() optimizes by returning this when unchanged.
  • Proper equals()/hashCode() implementation combining superclass state.
server/src/main/java/com/linecorp/centraldogma/server/metadata/Token.java (2)

34-46: LGTM! Token now properly extends the identity abstraction.

The refactoring to extend AbstractAppIdentity is clean. The allowGuestAccess default of true (line 61) maintains backward compatibility for existing tokens.

Also applies to: 59-66


120-128: The equals() method is correct and safe. The AbstractAppIdentity.equals() implementation at line 104 enforces strict class equality via getClass() != o.getClass(), which ensures that super.equals(o) only returns true if o is of the exact same class. Therefore, the cast to Token at line 126 is guaranteed to succeed and will not throw a ClassCastException.

server/src/main/java/com/linecorp/centraldogma/server/metadata/Tokens.java (3)

126-133: LGTM! Proper type checking before casting.

The getOrDefault() method correctly checks appIdentity.type() != AppIdentityType.TOKEN before casting to Token, preventing ClassCastException when the identity is a certificate-based one.


166-177: LGTM! withoutSecret() correctly handles mixed identity types.

The stream-based transformation properly preserves non-token identities while stripping secrets from tokens.


190-197: Good security practice: logging secrets.size instead of actual secrets.

This prevents accidental exposure of sensitive data in logs.

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)
server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityDeserializer.java (1)

54-62: Replace throw new Error() with a default case or IllegalStateException.

Line 61 uses throw new Error() without a message, which is inappropriate for application logic. While currently unreachable (assuming AppIdentityType only contains TOKEN and CERTIFICATE), if a new enum value is added in the future and this switch isn't updated, the raw Error will be thrown without context.

🔎 Suggested fix

Replace the switch statement with one that includes a default case:

 switch (type) {
     case TOKEN:
         return deserializeToken(node);
     case CERTIFICATE:
         return deserializeCertificate(node);
+    default:
+        throw new IllegalStateException("Unhandled AppIdentityType: " + type);
 }
-// Should never reach here
-throw new 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 6c0f7ab and 0407e25.

📒 Files selected for processing (3)
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/sysadmin/TokenService.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityDeserializer.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/Tokens.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/Tokens.java
🧰 Additional context used
🧬 Code graph analysis (1)
server/src/main/java/com/linecorp/centraldogma/server/internal/api/sysadmin/TokenService.java (1)
server/src/main/java/com/linecorp/centraldogma/server/metadata/Token.java (1)
  • Token (34-134)
⏰ 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). (10)
  • GitHub Check: build-macos-latest-jdk-25
  • GitHub Check: build-ubuntu-latest-jdk-25-snapshot
  • GitHub Check: build-ubuntu-latest-jdk-21
  • GitHub Check: build-ubuntu-latest-jdk-17-min-java-11
  • GitHub Check: build-ubuntu-latest-jdk-17-coverage
  • GitHub Check: build-windows-latest-jdk-25
  • GitHub Check: build-ubuntu-latest-jdk-11
  • GitHub Check: lint
  • GitHub Check: docker-build
  • GitHub Check: flaky-tests
🔇 Additional comments (5)
server/src/main/java/com/linecorp/centraldogma/server/internal/api/sysadmin/TokenService.java (2)

92-98: LGTM!

The refactoring correctly maintains the admin vs non-admin distinction (secrets visible only to admins) while adapting to the new AppIdentity abstraction. The use of tokenStream() cleanly encapsulates the filtering logic.


101-108: LGTM!

The filter-then-cast pattern is appropriate here—filtering by AppIdentityType.TOKEN ensures the subsequent cast to Token is type-safe. The method cleanly extracts the token-specific logic for reuse.

server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityDeserializer.java (3)

75-98: LGTM! Clean and consistent deserialization helpers.

Both deserializeToken and deserializeCertificate follow a consistent pattern, properly extracting required and optional fields with appropriate validation. The type-specific differences (secret vs. certificateId) are handled correctly.


100-113: LGTM! Proper handling of required and optional nested objects.

The UserAndTimestamp deserialization methods correctly distinguish between required and optional fields, with appropriate null-safety checks for both missing fields and explicit JSON null values.


115-155: LGTM! Robust validation utilities with clear error messages.

The helper methods provide thorough validation for required and optional fields with appropriate type checking. Error messages are informative, specifying both the field name and the nature of the validation failure.

@minwoox minwoox changed the title Introduce AppIdentity abstraction and robust metadata transformer Introduce AppIdentity and use transformer in MetadataService Dec 24, 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.

Understood this PR is basically refactoring/schema change to support AppIdentity more natively.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants