OpenAPI support

This commit is contained in:
Sergey Skrobotov 2023-02-24 13:03:30 -08:00
parent 047f4a1c00
commit c3f4956ead
36 changed files with 403 additions and 17 deletions

33
.github/workflows/documentation.yml vendored Normal file
View File

@ -0,0 +1,33 @@
name: Update Documentation
on:
push:
branches:
- main
jobs:
build:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
cache: 'maven'
- name: Compile and Build OpenAPI file
run: ./mvnw compile
- name: Update Documentation
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cp -r api-doc/target/openapi/signal-server-openapi.yaml /tmp/
git config user.email "github@signal.org"
git config user.name "Documentation Updater"
git fetch origin gh-pages
git checkout gh-pages
cp /tmp/signal-server-openapi.yaml .
git commit -a -m "Updating documentation"
git push origin gh-pages

View File

@ -1,6 +1,9 @@
name: Service CI
on: [push]
on:
push:
branches-ignore:
- gh-pages
jobs:
build:

45
api-doc/pom.xml Normal file
View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>TextSecureServer</artifactId>
<groupId>org.whispersystems.textsecure</groupId>
<version>JGITVER</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api-doc</artifactId>
<dependencies>
<dependency>
<groupId>org.whispersystems.textsecure</groupId>
<artifactId>service</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-maven-plugin</artifactId>
<version>2.2.8</version>
<configuration>
<outputFileName>signal-server-openapi</outputFileName>
<outputPath>${project.build.directory}/openapi</outputPath>
<outputFormat>YAML</outputFormat>
<configurationFilePath>${project.basedir}/src/main/resources/openapi/openapi-configuration.yaml
</configurationFilePath>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>resolve</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,110 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.openapi;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.SimpleType;
import io.dropwizard.auth.Auth;
import io.swagger.v3.jaxrs2.ResolvedParameter;
import io.swagger.v3.jaxrs2.ext.AbstractOpenAPIExtension;
import io.swagger.v3.jaxrs2.ext.OpenAPIExtension;
import io.swagger.v3.oas.models.Components;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import javax.ws.rs.Consumes;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
/**
* One of the extension mechanisms of Swagger Core library (OpenAPI processor) is via custom implementations
* of the {@link AbstractOpenAPIExtension} class.
* <p/>
* The purpose of this extension is to customize certain aspects of the OpenAPI model generation on a lower level.
* This extension works in coordination with {@link OpenApiReader} that has access to the model on a higher level.
* <p/>
* The extension is enabled by being listed in {@code META-INF/services/io.swagger.v3.jaxrs2.ext.OpenAPIExtension} file.
* @see ServiceLoader
* @see OpenApiReader
* @see <a href="https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Extensions">Swagger 2.X Extensions</a>
*/
public class OpenApiExtension extends AbstractOpenAPIExtension {
public static final ResolvedParameter AUTHENTICATED_ACCOUNT = new ResolvedParameter();
public static final ResolvedParameter OPTIONAL_AUTHENTICATED_ACCOUNT = new ResolvedParameter();
public static final ResolvedParameter DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT = new ResolvedParameter();
public static final ResolvedParameter OPTIONAL_DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT = new ResolvedParameter();
/**
* When parsing endpoint methods, Swagger will treat the first parameter not annotated as header/path/query param
* as a request body (and will ignore other not annotated parameters). In our case, this behavior conflicts with
* the {@code @Auth}-annotated parameters. Here we're checking if parameters are known to be anything other than
* a body and return an appropriate {@link ResolvedParameter} representation.
*/
@Override
public ResolvedParameter extractParameters(
final List<Annotation> annotations,
final Type type,
final Set<Type> typesToSkip,
final Components components,
final Consumes classConsumes,
final Consumes methodConsumes,
final boolean includeRequestBody,
final JsonView jsonViewAnnotation,
final Iterator<OpenAPIExtension> chain) {
if (annotations.stream().anyMatch(a -> a.annotationType().equals(Auth.class))) {
// this is the case of authenticated endpoint,
if (type instanceof SimpleType simpleType
&& simpleType.getRawClass().equals(AuthenticatedAccount.class)) {
return AUTHENTICATED_ACCOUNT;
}
if (type instanceof SimpleType simpleType
&& simpleType.getRawClass().equals(DisabledPermittedAuthenticatedAccount.class)) {
return DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT;
}
if (type instanceof SimpleType simpleType
&& isOptionalOfType(simpleType, AuthenticatedAccount.class)) {
return OPTIONAL_AUTHENTICATED_ACCOUNT;
}
if (type instanceof SimpleType simpleType
&& isOptionalOfType(simpleType, DisabledPermittedAuthenticatedAccount.class)) {
return OPTIONAL_DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT;
}
}
return super.extractParameters(
annotations,
type,
typesToSkip,
components,
classConsumes,
methodConsumes,
includeRequestBody,
jsonViewAnnotation,
chain);
}
private static boolean isOptionalOfType(final SimpleType simpleType, final Class<?> expectedType) {
if (!simpleType.getRawClass().equals(Optional.class)) {
return false;
}
final List<JavaType> typeParameters = simpleType.getBindings().getTypeParameters();
if (typeParameters.isEmpty()) {
return false;
}
return typeParameters.get(0) instanceof SimpleType optionalParameterType
&& optionalParameterType.getRawClass().equals(expectedType);
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.openapi;
import static com.google.common.base.MoreObjects.firstNonNull;
import static org.signal.openapi.OpenApiExtension.AUTHENTICATED_ACCOUNT;
import static org.signal.openapi.OpenApiExtension.DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT;
import static org.signal.openapi.OpenApiExtension.OPTIONAL_AUTHENTICATED_ACCOUNT;
import static org.signal.openapi.OpenApiExtension.OPTIONAL_DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT;
import com.fasterxml.jackson.annotation.JsonView;
import com.google.common.collect.ImmutableList;
import io.swagger.v3.jaxrs2.Reader;
import io.swagger.v3.jaxrs2.ResolvedParameter;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import javax.ws.rs.Consumes;
/**
* One of the extension mechanisms of Swagger Core library (OpenAPI processor) is via custom implementations
* of the {@link Reader} class.
* <p/>
* The purpose of this extension is to customize certain aspects of the OpenAPI model generation on a higher level.
* This extension works in coordination with {@link OpenApiExtension} that has access to the model on a lower level.
* <p/>
* The extension is enabled by being listed in {@code resources/openapi/openapi-configuration.yaml} file.
* @see OpenApiExtension
* @see <a href="https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Extensions">Swagger 2.X Extensions</a>
*/
public class OpenApiReader extends Reader {
private static final String AUTHENTICATED_ACCOUNT_AUTH_SCHEMA = "authenticatedAccount";
/**
* Overriding this method allows converting a resolved parameter into other operation entities,
* in this case, into security requirements.
*/
@Override
protected ResolvedParameter getParameters(
final Type type,
final List<Annotation> annotations,
final Operation operation,
final Consumes classConsumes,
final Consumes methodConsumes,
final JsonView jsonViewAnnotation) {
final ResolvedParameter resolved = super.getParameters(
type, annotations, operation, classConsumes, methodConsumes, jsonViewAnnotation);
if (resolved == AUTHENTICATED_ACCOUNT || resolved == DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT) {
operation.setSecurity(ImmutableList.<SecurityRequirement>builder()
.addAll(firstNonNull(operation.getSecurity(), Collections.emptyList()))
.add(new SecurityRequirement().addList(AUTHENTICATED_ACCOUNT_AUTH_SCHEMA))
.build());
}
if (resolved == OPTIONAL_AUTHENTICATED_ACCOUNT || resolved == OPTIONAL_DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT) {
operation.setSecurity(ImmutableList.<SecurityRequirement>builder()
.addAll(firstNonNull(operation.getSecurity(), Collections.emptyList()))
.add(new SecurityRequirement().addList(AUTHENTICATED_ACCOUNT_AUTH_SCHEMA))
.add(new SecurityRequirement())
.build());
}
return resolved;
}
}

View File

@ -0,0 +1 @@
org.signal.openapi.OpenApiExtension

View File

@ -0,0 +1,25 @@
resourcePackages:
- org.whispersystems.textsecuregcm
prettyPrint: true
cacheTTL: 0
readerClass: org.signal.openapi.OpenApiReader
openAPI:
info:
title: Signal Server API
license:
name: AGPL-3.0-only
url: https://www.gnu.org/licenses/agpl-3.0.txt
servers:
- url: https://chat.signal.org
description: Production service
- url: https://chat.staging.signal.org
description: Staging service
components:
securitySchemes:
authenticatedAccount:
type: http
scheme: basic
description: |
Account authentication is based on Basic authentication schema,
where `username` has a format of `<user_id>[.<device_id>]`. If `device_id` is not specified,
user's `main` device is assumed.

View File

@ -38,10 +38,11 @@
</pluginRepositories>
<modules>
<module>api-doc</module>
<module>event-logger</module>
<module>redis-dispatch</module>
<module>websocket-resources</module>
<module>service</module>
<module>websocket-resources</module>
</modules>
<properties>

View File

@ -11,6 +11,18 @@
<artifactId>service</artifactId>
<dependencies>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2</artifactId>
<version>2.2.8</version>
<exclusions>
<!-- org.yaml:snakeyaml is causing a dependency convergence error -->
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>

View File

@ -111,6 +111,7 @@ import org.whispersystems.textsecuregcm.util.Util;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v1/accounts")
@io.swagger.v3.oas.annotations.tags.Tag(name = "Account")
public class AccountController {
public static final int MAXIMUM_USERNAME_HASHES_LIST_LENGTH = 20;
public static final int USERNAME_HASH_LENGTH = 32;

View File

@ -43,6 +43,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
@Path("/v2/accounts")
@io.swagger.v3.oas.annotations.tags.Tag(name = "Account")
public class AccountControllerV2 {
private static final String CHANGE_NUMBER_COUNTER_NAME = name(AccountControllerV2.class, "changeNumber");

View File

@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.UUID;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@ -19,6 +20,7 @@ import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
@Path("/v1/art")
@Tag(name = "Art")
public class ArtController {
private final ExternalServiceCredentialsGenerator artServiceCredentialsGenerator;
private final RateLimiters rateLimiters;

View File

@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.security.SecureRandom;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
@ -24,6 +25,7 @@ import org.whispersystems.textsecuregcm.util.Conversions;
import org.whispersystems.textsecuregcm.util.Pair;
@Path("/v2/attachments")
@Tag(name = "Attachments")
public class AttachmentControllerV2 {
private final PostPolicyGenerator policyGenerator;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
@ -29,6 +30,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
@Path("/v3/attachments")
@Tag(name = "Attachments")
public class AttachmentControllerV3 {
@Nonnull

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -11,6 +11,7 @@ import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.security.InvalidKeyException;
import java.time.Clock;
import java.time.Duration;
@ -42,6 +43,7 @@ import org.whispersystems.textsecuregcm.util.Util;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v1/certificate")
@Tag(name = "Certificate")
public class CertificateController {
private final CertificateGenerator certificateGenerator;

View File

@ -12,6 +12,7 @@ import com.google.common.net.HttpHeaders;
import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException;
import java.util.NoSuchElementException;
import javax.validation.Valid;
@ -33,6 +34,7 @@ import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.util.HeaderUtils;
@Path("/v1/challenge")
@Tag(name = "Challenge")
public class ChallengeController {
private final RateLimitChallengeManager rateLimitChallengeManager;

View File

@ -8,6 +8,7 @@ import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.net.HttpHeaders;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
@ -50,6 +51,7 @@ import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
@Path("/v1/devices")
@Tag(name = "Devices")
public class DeviceController {
private static final int MAX_DEVICES = 6;

View File

@ -6,6 +6,7 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
@ -18,6 +19,7 @@ import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator
import org.whispersystems.textsecuregcm.configuration.DirectoryClientConfiguration;
@Path("/v1/directory")
@Tag(name = "Directory")
public class DirectoryController {
private final ExternalServiceCredentialsGenerator directoryServiceTokenGenerator;

View File

@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.Clock;
import java.util.UUID;
import javax.ws.rs.GET;
@ -20,6 +21,7 @@ import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator
import org.whispersystems.textsecuregcm.configuration.DirectoryV2ClientConfiguration;
@Path("/v2/directory")
@Tag(name = "Directory")
public class DirectoryV2Controller {
private final ExternalServiceCredentialsGenerator directoryServiceTokenGenerator;

View File

@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.Clock;
import java.time.Instant;
import java.util.Objects;
@ -42,6 +43,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager;
@Path("/v1/donation")
@Tag(name = "Donations")
public class DonationController {
public interface ReceiptCredentialPresentationFactory {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -11,6 +11,7 @@ import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
@ -24,6 +25,7 @@ import org.whispersystems.websocket.session.WebSocketSessionContext;
@Path("/v1/keepalive")
@Tag(name = "Keep Alive")
public class KeepAliveController {
private final Logger logger = LoggerFactory.getLogger(KeepAliveController.class);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
@ -11,6 +11,7 @@ import com.google.common.net.HttpHeaders;
import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
@ -53,6 +54,7 @@ import org.whispersystems.textsecuregcm.storage.Keys;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v2/keys")
@Tag(name = "Keys")
public class KeysController {
private final RateLimiters rateLimiters;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
@ -108,6 +108,7 @@ import reactor.core.scheduler.Schedulers;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v1/messages")
@io.swagger.v3.oas.annotations.tags.Tag(name = "Messages")
public class MessageController {
private static final Logger logger = LoggerFactory.getLogger(MessageController.class);

View File

@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
@ -19,6 +20,7 @@ import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntityList;
@Path("/v1/payments")
@Tag(name = "Payments")
public class PaymentsController {
private final ExternalServiceCredentialsGenerator paymentsServiceCredentialsGenerator;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -14,6 +14,7 @@ import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.vavr.Tuple;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@ -111,6 +112,7 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v1/profile")
@Tag(name = "Profile")
public class ProfileController {
private final Logger logger = LoggerFactory.getLogger(ProfileController.class);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.Base64;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@ -25,6 +26,7 @@ import org.whispersystems.textsecuregcm.push.ProvisioningManager;
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
@Path("/v1/provisioning")
@Tag(name = "Provisioning")
public class ProvisioningController {
private final RateLimiters rateLimiters;

View File

@ -44,6 +44,7 @@ import org.whispersystems.textsecuregcm.util.HeaderUtils;
import org.whispersystems.textsecuregcm.util.Util;
@Path("/v1/registration")
@io.swagger.v3.oas.annotations.tags.Tag(name = "Registration")
public class RegistrationController {
private static final Logger logger = LoggerFactory.getLogger(RegistrationController.class);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@ -44,6 +45,7 @@ import org.whispersystems.textsecuregcm.util.Conversions;
import org.whispersystems.textsecuregcm.util.Util;
@Path("/v1/config")
@Tag(name = "Remote Config")
public class RemoteConfigController {
private final RemoteConfigsManager remoteConfigsManager;

View File

@ -9,6 +9,9 @@ import static java.util.Objects.requireNonNull;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.Clock;
import java.util.HashMap;
import java.util.Map;
@ -37,6 +40,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
@Path("/v1/backup")
@Tag(name = "Secure Value Recovery")
public class SecureBackupController {
private static final long MAX_AGE_SECONDS = TimeUnit.DAYS.toSeconds(30);
@ -70,6 +74,15 @@ public class SecureBackupController {
@GET
@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Generate credentials for SVR",
description = """
Generate SVR service credentials. Generated credentials have an expiration time of 30 days
(however, the TTL is fully controlled by the server side and may change even for already generated credentials).
"""
)
@ApiResponse(responseCode = "200", description = "`JSON` with generated credentials.", useReturnTypeSchema = true)
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
public ExternalServiceCredentials getAuth(final @Auth AuthenticatedAccount auth) {
return credentialsGenerator.generateForUuid(auth.getAccount().getUuid());
}
@ -80,6 +93,18 @@ public class SecureBackupController {
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@RateLimitedByIp(RateLimiters.For.BACKUP_AUTH_CHECK)
@Operation(
summary = "Check SVR credentials",
description = """
Over time, clients may wind up with multiple sets of KBS authentication credentials in cloud storage.
To determine which set is most current and should be used to communicate with SVR to retrieve a master password
(from which a registration recovery password can be derived), clients should call this endpoint
with a list of stored credentials. The response will identify which (if any) set of credentials are appropriate for communicating with SVR.
"""
)
@ApiResponse(responseCode = "200", description = "`JSON` with the check results.", useReturnTypeSchema = true)
@ApiResponse(responseCode = "422", description = "Provided list of KBS credentials could not be parsed")
@ApiResponse(responseCode = "400", description = "`POST` request body is not a valid `JSON`")
public AuthCheckResponse authCheck(@NotNull @Valid final AuthCheckRequest request) {
final Map<String, AuthCheckResponse.Result> results = new HashMap<>();
final Map<String, Pair<UUID, Long>> tokenToUuid = new HashMap<>();

View File

@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
@ -17,6 +18,7 @@ import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator
import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration;
@Path("/v1/storage")
@Tag(name = "Secure Storage")
public class SecureStorageController {
private final ExternalServiceCredentialsGenerator storageServiceCredentialsGenerator;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -7,6 +7,9 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
@ -17,6 +20,7 @@ import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
@Path("/v2/backup")
@Tag(name = "Secure Value Recovery")
public class SecureValueRecovery2Controller {
public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureValueRecovery2Configuration cfg) {
@ -28,7 +32,7 @@ public class SecureValueRecovery2Controller {
private final ExternalServiceCredentialsGenerator backupServiceCredentialGenerator;
public SecureValueRecovery2Controller(ExternalServiceCredentialsGenerator backupServiceCredentialGenerator) {
public SecureValueRecovery2Controller(final ExternalServiceCredentialsGenerator backupServiceCredentialGenerator) {
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
}
@ -36,7 +40,16 @@ public class SecureValueRecovery2Controller {
@GET
@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
public ExternalServiceCredentials getAuth(@Auth AuthenticatedAccount auth) {
@Operation(
summary = "Generate credentials for SVR2",
description = """
Generate SVR2 service credentials. Generated credentials have an expiration time of 30 days
(however, the TTL is fully controlled by the server side and may change even for already generated credentials).
"""
)
@ApiResponse(responseCode = "200", description = "`JSON` with generated credentials.", useReturnTypeSchema = true)
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
public ExternalServiceCredentials getAuth(@Auth final AuthenticatedAccount auth) {
return backupServiceCredentialGenerator.generateFor(auth.getAccount().getUuid().toString());
}
}

View File

@ -1,11 +1,12 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.security.SecureRandom;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
@ -29,6 +30,7 @@ import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Pair;
@Path("/v1/sticker")
@Tag(name = "Stickers")
public class StickerController {
private final RateLimiters rateLimiters;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021-2022 Signal Messenger, LLC
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -99,6 +99,7 @@ import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorManag
import org.whispersystems.textsecuregcm.util.ExactlySize;
@Path("/v1/subscription")
@io.swagger.v3.oas.annotations.tags.Tag(name = "Subscriptions")
public class SubscriptionController {
private static final Logger logger = LoggerFactory.getLogger(SubscriptionController.class);

View File

@ -84,6 +84,7 @@ import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.Util;
@Path("/v1/verification")
@io.swagger.v3.oas.annotations.tags.Tag(name = "Verification")
public class VerificationController {
private static final Logger logger = LoggerFactory.getLogger(VerificationController.class);

View File

@ -5,12 +5,15 @@
package org.whispersystems.textsecuregcm.entities;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.whispersystems.textsecuregcm.util.E164;
public record AuthCheckRequest(@NotNull @E164 String number,
public record AuthCheckRequest(@Schema(description = "The e164-formatted phone number.")
@NotNull @E164 String number,
@Schema(description = "A list of SVR auth values, previously retrieved from `/v1/backup/auth`; may contain at most 10.")
@NotEmpty @Size(max = 10) List<String> passwords) {
}

View File

@ -6,10 +6,12 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Map;
import javax.validation.constraints.NotNull;
public record AuthCheckResponse(@NotNull Map<String, Result> matches) {
public record AuthCheckResponse(@Schema(description = "A dictionary with the auth check results: `KBS Credentials -> 'match'/'no-match'/'invalid'`")
@NotNull Map<String, Result> matches) {
public enum Result {
MATCH("match"),