Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,6 @@ typings/

# Codex
AGENTS.md

# Claude
.claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ public class CreateRepositoryRequest {

private final String name;
private final boolean encrypt;
private final boolean isPublic;

@JsonCreator
public CreateRepositoryRequest(@JsonProperty("name") String name,
@JsonProperty("encrypt") @Nullable Boolean encrypt) {
@JsonProperty("encrypt") @Nullable Boolean encrypt,
@JsonProperty("isPublic") @Nullable Boolean isPublic) {
this.name = validateRepositoryName(name, "name");
this.encrypt = firstNonNull(encrypt, false);
this.isPublic = firstNonNull(isPublic, false);
}

@JsonProperty
Expand All @@ -50,11 +53,17 @@ public boolean encrypt() {
return encrypt;
}

@JsonProperty("isPublic")
public boolean isPublic() {
return isPublic;
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("name", name())
.add("encrypt", encrypt)
.add("isPublic", isPublic)
.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.linecorp.armeria.server.annotation.Patch;
import com.linecorp.armeria.server.annotation.Post;
import com.linecorp.armeria.server.annotation.ProducesJson;
import com.linecorp.armeria.server.annotation.Put;
import com.linecorp.centraldogma.common.Author;
import com.linecorp.centraldogma.common.ProjectRole;
import com.linecorp.centraldogma.common.RepositoryRole;
Expand Down Expand Up @@ -139,7 +140,7 @@ public CompletableFuture<Revision> updateRepositoryProjectRoles(
JsonNode payload,
Author author) throws JsonProcessingException {
final JsonNode guest = payload.get("guest");
if (guest.isTextual()) {
if (guest != null && guest.isTextual()) {
// TODO(ikhoon): Move this validation to the constructor of ProjectRoles once GUEST WRITE role is
// migrated to GUEST READ.
final String role = guest.asText();
Expand All @@ -151,6 +152,30 @@ public CompletableFuture<Revision> updateRepositoryProjectRoles(
return mds.updateRepositoryProjectRoles(author, projectName, repoName, projectRoles);
}

/**
* PUT /metadata/{projectName}/settings
*
* <p>Updates the settings of the specified {@code projectName}. A field which is not specified
* is left unchanged. The body of the request will be:
* <pre>{@code
* {
* "allowPublicRepositories": false
* }
* }</pre>
*/
@RequiresProjectRole(ProjectRole.OWNER)
@Put("/metadata/{projectName}/settings")
public CompletableFuture<Revision> updateProjectSettings(
@Param String projectName,
UpdateProjectSettingsRequest request,
Author author) {
final Boolean allowPublicRepositories = request.allowPublicRepositories();
if (allowPublicRepositories == null) {
throw new IllegalArgumentException("No settings are specified.");
}
return mds.updateAllowPublicRepositories(author, projectName, allowPublicRepositories);
}

/**
* POST /metadata/{projectName}/repos/{repoName}/roles/users
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.linecorp.centraldogma.server.command.Command;
import com.linecorp.centraldogma.server.command.CommandExecutor;
import com.linecorp.centraldogma.server.metadata.MetadataService;
import com.linecorp.centraldogma.server.metadata.ProjectRoles;
import com.linecorp.centraldogma.server.metadata.RepositoryMetadata;
import com.linecorp.centraldogma.server.metadata.Roles;
import com.linecorp.centraldogma.server.metadata.UserAndTimestamp;
Expand All @@ -43,6 +44,14 @@ public static CompletableFuture<Revision> createRepository(
CommandExecutor commandExecutor, MetadataService mds,
Author author, String projectName, String repoName, boolean encrypt,
@Nullable EncryptionStorageManager encryptionStorageManager) {
return createRepository(commandExecutor, mds, author, projectName, repoName,
DEFAULT_PROJECT_ROLES, encrypt, encryptionStorageManager);
}

public static CompletableFuture<Revision> createRepository(
CommandExecutor commandExecutor, MetadataService mds,
Author author, String projectName, String repoName, ProjectRoles projectRoles, boolean encrypt,
@Nullable EncryptionStorageManager encryptionStorageManager) {
final Map<String, RepositoryRole> users;
final Map<String, RepositoryRole> appIds;
if (author.isAppIdentity()) {
Expand All @@ -54,7 +63,7 @@ public static CompletableFuture<Revision> createRepository(
appIds = ImmutableMap.of();
}

final Roles roles = new Roles(DEFAULT_PROJECT_ROLES, users, null, appIds);
final Roles roles = new Roles(projectRoles, users, null, appIds);
final RepositoryMetadata repositoryMetadata =
RepositoryMetadata.of(repoName, roles, UserAndTimestamp.of(author));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import static com.linecorp.centraldogma.server.internal.api.DtoConverter.newRepositoryDto;
import static com.linecorp.centraldogma.server.internal.api.HttpApiUtil.checkUnremoveArgument;
import static com.linecorp.centraldogma.server.internal.api.HttpApiUtil.returnOrThrow;
import static com.linecorp.centraldogma.server.metadata.RepositoryMetadata.DEFAULT_PROJECT_ROLES;
import static com.linecorp.centraldogma.server.metadata.RepositoryMetadata.PUBLIC_PROJECT_ROLES;
import static java.util.Objects.requireNonNull;

import java.util.List;
Expand Down Expand Up @@ -66,6 +68,7 @@
import com.linecorp.centraldogma.server.internal.api.converter.CreateApiResponseConverter;
import com.linecorp.centraldogma.server.metadata.MetadataService;
import com.linecorp.centraldogma.server.metadata.ProjectMetadata;
import com.linecorp.centraldogma.server.metadata.ProjectRoles;
import com.linecorp.centraldogma.server.metadata.RepositoryMetadata;
import com.linecorp.centraldogma.server.metadata.User;
import com.linecorp.centraldogma.server.storage.encryption.EncryptionStorageManager;
Expand Down Expand Up @@ -198,12 +201,25 @@ public CompletableFuture<RepositoryDto> createRepository(ServiceRequestContext c
"Encryption is not enabled in the server.");
}

if (request.isPublic()) {
// Reject before the storage repository is created so a rejected public creation does not
// leave an orphaned repository in the common case. MetadataService.addRepo re-validates
// atomically.
final ProjectMetadata metadata = project.metadata();
if (metadata == null || !metadata.allowPublicRepositories()) {
return HttpApiUtil.throwResponse(ctx, HttpStatus.BAD_REQUEST,
"Public repositories are not allowed in the project: %s",
project.name());
}
}

final boolean encrypt = request.encrypt() || isEncryptedProject(project);
final ProjectRoles projectRoles = request.isPublic() ? PUBLIC_PROJECT_ROLES : DEFAULT_PROJECT_ROLES;

final CommandExecutor commandExecutor = executor();
final CompletableFuture<Revision> future =
RepositoryServiceUtil.createRepository(commandExecutor, mds, author, project.name(), repoName,
encrypt, encryptionStorageManager);
projectRoles, encrypt, encryptionStorageManager);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return future.handle(returnOrThrow(() -> {
final Repository repository = project.repos().get(repoName);
return newRepositoryDto(repository, repositoryStatus(repository));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2026 LY Corporation
*
* LY 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.internal.api;

import java.util.Objects;

import org.jspecify.annotations.Nullable;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.MoreObjects;

/**
* A request to update the settings of a project. A {@code null} field is left unchanged.
*/
final class UpdateProjectSettingsRequest {

@Nullable
private final Boolean allowPublicRepositories;

@JsonCreator
UpdateProjectSettingsRequest(
@JsonProperty("allowPublicRepositories") @Nullable Boolean allowPublicRepositories) {
this.allowPublicRepositories = allowPublicRepositories;
}

/**
* Returns whether the repositories of the project can be made public, or {@code null} if unchanged.
*/
@Nullable
@JsonProperty
public Boolean allowPublicRepositories() {
return allowPublicRepositories;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof UpdateProjectSettingsRequest)) {
return false;
}
final UpdateProjectSettingsRequest that = (UpdateProjectSettingsRequest) o;
return Objects.equals(allowPublicRepositories, that.allowPublicRepositories);
}

@Override
public int hashCode() {
return Objects.hashCode(allowPublicRepositories);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("allowPublicRepositories", allowPublicRepositories())
.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ public CompletableFuture<ResponseEntity<AppIdentity>> createAppIdentity(
@Param AppIdentityType type,
@Param @Nullable String secret,
@Param @Nullable String certificateId,
@Param @Default("false") boolean allowGuestAccess,
Author author, User loginUser) {
if (!mtlsEnabled && type == AppIdentityType.CERTIFICATE) {
throw new IllegalArgumentException(
Expand All @@ -137,16 +138,12 @@ public CompletableFuture<ResponseEntity<AppIdentity>> createAppIdentity(
if (type == AppIdentityType.TOKEN) {
checkArgument(certificateId == null,
"TOKEN type cannot have a certificateId: %s", certificateId);
if (secret != null) {
future = mds.createToken(author, appId, secret, isSystemAdmin);
} else {
future = mds.createToken(author, appId, isSystemAdmin);
}
future = mds.createToken(author, appId, secret, isSystemAdmin, allowGuestAccess);
} else {
checkArgument(certificateId != null, "CERTIFICATE type must have a certificateId.");
checkArgument(secret == null,
"CERTIFICATE type cannot have a secret: %s", secret);
future = mds.createCertificate(author, appId, certificateId, isSystemAdmin);
future = mds.createCertificate(author, appId, certificateId, isSystemAdmin, allowGuestAccess);
}
return future.thenCompose(unused -> fetchAppIdentity(appId))
.thenApply(appIdentity -> {
Expand Down Expand Up @@ -329,17 +326,19 @@ public Collection<Token> listTokens(User loginUser) {
* <p>Returns a newly-generated token belonging to the current login user.
*
* @deprecated Use {@link #createAppIdentity(
* String, boolean, AppIdentityType, String, String, Author, User)}.
* String, boolean, AppIdentityType, String, String, boolean, Author, User)}.
*/
@Post("/tokens")
@StatusCode(201)
@ResponseConverter(CreateApiResponseConverter.class)
@Deprecated
public CompletableFuture<ResponseEntity<Token>> createToken(@Param String appId,
@Param @Default("false") boolean isSystemAdmin,
@Param @Nullable String secret,
Author author, User loginUser) {
return createAppIdentity(appId, isSystemAdmin, AppIdentityType.TOKEN, secret, null,
public CompletableFuture<ResponseEntity<Token>> createToken(
@Param String appId,
@Param @Default("false") boolean isSystemAdmin,
@Param @Nullable String secret,
@Param @Default("false") boolean allowGuestAccess,
Author author, User loginUser) {
return createAppIdentity(appId, isSystemAdmin, AppIdentityType.TOKEN, secret, null, allowGuestAccess,
author, loginUser)
.thenApply(responseEntity -> {
final AppIdentity app = responseEntity.content();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ private void initializeMetadata(long creationTimeMillis, Author author) {
members,
null,
appIds,
null,
userAndTimestamp, null);
final CommitResult result =
dogmaRepo.commit(headRev, creationTimeMillis, Author.SYSTEM,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

import org.jspecify.annotations.Nullable;

import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.ImmutableMap;
Expand Down Expand Up @@ -69,26 +71,17 @@ AppIdentityRegistry getAppIdentityRegistry() {
return projectInitializer.appIdentityRegistry();
}

CompletableFuture<Revision> createToken(Author author, String appId) {
return createToken(author, appId, false);
}

CompletableFuture<Revision> createToken(Author author, String appId, boolean isSystemAdmin) {
return createToken(author, appId, SECRET_PREFIX + UUID.randomUUID(), isSystemAdmin);
}

CompletableFuture<Revision> createToken(Author author, String appId, String secret) {
return createToken(author, appId, secret, false);
}

CompletableFuture<Revision> createToken(Author author, String appId, String secret,
boolean isSystemAdmin) {
CompletableFuture<Revision> createToken(Author author, String appId, @Nullable String secret,
boolean isSystemAdmin, boolean allowGuestAccess) {
requireNonNull(author, "author");
requireNonNull(appId, "appId");
requireNonNull(secret, "secret");
if (secret == null) {
secret = SECRET_PREFIX + UUID.randomUUID();
}
validateSecret(secret);

final Token newToken = new Token(appId, secret, isSystemAdmin, isSystemAdmin,
// A system admin app identity can access any repository, so guest access is implied.
final Token newToken = new Token(appId, secret, isSystemAdmin, isSystemAdmin || allowGuestAccess,
UserAndTimestamp.of(author));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
final AppIdentityRegistryTransformer transformer = new AppIdentityRegistryTransformer(
(headRevision, tokens) -> {
Expand Down Expand Up @@ -280,14 +273,15 @@ private static void throwIfInvalidType(String appId, AppIdentity appIdentity,
}

CompletableFuture<Revision> createCertificate(Author author, String appId, String certificateId,
boolean isSystemAdmin) {
boolean isSystemAdmin, boolean allowGuestAccess) {
requireNonNull(author, "author");
requireNonNull(appId, "appId");
checkArgument(!isNullOrEmpty(certificateId), "certificateId must not be null or empty");

// Does not allow guest access for non admin certificate.
// A system admin app identity can access any repository, so guest access is implied.
final CertificateAppIdentity certificate =
new CertificateAppIdentity(appId, certificateId, isSystemAdmin, isSystemAdmin,
new CertificateAppIdentity(appId, certificateId, isSystemAdmin,
isSystemAdmin || allowGuestAccess,
UserAndTimestamp.of(author));
final JsonPointer appIdPath = JsonPointer.compile("/appIds" + encodeSegment(certificate.appId()));
final JsonPointer certificateIdPath =
Expand Down
Loading
Loading