Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

package com.linecorp.centraldogma.common;

import static com.linecorp.centraldogma.internal.Util.TOKEN_EMAIL_SUFFIX;
import static com.linecorp.centraldogma.internal.Util.APP_IDENTITY_EMAIL_SUFFIX;
import static com.linecorp.centraldogma.internal.Util.LEGACY_APP_IDENTITY_EMAIL_SUFFIX;
import static com.linecorp.centraldogma.internal.Util.emailToUsername;
import static java.util.Objects.requireNonNull;

Expand Down Expand Up @@ -95,11 +96,12 @@ public String email() {
}

/**
* Returns {@code true} if this author is a token.
* Returns {@code true} if this author is an app identity.
*/
@JsonIgnore
public boolean isToken() {
return email().endsWith(TOKEN_EMAIL_SUFFIX);
public boolean isAppIdentity() {
return email().endsWith(APP_IDENTITY_EMAIL_SUFFIX) ||
email().endsWith(LEGACY_APP_IDENTITY_EMAIL_SUFFIX);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@
public final class Util {

/**
* The domain part when generating an email address for the token.
* The domain part when generating an email address for the app identity.
*/
public static final String TOKEN_EMAIL_SUFFIX = "@dogma-token.local";
public static final String APP_IDENTITY_EMAIL_SUFFIX = "@dogma-app-identity.local";
public static final String LEGACY_APP_IDENTITY_EMAIL_SUFFIX = "@dogma-token.local";

/**
* The domain part used when generating an email address for the user if the user did not provide
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ void createNonRandomToken() throws Exception {
.auth(AuthToken.ofOAuth2(accessToken)).build();

final HttpRequest request = HttpRequest.builder()
.post("/api/v1/tokens")
.post("/api/v1/appIdentities")
.content(MediaType.FORM_DATA,
"secret=appToken-secret&isSystemAdmin=true&appId=foo")
"secret=appToken-secret&type=TOKEN" +
"&isSystemAdmin=true&appId=foo")
.build();
AggregatedHttpResponse res = systemAdminClient.execute(request).aggregate().join();
assertThat(res.status()).isEqualTo(HttpStatus.CREATED);
res = systemAdminClient.get("/api/v1/tokens").aggregate().join();
res = systemAdminClient.get("/api/v1/appIdentities").aggregate().join();
assertThat(res.contentUtf8()).contains("\"secret\":\"appToken-secret\"");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,12 @@ void shouldAllowMembersToAccessInternalProjects() throws Exception {
// Grant the user role to access the internal project.
final AggregatedHttpResponse res =
adminWebClient.prepare()
.post("/api/v1/metadata/@xds/tokens")
.post("/api/v1/metadata/@xds/appIdentities")
.content(MediaType.JSON, "{\"id\":\"" + "testAppId2" + "\",\"role\":\"MEMBER\"}")
.execute();
assertThat(res.status()).isEqualTo(HttpStatus.OK);

// @xds project should be visible to member tokens.
// @xds project should be visible to member app identities.
await().untilAsserted(
() -> assertThat(nonAdminClient.listProjects().join()).containsOnly("foo", "@xds")
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ private static void rejectInvalidRepositoryUri() {
private static void setUpRole() {
final ResponseEntity<Revision> res =
systemAdminClient.prepare()
.post("/api/v1/metadata/{proj}/tokens")
.post("/api/v1/metadata/{proj}/appIdentities")
.pathParam("proj", FOO_PROJ)
.contentJson(ImmutableMap.of("id", "appId2", "role", "OWNER"))
.asJson(Revision.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -84,6 +85,9 @@
import com.linecorp.armeria.common.MediaType;
import com.linecorp.armeria.common.ServerCacheControl;
import com.linecorp.armeria.common.SessionProtocol;
import com.linecorp.armeria.common.TlsKeyPair;
import com.linecorp.armeria.common.TlsProvider;
import com.linecorp.armeria.common.TlsProviderBuilder;
import com.linecorp.armeria.common.metric.MeterIdPrefixFunction;
import com.linecorp.armeria.common.prometheus.PrometheusMeterRegistries;
import com.linecorp.armeria.common.util.EventLoopGroups;
Expand All @@ -100,10 +104,13 @@
import com.linecorp.armeria.server.Server;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.ServerPort;
import com.linecorp.armeria.server.ServerTlsConfig;
import com.linecorp.armeria.server.ServerTlsConfigBuilder;
import com.linecorp.armeria.server.ServiceNaming;
import com.linecorp.armeria.server.ServiceRequestContext;
import com.linecorp.armeria.server.annotation.JacksonRequestConverterFunction;
import com.linecorp.armeria.server.auth.AuthService;
import com.linecorp.armeria.server.auth.Authorizer;
import com.linecorp.armeria.server.cors.CorsService;
import com.linecorp.armeria.server.cors.CorsServiceBuilder;
import com.linecorp.armeria.server.docs.DocService;
Expand Down Expand Up @@ -148,14 +155,15 @@
import com.linecorp.centraldogma.server.internal.api.ProjectServiceV1;
import com.linecorp.centraldogma.server.internal.api.RepositoryServiceV1;
import com.linecorp.centraldogma.server.internal.api.WatchService;
import com.linecorp.centraldogma.server.internal.api.auth.ApplicationCertificateAuthorizer;
import com.linecorp.centraldogma.server.internal.api.auth.ApplicationTokenAuthorizer;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresProjectRoleDecorator.RequiresProjectRoleDecoratorFactory;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresRepositoryRoleDecorator.RequiresRepositoryRoleDecoratorFactory;
import com.linecorp.centraldogma.server.internal.api.converter.HttpApiRequestConverter;
import com.linecorp.centraldogma.server.internal.api.sysadmin.AppIdentityRegistryService;
import com.linecorp.centraldogma.server.internal.api.sysadmin.KeyManagementService;
import com.linecorp.centraldogma.server.internal.api.sysadmin.MirrorAccessControlService;
import com.linecorp.centraldogma.server.internal.api.sysadmin.ServerStatusService;
import com.linecorp.centraldogma.server.internal.api.sysadmin.TokenService;
import com.linecorp.centraldogma.server.internal.api.variable.VariableServiceV1;
import com.linecorp.centraldogma.server.internal.mirror.DefaultMirrorAccessController;
import com.linecorp.centraldogma.server.internal.mirror.DefaultMirroringServicePlugin;
Expand Down Expand Up @@ -191,6 +199,7 @@
import io.micrometer.core.instrument.binder.system.UptimeMetrics;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import io.netty.handler.ssl.ClientAuth;
import io.netty.util.concurrent.DefaultThreadFactory;
import io.netty.util.concurrent.GlobalEventExecutor;

Expand Down Expand Up @@ -709,23 +718,47 @@ private Server startServer(ProjectManager pm, CommandExecutor executor,
final boolean needsTls =
cfg.ports().stream().anyMatch(ServerPort::hasTls) ||
(cfg.managementConfig() != null && cfg.managementConfig().protocol().isTls());
final AuthConfig authConfig = cfg.authConfig();
final boolean mtlsEnabled = authConfig != null && authConfig.mtlsConfig().enabled();

if (needsTls) {
try {
final TlsConfig tlsConfig = cfg.tls();
if (tlsConfig != null) {
final TlsKeyPair tlsKeyPair;
try (InputStream keyCertChainInputStream = tlsConfig.keyCertChainInputStream();
InputStream keyInputStream = tlsConfig.keyInputStream()) {
sb.tls(keyCertChainInputStream, keyInputStream, tlsConfig.keyPassword());
tlsKeyPair = TlsKeyPair.of(keyInputStream, keyCertChainInputStream);
}
if (!mtlsEnabled) {
sb.tls(tlsKeyPair);
} else {
final ServerTlsConfigBuilder serverTlsConfigBuilder =
ServerTlsConfig.builder().clientAuth(ClientAuth.OPTIONAL);
final TlsProviderBuilder tlsProviderBuilder = TlsProvider.builder().keyPair(tlsKeyPair);
if (!authConfig.mtlsConfig().caCertificateFiles().isEmpty()) {
final List<X509Certificate> caCertificates =
authConfig.mtlsConfig().caCertificates();
if (!caCertificates.isEmpty()) {
tlsProviderBuilder.trustedCertificates(caCertificates);
}
}

sb.tlsProvider(tlsProviderBuilder.build(), serverTlsConfigBuilder.build());
}
} else {
if (mtlsEnabled) {
throw new IllegalStateException("mTLS is enabled but TLS key/cert is not configured.");
}
logger.warn(
"Missing TLS configuration. Generating a self-signed certificate for TLS support.");
sb.tlsSelfSigned();
}
} catch (Exception e) {
Exceptions.throwUnsafely(e);
}
} else if (mtlsEnabled) {
throw new IllegalStateException("mTLS is enabled but no TLS port is configured.");
}

sb.clientAddressSources(cfg.clientAddressSourceList());
Expand Down Expand Up @@ -755,8 +788,8 @@ private Server startServer(ProjectManager pm, CommandExecutor executor,

final MetadataService mds = new MetadataService(pm, executor, projectInitializer);
final WatchService watchService = new WatchService(meterRegistry);
final AuthProvider authProvider =
createAuthProvider(executor, sessionManager, mds, needsTls, encryptionStorageManager);
final AuthProvider authProvider = createAuthProvider(executor, sessionManager, mds, needsTls,
mtlsEnabled, encryptionStorageManager);
final ProjectApiManager projectApiManager =
new ProjectApiManager(pm, executor, mds, encryptionStorageManager);

Expand All @@ -770,6 +803,9 @@ private Server startServer(ProjectManager pm, CommandExecutor executor,
}

sb.service("/title", webAppTitleFile(cfg.webAppTitle(), SystemInfo.hostname()).asService());
final String configContent = "{\"mtlsEnabled\": " + mtlsEnabled + '}';
sb.service("/configs", (ctx, req) ->
HttpResponse.of(HttpStatus.OK, MediaType.JSON, configContent));

sb.service(HEALTH_CHECK_PATH, HealthCheckService.builder()
.checkers(serverHealth)
Expand All @@ -785,7 +821,7 @@ private Server startServer(ProjectManager pm, CommandExecutor executor,
final Function<? super HttpService, AuthService> authService =
authService(authProvider, sessionManager);
configureHttpApi(sb, projectApiManager, executor, watchService, mds, pm, authProvider, authService,
meterRegistry, encryptionStorageManager, sessionManager, needsTls);
meterRegistry, encryptionStorageManager, sessionManager, needsTls, mtlsEnabled);

configureMetrics(sb, meterRegistry);
// Add the CORS service as the last decorator(executed first) so that the CORS service is applied
Expand Down Expand Up @@ -846,21 +882,27 @@ static HttpFile webAppTitleFile(@Nullable String webAppTitle, String hostname) {
@Nullable
private AuthProvider createAuthProvider(
CommandExecutor commandExecutor, @Nullable SessionManager sessionManager, MetadataService mds,
boolean tlsEnabled, EncryptionStorageManager encryptionStorageManager) {
boolean tlsEnabled, boolean mtlsEnabled, EncryptionStorageManager encryptionStorageManager) {
final AuthConfig authCfg = cfg.authConfig();
if (authCfg == null) {
return null;
}

checkState(sessionManager != null, "SessionManager is null");
final BooleanSupplier sessionPropagatorWritableChecker = commandExecutor::isWritable;
Authorizer<HttpRequest> authorizer = new ApplicationTokenAuthorizer(mds::findTokenBySecret)
.orElse(new SessionCookieAuthorizer(
sessionManager, sessionPropagatorWritableChecker,
tlsEnabled, encryptionStorageManager,
authCfg.systemAdministrators()));
if (mtlsEnabled) {
// Find the certificate lastly because it raises an exception if no certificate
// is found in the connection.
authorizer = authorizer.orElse(new ApplicationCertificateAuthorizer(mds::findCertificateById));
}

final AuthProviderParameters parameters = new AuthProviderParameters(
// Find application first, then find the session token.
new ApplicationTokenAuthorizer(mds::findTokenBySecret)
.orElse(new SessionCookieAuthorizer(
sessionManager, sessionPropagatorWritableChecker,
tlsEnabled, encryptionStorageManager,
authCfg.systemAdministrators())),
authorizer,
cfg,
sessionManager::generateSessionId,
// Propagate login and logout events to the other replicas.
Expand Down Expand Up @@ -929,7 +971,8 @@ private void configureHttpApi(ServerBuilder sb,
Function<? super HttpService, AuthService> authService,
MeterRegistry meterRegistry,
EncryptionStorageManager encryptionStorageManager,
@Nullable SessionManager sessionManager, boolean needsTls) {
@Nullable SessionManager sessionManager, boolean needsTls,
boolean mtlsEnabled) {
final DependencyInjector dependencyInjector = DependencyInjector.ofSingletons(
// Use the default ObjectMapper without any configuration.
// See JacksonRequestConverterFunctionTest
Expand Down Expand Up @@ -1009,7 +1052,7 @@ protected HttpResponse doGet(ServiceRequestContext ctx, HttpRequest req) {
assert sessionManager != null : "sessionManager";
apiV1ServiceBuilder
.annotatedService(new MetadataApiService(executor, mds, authCfg.loginNameNormalizer()))
.annotatedService(new TokenService(executor, mds));
.annotatedService(new AppIdentityRegistryService(executor, mds, mtlsEnabled));

// authentication services:
Optional.ofNullable(authProvider.loginApiService())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import com.linecorp.centraldogma.server.auth.AuthConfig;
import com.linecorp.centraldogma.server.auth.AuthProvider;
import com.linecorp.centraldogma.server.auth.AuthProviderFactory;
import com.linecorp.centraldogma.server.auth.MtlsConfig;
import com.linecorp.centraldogma.server.auth.Session;
import com.linecorp.centraldogma.server.plugin.Plugin;
import com.linecorp.centraldogma.server.plugin.PluginConfig;
Expand Down Expand Up @@ -122,6 +123,7 @@ public final class CentralDogmaBuilder {
private String sessionCacheSpec = DEFAULT_SESSION_CACHE_SPEC;
private long sessionTimeoutMillis = DEFAULT_SESSION_TIMEOUT_MILLIS;
private String sessionValidationSchedule = DEFAULT_SESSION_VALIDATION_SCHEDULE;
private MtlsConfig mtlsConfig = MtlsConfig.disabled();
@Nullable
private Object authProviderProperties;
private MeterRegistry meterRegistry = Flags.meterRegistry();
Expand Down Expand Up @@ -489,6 +491,14 @@ public CentralDogmaBuilder sessionValidationSchedule(String sessionValidationSch
return this;
}

/**
* Sets the mTLS configuration.
*/
public CentralDogmaBuilder mtlsConfig(MtlsConfig mtlsConfig) {
this.mtlsConfig = requireNonNull(mtlsConfig, "mtlsConfig");
return this;
}

/**
* Sets an additional properties for an {@link AuthProviderFactory}.
*/
Expand Down Expand Up @@ -602,7 +612,7 @@ private CentralDogmaConfig buildConfig() {
authCfg = new AuthConfig(
authProviderFactory, systemAdminSet, caseSensitiveLoginNames,
sessionCacheSpec, sessionTimeoutMillis, sessionValidationSchedule,
authProviderProperties);
mtlsConfig, authProviderProperties);
} else {
authCfg = null;
logger.info("{} is not specified, so {} will not be configured.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2025 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.auth;

import java.security.cert.X509Certificate;

import org.jspecify.annotations.Nullable;

/**
* Extracts the certificate ID from the peer certificate.
*/
@FunctionalInterface
public interface ApplicationCertificateIdExtractor {

/**
* Extracts the certificate ID from the peer certificate.
*/
@Nullable
String extractCertificateId(X509Certificate certificate);
}
Loading