From 09a65935cba837dc85861b39f3a8cb92c2cfacb9 Mon Sep 17 00:00:00 2001 From: minwoox Date: Thu, 14 Nov 2024 14:17:42 +0900 Subject: [PATCH] Prepare PerPermission Updates for #1060 Compatibility Motivation To ensure smooth compatibility with #1060, the `PerRolePermissions` and `RepositoryMetadata` structure need an update to support changes. Modifications: - Enhanced the `PerRolePermissions` and `RepositoryMetadata` deserializers to accept both an array or permissions and a permission. - Added `REPO_ADMIN` `Permission`. - Removed `MetadataApiService.updateSpecificUserPermission` and `updateSpecificTokenPermission`. - They are not used in the UI and we don't even have test cases for that. - Will add those APIs later when we need it. Result: - The `PerRolePermissions` and `RepositoryMetadata` structures now support the upcoming changes in #1060. --- .../internal/api/MetadataApiService.java | 43 ---------- .../server/metadata/PerRolePermissions.java | 14 ++-- .../PerRolePermissionsDeserializer.java | 70 +++++++++++++++++ .../server/metadata/Permission.java | 6 +- .../server/metadata/RepositoryMetadata.java | 4 +- .../RepositoryMetadataDeserializer.java | 78 +++++++++++++++++++ .../metadata/PerRolePermissionsTest.java | 77 ++++++++++++++++++ 7 files changed, 240 insertions(+), 52 deletions(-) create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/metadata/PerRolePermissionsDeserializer.java create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadataDeserializer.java create mode 100644 server/src/test/java/com/linecorp/centraldogma/server/metadata/PerRolePermissionsTest.java diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MetadataApiService.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MetadataApiService.java index a10fcf667f..5f41c5a526 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MetadataApiService.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MetadataApiService.java @@ -42,7 +42,6 @@ import com.linecorp.centraldogma.common.Author; import com.linecorp.centraldogma.common.ProjectRole; import com.linecorp.centraldogma.common.Revision; -import com.linecorp.centraldogma.internal.Jackson; import com.linecorp.centraldogma.internal.jsonpatch.JsonPatch; import com.linecorp.centraldogma.internal.jsonpatch.JsonPatchOperation; import com.linecorp.centraldogma.internal.jsonpatch.ReplaceOperation; @@ -199,28 +198,6 @@ public CompletableFuture addSpecificUserPermission( member, memberWithPermissions.permissions()); } - /** - * PATCH /metadata/{projectName}/repos/{repoName}/perm/users/{memberId} - * - *

