Skip to content

Commit

Permalink
Add xDS group service. (#981)
Browse files Browse the repository at this point in the history
Motivation:
A user might want to manage her own xDS resources with a group of people who have permission to create and update these resources. By creating an xDS group (a Central Dogma repository), she can easily manage these xDS resources and grant write permissions to specific people, allowing them to manage the resources under Central Dogma's authorization system.

Modifications:
- Added `XdsGroupService` to create a group, which is a Central Dogma repository.
  - With this group, a user can easily manage privileged people under the Central Dogma authorization system.
  - `xds_group.proto` file is also added.
    - All proto files might be published in a separate module later for other clients such as LEGY.
- Removed `MetadataServiceInjector`.
  - `MetadataService` is injected when a decorator is created using a `DependencyInjector`.

Result:
- You can now create xDS groups to manage your xDS resources under the Central Dogma authorization system.
  • Loading branch information
minwoox authored Aug 4, 2024
1 parent 42138d1 commit 5912bf7
Show file tree
Hide file tree
Showing 15 changed files with 493 additions and 146 deletions.
11 changes: 11 additions & 0 deletions dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dropwizard-metrics = "4.2.21"
eddsa = "0.3.0"
findbugs = "3.0.2"
futures-completable = "0.3.6"
grpc-java = "1.64.0"
guava = "33.2.1-jre"
guava-failureaccess = "1.0.1"
hamcrest-library = "2.2"
Expand Down Expand Up @@ -53,6 +54,8 @@ nexus-publish-plugin = "2.0.0"
node-gradle-plugin = "7.0.2"
osdetector = "1.7.3"
proguard = "7.4.2"
protobuf = "3.25.1"
protobuf-gradle-plugin = "0.8.19"
quartz = "2.3.2"
shadow-gradle-plugin = "7.1.2"
shiro = "1.3.2"
Expand All @@ -72,6 +75,7 @@ zookeeper = "3.7.2"
[boms]
armeria = { module = "com.linecorp.armeria:armeria-bom", version.ref = "armeria" }
jackson = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "jackson" }
grpc-java = { module = "io.grpc:grpc-bom", version.ref = "grpc-java" }
junit5 = { module = "org.junit:junit-bom", version.ref = "junit5" }

[libraries.armeria]
Expand Down Expand Up @@ -328,6 +332,13 @@ version.ref = "mockito"
module = "com.guardsquare:proguard-gradle"
version.ref = "proguard"

[libraries.protobuf-protoc]
module = "com.google.protobuf:protoc"
version.ref = "protobuf"
[libraries.protobuf-gradle-plugin]
module = "com.google.protobuf:protobuf-gradle-plugin"
version.ref = "protobuf-gradle-plugin"

[libraries.quartz]
module = "org.quartz-scheduler:quartz"
version.ref = "quartz"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@
import com.linecorp.centraldogma.server.internal.api.TokenService;
import com.linecorp.centraldogma.server.internal.api.WatchService;
import com.linecorp.centraldogma.server.internal.api.auth.ApplicationTokenAuthorizer;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresPermissionDecorator.RequiresReadPermissionDecoratorFactory;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresPermissionDecorator.RequiresWritePermissionDecoratorFactory;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresRoleDecorator.RequiresRoleDecoratorFactory;
import com.linecorp.centraldogma.server.internal.api.converter.HttpApiRequestConverter;
import com.linecorp.centraldogma.server.internal.mirror.DefaultMirroringServicePlugin;
import com.linecorp.centraldogma.server.internal.replication.ZooKeeperCommandExecutor;
Expand All @@ -154,7 +157,6 @@
import com.linecorp.centraldogma.server.management.ServerStatus;
import com.linecorp.centraldogma.server.management.ServerStatusManager;
import com.linecorp.centraldogma.server.metadata.MetadataService;
import com.linecorp.centraldogma.server.metadata.MetadataServiceInjector;
import com.linecorp.centraldogma.server.mirror.MirrorProvider;
import com.linecorp.centraldogma.server.plugin.AllReplicasPlugin;
import com.linecorp.centraldogma.server.plugin.Plugin;
Expand Down Expand Up @@ -615,8 +617,9 @@ private Server startServer(ProjectManager pm, CommandExecutor executor,
HttpHeaders.of(HttpHeaderNames.AUTHORIZATION,
"Bearer " + CsrfToken.ANONYMOUS))
.build());

