Skip to content

Commit 7189749

Browse files
committed
Merge branch 'main' into allow_dot
2 parents 959e1f4 + 2ecdca7 commit 7189749

46 files changed

Lines changed: 1415 additions & 302 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

it/xds-k8s-node-ip-extractor/src/test/java/com/linecorp/centraldogma/it/xds/k8s/XdsKubernetesNodeIpExtractorTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static com.google.common.collect.ImmutableList.toImmutableList;
1919
import static com.linecorp.centraldogma.it.xds.k8s.LabelBasedNodeIpExtractor.NODE_IP_LABEL_PROPERTY;
20+
import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS;
2021
import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson;
2122
import static org.assertj.core.api.Assertions.assertThat;
2223
import static org.awaitility.Awaitility.await;
@@ -85,7 +86,6 @@
8586
@EnableKubernetesMockClient(crud = true)
8687
class XdsKubernetesNodeIpExtractorTest {
8788

88-
private static final String XDS_CENTRAL_DOGMA_PROJECT = "@xds";
8989
private static final String K8S_ENDPOINT_AGGREGATORS_DIRECTORY = "/k8s/endpointAggregators/";
9090
private static final String K8S_ENDPOINTS_DIRECTORY = "/k8s/endpoints/";
9191

@@ -149,7 +149,7 @@ void extractsNodeIpFromLabel() throws Exception {
149149
assertThat(response.status()).isSameAs(HttpStatus.OK);
150150
assertThat(response.headers().get("grpc-status")).isEqualTo("0");
151151

152-
final Repository fooGroup = dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT)
152+
final Repository fooGroup = dogma.projectManager().get(INTERNAL_PROJECT_XDS)
153153
.repos().get("foo");
154154
final Entry<JsonNode> aggregatorEntry =
155155
fooGroup.get(Revision.HEAD, Query.ofYaml(
@@ -225,7 +225,7 @@ void fallsBackToInternalIpWhenLabelKeyIsAbsent() throws Exception {
225225
assertThat(response.status()).isSameAs(HttpStatus.OK);
226226
assertThat(response.headers().get("grpc-status")).isEqualTo("0");
227227

228-
final Repository fooGroup = dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT)
228+
final Repository fooGroup = dogma.projectManager().get(INTERNAL_PROJECT_XDS)
229229
.repos().get("foo");
230230
final Entry<JsonNode> aggregatorEntry =
231231
fooGroup.get(Revision.HEAD, Query.ofYaml(

it/xds-member-permission/src/test/java/com/linecorp/centraldogma/server/test/XdsMemberPermissionTest.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.linecorp.centraldogma.server.test;
1818

1919
import static com.linecorp.centraldogma.internal.CredentialUtil.credentialName;
20+
import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS;
2021
import static com.linecorp.centraldogma.testing.internal.auth.TestAuthMessageUtil.PASSWORD;
2122
import static com.linecorp.centraldogma.testing.internal.auth.TestAuthMessageUtil.USERNAME;
2223
import static org.assertj.core.api.Assertions.assertThat;
@@ -91,12 +92,13 @@ void shouldAllowMembersToAccessInternalProjects() throws Exception {
9192
.build()
9293
.blocking();
9394

94-
assertThat(adminClient.listProjects().join()).containsOnly("dogma", "foo", "@xds");
95+
assertThat(adminClient.listProjects().join()).containsOnly("dogma", "foo", INTERNAL_PROJECT_XDS);
9596
// The xDS project is a self-service project, so it is visible to any authenticated user even before
9697
// being granted a role. Other internal projects (e.g. dogma) remain hidden.
97-
assertThat(nonAdminClient.listProjects().join()).containsOnly("foo", "@xds");
98+
assertThat(nonAdminClient.listProjects().join()).containsOnly("foo", INTERNAL_PROJECT_XDS);
9899

99-
final CentralDogmaRepository adminRepo = adminClient.createRepository("@xds", "test").join();
100+
final CentralDogmaRepository adminRepo = adminClient.createRepository(INTERNAL_PROJECT_XDS, "test")
101+
.join();
100102
adminRepo.commit("Add test.txt", Change.ofTextUpsert("/text.txt", "foo"))
101103
.push()
102104
.join();
@@ -105,18 +107,18 @@ void shouldAllowMembersToAccessInternalProjects() throws Exception {
105107
adminWebClient.prepare()
106108
.post("/api/v1/projects/@xds/credentials")
107109
.contentJson(new CreateCredentialRequest(
108-
"test", new NoneCredential(credentialName("@xds", "test"))))
110+
"test", new NoneCredential(credentialName(INTERNAL_PROJECT_XDS, "test"))))
109111
.execute();
110112
assertThat(credentialResponse.status()).isEqualTo(HttpStatus.CREATED);
111113

112114
// All CRUD operations should be blocked.
113115
assertThatThrownBy(() -> {
114-
nonAdminClient.createRepository("@xds", "test2").join();
116+
nonAdminClient.createRepository(INTERNAL_PROJECT_XDS, "test2").join();
115117
}).isInstanceOf(CompletionException.class)
116118
.hasCauseInstanceOf(PermissionException.class)
117119
.hasMessageContaining("You must have the MEMBER project role to access the project '@xds'.");
118120

119-
final CentralDogmaRepository userRepo = nonAdminClient.forRepo("@xds", "test");
121+
final CentralDogmaRepository userRepo = nonAdminClient.forRepo(INTERNAL_PROJECT_XDS, "test");
120122
assertThatThrownBy(() -> {
121123
userRepo.commit("Update test.txt", Change.ofTextUpsert("/text.txt", "bar"))
122124
.push()
@@ -148,7 +150,7 @@ void shouldAllowMembersToAccessInternalProjects() throws Exception {
148150

149151
// @xds project should be visible to member app identities.
150152
await().untilAsserted(
151-
() -> assertThat(nonAdminClient.listProjects().join()).containsOnly("foo", "@xds")
153+
() -> assertThat(nonAdminClient.listProjects().join()).containsOnly("foo", INTERNAL_PROJECT_XDS)
152154
);
153155
// Read and write should be granted as well.
154156
userRepo.commit("Update test.txt", Change.ofTextUpsert("/text.txt", "bar"))

server-mirror-dogma/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ protected MirrorResult mirrorRemoteToLocal(File workDir, CommandExecutor executo
408408
changes.put(path, Change.ofRemoval(path));
409409
}
410410
});
411+
validateChanges(changes);
411412

412413
final String summary = "Mirror " + remoteHead + ", '" + remoteUri() +
413414
"' to the repository '" + localRepo().name() + '\'';

server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractGitMirror.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ MirrorResult mirrorRemoteToLocal(
450450
}
451451
});
452452

453+
validateChanges(changes);
453454
try {
454455
final Revision revision = executor.execute(Command.push(
455456
MIRROR_AUTHOR, localRepo().parent().name(), localRepo().name(),

server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMetaRepositoryWithMirrorTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
import static com.google.common.collect.ImmutableList.toImmutableList;
2020
import static com.linecorp.centraldogma.internal.CredentialUtil.credentialFile;
2121
import static com.linecorp.centraldogma.internal.CredentialUtil.credentialName;
22+
import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS;
2223
import static com.linecorp.centraldogma.server.internal.storage.repository.MirrorConfig.DEFAULT_SCHEDULE;
2324
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.assertj.core.api.Assertions.assertThatCode;
2426
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2527

2628
import java.util.Comparator;
@@ -245,6 +247,27 @@ void testMirrorWithCredentialId() {
245247
assertThat(((SshKeyCredential) m.credential()).username()).isEqualTo("alice");
246248
}
247249

250+
@Test
251+
void xdsMirrorWithNonRootLocalPath_isRejected() {
252+
final MirrorRequest badMirror = new MirrorRequest(
253+
"xds-mirror", true, INTERNAL_PROJECT_XDS, DEFAULT_SCHEDULE, "REMOTE_TO_LOCAL", "some-group",
254+
"/clusters/", "git+ssh", "git.example.com/org/repo.git", "/", "main", null, "", null);
255+
assertThatThrownBy(() ->
256+
metaRepo.createMirrorPushCommand("some-group", badMirror, Author.SYSTEM, null, false))
257+
.isInstanceOf(IllegalArgumentException.class)
258+
.hasMessageContaining("localPath");
259+
}
260+
261+
@Test
262+
void xdsMirrorWithRootLocalPath_isAccepted() {
263+
final MirrorRequest validMirror = new MirrorRequest(
264+
"xds-mirror", true, INTERNAL_PROJECT_XDS, DEFAULT_SCHEDULE, "REMOTE_TO_LOCAL", "some-group",
265+
"/", "git+ssh", "git.example.com/org/repo.git", "/", "main", null, "", null);
266+
assertThatCode(() ->
267+
metaRepo.createMirrorPushCommand("some-group", validMirror, Author.SYSTEM, null, false))
268+
.doesNotThrowAnyException();
269+
}
270+
248271
private List<Mirror> findMirrors() {
249272
// Get the mirror list and sort it by localRepo name alphabetically for easier testing.
250273
return metaRepo.mirrors().join().stream()

server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractMirror.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,40 +26,57 @@
2626
import java.time.Instant;
2727
import java.time.ZonedDateTime;
2828
import java.time.temporal.ChronoUnit;
29+
import java.util.ArrayList;
2930
import java.util.HashMap;
31+
import java.util.List;
3032
import java.util.Map;
3133
import java.util.Objects;
3234
import java.util.Optional;
35+
import java.util.ServiceLoader;
3336

3437
import org.eclipse.jgit.ignore.IgnoreNode;
3538
import org.eclipse.jgit.ignore.IgnoreNode.MatchResult;
3639
import org.jspecify.annotations.Nullable;
40+
import org.slf4j.Logger;
41+
import org.slf4j.LoggerFactory;
3742

3843
import com.cronutils.descriptor.CronDescriptor;
3944
import com.cronutils.model.Cron;
4045
import com.cronutils.model.time.ExecutionTime;
4146
import com.google.common.annotations.VisibleForTesting;
4247
import com.google.common.base.MoreObjects;
4348
import com.google.common.base.MoreObjects.ToStringHelper;
49+
import com.google.common.collect.ImmutableList;
4450
import com.google.common.hash.Hashing;
4551

4652
import com.linecorp.centraldogma.common.Author;
53+
import com.linecorp.centraldogma.common.Change;
4754
import com.linecorp.centraldogma.common.Entry;
4855
import com.linecorp.centraldogma.common.EntryType;
4956
import com.linecorp.centraldogma.common.MirrorException;
5057
import com.linecorp.centraldogma.server.command.CommandExecutor;
5158
import com.linecorp.centraldogma.server.credential.Credential;
5259
import com.linecorp.centraldogma.server.mirror.Mirror;
5360
import com.linecorp.centraldogma.server.mirror.MirrorDirection;
61+
import com.linecorp.centraldogma.server.mirror.MirrorFileValidator;
5462
import com.linecorp.centraldogma.server.mirror.MirrorResult;
5563
import com.linecorp.centraldogma.server.mirror.MirrorStatus;
5664
import com.linecorp.centraldogma.server.mirror.RepositoryUri;
5765
import com.linecorp.centraldogma.server.storage.repository.Repository;
5866

5967
public abstract class AbstractMirror implements Mirror {
6068

69+
private static final Logger logger = LoggerFactory.getLogger(AbstractMirror.class);
70+
6171
private static final CronDescriptor CRON_DESCRIPTOR = CronDescriptor.instance();
6272

73+
private static final List<MirrorFileValidator> FILE_VALIDATORS;
74+
75+
static {
76+
FILE_VALIDATORS = ImmutableList.copyOf(ServiceLoader.load(MirrorFileValidator.class));
77+
logger.debug("Available {}s: {}", MirrorFileValidator.class.getSimpleName(), FILE_VALIDATORS);
78+
}
79+
6380
protected static final Author MIRROR_AUTHOR = new Author("Mirror", "mirror@localhost.localdomain");
6481

6582
protected static final String MIRROR_STATE_FILE_NAME = "mirror_state.json";
@@ -252,6 +269,39 @@ protected final IgnoreNode ignoreNode() {
252269
return ignoreNode;
253270
}
254271

272+
/**
273+
* Validates the given changes using all registered {@link MirrorFileValidator}s before they are
274+
* committed. Mirror state files are excluded from validation.
275+
*
276+
* @throws MirrorException if any change fails validation
277+
*/
278+
protected final void validateChanges(Map<String, Change<?>> changes) {
279+
if (FILE_VALIDATORS.isEmpty()) {
280+
return;
281+
}
282+
final String projectName = localRepo().parent().name();
283+
final String repoName = localRepo().name();
284+
final List<String> errors = new ArrayList<>();
285+
for (Change<?> change : changes.values()) {
286+
// Skip mirror state files — they are internal bookkeeping, not user content.
287+
if (change.path().endsWith(MIRROR_STATE_FILE_NAME)) {
288+
continue;
289+
}
290+
for (MirrorFileValidator validator : FILE_VALIDATORS) {
291+
try {
292+
validator.validate(projectName, repoName, change);
293+
} catch (MirrorException e) {
294+
errors.add(e.getMessage());
295+
}
296+
}
297+
}
298+
if (!errors.isEmpty()) {
299+
throw new MirrorException(
300+
"Mirror validation failed for '" + projectName + '/' + repoName + "':\n" +
301+
String.join("\n", errors));
302+
}
303+
}
304+
255305
/**
256306
* Filters the entries using gitignore patterns. Returns the entries as-is if no gitignore is configured.
257307
* The entries should be sorted by path so that directory entries come before their children.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2026 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.linecorp.centraldogma.server.internal.storage;
18+
19+
/**
20+
* Constants for internal project names.
21+
*/
22+
public final class InternalProjectConstants {
23+
24+
/**
25+
* The name of the internal project used by the xDS control plane.
26+
*/
27+
public static final String INTERNAL_PROJECT_XDS = "@xds";
28+
29+
private InternalProjectConstants() {}
30+
}

server/src/main/java/com/linecorp/centraldogma/server/internal/storage/project/ProjectApiManager.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.linecorp.centraldogma.server.internal.storage.project;
1717

1818
import static com.linecorp.centraldogma.internal.Util.INTERNAL_PROJECT_PREFIX;
19+
import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS;
1920
import static com.linecorp.centraldogma.server.storage.project.InternalProjectInitializer.INTERNAL_PROJECT_DOGMA;
2021

2122
import java.time.Instant;
@@ -47,10 +48,6 @@
4748
*/
4849
public final class ProjectApiManager {
4950

50-
// The xDS project is a self-service internal project: any authenticated user may access it (e.g. to list
51-
// and create groups via the xDS web UI). It is intentionally more permissive than other internal projects.
52-
private static final String INTERNAL_PROJECT_XDS = "@xds";
53-
5451
private final ProjectManager projectManager;
5552
private final CommandExecutor commandExecutor;
5653
private final MetadataService metadataService;

server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.google.common.base.Preconditions.checkArgument;
2020
import static com.google.common.collect.ImmutableList.toImmutableList;
2121
import static com.linecorp.centraldogma.internal.CredentialUtil.credentialFile;
22+
import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS;
2223
import static com.linecorp.centraldogma.server.internal.storage.repository.MirrorConverter.converterToMirrorConfig;
2324
import static java.util.Objects.requireNonNull;
2425

@@ -416,5 +417,11 @@ private static void validateMirror(MirrorRequest mirror, @Nullable ZoneConfig zo
416417
checkArgument(zoneConfig.allZones().contains(zone),
417418
"The zone '%s' is not in the zone configuration: %s", zone, zoneConfig);
418419
}
420+
421+
if (INTERNAL_PROJECT_XDS.equals(mirror.projectName())) {
422+
final String localPath = mirror.localPath();
423+
checkArgument("/".equals(localPath) || localPath.isEmpty(),
424+
"xDS mirrors must use localPath '/', but got: %s", localPath);
425+
}
419426
}
420427
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2026 LY Corporation
3+
*
4+
* LY Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.linecorp.centraldogma.server.mirror;
18+
19+
import com.linecorp.centraldogma.common.Change;
20+
21+
/**
22+
* Validates file changes before they are committed to a repository during mirroring.
23+
*
24+
* <p>Implementations are loaded via {@link java.util.ServiceLoader} and invoked in
25+
* {@link com.linecorp.centraldogma.server.internal.mirror.AbstractMirror} before each push.</p>
26+
*/
27+
@FunctionalInterface
28+
public interface MirrorFileValidator {
29+
30+
/**
31+
* Validates a file change before it is committed to a repository during mirroring.
32+
*/
33+
void validate(String projectName, String repoName, Change<?> change);
34+
}

0 commit comments

Comments
 (0)