diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
new file mode 100644
index 000000000..a84545c03
--- /dev/null
+++ b/.github/workflows/documentation.yml
@@ -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
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 041042c4b..c26c63adf 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,6 +1,9 @@
name: Service CI
-on: [push]
+on:
+ push:
+ branches-ignore:
+ - gh-pages
jobs:
build:
diff --git a/api-doc/pom.xml b/api-doc/pom.xml
new file mode 100644
index 000000000..3d7ecf6e1
--- /dev/null
+++ b/api-doc/pom.xml
@@ -0,0 +1,45 @@
+
+
+
+ TextSecureServer
+ org.whispersystems.textsecure
+ JGITVER
+
+ 4.0.0
+ api-doc
+
+
+
+ org.whispersystems.textsecure
+ service
+ ${project.version}
+
+
+
+
+
+
+ io.swagger.core.v3
+ swagger-maven-plugin
+ 2.2.8
+
+ signal-server-openapi
+ ${project.build.directory}/openapi
+ YAML
+ ${project.basedir}/src/main/resources/openapi/openapi-configuration.yaml
+
+
+
+
+ compile
+
+ resolve
+
+
+
+
+
+
+
diff --git a/api-doc/src/main/java/org/signal/openapi/OpenApiExtension.java b/api-doc/src/main/java/org/signal/openapi/OpenApiExtension.java
new file mode 100644
index 000000000..136cbcf62
--- /dev/null
+++ b/api-doc/src/main/java/org/signal/openapi/OpenApiExtension.java
@@ -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.
+ *
+ * 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.
+ *
+ * The extension is enabled by being listed in {@code META-INF/services/io.swagger.v3.jaxrs2.ext.OpenAPIExtension} file.
+ * @see ServiceLoader
+ * @see OpenApiReader
+ * @see Swagger 2.X Extensions
+ */
+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 annotations,
+ final Type type,
+ final Set typesToSkip,
+ final Components components,
+ final Consumes classConsumes,
+ final Consumes methodConsumes,
+ final boolean includeRequestBody,
+ final JsonView jsonViewAnnotation,
+ final Iterator 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 typeParameters = simpleType.getBindings().getTypeParameters();
+ if (typeParameters.isEmpty()) {
+ return false;
+ }
+ return typeParameters.get(0) instanceof SimpleType optionalParameterType
+ && optionalParameterType.getRawClass().equals(expectedType);
+ }
+}
diff --git a/api-doc/src/main/java/org/signal/openapi/OpenApiReader.java b/api-doc/src/main/java/org/signal/openapi/OpenApiReader.java
new file mode 100644
index 000000000..61585efe1
--- /dev/null
+++ b/api-doc/src/main/java/org/signal/openapi/OpenApiReader.java
@@ -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.
+ *
+ * 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.
+ *
+ * The extension is enabled by being listed in {@code resources/openapi/openapi-configuration.yaml} file.
+ * @see OpenApiExtension
+ * @see Swagger 2.X Extensions
+ */
+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 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.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.builder()
+ .addAll(firstNonNull(operation.getSecurity(), Collections.emptyList()))
+ .add(new SecurityRequirement().addList(AUTHENTICATED_ACCOUNT_AUTH_SCHEMA))
+ .add(new SecurityRequirement())
+ .build());
+ }
+
+ return resolved;
+ }
+}
diff --git a/api-doc/src/main/resources/META-INF/services/io.swagger.v3.jaxrs2.ext.OpenAPIExtension b/api-doc/src/main/resources/META-INF/services/io.swagger.v3.jaxrs2.ext.OpenAPIExtension
new file mode 100644
index 000000000..d720ba4fb
--- /dev/null
+++ b/api-doc/src/main/resources/META-INF/services/io.swagger.v3.jaxrs2.ext.OpenAPIExtension
@@ -0,0 +1 @@
+org.signal.openapi.OpenApiExtension
diff --git a/api-doc/src/main/resources/openapi/openapi-configuration.yaml b/api-doc/src/main/resources/openapi/openapi-configuration.yaml
new file mode 100644
index 000000000..ff49154c6
--- /dev/null
+++ b/api-doc/src/main/resources/openapi/openapi-configuration.yaml
@@ -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 `[.]`. If `device_id` is not specified,
+ user's `main` device is assumed.
diff --git a/pom.xml b/pom.xml
index 27033b418..c370c4bd8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,10 +38,11 @@
+ api-doc
event-logger
redis-dispatch
- websocket-resources
service
+ websocket-resources
diff --git a/service/pom.xml b/service/pom.xml
index c27c6efe1..8a4b8d927 100644
--- a/service/pom.xml
+++ b/service/pom.xml
@@ -11,6 +11,18 @@
service
+
+ io.swagger.core.v3
+ swagger-jaxrs2
+ 2.2.8
+
+
+
+ org.yaml
+ snakeyaml
+
+
+
jakarta.servlet
jakarta.servlet-api
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java
index e3da219b2..c47552a5c 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2.java
index 765dd3720..ff22b02e9 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2.java
@@ -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");
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArtController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArtController.java
index 47fb1f2a3..d8f4fa22e 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArtController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArtController.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV2.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV2.java
index fc8680c78..b7e902878 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV2.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV2.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV3.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV3.java
index cb54b22dc..23a59c480 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV3.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV3.java
@@ -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
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CertificateController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CertificateController.java
index 845d12e3e..dc52505d6 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CertificateController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CertificateController.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ChallengeController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ChallengeController.java
index 9b8954a5e..737efa763 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ChallengeController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ChallengeController.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java
index fbce3c8e1..667584d5f 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryController.java
index ff6c2441e..58af12b07 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryController.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryV2Controller.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryV2Controller.java
index ea221c195..489c8ab3f 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryV2Controller.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryV2Controller.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DonationController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DonationController.java
index c6d363ae7..959bca465 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DonationController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DonationController.java
@@ -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 {
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeepAliveController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeepAliveController.java
index 87ef34a27..858f52e1d 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeepAliveController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeepAliveController.java
@@ -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);
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java
index 78a6d4a18..13697fbdc 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java
index 9c1f77029..55619813e 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java
@@ -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);
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/PaymentsController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/PaymentsController.java
index ed13445d7..1d33d090d 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/PaymentsController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/PaymentsController.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java
index 0ba8a9c9d..a94ef6092 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java
@@ -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);
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProvisioningController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProvisioningController.java
index a941d6dbd..af0ac58af 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProvisioningController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProvisioningController.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java
index b1380b81e..9f71cfb14 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java
@@ -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);
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigController.java
index 3a36838ee..a6f0e5bdd 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigController.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureBackupController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureBackupController.java
index 83630aabc..93543c142 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureBackupController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureBackupController.java
@@ -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 results = new HashMap<>();
final Map> tokenToUuid = new HashMap<>();
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureStorageController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureStorageController.java
index acbc0b024..b93126d9f 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureStorageController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureStorageController.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2Controller.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2Controller.java
index f0e181c9c..be4a163ab 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2Controller.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2Controller.java
@@ -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());
}
}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/StickerController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/StickerController.java
index 4b53ecbbf..86ffec4de 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/StickerController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/StickerController.java
@@ -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;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java
index 83f339d85..343d45058 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java
@@ -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);
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java
index 41b18fa15..074d73ef4 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java
@@ -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);
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckRequest.java
index e8a4a3500..1b493e681 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckRequest.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckRequest.java
@@ -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 passwords) {
}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckResponse.java
index a9645b0ea..e0f94a75a 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckResponse.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckResponse.java
@@ -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 matches) {
+public record AuthCheckResponse(@Schema(description = "A dictionary with the auth check results: `KBS Credentials -> 'match'/'no-match'/'invalid'`")
+ @NotNull Map matches) {
public enum Result {
MATCH("match"),