Updates {@link Permission}s for the specified {@code memberId} of the specified {@code repoName} - * in the specified {@code projectName}. - */ - @Patch("/metadata/{projectName}/repos/{repoName}/perm/users/{memberId}") - @Consumes("application/json-patch+json") - public CompletableFuture updateSpecificUserPermission(@Param String projectName, - @Param String repoName, - @Param String memberId, - JsonPatch jsonPatch, - Author author) { - final ReplaceOperation operation = ensureSingleReplaceOperation(jsonPatch, "/permissions"); - final Collection permissions = Jackson.convertValue(operation.value(), permissionsTypeRef); - final User member = new User(loginNameNormalizer.apply(urlDecode(memberId))); - return mds.findPermissions(projectName, repoName, member) - .thenCompose(unused -> mds.updatePerUserPermission(author, - projectName, repoName, member, - permissions)); - } - /** * DELETE /metadata/{projectName}/repos/{repoName}/perm/users/{memberId} * @@ -254,26 +231,6 @@ public CompletableFuture addSpecificTokenPermission( tokenWithPermissions.id(), tokenWithPermissions.permissions()); } - /** - * PATCH /metadata/{projectName}/repos/{repoName}/perm/tokens/{appId} - * - *

Updates {@link Permission}s for the specified {@code appId} of the specified {@code repoName} - * in the specified {@code projectName}. - */ - @Patch("/metadata/{projectName}/repos/{repoName}/perm/tokens/{appId}") - @Consumes("application/json-patch+json") - public CompletableFuture updateSpecificTokenPermission(@Param String projectName, - @Param String repoName, - @Param String appId, - JsonPatch jsonPatch, - Author author) { - final ReplaceOperation operation = ensureSingleReplaceOperation(jsonPatch, "/permissions"); - final Collection permissions = Jackson.convertValue(operation.value(), permissionsTypeRef); - return mds.findTokenByAppId(appId) - .thenCompose(token -> mds.updatePerTokenPermission( - author, projectName, repoName, appId, permissions)); - } - /** * DELETE /metadata/{projectName}/repos/{repoName}/perm/tokens/{appId} * diff --git a/server/src/main/java/com/linecorp/centraldogma/server/metadata/PerRolePermissions.java b/server/src/main/java/com/linecorp/centraldogma/server/metadata/PerRolePermissions.java index 213f83087b..af2a52fb82 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/metadata/PerRolePermissions.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/metadata/PerRolePermissions.java @@ -25,8 +25,8 @@ import javax.annotation.Nullable; -import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.common.base.MoreObjects; import com.google.common.collect.Sets; @@ -36,7 +36,8 @@ /** * A default permission for a {@link Repository}. */ -public class PerRolePermissions { +@JsonDeserialize(using = PerRolePermissionsDeserializer.class) +public final class PerRolePermissions { /** * {@link Permission}s for administrators. @@ -104,12 +105,11 @@ public static PerRolePermissions ofPrivate() { /** * Creates an instance. */ - @JsonCreator - public PerRolePermissions(@JsonProperty("owner") Iterable owner, - @JsonProperty("member") Iterable member, - @JsonProperty("guest") Iterable guest, + public PerRolePermissions(Iterable owner, + Iterable member, + Iterable guest, // TODO(minwoox): Remove anonymous field after the migration. - @JsonProperty("anonymous") @Nullable Iterable unused) { + @Nullable Iterable unused) { this.owner = Sets.immutableEnumSet(requireNonNull(owner, "owner")); this.member = Sets.immutableEnumSet(requireNonNull(member, "member")); this.guest = Sets.immutableEnumSet(requireNonNull(guest, "guest")); diff --git a/server/src/main/java/com/linecorp/centraldogma/server/metadata/PerRolePermissionsDeserializer.java b/server/src/main/java/com/linecorp/centraldogma/server/metadata/PerRolePermissionsDeserializer.java new file mode 100644 index 0000000000..f64eb7473d --- /dev/null +++ b/server/src/main/java/com/linecorp/centraldogma/server/metadata/PerRolePermissionsDeserializer.java @@ -0,0 +1,70 @@ +/* + * 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 java.io.IOException; +import java.util.Set; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.google.common.collect.ImmutableSet; + +import com.linecorp.centraldogma.internal.Jackson; + +final class PerRolePermissionsDeserializer extends StdDeserializer { + + private static final long serialVersionUID = 1173216371065909688L; + + private static final TypeReference> PERMISSION_SET_TYPE = + new TypeReference>() {}; + + PerRolePermissionsDeserializer() { + super(PerRolePermissions.class); + } + + @Override + public PerRolePermissions deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException { + final JsonNode jsonNode = p.readValueAsTree(); + final Set ownerPermission = getPermission(jsonNode.get("owner")); + final Set memberPermission = getPermission(jsonNode.get("member")); + final Set guestPermission = getPermission(jsonNode.get("guest")); + + return new PerRolePermissions(ownerPermission, memberPermission, guestPermission, null); + } + + static Set getPermission(@Nullable JsonNode jsonNode) { + if (jsonNode == null || jsonNode.isNull()) { + return ImmutableSet.of(); + } + if (jsonNode.isArray()) { + // legacy format. e.g. [], ["READ"] or ["READ", "WRITE"] + return Jackson.convertValue(jsonNode, PERMISSION_SET_TYPE); + } + // e.g. "READ", "WRITE" or "REPO_ADMIN" + final Permission permission = Permission.valueOf(jsonNode.textValue()); + if (permission == Permission.READ) { + return ImmutableSet.of(Permission.READ); + } + // In this legacy format, REPO_ADMIN is the same as WRITE. + return ImmutableSet.of(Permission.READ, Permission.WRITE); + } +} diff --git a/server/src/main/java/com/linecorp/centraldogma/server/metadata/Permission.java b/server/src/main/java/com/linecorp/centraldogma/server/metadata/Permission.java index 4071add4ef..3a426f77a8 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/metadata/Permission.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/metadata/Permission.java @@ -27,5 +27,9 @@ public enum Permission { /** * Able to write a file to a repository. */ - WRITE + WRITE, + /** + * Able to manage a repository. + */ + REPO_ADMIN } diff --git a/server/src/main/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadata.java b/server/src/main/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadata.java index 9185867488..2c0d83c663 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadata.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadata.java @@ -28,6 +28,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; @@ -39,7 +40,8 @@ * Specifies details of a {@link Repository}. */ @JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(Include.NON_NULL) +@JsonInclude(Include.NON_NULL) // These are used when serializing. +@JsonDeserialize(using = RepositoryMetadataDeserializer.class) public class RepositoryMetadata implements Identifiable { /** diff --git a/server/src/main/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadataDeserializer.java b/server/src/main/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadataDeserializer.java new file mode 100644 index 0000000000..60b495a346 --- /dev/null +++ b/server/src/main/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadataDeserializer.java @@ -0,0 +1,78 @@ +/* + * 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 java.io.IOException; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.google.common.collect.ImmutableMap; + +import com.linecorp.centraldogma.internal.Jackson; +import com.linecorp.centraldogma.server.QuotaConfig; + +final class RepositoryMetadataDeserializer extends StdDeserializer { + + private static final long serialVersionUID = 1173216371065909688L; + + RepositoryMetadataDeserializer() { + super(RepositoryMetadata.class); + } + + @Override + public RepositoryMetadata deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException { + final JsonNode jsonNode = p.readValueAsTree(); + final String name = jsonNode.get("name").textValue(); + final PerRolePermissions perRolePermissions = + Jackson.treeToValue(jsonNode.get("perRolePermissions"), PerRolePermissions.class); + final Map> perUserPermissions = + perPermission(jsonNode, "perUserPermissions"); + final Map> perTokenPermissions = + perPermission(jsonNode, "perTokenPermissions"); + final UserAndTimestamp creation = Jackson.treeToValue(jsonNode.get("creation"), UserAndTimestamp.class); + final JsonNode removalNode = jsonNode.get("removal"); + final UserAndTimestamp removal = + removalNode == null ? null : Jackson.treeToValue(removalNode, UserAndTimestamp.class); + + final JsonNode writeQuotaNode = jsonNode.get("writeQuota"); + final QuotaConfig writeQuota = + writeQuotaNode == null ? null : Jackson.treeToValue(writeQuotaNode, QuotaConfig.class); + + return new RepositoryMetadata(name, perRolePermissions, perUserPermissions, perTokenPermissions, + creation, removal, writeQuota); + } + + private static Map> perPermission(JsonNode rootNode, String filed) { + final JsonNode permissionsNode = rootNode.get(filed); + + final ImmutableMap.Builder> builder = ImmutableMap.builder(); + final Iterator> fields = permissionsNode.fields(); + while (fields.hasNext()) { + final Entry field = fields.next(); + final String id = field.getKey(); + builder.put(id, PerRolePermissionsDeserializer.getPermission(field.getValue())); + } + + return builder.build(); + } +} diff --git a/server/src/test/java/com/linecorp/centraldogma/server/metadata/PerRolePermissionsTest.java b/server/src/test/java/com/linecorp/centraldogma/server/metadata/PerRolePermissionsTest.java new file mode 100644 index 0000000000..be8bb4deba --- /dev/null +++ b/server/src/test/java/com/linecorp/centraldogma/server/metadata/PerRolePermissionsTest.java @@ -0,0 +1,77 @@ +/* + * 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 PerRolePermissionsTest { + + @Test + void deserialize() throws Exception { + final String oldFormat = '{' + + " \"owner\": [" + + " \"READ\"," + + " \"WRITE\"" + + " ]," + + " \"member\": [" + + " \"READ\"," + + " \"WRITE\"" + + " ]," + + " \"guest\": [" + + " \"READ\"" + + " ]," + + " \"anonymous\": []" + + '}'; + final PerRolePermissions oldFormatPermission = Jackson.readValue(oldFormat, PerRolePermissions.class); + assertThat(oldFormatPermission.member()).containsExactlyInAnyOrder(Permission.READ, Permission.WRITE); + assertThat(oldFormatPermission.guest()).containsExactly(Permission.READ); + + final String newFormat = '{' + + " \"member\": \"WRITE\"," + + " \"guest\": \"READ\"" + + '}'; + final PerRolePermissions newFormatPermission = Jackson.readValue(newFormat, PerRolePermissions.class); + assertThat(newFormatPermission.member()).containsExactlyInAnyOrder(Permission.READ, Permission.WRITE); + assertThat(newFormatPermission.guest()).containsExactly(Permission.READ); + + final String newFormat2 = '{' + + " \"member\": null," + + " \"guest\": null" + + '}'; + final PerRolePermissions newFormatPermission2 = Jackson.readValue(newFormat2, PerRolePermissions.class); + assertThat(newFormatPermission2.member()).isEmpty(); + assertThat(newFormatPermission2.guest()).isEmpty(); + + final String mixed = '{' + + " \"owner\": [" + + " \"READ\"," + + " \"WRITE\"" + + " ]," + + " \"member\": [" + + " \"READ\"," + + " \"WRITE\"" + + " ]," + + " \"guest\": \"READ\"" + + '}'; + final PerRolePermissions mixedFormatPermission = Jackson.readValue(mixed, PerRolePermissions.class); + assertThat(mixedFormatPermission.member()).containsExactlyInAnyOrder(Permission.READ, Permission.WRITE); + assertThat(mixedFormatPermission.guest()).containsExactly(Permission.READ); + } +}