Skip to content

Commit

Permalink
Prepare Token Updates for line#1054 Compatibility
Browse files Browse the repository at this point in the history
Motivation
To ensure smooth compatibility with line#1054, the `Token` structure needs an update to support changes in `admin` property.

Modifications:
- Enhanced the Token deserializer to accept both `admin` and `systemAdmin` properties, ensuring backward compatibility.
- Updated `MetadataService.updateTokenLevel()` to avoid direct access to the `admin` property.

Result:
- The `Token` structure now supports the upcoming changes, allowing seamless updates with line#1054.
  • Loading branch information
minwoox committed Nov 8, 2024
1 parent 4d4af7e commit d445787
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import com.linecorp.centraldogma.internal.jsonpatch.TestAbsenceOperation;
import com.linecorp.centraldogma.server.QuotaConfig;
import com.linecorp.centraldogma.server.command.CommandExecutor;
import com.linecorp.centraldogma.server.internal.admin.service.TokenNotFoundException;
import com.linecorp.centraldogma.server.storage.project.Project;
import com.linecorp.centraldogma.server.storage.project.ProjectManager;

Expand Down Expand Up @@ -1011,14 +1012,25 @@ public CompletableFuture<Revision> updateTokenLevel(Author author, String appId,
"Update the token level: " + appId + " to " + (toBeAdmin ? "admin" : "user"),
() -> tokenRepo.fetch(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, TOKEN_JSON)
.thenApply(tokens -> {
final JsonPointer adminPath = JsonPointer.compile(
"/appIds" + encodeSegment(appId) +
"/admin");
final Tokens tokens0 = tokens.object();
final Token token = tokens0.get(appId);
if (token == null) {
throw new TokenNotFoundException("App ID: " + appId);
}
if (toBeAdmin == token.isAdmin()) {
throw new IllegalArgumentException(
"The token is already " +
(toBeAdmin ? "admin" : "user"));
}
final JsonPointer path = JsonPointer.compile(
"/appIds" + encodeSegment(appId));

final Change<JsonNode> change = Change.ofJsonPatch(
TOKEN_JSON,
new ReplaceOperation(adminPath,
Jackson.valueToTree(
toBeAdmin)).toJsonNode());
new ReplaceOperation(
path,
Jackson.valueToTree(token.withAdmin(
toBeAdmin))).toJsonNode());
return HolderWithRevision.of(change, tokens.revision());
}));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public final class Token implements Identifiable {
private final UserAndTimestamp deletion;

Token(String appId, String secret, boolean isAdmin, UserAndTimestamp creation) {
this(appId, secret, isAdmin, creation, null, null);
this(appId, secret, isAdmin, isAdmin, creation, null, null);
}

/**
Expand All @@ -77,13 +77,16 @@ public final class Token implements Identifiable {
@JsonCreator
public Token(@JsonProperty("appId") String appId,
@JsonProperty("secret") String secret,
@JsonProperty("admin") boolean isAdmin,
// TODO(minwoox): Remove admin field after all tokens are migrated.
@JsonProperty("admin") @Nullable Boolean isAdmin,
@JsonProperty("systemAdmin") @Nullable Boolean isSystemAdmin,
@JsonProperty("creation") UserAndTimestamp creation,
@JsonProperty("deactivation") @Nullable UserAndTimestamp deactivation,
@JsonProperty("deletion") @Nullable UserAndTimestamp deletion) {
assert isAdmin != null || isSystemAdmin != null;
this.appId = Util.validateFileName(appId, "appId");
this.secret = Util.validateFileName(secret, "secret");
this.isAdmin = isAdmin;
this.isAdmin = isSystemAdmin != null ? isSystemAdmin : isAdmin;
this.creation = requireNonNull(creation, "creation");
this.deactivation = deactivation;
this.deletion = deletion;
Expand Down Expand Up @@ -189,4 +192,17 @@ public String toString() {
public Token withoutSecret() {
return new Token(appId(), isAdmin(), creation(), deactivation(), deletion());
}

/**
* Returns a new {@link Token} instance with {@code isAdmin}.
* This method must be called by the token whose secret is not null.
*/
public Token withAdmin(boolean isAdmin) {
if (isAdmin == isAdmin()) {
return this;
}
final String secret = secret();
assert secret != null;
return new Token(appId(), secret, isAdmin, isAdmin, creation(), deactivation(), deletion());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ void testValidProject() throws IOException {
final Member member = new Member(userLogin, ProjectRole.MEMBER, newCreationTag());
final RepositoryMetadata repositoryMetadata = new RepositoryMetadata("sample", newCreationTag(),
PerRolePermissions.ofDefault());
final Token token = new Token("testApp", "testSecret", false, newCreationTag(), null, null);
final Token token = new Token("testApp", "testSecret", false, false, newCreationTag(), null, null);
final ProjectMetadata metadata =
new ProjectMetadata("test",
ImmutableMap.of(repositoryMetadata.name(), repositoryMetadata),
Expand Down Expand Up @@ -141,7 +141,7 @@ void testRemovedProject() throws IOException {
newCreationTag());
final RepositoryMetadata repositoryMetadata = new RepositoryMetadata("sample", newCreationTag(),
PerRolePermissions.ofDefault());
final Token token = new Token("testApp", "testSecret", false, newCreationTag(), null, null);
final Token token = new Token("testApp", "testSecret", false, false, newCreationTag(), null, null);
final ProjectMetadata metadata =
new ProjectMetadata("test",
ImmutableMap.of(repositoryMetadata.name(), repositoryMetadata),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2024 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linecorp.centraldogma.server.metadata;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

import com.linecorp.centraldogma.internal.Jackson;

class TokenTest {

private static final String APP_ID = "foo-id";
private static final String APP_SECRET = "appToken-foo";

@Test
void deserializeToken() throws Exception {
final String legacyTokenJson = tokenJson(true);
final Token legacyToken = Jackson.readValue(legacyTokenJson, Token.class);
assertThat(legacyToken.appId()).isEqualTo(APP_ID);
assertThat(legacyToken.isAdmin()).isTrue();

final String tokenJson = tokenJson(false);
final Token token = Jackson.readValue(tokenJson, Token.class);
assertThat(token.appId()).isEqualTo(APP_ID);
assertThat(token.isAdmin()).isTrue();
}

private static String tokenJson(boolean legacy) {
return "{\"appId\": \"" + APP_ID + "\"," +
" \"secret\": \"" + APP_SECRET + "\"," +
(legacy ? " \"admin\": true," : " \"systemAdmin\": true,") +
" \"creation\": {" +
" \"user\": \"[email protected]\"," +
" \"timestamp\": \"2018-04-10T09:58:20.032Z\"" +
"}}";
}
}

0 comments on commit d445787

Please sign in to comment.