mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 19:28:05 +01:00
Support ID token at PUT /v1/config and DELETE /v1/config
This commit is contained in:
@@ -11,6 +11,9 @@ import com.amazonaws.ClientConfiguration;
|
||||
import com.amazonaws.auth.AWSCredentialsProviderChain;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
|
||||
import com.google.api.client.http.apache.v2.ApacheHttpTransport;
|
||||
import com.google.api.client.json.gson.GsonFactory;
|
||||
import com.google.auth.oauth2.GoogleCredentials;
|
||||
import com.google.cloud.logging.LoggingOptions;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
@@ -753,6 +756,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
keys, rateLimiters),
|
||||
new RemoteConfigController(remoteConfigsManager, adminEventLogger,
|
||||
config.getRemoteConfigConfiguration().authorizedTokens().value(),
|
||||
config.getRemoteConfigConfiguration().authorizedUsers(),
|
||||
config.getRemoteConfigConfiguration().requiredHostedDomain(),
|
||||
config.getRemoteConfigConfiguration().audiences(),
|
||||
new GoogleIdTokenVerifier.Builder(new ApacheHttpTransport(), new GsonFactory()),
|
||||
config.getRemoteConfigConfiguration().globalConfig()),
|
||||
new SecureBackupController(backupCredentialsGenerator, accountsManager),
|
||||
new SecureStorageController(storageCredentialsGenerator),
|
||||
|
||||
@@ -5,10 +5,17 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretStringList;
|
||||
|
||||
public record RemoteConfigConfiguration(@NotNull SecretStringList authorizedTokens,
|
||||
@NotNull Set<String> authorizedUsers,
|
||||
@NotNull String requiredHostedDomain,
|
||||
@NotNull @NotEmpty List<String> audiences,
|
||||
@NotNull Map<String, String> globalConfig) {
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -16,10 +18,12 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.ws.rs.Consumes;
|
||||
@@ -51,15 +55,26 @@ public class RemoteConfigController {
|
||||
private final RemoteConfigsManager remoteConfigsManager;
|
||||
private final AdminEventLogger adminEventLogger;
|
||||
private final List<String> configAuthTokens;
|
||||
private final Set<String> configAuthUsers;
|
||||
private final Map<String, String> globalConfig;
|
||||
|
||||
private final String requiredHostedDomain;
|
||||
|
||||
private final GoogleIdTokenVerifier googleIdTokenVerifier;
|
||||
|
||||
private static final String GLOBAL_CONFIG_PREFIX = "global.";
|
||||
|
||||
public RemoteConfigController(RemoteConfigsManager remoteConfigsManager, AdminEventLogger adminEventLogger, List<String> configAuthTokens, Map<String, String> globalConfig) {
|
||||
public RemoteConfigController(RemoteConfigsManager remoteConfigsManager, AdminEventLogger adminEventLogger,
|
||||
List<String> configAuthTokens, Set<String> configAuthUsers, String requiredHostedDomain, List<String> audience,
|
||||
final GoogleIdTokenVerifier.Builder googleIdTokenVerifierBuilder, Map<String, String> globalConfig) {
|
||||
this.remoteConfigsManager = remoteConfigsManager;
|
||||
this.adminEventLogger = Objects.requireNonNull(adminEventLogger);
|
||||
this.configAuthTokens = configAuthTokens;
|
||||
this.configAuthUsers = configAuthUsers;
|
||||
this.globalConfig = globalConfig;
|
||||
|
||||
this.requiredHostedDomain = requiredHostedDomain;
|
||||
this.googleIdTokenVerifier = googleIdTokenVerifierBuilder.setAudience(audience).build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -89,9 +104,9 @@ public class RemoteConfigController {
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public void set(@HeaderParam("Config-Token") String configToken, @NotNull @Valid RemoteConfig config) {
|
||||
if (!isAuthorized(configToken)) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
final String authIdentity = getAuthIdentity(configToken)
|
||||
.orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED));
|
||||
|
||||
if (config.getName().startsWith(GLOBAL_CONFIG_PREFIX)) {
|
||||
throw new WebApplicationException(Response.Status.FORBIDDEN);
|
||||
@@ -99,7 +114,7 @@ public class RemoteConfigController {
|
||||
|
||||
adminEventLogger.logEvent(
|
||||
new RemoteConfigSetEvent(
|
||||
configToken,
|
||||
authIdentity,
|
||||
config.getName(),
|
||||
config.getPercentage(),
|
||||
config.getDefaultValue(),
|
||||
@@ -113,21 +128,48 @@ public class RemoteConfigController {
|
||||
@DELETE
|
||||
@Path("/{name}")
|
||||
public void delete(@HeaderParam("Config-Token") String configToken, @PathParam("name") String name) {
|
||||
if (!isAuthorized(configToken)) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
final String authIdentity = getAuthIdentity(configToken)
|
||||
.orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED));
|
||||
|
||||
if (name.startsWith(GLOBAL_CONFIG_PREFIX)) {
|
||||
throw new WebApplicationException(Response.Status.FORBIDDEN);
|
||||
}
|
||||
|
||||
adminEventLogger.logEvent(new RemoteConfigDeleteEvent(configToken, name));
|
||||
adminEventLogger.logEvent(new RemoteConfigDeleteEvent(authIdentity, name));
|
||||
remoteConfigsManager.delete(name);
|
||||
}
|
||||
|
||||
private Optional<String> getAuthIdentity(String token) {
|
||||
return getAuthorizedGoogleIdentity(token)
|
||||
.map(googleIdToken -> googleIdToken.getPayload().getEmail())
|
||||
.or(() -> Optional.ofNullable(isAuthorized(token) ? token : null));
|
||||
}
|
||||
|
||||
private Optional<GoogleIdToken> getAuthorizedGoogleIdentity(String token) {
|
||||
try {
|
||||
final @Nullable GoogleIdToken googleIdToken = googleIdTokenVerifier.verify(token);
|
||||
|
||||
if (googleIdToken != null
|
||||
&& googleIdToken.getPayload().getHostedDomain().equals(requiredHostedDomain)
|
||||
&& googleIdToken.getPayload().getEmailVerified()
|
||||
&& configAuthUsers.contains(googleIdToken.getPayload().getEmail())) {
|
||||
|
||||
return Optional.of(googleIdToken);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
|
||||
} catch (final Exception ignored) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static boolean isInBucket(MessageDigest digest, UUID uid, byte[] hashKey, int configPercentage, Set<UUID> uuidsInBucket) {
|
||||
if (uuidsInBucket.contains(uid)) return true;
|
||||
public static boolean isInBucket(MessageDigest digest, UUID uid, byte[] hashKey, int configPercentage,
|
||||
Set<UUID> uuidsInBucket) {
|
||||
if (uuidsInBucket.contains(uid)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
|
||||
bb.putLong(uid.getMostSignificantBits());
|
||||
@@ -135,8 +177,8 @@ public class RemoteConfigController {
|
||||
|
||||
digest.update(bb.array());
|
||||
|
||||
byte[] hash = digest.digest(hashKey);
|
||||
int bucket = (int)(Util.ensureNonNegativeLong(Conversions.byteArrayToLong(hash)) % 100);
|
||||
byte[] hash = digest.digest(hashKey);
|
||||
int bucket = (int) (Util.ensureNonNegativeLong(Conversions.byteArrayToLong(hash)) % 100);
|
||||
|
||||
return bucket < configPercentage;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user