Skip to content

Commit

Permalink
Support ID token at PUT /v1/config and DELETE /v1/config
Browse files Browse the repository at this point in the history
  • Loading branch information
eager-signal committed May 30, 2023
1 parent f17de58 commit d1e3873
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 92 deletions.
4 changes: 2 additions & 2 deletions event-logger/src/main/kotlin/events.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ sealed interface Event

@Serializable
data class RemoteConfigSetEvent(
val token: String,
val identity: String,
val name: String,
val percentage: Int,
val defaultValue: String? = null,
Expand All @@ -35,6 +35,6 @@ data class RemoteConfigSetEvent(

@Serializable
data class RemoteConfigDeleteEvent(
val token: String,
val identity: String,
val name: String,
) : Event
11 changes: 11 additions & 0 deletions service/config/sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,17 @@ appConfig:

remoteConfig:
authorizedTokens: secret://remoteConfig.authorizedTokens
authorizedUsers:
- # 1st authorized user
- # 2nd authorized user
- # ...
- # Nth authorized user
requiredHostedDomain: example.com
audiences:
- # 1st audience
- # 2nd audience
- # ...
- # Nth audience
globalConfig: # keys and values that are given to clients on GET /v1/config
EXAMPLE_KEY: VALUE

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
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;
Expand Down Expand Up @@ -753,6 +756,10 @@ public void run(WhisperServerConfiguration config, Environment environment) thro
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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,10 +18,12 @@
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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -89,17 +104,17 @@ public UserRemoteConfigList getAll(@Auth AuthenticatedAccount auth) {
@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);
}

adminEventLogger.logEvent(
new RemoteConfigSetEvent(
configToken,
authIdentity,
config.getName(),
config.getPercentage(),
config.getDefaultValue(),
Expand All @@ -113,30 +128,57 @@ public void set(@HeaderParam("Config-Token") String configToken, @NotNull @Valid
@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());
bb.putLong(uid.getLeastSignificantBits());

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;
}
Expand Down
Loading

0 comments on commit d1e3873

Please sign in to comment.