Skip to content

Commit

Permalink
Prepare PerPermission Updates for line#1060 Compatibility
Browse files Browse the repository at this point in the history
Motivation
To ensure smooth compatibility with line#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 line#1060.
  • Loading branch information
minwoox committed Nov 14, 2024
1 parent 302dcda commit 09a6593
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -199,28 +198,6 @@ public CompletableFuture<Revision> addSpecificUserPermission(
member, memberWithPermissions.permissions());
}

/**
* PATCH /metadata/{projectName}/repos/{repoName}/perm/users/{memberId}
*
* <p>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<Revision> updateSpecificUserPermission(@Param String projectName,
@Param String repoName,
@Param String memberId,
JsonPatch jsonPatch,
Author author) {
final ReplaceOperation operation = ensureSingleReplaceOperation(jsonPatch, "/permissions");
final Collection<Permission> 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}
*
Expand Down Expand Up @@ -254,26 +231,6 @@ public CompletableFuture<Revision> addSpecificTokenPermission(
tokenWithPermissions.id(), tokenWithPermissions.permissions());
}

/**
* PATCH /metadata/{projectName}/repos/{repoName}/perm/tokens/{appId}
*
* <p>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<Revision> updateSpecificTokenPermission(@Param String projectName,
@Param String repoName,
@Param String appId,
JsonPatch jsonPatch,
Author author) {
final ReplaceOperation operation = ensureSingleReplaceOperation(jsonPatch, "/permissions");
final Collection<Permission> 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}
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand Down Expand Up @@ -104,12 +105,11 @@ public static PerRolePermissions ofPrivate() {
/**
* Creates an instance.
*/
@JsonCreator
public PerRolePermissions(@JsonProperty("owner") Iterable<Permission> owner,
@JsonProperty("member") Iterable<Permission> member,
@JsonProperty("guest") Iterable<Permission> guest,
public PerRolePermissions(Iterable<Permission> owner,
Iterable<Permission> member,
Iterable<Permission> guest,
// TODO(minwoox): Remove anonymous field after the migration.
@JsonProperty("anonymous") @Nullable Iterable<Permission> unused) {
@Nullable Iterable<Permission> unused) {
this.owner = Sets.immutableEnumSet(requireNonNull(owner, "owner"));
this.member = Sets.immutableEnumSet(requireNonNull(member, "member"));
this.guest = Sets.immutableEnumSet(requireNonNull(guest, "guest"));
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PerRolePermissions> {

private static final long serialVersionUID = 1173216371065909688L;

private static final TypeReference<Set<Permission>> PERMISSION_SET_TYPE =
new TypeReference<Set<Permission>>() {};

PerRolePermissionsDeserializer() {
super(PerRolePermissions.class);
}

@Override
public PerRolePermissions deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
final JsonNode jsonNode = p.readValueAsTree();
final Set<Permission> ownerPermission = getPermission(jsonNode.get("owner"));
final Set<Permission> memberPermission = getPermission(jsonNode.get("member"));
final Set<Permission> guestPermission = getPermission(jsonNode.get("guest"));

return new PerRolePermissions(ownerPermission, memberPermission, guestPermission, null);
}

static Set<Permission> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,9 @@ public enum Permission {
/**
* Able to write a file to a repository.
*/
WRITE
WRITE,
/**
* Able to manage a repository.
*/
REPO_ADMIN
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 {

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RepositoryMetadata> {

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<String, Collection<Permission>> perUserPermissions =
perPermission(jsonNode, "perUserPermissions");
final Map<String, Collection<Permission>> 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<String, Collection<Permission>> perPermission(JsonNode rootNode, String filed) {
final JsonNode permissionsNode = rootNode.get(filed);

final ImmutableMap.Builder<String, Collection<Permission>> builder = ImmutableMap.builder();
final Iterator<Entry<String, JsonNode>> fields = permissionsNode.fields();
while (fields.hasNext()) {
final Entry<String, JsonNode> field = fields.next();
final String id = field.getKey();
builder.put(id, PerRolePermissionsDeserializer.getPermission(field.getValue()));
}

return builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit 09a6593

Please sign in to comment.