configureHttpApi(sb, projectApiManager, executor, watchService, mds, authProvider, sessionManager,
final Function<? super HttpService, AuthService> authService =
authService(mds, authProvider, sessionManager);
configureHttpApi(sb, projectApiManager, executor, watchService, mds, authProvider, authService,
meterRegistry);

configureMetrics(sb, meterRegistry);
Expand All @@ -639,7 +642,7 @@ private Server startServer(ProjectManager pm, CommandExecutor executor,
if (pluginsForAllReplicas != null) {
final PluginInitContext pluginInitContext =
new PluginInitContext(config(), pm, executor, meterRegistry, purgeWorker, sb,
projectInitializer);
authService, projectInitializer);
pluginsForAllReplicas.plugins()
.forEach(p -> {
if (!(p instanceof AllReplicasPlugin)) {
Expand Down Expand Up @@ -742,53 +745,47 @@ private void configureThriftService(ServerBuilder sb, ProjectApiManager projectA
sb.service("/cd/thrift/v1", thriftService);
}

private Function<? super HttpService, AuthService> authService(
MetadataService mds, @Nullable AuthProvider authProvider, @Nullable SessionManager sessionManager) {
if (authProvider == null) {
return AuthService.newDecorator(new CsrfTokenAuthorizer());
}
final AuthConfig authCfg = cfg.authConfig();
assert authCfg != null : "authCfg";
assert sessionManager != null : "sessionManager";
final Authorizer<HttpRequest> tokenAuthorizer =
new ApplicationTokenAuthorizer(mds::findTokenBySecret)
.orElse(new SessionTokenAuthorizer(sessionManager,
authCfg.administrators()));
return AuthService.builder()
.add(tokenAuthorizer)
.onFailure(new CentralDogmaAuthFailureHandler())
.newDecorator();
}

private void configureHttpApi(ServerBuilder sb,
ProjectApiManager projectApiManager, CommandExecutor executor,
WatchService watchService, MetadataService mds,
@Nullable AuthProvider authProvider,
@Nullable SessionManager sessionManager, MeterRegistry meterRegistry) {
Function<? super HttpService, ? extends HttpService> decorator;

if (authProvider != null) {
sb.service("/security_enabled", new AbstractHttpService() {
@Override
protected HttpResponse doGet(ServiceRequestContext ctx, HttpRequest req) {
return HttpResponse.of(HttpStatus.OK);
}
});

final AuthConfig authCfg = cfg.authConfig();
assert authCfg != null : "authCfg";
assert sessionManager != null : "sessionManager";
final Authorizer<HttpRequest> tokenAuthorizer =
new ApplicationTokenAuthorizer(mds::findTokenBySecret)
.orElse(new SessionTokenAuthorizer(sessionManager,
authCfg.administrators()));
decorator = MetadataServiceInjector
.newDecorator(mds)
.andThen(AuthService.builder()
.add(tokenAuthorizer)
.onFailure(new CentralDogmaAuthFailureHandler())
.newDecorator());
} else {
decorator = MetadataServiceInjector
.newDecorator(mds)
.andThen(AuthService.newDecorator(new CsrfTokenAuthorizer()));
}

Function<? super HttpService, AuthService> authService,
MeterRegistry meterRegistry) {
final DependencyInjector dependencyInjector = DependencyInjector.ofSingletons(
// Use the default ObjectMapper without any configuration.
// See JacksonRequestConverterFunctionTest
new JacksonRequestConverterFunction(new ObjectMapper()),
new HttpApiRequestConverter(projectApiManager)
new HttpApiRequestConverter(projectApiManager),
new RequiresReadPermissionDecoratorFactory(mds),
new RequiresWritePermissionDecoratorFactory(mds),
new RequiresRoleDecoratorFactory(mds)
);
sb.dependencyInjector(dependencyInjector, false)
// TODO(ikhoon): Consider exposing ReflectiveDependencyInjector as a public API via
// DependencyInjector.ofReflective()
.dependencyInjector(new ReflectiveDependencyInjector(), false);

// Enable content compression for API responses.
decorator = decorator.andThen(contentEncodingDecorator());
final Function<? super HttpService, ? extends HttpService> decorator =
authService.andThen(contentEncodingDecorator());
for (String path : ImmutableList.of(API_V0_PATH_PREFIX, API_V1_PATH_PREFIX)) {
final DecoratingServiceBindingBuilder decoratorBuilder =
sb.routeDecorator().pathPrefix(path);
Expand All @@ -802,7 +799,6 @@ protected HttpResponse doGet(ServiceRequestContext ctx, HttpRequest req) {
}

assert statusManager != null;
final ContextPathServicesBuilder apiV0ServiceBuilder = sb.contextPath(API_V0_PATH_PREFIX);
final ContextPathServicesBuilder apiV1ServiceBuilder = sb.contextPath(API_V1_PATH_PREFIX);
apiV1ServiceBuilder
.annotatedService(new AdministrativeService(executor, statusManager))
Expand Down Expand Up @@ -830,12 +826,14 @@ public String serviceName(ServiceRequestContext ctx) {
})
.build(new ContentServiceV1(executor, watchService, meterRegistry));

sb.annotatedService()
.decorator(decorator)
.decorator(DecodingService.newDecorator())
.build(new GitHttpService(projectApiManager));

if (authProvider != null) {
sb.service("/security_enabled", new AbstractHttpService() {
@Override
protected HttpResponse doGet(ServiceRequestContext ctx, HttpRequest req) {
return HttpResponse.of(HttpStatus.OK);
}
});

final AuthConfig authCfg = cfg.authConfig();
assert authCfg != null : "authCfg";
apiV1ServiceBuilder
Expand All @@ -857,10 +855,15 @@ public String serviceName(ServiceRequestContext ctx) {
authProvider.moreServices().forEach(sb::service);
}

sb.annotatedService()
.decorator(decorator)
.decorator(DecodingService.newDecorator())
.build(new GitHttpService(projectApiManager));

if (cfg.isWebAppEnabled()) {
apiV0ServiceBuilder
.annotatedService(new UserService(executor))
.annotatedService(new RepositoryService(projectApiManager, executor));
sb.contextPath(API_V0_PATH_PREFIX)
.annotatedService(new UserService(executor))
.annotatedService(new RepositoryService(projectApiManager, executor));

if (authProvider != null) {
// Will redirect to /web/auth/login by default.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,18 @@ public final class AuthUtil {
public static Author currentAuthor(ServiceRequestContext ctx) {
final User user = ctx.attr(CURRENT_USER);
assert user != null;
return user == User.DEFAULT ? Author.DEFAULT
: new Author(user.name(), user.email());
return getAuthor(user);
}

public static Author currentAuthor() {
return currentAuthor(RequestContext.current());
}

public static Author getAuthor(User user) {
return user == User.DEFAULT ? Author.DEFAULT
: new Author(user.name(), user.email());
}

public static User currentUser(ServiceRequestContext ctx) {
return ctx.attr(CURRENT_USER);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.internal.api;

import java.util.concurrent.CompletableFuture;

import com.linecorp.centraldogma.common.Author;
import com.linecorp.centraldogma.common.Revision;
import com.linecorp.centraldogma.server.command.Command;
import com.linecorp.centraldogma.server.command.CommandExecutor;
import com.linecorp.centraldogma.server.metadata.MetadataService;

public final class RepositoryServiceUtil {

public static CompletableFuture<Revision> createRepository(
CommandExecutor commandExecutor, MetadataService mds,
Author author, String projectName, String repoName) {
return commandExecutor.execute(Command.createRepository(author, projectName, repoName))
.thenCompose(unused -> mds.addRepo(author, projectName, repoName));
}

public static CompletableFuture<Revision> removeRepository(
CommandExecutor commandExecutor, MetadataService mds, Author author,
String projectName, String repoName) {
return commandExecutor.execute(Command.removeRepository(author, projectName, repoName))
.thenCompose(unused -> mds.removeRepo(author, projectName, repoName));
}

private RepositoryServiceUtil() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,15 @@ public CompletableFuture<List<RepositoryDto>> listRepositories(ServiceRequestCon
public CompletableFuture<RepositoryDto> createRepository(ServiceRequestContext ctx, Project project,
CreateRepositoryRequest request,
Author author) {
if (Project.isReservedRepoName(request.name())) {
final String repoName = request.name();
if (Project.isReservedRepoName(repoName)) {
return HttpApiUtil.throwResponse(ctx, HttpStatus.FORBIDDEN,
"A reserved repository cannot be created.");
}
return execute(Command.createRepository(author, project.name(), request.name()))
.thenCompose(unused -> mds.addRepo(author, project.name(), request.name()))
.handle(returnOrThrow(() -> DtoConverter.convert(project.repos().get(request.name()))));
final CommandExecutor commandExecutor = executor();
final CompletableFuture<Revision> future =
RepositoryServiceUtil.createRepository(commandExecutor, mds, author, project.name(), repoName);
return future.handle(returnOrThrow(() -> DtoConverter.convert(project.repos().get(repoName))));
}

/**
Expand All @@ -143,9 +145,9 @@ public CompletableFuture<Void> removeRepository(ServiceRequestContext ctx,
return HttpApiUtil.throwResponse(ctx, HttpStatus.FORBIDDEN,
"A reserved repository cannot be removed.");
}
return execute(Command.removeRepository(author, repository.parent().name(), repository.name()))
.thenCompose(unused -> mds.removeRepo(author, repository.parent().name(), repository.name()))
.handle(HttpApiUtil::throwUnsafelyIfNonNull);
return RepositoryServiceUtil.removeRepository(executor(), mds, author,
repository.parent().name(), repoName)
.handle(HttpApiUtil::throwUnsafelyIfNonNull);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
import com.linecorp.centraldogma.server.internal.api.GitHttpService;
import com.linecorp.centraldogma.server.internal.api.HttpApiUtil;
import com.linecorp.centraldogma.server.metadata.MetadataService;
import com.linecorp.centraldogma.server.metadata.MetadataServiceInjector;
import com.linecorp.centraldogma.server.metadata.Permission;
import com.linecorp.centraldogma.server.metadata.User;
import com.linecorp.centraldogma.server.storage.project.Project;
Expand All @@ -52,24 +51,26 @@
*/
public final class RequiresPermissionDecorator extends SimpleDecoratingHttpService {

private final MetadataService mds;

private final Permission requiredPermission;
@Nullable
private final String projectName;
@Nullable
private final String repoName;

RequiresPermissionDecorator(HttpService delegate, Permission requiredPermission,
RequiresPermissionDecorator(HttpService delegate, MetadataService mds, Permission requiredPermission,
@Nullable String projectName,
@Nullable String repoName) {
super(delegate);
this.mds = requireNonNull(mds, "mds");
this.requiredPermission = requireNonNull(requiredPermission, "requiredPermission");
this.projectName = projectName;
this.repoName = repoName;
}

@Override
public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception {
final MetadataService mds = MetadataServiceInjector.getMetadataService(ctx);
final User user = AuthUtil.currentUser(ctx);

String projectName = this.projectName;
Expand All @@ -89,7 +90,7 @@ public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exc
}
return unwrap().serve(ctx, req);
}
return serveUserRepo(ctx, req, mds, user, projectName, maybeRemoveGitSuffix(repoName));
return serveUserRepo(ctx, req, user, projectName, maybeRemoveGitSuffix(repoName));
}

private static HttpResponse throwForbiddenResponse(ServiceRequestContext ctx, String projectName,
Expand All @@ -111,8 +112,7 @@ private static String maybeRemoveGitSuffix(String repoName) {
}

private HttpResponse serveUserRepo(ServiceRequestContext ctx, HttpRequest req,
MetadataService mds, User user,
String projectName, String repoName) throws Exception {
User user, String projectName, String repoName) throws Exception {
final CompletionStage<Collection<Permission>> f;
try {
f = mds.findPermissions(projectName, repoName, user);
Expand Down Expand Up @@ -144,10 +144,17 @@ private HttpResponse serveUserRepo(ServiceRequestContext ctx, HttpRequest req,
*/
public static final class RequiresReadPermissionDecoratorFactory
implements DecoratorFactoryFunction<RequiresReadPermission> {

private final MetadataService mds;

public RequiresReadPermissionDecoratorFactory(MetadataService mds) {
this.mds = requireNonNull(mds, "mds");
}

@Override
public Function<? super HttpService, ? extends HttpService>
newDecorator(RequiresReadPermission parameter) {
return delegate -> new RequiresPermissionDecorator(delegate, Permission.READ,
return delegate -> new RequiresPermissionDecorator(delegate, mds, Permission.READ,
Strings.emptyToNull(parameter.project()),
Strings.emptyToNull(parameter.repository()));
}
Expand All @@ -159,10 +166,17 @@ public static final class RequiresReadPermissionDecoratorFactory
*/
public static final class RequiresWritePermissionDecoratorFactory
implements DecoratorFactoryFunction<RequiresWritePermission> {

private final MetadataService mds;

public RequiresWritePermissionDecoratorFactory(MetadataService mds) {
this.mds = requireNonNull(mds, "mds");
}

@Override
public Function<? super HttpService, ? extends HttpService>
newDecorator(RequiresWritePermission parameter) {
return delegate -> new RequiresPermissionDecorator(delegate, Permission.WRITE,
return delegate -> new RequiresPermissionDecorator(delegate, mds, Permission.WRITE,
Strings.emptyToNull(parameter.project()),
Strings.emptyToNull(parameter.repository()));
}
Expand Down
Loading

0 comments on commit 5912bf7

Please sign in to comment.