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"),