diff --git a/bubble-server/src/main/java/bubble/ApiConstants.java b/bubble-server/src/main/java/bubble/ApiConstants.java index 1a1d141f..ee85b912 100644 --- a/bubble-server/src/main/java/bubble/ApiConstants.java +++ b/bubble-server/src/main/java/bubble/ApiConstants.java @@ -301,6 +301,9 @@ public class ApiConstants { public static final String API_TAG_DEVICES = "devices"; public static final String API_TAG_SEARCH = "search"; public static final String API_TAG_BACKUP_RESTORE = "backup and restore"; + public static final String API_TAG_CLOUDS = "clouds"; + public static final String API_TAG_MITMPROXY = "mitmproxy"; + public static final String API_TAG_APP_RUNTIME = "bubble app runtime"; public static final String API_TAG_NODE = "node"; public static final String API_TAG_NODE_MANAGER = "node manager"; public static final String API_TAG_PAYMENT = "payment"; diff --git a/bubble-server/src/main/java/bubble/cloud/CloudRegion.java b/bubble-server/src/main/java/bubble/cloud/CloudRegion.java index be997c70..2153cedd 100644 --- a/bubble-server/src/main/java/bubble/cloud/CloudRegion.java +++ b/bubble-server/src/main/java/bubble/cloud/CloudRegion.java @@ -10,10 +10,11 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.experimental.Accessors; +import org.cobbzilla.util.reflect.OpenApiSchema; import static java.util.UUID.randomUUID; -@Accessors(chain=true) +@Accessors(chain=true) @OpenApiSchema @EqualsAndHashCode(of={"cloud", "internalName"}) @ToString(of={"cloud", "name", "internalName"}) public class CloudRegion { diff --git a/bubble-server/src/main/java/bubble/cloud/CloudRegionRelative.java b/bubble-server/src/main/java/bubble/cloud/CloudRegionRelative.java index eaf4e86f..e0b976d3 100644 --- a/bubble-server/src/main/java/bubble/cloud/CloudRegionRelative.java +++ b/bubble-server/src/main/java/bubble/cloud/CloudRegionRelative.java @@ -8,10 +8,11 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; +import org.cobbzilla.util.reflect.OpenApiSchema; import static org.cobbzilla.util.reflect.ReflectionUtil.copy; -@NoArgsConstructor @Accessors(chain=true) +@NoArgsConstructor @Accessors(chain=true) @OpenApiSchema public class CloudRegionRelative extends CloudRegion { public CloudRegionRelative(CloudRegion region) { copy(this, region); } diff --git a/bubble-server/src/main/java/bubble/cloud/email/RenderedEmail.java b/bubble-server/src/main/java/bubble/cloud/email/RenderedEmail.java index b9e3cacc..92434fcf 100644 --- a/bubble-server/src/main/java/bubble/cloud/email/RenderedEmail.java +++ b/bubble-server/src/main/java/bubble/cloud/email/RenderedEmail.java @@ -10,7 +10,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; import org.cobbzilla.mail.SimpleEmailMessage; -import org.cobbzilla.wizard.model.OpenApiSchema; +import org.cobbzilla.util.reflect.OpenApiSchema; import org.cobbzilla.wizard.model.entityconfig.annotations.ECField; import java.util.Map; diff --git a/bubble-server/src/main/java/bubble/cloud/geoCode/GeoCodeResult.java b/bubble-server/src/main/java/bubble/cloud/geoCode/GeoCodeResult.java index 81a8b2e6..1fe37081 100644 --- a/bubble-server/src/main/java/bubble/cloud/geoCode/GeoCodeResult.java +++ b/bubble-server/src/main/java/bubble/cloud/geoCode/GeoCodeResult.java @@ -10,7 +10,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; -import org.cobbzilla.wizard.model.OpenApiSchema; +import org.cobbzilla.util.reflect.OpenApiSchema; import org.cobbzilla.wizard.model.entityconfig.EntityFieldType; import org.cobbzilla.wizard.model.entityconfig.annotations.ECField; diff --git a/bubble-server/src/main/java/bubble/cloud/geoLocation/GeoLocation.java b/bubble-server/src/main/java/bubble/cloud/geoLocation/GeoLocation.java index 8d42049a..44272644 100644 --- a/bubble-server/src/main/java/bubble/cloud/geoLocation/GeoLocation.java +++ b/bubble-server/src/main/java/bubble/cloud/geoLocation/GeoLocation.java @@ -12,7 +12,7 @@ import lombok.Setter; import lombok.ToString; import lombok.experimental.Accessors; import org.cobbzilla.util.math.Haversine; -import org.cobbzilla.wizard.model.OpenApiSchema; +import org.cobbzilla.util.reflect.OpenApiSchema; import org.cobbzilla.wizard.model.entityconfig.EntityFieldType; import org.cobbzilla.wizard.model.entityconfig.annotations.ECField; diff --git a/bubble-server/src/main/java/bubble/cloud/geoTime/GeoTimeZone.java b/bubble-server/src/main/java/bubble/cloud/geoTime/GeoTimeZone.java index 81785e7b..94787848 100644 --- a/bubble-server/src/main/java/bubble/cloud/geoTime/GeoTimeZone.java +++ b/bubble-server/src/main/java/bubble/cloud/geoTime/GeoTimeZone.java @@ -9,7 +9,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; -import org.cobbzilla.wizard.model.OpenApiSchema; +import org.cobbzilla.util.reflect.OpenApiSchema; @NoArgsConstructor @AllArgsConstructor @Accessors(chain=true) @OpenApiSchema public class GeoTimeZone { diff --git a/bubble-server/src/main/java/bubble/model/AppLinks.java b/bubble-server/src/main/java/bubble/model/AppLinks.java index c5b2eb31..f7f279ee 100644 --- a/bubble-server/src/main/java/bubble/model/AppLinks.java +++ b/bubble-server/src/main/java/bubble/model/AppLinks.java @@ -7,7 +7,7 @@ package bubble.model; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import lombok.Setter; -import org.cobbzilla.wizard.model.OpenApiSchema; +import org.cobbzilla.util.reflect.OpenApiSchema; import java.util.HashMap; import java.util.Map; diff --git a/bubble-server/src/main/java/bubble/model/account/TrustedClientResponse.java b/bubble-server/src/main/java/bubble/model/account/TrustedClientResponse.java index 800b9ebe..6198f224 100644 --- a/bubble-server/src/main/java/bubble/model/account/TrustedClientResponse.java +++ b/bubble-server/src/main/java/bubble/model/account/TrustedClientResponse.java @@ -8,7 +8,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.cobbzilla.wizard.model.OpenApiSchema; +import org.cobbzilla.util.reflect.OpenApiSchema; @NoArgsConstructor @AllArgsConstructor @OpenApiSchema public class TrustedClientResponse { diff --git a/bubble-server/src/main/java/bubble/model/app/config/AppConfigView.java b/bubble-server/src/main/java/bubble/model/app/config/AppConfigView.java index c626c230..2ca783ef 100644 --- a/bubble-server/src/main/java/bubble/model/app/config/AppConfigView.java +++ b/bubble-server/src/main/java/bubble/model/app/config/AppConfigView.java @@ -6,7 +6,7 @@ package bubble.model.app.config; import lombok.Getter; import lombok.Setter; -import org.cobbzilla.wizard.model.OpenApiSchema; +import org.cobbzilla.util.reflect.OpenApiSchema; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; diff --git a/bubble-server/src/main/java/bubble/notify/NewNodeNotification.java b/bubble-server/src/main/java/bubble/notify/NewNodeNotification.java index 5356ccc7..9a25acde 100644 --- a/bubble-server/src/main/java/bubble/notify/NewNodeNotification.java +++ b/bubble-server/src/main/java/bubble/notify/NewNodeNotification.java @@ -15,6 +15,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; +import org.cobbzilla.util.reflect.OpenApiSchema; import javax.persistence.Transient; import java.util.ArrayList; @@ -25,7 +26,7 @@ import static bubble.model.account.AccountContact.mask; import static java.util.UUID.randomUUID; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; -@NoArgsConstructor @Accessors(chain=true) +@NoArgsConstructor @Accessors(chain=true) @OpenApiSchema public class NewNodeNotification { @Getter @Setter private String uuid = randomUUID().toString(); diff --git a/bubble-server/src/main/java/bubble/notify/storage/StorageListing.java b/bubble-server/src/main/java/bubble/notify/storage/StorageListing.java index bc80fafd..161b3622 100644 --- a/bubble-server/src/main/java/bubble/notify/storage/StorageListing.java +++ b/bubble-server/src/main/java/bubble/notify/storage/StorageListing.java @@ -8,8 +8,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; +import org.cobbzilla.util.reflect.OpenApiSchema; -@NoArgsConstructor @Accessors(chain=true) +@NoArgsConstructor @Accessors(chain=true) @OpenApiSchema public class StorageListing { @Getter @Setter private String[] keys; diff --git a/bubble-server/src/main/java/bubble/resources/IdentityResource.java b/bubble-server/src/main/java/bubble/resources/IdentityResource.java index 0939235f..8ee0c825 100644 --- a/bubble-server/src/main/java/bubble/resources/IdentityResource.java +++ b/bubble-server/src/main/java/bubble/resources/IdentityResource.java @@ -43,7 +43,7 @@ public class IdentityResource { @Autowired private BubbleConfiguration configuration; - @GET + @GET @Operation(hidden=true) public Response identifyNothing(@Context Request req, @Context ContainerRequest ctx) { return ok_empty(); } @@ -52,7 +52,7 @@ public class IdentityResource { tags=API_TAG_UTILITY, summary="Find what object(s) an ID belongs to. Useful when you have a UUID but don't know what kind of thing it refers to, if any.", description="Searches all model objects by ID. The id parameter is typically a UUID or name", - parameters=@Parameter(name="id", description="an identifier (typically UUID or name) to search for"), + parameters=@Parameter(name="id", description="an identifier (typically UUID or name) to search for", required=true), responses=@ApiResponse(responseCode=SC_OK, description="a JSON object where the property names are entity types, and a property's corresponding value is the object of that type found with the given ID", content=@Content(mediaType=APPLICATION_JSON, examples={ @ExampleObject(name="usually a UUID only matches one object", value="{\"CloudService\": {\"uuid\": \"the-ID-you-searched-for\", \"other-cloud-service-fields\": \"would-be-shown\"}}"), diff --git a/bubble-server/src/main/java/bubble/resources/TagsResource.java b/bubble-server/src/main/java/bubble/resources/TagsResource.java index 0693e05c..0f2eefef 100644 --- a/bubble-server/src/main/java/bubble/resources/TagsResource.java +++ b/bubble-server/src/main/java/bubble/resources/TagsResource.java @@ -24,6 +24,7 @@ import javax.ws.rs.core.Response; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; import static org.cobbzilla.wizard.resources.ResourceUtil.*; +import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.API_TAG_UTILITY; import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY; @Consumes(APPLICATION_JSON) @@ -44,6 +45,7 @@ public class TagsResource { @POST @Path("/{name}") @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_UTILITY, summary="Set a tag", description="Set a tag", parameters=@Parameter(name="name", description="name of the tag"), diff --git a/bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java b/bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java index 6909f2c9..290f171c 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java @@ -211,7 +211,7 @@ public class AccountOwnedResource { @@ -318,6 +324,13 @@ public class AccountPlansResource extends AccountOwnedResource { @@ -100,6 +105,16 @@ public class BillsResource extends ReadOnlyAccountOwnedResource { } @POST @Path("/{id}"+EP_PAY) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_PAYMENT, + summary="Pay a bill", + description="Pay a bill", + parameters=@Parameter(name="id", description="uuid of the Bill to pay"), + responses={ + @ApiResponse(responseCode=SC_OK, description="true"), + @ApiResponse(responseCode=SC_INVALID, description="validation error, for example if the Bill has already been paid") + } + ) public Response payBill(@Context ContainerRequest ctx, @PathParam("id") String id, AccountPaymentMethod paymentMethod) { diff --git a/bubble-server/src/main/java/bubble/resources/bill/PromotionsResource.java b/bubble-server/src/main/java/bubble/resources/bill/PromotionsResource.java index b0d4d65c..d3c57b18 100644 --- a/bubble-server/src/main/java/bubble/resources/bill/PromotionsResource.java +++ b/bubble-server/src/main/java/bubble/resources/bill/PromotionsResource.java @@ -30,8 +30,7 @@ import static bubble.ApiConstants.PROMOTIONS_ENDPOINT; import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; -import static org.cobbzilla.util.http.HttpStatusCodes.SC_NOT_FOUND; -import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; +import static org.cobbzilla.util.http.HttpStatusCodes.*; import static org.cobbzilla.util.string.LocaleUtil.currencyForLocale; import static org.cobbzilla.wizard.resources.ResourceUtil.*; import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY; @@ -52,7 +51,7 @@ public class PromotionsResource { @Operation(security=@SecurityRequirement(name=SEC_API_KEY), tags=API_TAG_PAYMENT, summary="List all promotions", - description="List all promotions", + description="List all promotions. If caller is admin, every defined promotion is returned. If caller is non-admin, then only promotions visible to the caller are returned", responses=@ApiResponse(responseCode=SC_OK, description="a JSON array of Promotion objects") ) public Response listPromos(@Context ContainerRequest ctx, @@ -71,10 +70,11 @@ public class PromotionsResource { tags=API_TAG_PAYMENT, summary="Find a promotion by ID", description="Find a promotion by ID", - parameters=@Parameter(name="id", description="UUID or name of promotion"), + parameters=@Parameter(name="id", description="UUID or name of promotion", required=true), responses={ @ApiResponse(responseCode=SC_OK, description="a Promotion object"), - @ApiResponse(responseCode=SC_NOT_FOUND, description="no promotion found for ID given") + @ApiResponse(responseCode=SC_NOT_FOUND, description="no promotion found for ID given"), + @ApiResponse(responseCode=SC_FORBIDDEN, description="caller is not an admin") } ) public Response findPromo(@Context ContainerRequest ctx, @@ -93,7 +93,8 @@ public class PromotionsResource { summary="Create a promotion", description="Create a promotion. Must be admin.", responses={ - @ApiResponse(responseCode=SC_OK, description="the Promotion that was created") + @ApiResponse(responseCode=SC_OK, description="the Promotion that was created"), + @ApiResponse(responseCode=SC_FORBIDDEN, description="caller is not an admin") } ) public Response createPromo(@Context ContainerRequest ctx, @@ -118,10 +119,11 @@ public class PromotionsResource { tags=API_TAG_PAYMENT, summary="Update a promotion by ID", description="Update a promotion by ID", - parameters=@Parameter(name="id", description="UUID or name of promotion"), + parameters=@Parameter(name="id", description="UUID or name of promotion", required=true), responses={ @ApiResponse(responseCode=SC_OK, description="the updated Promotion object"), - @ApiResponse(responseCode=SC_NOT_FOUND, description="no promotion found for ID given") + @ApiResponse(responseCode=SC_NOT_FOUND, description="no promotion found for ID given"), + @ApiResponse(responseCode=SC_FORBIDDEN, description="caller is not an admin") } ) public Response updatePromo(@Context ContainerRequest ctx, @@ -141,11 +143,12 @@ public class PromotionsResource { @Operation(security=@SecurityRequirement(name=SEC_API_KEY), tags=API_TAG_PAYMENT, summary="Delete a promotion by ID", - description="Delete a promotion by ID", - parameters=@Parameter(name="id", description="UUID or name of promotion"), + description="Delete a promotion by ID. Must be admin.", + parameters=@Parameter(name="id", description="UUID or name of promotion", required=true), responses={ @ApiResponse(responseCode=SC_OK, description="an empty JSON object is returned upon successful deletion"), - @ApiResponse(responseCode=SC_NOT_FOUND, description="no promotion found for ID given") + @ApiResponse(responseCode=SC_NOT_FOUND, description="no promotion found for ID given"), + @ApiResponse(responseCode=SC_FORBIDDEN, description="caller is not an admin") } ) public Response deletePromo(@Context ContainerRequest ctx, diff --git a/bubble-server/src/main/java/bubble/resources/cloud/BackupsResource.java b/bubble-server/src/main/java/bubble/resources/cloud/BackupsResource.java index 73b784a5..11efee15 100644 --- a/bubble-server/src/main/java/bubble/resources/cloud/BackupsResource.java +++ b/bubble-server/src/main/java/bubble/resources/cloud/BackupsResource.java @@ -9,10 +9,11 @@ import bubble.model.account.Account; import bubble.model.cloud.BackupStatus; import bubble.model.cloud.BubbleBackup; import bubble.model.cloud.BubbleNetwork; -import bubble.server.BubbleConfiguration; import bubble.service.backup.BackupCleanerService; import bubble.service.backup.BackupService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import org.glassfish.jersey.server.ContainerRequest; import org.springframework.beans.factory.annotation.Autowired; @@ -21,10 +22,14 @@ import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; +import static bubble.ApiConstants.API_TAG_BACKUP_RESTORE; import static bubble.ApiConstants.EP_CLEAN_BACKUPS; import static bubble.cloud.storage.StorageServiceDriver.STORAGE_PREFIX; import static bubble.cloud.storage.StorageServiceDriver.STORAGE_PREFIX_TRUNCATED; +import static bubble.service.backup.BackupCleanerService.MAX_BACKUPS; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_NOT_FOUND; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; import static org.cobbzilla.wizard.resources.ResourceUtil.*; import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY; @@ -40,20 +45,36 @@ public class BackupsResource { this.network = network; } - @Autowired private BubbleConfiguration configuration; @Autowired private BubbleBackupDAO backupDAO; @Autowired private BackupService backupService; @Autowired private BackupCleanerService backupCleanerService; @GET - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_BACKUP_RESTORE, + summary="List backups", + description="List backups for the current Bubble", + responses=@ApiResponse(responseCode=SC_OK, description="a JSON array of BubbleBackup objects") + ) public Response listBackups(@Context ContainerRequest ctx) { final Account account = getAccount(ctx); return ok(backupDAO.findByNetwork(network.getUuid())); } @GET @Path("/{id}") - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_BACKUP_RESTORE, + summary="Get details for a backup by ID", + description="Get details for a backup by ID. If the `status` parameter is specified, then the backup is only returned if the status matches this", + parameters={ + @Parameter(name="id", description="UUID or path of a backup", required=true), + @Parameter(name="status", description="only return backup if it's status matches this BackupStatus") + }, + responses={ + @ApiResponse(responseCode=SC_OK, description="the BubbleBackup object representing the backup"), + @ApiResponse(responseCode=SC_NOT_FOUND, description="no backup found with the given ID and/or status") + } + ) public Response viewBackup(@Context ContainerRequest ctx, @PathParam("id") String id, @QueryParam("status") BackupStatus status) { @@ -65,7 +86,16 @@ public class BackupsResource { } @PUT @Path("/{label}") - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_BACKUP_RESTORE, + summary="Queue a new backup job", + description="Queue a new backup job. It will run soon. If an existing backup is in progress, it will run after that backup has completed.", + parameters={ + @Parameter(name="id", description="UUID or path of a backup", required=true), + @Parameter(name="label", description="label for the backup", required=true) + }, + responses=@ApiResponse(responseCode=SC_OK, description="the BubbleBackup object representing the backup that was enqueued") + ) public Response addLabeledBackup(@Context ContainerRequest ctx, @PathParam("label") String label) { final Account account = getAccount(ctx); @@ -73,14 +103,28 @@ public class BackupsResource { } @POST @Path(EP_CLEAN_BACKUPS) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_BACKUP_RESTORE, + summary="Remove old backups", + description="Bubble automatically cleans up old backups every 24 hours, retaining the most recent "+MAX_BACKUPS+" backups. Use this endpoint to run the cleaner now.", + responses=@ApiResponse(responseCode=SC_OK, description="the BubbleBackup object representing the backup that was enqueued") + ) public Response cleanBackups(@Context ContainerRequest ctx) { final Account account = getAccount(ctx); return ok(backupCleanerService.cleanNow()); } @DELETE @Path("/{id : .+}") - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_BACKUP_RESTORE, + summary="Delete a backup", + description="Delete a backup", + parameters=@Parameter(name="id", description="UUID or path of a backup", required=true), + responses={ + @ApiResponse(responseCode=SC_OK, description="an empty response with status 200 indicates success"), + @ApiResponse(responseCode=SC_NOT_FOUND, description="no backup found with the given ID and/or status") + } + ) public Response deleteBackup(@Context ContainerRequest ctx, @PathParam("id") String id) { if (id.startsWith(STORAGE_PREFIX_TRUNCATED) && !id.startsWith(STORAGE_PREFIX)) { diff --git a/bubble-server/src/main/java/bubble/resources/cloud/CloudServiceDataResource.java b/bubble-server/src/main/java/bubble/resources/cloud/CloudServiceDataResource.java index 29261cd6..d46fed78 100644 --- a/bubble-server/src/main/java/bubble/resources/cloud/CloudServiceDataResource.java +++ b/bubble-server/src/main/java/bubble/resources/cloud/CloudServiceDataResource.java @@ -15,7 +15,7 @@ import java.util.List; public class CloudServiceDataResource extends AccountOwnedResource { - private CloudService cloud; + private final CloudService cloud; public CloudServiceDataResource(Account account, CloudService cloud) { super(account); diff --git a/bubble-server/src/main/java/bubble/resources/cloud/CloudServiceRegionsResource.java b/bubble-server/src/main/java/bubble/resources/cloud/CloudServiceRegionsResource.java index b98edf34..37485a50 100644 --- a/bubble-server/src/main/java/bubble/resources/cloud/CloudServiceRegionsResource.java +++ b/bubble-server/src/main/java/bubble/resources/cloud/CloudServiceRegionsResource.java @@ -15,6 +15,8 @@ import bubble.model.cloud.CloudService; import bubble.server.BubbleConfiguration; import bubble.service.cloud.GeoService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.extern.slf4j.Slf4j; import org.glassfish.grizzly.http.server.Request; @@ -27,12 +29,13 @@ import javax.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; -import static bubble.ApiConstants.EP_CLOSEST; -import static bubble.ApiConstants.getRemoteHost; +import static bubble.ApiConstants.*; import static bubble.model.cloud.RegionalServiceDriver.findClosestRegions; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_NOT_FOUND; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; import static org.cobbzilla.wizard.resources.ResourceUtil.*; import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY; @@ -41,7 +44,7 @@ import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KE @Slf4j public class CloudServiceRegionsResource { - private Account account; + private final Account account; public CloudServiceRegionsResource(Account account) { this.account = account; } @@ -51,7 +54,16 @@ public class CloudServiceRegionsResource { @Autowired private GeoService geoService; @GET - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_CLOUDS, + summary="List cloud regions", + description="List cloud regions. If the `footprint` param is provided, then only regions within that footprint will be returned", + parameters=@Parameter(name="footprint", description="UUID or name of a BubbleFootprint to match"), + responses={ + @ApiResponse(responseCode=SC_OK, description="a JSON array of CloudRegion objects"), + @ApiResponse(responseCode=SC_NOT_FOUND, description="if footprint param was present and does not refer to a valid BubbleFootprint") + } + ) public Response listRegions(@Context Request req, @Context ContainerRequest ctx, @QueryParam("footprint") String footprintId) { @@ -69,7 +81,16 @@ public class CloudServiceRegionsResource { } @GET @Path(EP_CLOSEST) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_CLOUDS, + summary="List nearest cloud regions", + description="List nearest cloud regions, using geo-location services. If the `footprint` param is provided, then only regions within that footprint will be returned", + parameters=@Parameter(name="footprint", description="UUID or name of a BubbleFootprint to match"), + responses={ + @ApiResponse(responseCode=SC_OK, description="a JSON array of CloudRegionRelative objects, sorted by nearest first"), + @ApiResponse(responseCode=SC_NOT_FOUND, description="if footprint param was present and does not refer to a valid BubbleFootprint") + } + ) public Response listClosestRegions(@Context Request req, @Context ContainerRequest ctx, @QueryParam("footprint") String footprintId) { diff --git a/bubble-server/src/main/java/bubble/resources/cloud/ComputePackerResource.java b/bubble-server/src/main/java/bubble/resources/cloud/ComputePackerResource.java index b287afce..17832196 100644 --- a/bubble-server/src/main/java/bubble/resources/cloud/ComputePackerResource.java +++ b/bubble-server/src/main/java/bubble/resources/cloud/ComputePackerResource.java @@ -10,6 +10,10 @@ import bubble.model.cloud.AnsibleInstallType; import bubble.model.cloud.CloudService; import bubble.server.BubbleConfiguration; import bubble.service.packer.PackerService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import org.glassfish.grizzly.http.server.Request; import org.glassfish.jersey.server.ContainerRequest; import org.springframework.beans.factory.annotation.Autowired; @@ -18,9 +22,13 @@ import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; +import static bubble.ApiConstants.API_TAG_ACTIVATION; import static bubble.resources.cloud.PackerResource.packerNotAllowedForUser; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; -import static org.cobbzilla.wizard.resources.ResourceUtil.*; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; +import static org.cobbzilla.wizard.resources.ResourceUtil.forbidden; +import static org.cobbzilla.wizard.resources.ResourceUtil.ok; +import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY; @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) @@ -38,6 +46,12 @@ public class ComputePackerResource { } @GET + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_ACTIVATION, + summary="List packer images", + description="List packer images present on this compute cloud", + responses=@ApiResponse(responseCode=SC_OK, description="a JSON array of PackerImage objects") + ) public Response listImages(@Context Request req, @Context ContainerRequest ctx) { if (packerNotAllowedForUser(ctx)) return forbidden(); @@ -46,6 +60,13 @@ public class ComputePackerResource { } @PUT @Path("/{type}") + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_ACTIVATION, + summary="Create a packer image", + description="Create a packer image on this compute cloud. The packer image will be created if it does not already exist. This call will return immediately and the image will be created in the background.", + parameters=@Parameter(name="type", description="The type of image to create, either `sage` or `node`"), + responses=@ApiResponse(responseCode=SC_OK, description="an empty response means the request was accepted") + ) public Response writeImages(@Context Request req, @Context ContainerRequest ctx, @PathParam("type") AnsibleInstallType installType) { diff --git a/bubble-server/src/main/java/bubble/resources/cloud/LogsResource.java b/bubble-server/src/main/java/bubble/resources/cloud/LogsResource.java index 68cd80ce..9e51e9c0 100644 --- a/bubble-server/src/main/java/bubble/resources/cloud/LogsResource.java +++ b/bubble-server/src/main/java/bubble/resources/cloud/LogsResource.java @@ -6,6 +6,10 @@ package bubble.resources.cloud; import bubble.model.account.Account; import bubble.service.boot.SelfNodeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.NonNull; import org.glassfish.jersey.server.ContainerRequest; import org.springframework.beans.factory.annotation.Autowired; @@ -16,47 +20,77 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import java.util.HashMap; import java.util.Optional; -import java.util.concurrent.TimeUnit; import static bubble.ApiConstants.*; +import static bubble.service.boot.StandardSelfNodeService.MAX_LOG_TTL_DAYS; +import static java.util.concurrent.TimeUnit.DAYS; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; -import static org.cobbzilla.wizard.resources.ResourceUtil.forbiddenEx; -import static org.cobbzilla.wizard.resources.ResourceUtil.ok; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; +import static org.cobbzilla.wizard.resources.ResourceUtil.*; +import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.API_TAG_UTILITY; +import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY; @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) public class LogsResource { + public static final String K_FLAG = "flag"; + public static final String K_EXPIRE_AT = "expireAt"; + @Autowired private SelfNodeService selfNodeService; - private Account account; + private final Account account; - public LogsResource(@NonNull final Account account) { - this.account = account; - } + public LogsResource(@NonNull final Account account) { this.account = account; } @GET @Path(EP_STATUS) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_UTILITY, + summary="Get logging status", + description="Get logging status. Must be admin. Returns a JSON object with keys `"+K_FLAG+"` (boolean, indicates if logging is enabled) and `"+K_EXPIRE_AT+"` (epoch time in milliseconds when logging will automatically be turned off)", + responses=@ApiResponse(responseCode=SC_OK, description="true if logs enabled, false otherwise") + ) @NonNull public Response getLoggingStatus(@NonNull @Context final ContainerRequest ctx) { + final Account caller = userPrincipal(ctx); + if (!caller.admin()) throw forbiddenEx(); + final var flag = new HashMap(2); - flag.put("flag", selfNodeService.getLogFlag()); - flag.put("expireAt", selfNodeService.getLogFlagExpirationTime().orElse(null)); + flag.put(K_FLAG, selfNodeService.getLogFlag()); + flag.put(K_EXPIRE_AT, selfNodeService.getLogFlagExpirationTime().orElse(null)); return ok(flag); } @POST @Path(EP_START) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_UTILITY, + summary="Enable logging", + description="Enable logging. Must be admin.", + parameters=@Parameter(name="ttlDays", description="Logging will be disabled after this many days have passed. Max is "+MAX_LOG_TTL_DAYS), + responses=@ApiResponse(responseCode=SC_OK, description="empty response indicates success") + ) @NonNull public Response startLogging(@NonNull @Context final ContainerRequest ctx, @Nullable @QueryParam("ttlDays") final Byte ttlDays) { + final Account caller = userPrincipal(ctx); + if (!caller.admin()) throw forbiddenEx(); return setLogFlag(true, Optional.ofNullable(ttlDays)); } @POST @Path(EP_STOP) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_UTILITY, + summary="Disable logging", + description="Disable logging. Must be admin.", + responses=@ApiResponse(responseCode=SC_OK, description="empty response indicates success") + ) @NonNull public Response stopLogging(@NonNull @Context final ContainerRequest ctx) { + final Account caller = userPrincipal(ctx); + if (!caller.admin()) throw forbiddenEx(); return setLogFlag(false, Optional.empty()); } @NonNull private Response setLogFlag(final boolean b, @NonNull final Optional ttlInDays) { if (!account.admin()) throw forbiddenEx(); // caller must be admin - selfNodeService.setLogFlag(b, ttlInDays.map(days -> (int) TimeUnit.DAYS.toSeconds(days))); + selfNodeService.setLogFlag(b, ttlInDays.map(days -> (int) DAYS.toSeconds(days))); return ok(); } } diff --git a/bubble-server/src/main/java/bubble/resources/cloud/NetworkActionsResource.java b/bubble-server/src/main/java/bubble/resources/cloud/NetworkActionsResource.java index 0d815ed5..103495ad 100644 --- a/bubble-server/src/main/java/bubble/resources/cloud/NetworkActionsResource.java +++ b/bubble-server/src/main/java/bubble/resources/cloud/NetworkActionsResource.java @@ -20,6 +20,8 @@ import bubble.service.cloud.NodeLaunchMonitor; import bubble.service.cloud.NodeProgressMeterTick; import bubble.service.cloud.StandardNetworkService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @@ -36,6 +38,8 @@ import java.util.List; import static bubble.ApiConstants.*; import static bubble.model.cloud.BubbleNetwork.TAG_ALLOW_REGISTRATION; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_FORBIDDEN; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; import static org.cobbzilla.wizard.resources.ResourceUtil.*; import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY; @@ -54,8 +58,8 @@ public class NetworkActionsResource { @Autowired private BubbleConfiguration configuration; @Autowired private StandardAuthenticatorService authenticatorService; - private Account account; - private BubbleNetwork network; + private final Account account; + private final BubbleNetwork network; public NetworkActionsResource (Account account, BubbleNetwork network) { this.account = account; @@ -63,7 +67,17 @@ public class NetworkActionsResource { } @POST @Path(EP_START) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_CLOUDS, + summary="Launch a Bubble", + description="Launch a Bubble. If cloud and region are provided, then the Bubble will be launched in that region of that cloud. If neither are provided, then we'll try to select the nearest region. Returns a NewNodeNotification containing info that can be used to track launch status.", + parameters={ + @Parameter(name="cloud", description="UUID or name of a CloudService whose type is `compute`. Optional, but if specified then `region` must also be supplied."), + @Parameter(name="region", description="Name of a region within the cloud. Optional, but if specified then `cloud` must also be supplied."), + @Parameter(name="exactRegion", description="If true and cloud and region are also supplied, then fail if the Bubble cannot be launched in the specified region. Otherwise, a relaunch in the next-closest region will be attempted") + }, + responses=@ApiResponse(responseCode=SC_OK, description="a NewNodeNotification object") + ) public Response startNetwork(@Context Request req, @Context ContainerRequest ctx, @QueryParam("cloud") String cloud, @@ -88,7 +102,12 @@ public class NetworkActionsResource { } @GET @Path(EP_STATUS) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_CLOUDS, + summary="List launch statuses", + description="List launch statuses. Returns an array of NodeProgressMeterTick objects, each representing the latest status update from a launching Bubble. Normally only one Bubble is launching at a time, so there will only be one element in the array.", + responses=@ApiResponse(responseCode=SC_OK, description="array of NodeProgressMeterTick objects") + ) public Response listLaunchStatuses(@Context Request req, @Context ContainerRequest ctx) { final Account caller = userPrincipal(ctx); @@ -97,7 +116,13 @@ public class NetworkActionsResource { } @GET @Path(EP_STATUS+"/{uuid}") - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_CLOUDS, + summary="Get launch status for a specific launch", + description="Get launch status for a specific launch. Returns a NodeProgressMeterTick object representing the latest status update from the launching Bubble.", + parameters=@Parameter(name="uuid", description="UUID of the NewNodeNotification returned when the Bubble was launched"), + responses=@ApiResponse(responseCode=SC_OK, description="a NodeProgressMeterTick object") + ) public Response requestLaunchStatus(@Context Request req, @Context ContainerRequest ctx, @PathParam("uuid") String uuid) { @@ -128,7 +153,15 @@ public class NetworkActionsResource { } @POST @Path(EP_STOP) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_CLOUDS, + summary="Stop a Bubble", + description="Stop a Bubble. The caller must own the Bubble or be an admin. Returns true", + responses={ + @ApiResponse(responseCode=SC_OK, description="true"), + @ApiResponse(responseCode=SC_FORBIDDEN, description="the caller is not authorized to stop the Bubble") + } + ) public Response stopNetwork(@Context ContainerRequest ctx) { final Account caller = userPrincipal(ctx); if (!authAccount(caller)) return forbidden(); @@ -143,7 +176,17 @@ public class NetworkActionsResource { } @POST @Path(EP_RESTORE) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags={API_TAG_CLOUDS, API_TAG_BACKUP_RESTORE}, + summary="Launch a Bubble in restore mode", + description="Launch a Bubble in restore mode. If cloud and region are provided, then the Bubble will be launched in that region of that cloud. If neither are provided, then we'll try to select the nearest region. Returns a NewNodeNotification containing info that can be used to track launch status.", + parameters={ + @Parameter(name="cloud", description="UUID or name of a CloudService whose type is `compute`. Optional, but if specified then `region` must also be supplied."), + @Parameter(name="region", description="Name of a region within the cloud. Optional, but if specified then `cloud` must also be supplied."), + @Parameter(name="exactRegion", description="If true and cloud and region are also supplied, then fail if the Bubble cannot be launched in the specified region. Otherwise, a relaunch in the next-closest region will be attempted") + }, + responses=@ApiResponse(responseCode=SC_OK, description="a NewNodeNotification object") + ) public Response restoreNetwork(@Context Request req, @Context ContainerRequest ctx, @QueryParam("cloud") String cloud, @@ -158,7 +201,12 @@ public class NetworkActionsResource { } @PUT @Path(EP_FORK) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_CLOUDS, + summary="Fork a Bubble", + description="Fork a Bubble. Must be admin. Clones this Bubble's data onto another system, can be configured as either a sage or a node", + responses=@ApiResponse(responseCode=SC_OK, description="a NewNodeNotification object") + ) public Response fork(@Context Request req, @Context ContainerRequest ctx, ForkRequest forkRequest) { diff --git a/bubble-server/src/main/java/bubble/resources/cloud/NetworkBackupKeysResource.java b/bubble-server/src/main/java/bubble/resources/cloud/NetworkBackupKeysResource.java index 30f3f78c..f5c8d250 100644 --- a/bubble-server/src/main/java/bubble/resources/cloud/NetworkBackupKeysResource.java +++ b/bubble-server/src/main/java/bubble/resources/cloud/NetworkBackupKeysResource.java @@ -13,6 +13,10 @@ import bubble.model.account.message.AccountMessageType; import bubble.model.account.message.ActionTarget; import bubble.model.cloud.BubbleNetwork; import bubble.service.backup.NetworkKeysService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.NonNull; import org.cobbzilla.util.collection.NameAndValue; import org.cobbzilla.wizard.stream.FileSendableResource; @@ -32,7 +36,9 @@ import static bubble.ApiConstants.*; import static bubble.model.account.Account.validatePassword; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_OCTET_STREAM; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; import static org.cobbzilla.wizard.resources.ResourceUtil.*; +import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY; /** * Ensure that only network admin can access these calls, and only for current network. Such admin should have verified @@ -54,6 +60,12 @@ public class NetworkBackupKeysResource { } @GET + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_BACKUP_RESTORE, + summary="Request Bubble keys", + description="Request Bubble keys. Sends a message to the owner of the Bubble to approve the request", + responses=@ApiResponse(responseCode=SC_OK, description="HTTP status 200 indicates success") + ) @NonNull public Response requestNetworkKeys(@NonNull @Context final Request req, @NonNull @Context final ContainerRequest ctx) { messageDAO.create(new AccountMessage().setMessageType(AccountMessageType.request) @@ -74,6 +86,12 @@ public class NetworkBackupKeysResource { } @POST @Path("/{keysCode}") + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_BACKUP_RESTORE, + summary="Retrieve Bubble keys", + description="Once request for Bubble keys is approved, retrieve them here. This returns a NetworkKeys objects which contains the encrypted keys.", + responses=@ApiResponse(responseCode=SC_OK, description="a NetworkKeys object containing the encrypted keys") + ) @NonNull public Response retrieveNetworkKeys(@NonNull @Context final Request req, @NonNull @Context final ContainerRequest ctx, @NonNull @PathParam("keysCode") final String keysCode, @@ -84,6 +102,16 @@ public class NetworkBackupKeysResource { } @POST @Path("/{keysCode}" + EP_BACKUPS + EP_START) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_BACKUP_RESTORE, + summary="Start downloading a backup", + description="Start downloading a backup", + parameters={ + @Parameter(name="keysCode", description="a code to associate with this download"), + @Parameter(name="backupId", description="the backup to download") + }, + responses=@ApiResponse(responseCode=SC_OK, description="empty response indicates success") + ) @NonNull public Response backupDownloadStart(@NonNull @Context final ContainerRequest ctx, @NonNull @PathParam("keysCode") final String keysCode, @NonNull @QueryParam("backupId") final String backupId, @@ -100,6 +128,13 @@ public class NetworkBackupKeysResource { } @GET @Path("/{keysCode}" + EP_BACKUPS + EP_STATUS) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_BACKUP_RESTORE, + summary="Check backup download status", + description="Check backup download status", + parameters=@Parameter(name="keysCode", description="the code supplied when the backup download was started"), + responses=@ApiResponse(responseCode=SC_OK, description="a BackupPackagingStatus object") + ) @NonNull public Response backupDownloadStatus(@NonNull @Context final ContainerRequest ctx, @NonNull @PathParam("keysCode") final String keysCode) { // not checking keys code now here. However, such key will be required in preparing/prepared backup downloads' @@ -109,6 +144,13 @@ public class NetworkBackupKeysResource { @GET @Path("/{keysCode}" + EP_BACKUPS + EP_DOWNLOAD) @Produces(APPLICATION_OCTET_STREAM) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_BACKUP_RESTORE, + summary="Download a backup", + description="Once a backup has fully downloaded to the Bubble, use this API call to retrieve the download.", + parameters=@Parameter(name="keysCode", description="the code supplied when the backup download was started"), + responses=@ApiResponse(responseCode=SC_OK, description="the backup file") + ) @NonNull public Response backupDownload(@NonNull @Context final ContainerRequest ctx, @NonNull @PathParam("keysCode") final String keysCode) { final var status = keysService.backupDownloadStatus(keysCode); diff --git a/bubble-server/src/main/java/bubble/resources/cloud/NetworkDnsResource.java b/bubble-server/src/main/java/bubble/resources/cloud/NetworkDnsResource.java index 7608032e..ee5216ad 100644 --- a/bubble-server/src/main/java/bubble/resources/cloud/NetworkDnsResource.java +++ b/bubble-server/src/main/java/bubble/resources/cloud/NetworkDnsResource.java @@ -12,6 +12,8 @@ import bubble.model.cloud.BubbleNetwork; import bubble.model.cloud.CloudService; import bubble.server.BubbleConfiguration; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import org.cobbzilla.util.dns.DnsRecord; import org.cobbzilla.util.dns.DnsRecordMatch; @@ -26,6 +28,7 @@ import javax.ws.rs.core.Response; import static bubble.ApiConstants.*; import static org.cobbzilla.util.dns.DnsType.A; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; import static org.cobbzilla.wizard.resources.ResourceUtil.*; import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY; @@ -36,9 +39,9 @@ public class NetworkDnsResource { @Autowired private CloudServiceDAO cloudDAO; @Autowired private BubbleConfiguration configuration; - private Account account; - private BubbleDomain domain; - private BubbleNetwork network; + private final Account account; + private final BubbleDomain domain; + private final BubbleNetwork network; public NetworkDnsResource (Account account, BubbleDomain domain, BubbleNetwork network) { this.account = account; @@ -47,14 +50,28 @@ public class NetworkDnsResource { } @GET - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_CLOUDS, + summary="List DNS records", + description="List DNS records visible to the Bubble's DNS driver.", + responses=@ApiResponse(responseCode=SC_OK, description="array of DnsRecord objects") + ) public Response listDns(@Context ContainerRequest ctx) { final DnsContext context = new DnsContext(ctx); return ok(context.dnsDriver.list()); } @GET @Path(EP_FIND_DNS) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_CLOUDS, + summary="Find DNS records", + description="Find DNS records that match the given type and/or name (which is a regex)", + parameters={ + @Parameter(name="type", description="Only return records with this DNS type"), + @Parameter(name="name", description="Only return records whose name matches this regex") + }, + responses=@ApiResponse(responseCode=SC_OK, description="array of DnsRecord objects") + ) public Response findDns(@Context ContainerRequest ctx, @QueryParam("type") DnsType type, @QueryParam("name") String name) { @@ -69,7 +86,16 @@ public class NetworkDnsResource { } @GET @Path(EP_DIG_DNS) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_CLOUDS, + summary="Dig DNS records", + description="Use dig to find DNS records that match name (which is a regex) and type (optional). This is used for verification - when we publish new records to our DNS provider, we want to check another (neutral, system) DNS provider to see that they are visible there too. Then we can feel more comfortable handing out that hostname to other people, who should be able to resolve it.", + parameters={ + @Parameter(name="type", description="Only return records with this DNS type"), + @Parameter(name="name", description="Only return records whose name matches this regex", required=true) + }, + responses=@ApiResponse(responseCode=SC_OK, description="array of DnsRecord objects") + ) public Response digDns(@Context ContainerRequest ctx, @QueryParam("type") DnsType type, @QueryParam("name") String name) { @@ -84,7 +110,12 @@ public class NetworkDnsResource { } @POST @Path(EP_UPDATE_DNS) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_CLOUDS, + summary="Update a DNS record", + description="Update a DNS record", + responses=@ApiResponse(responseCode=SC_OK, description="the updated DnsRecord object") + ) public Response updateDns(@Context ContainerRequest ctx, DnsRecord record) { final DnsContext context = new DnsContext(ctx, record); @@ -92,7 +123,12 @@ public class NetworkDnsResource { } @POST @Path(EP_DELETE_DNS) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_CLOUDS, + summary="Delete a DNS record", + description="Delete a DNS record", + responses=@ApiResponse(responseCode=SC_OK, description="the deleted DnsRecord object") + ) public Response removeDns(@Context ContainerRequest ctx, DnsRecord record) { final DnsContext context = new DnsContext(ctx, record); diff --git a/bubble-server/src/main/java/bubble/resources/cloud/NetworksResource.java b/bubble-server/src/main/java/bubble/resources/cloud/NetworksResource.java index b1a211e5..27787d31 100644 --- a/bubble-server/src/main/java/bubble/resources/cloud/NetworksResource.java +++ b/bubble-server/src/main/java/bubble/resources/cloud/NetworksResource.java @@ -21,6 +21,8 @@ import bubble.resources.account.AccountOwnedResource; import bubble.service.boot.SelfNodeService; import bubble.service.cloud.GeoService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @@ -38,6 +40,7 @@ import java.util.stream.Collectors; import static bubble.ApiConstants.*; import static bubble.model.cloud.RegionalServiceDriver.findClosestRegions; import static org.cobbzilla.util.daemon.ZillaRuntime.big; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; import static org.cobbzilla.wizard.resources.ResourceUtil.*; import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY; @@ -123,7 +126,18 @@ public class NetworksResource extends AccountOwnedResource> messageCache = new ConcurrentHashMap<>(); @DELETE - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_UTILITY, + summary="Flush message cache", + description="Flush message cache", + responses=@ApiResponse(responseCode=SC_OK, description="empty JSON object indicates success") + ) public Response flushMessageCache (@Context ContainerRequest ctx) { final Account caller = userPrincipal(ctx); if (!caller.admin()) return forbidden(); @@ -52,6 +61,16 @@ public class MessagesResource { } @GET @Path("/{locale}/{group}") + @Operation(tags=API_TAG_UTILITY, + summary="Get localized messages", + description="Get localized messages by group. `locale` specifies the desired locale. If the locale is not supported, another similar locale or the default locale will be used. `The `group` param is the message group to retrieve. Groups are: `pre_auth`, `post_auth`, `countries`, `timezones`, `apps`. Requesting the `post_auth` or `apps` groups requires a valid API session. `format` is an optional format for the messages. Format can be `raw` or `underscore` (which converts dots to underscores). Default is `underscore`.", + parameters={ + @Parameter(name="locale", description="The desired locale. If the locale is not supported, another similar locale or the default locale will be used", required=true), + @Parameter(name="group", description="Message group to retrieve. Groups are: `pre_auth`, `post_auth`, `countries`, `timezones`, `apps`. Requesting the `post_auth` or `apps` groups requires a valid API session.", required=true), + @Parameter(name="format", description="Format for the messages. Format can be `raw` or `underscore` (which converts dots to underscores). Default is `underscore`.") + }, + responses=@ApiResponse(responseCode=SC_OK, description="status of the router, which can be one of: `none`, `active`, `unreachable`, `deleted`") + ) public Response loadMessagesByGroup(@Context ContainerRequest ctx, @PathParam("locale") String locale, @PathParam("group") String group, @@ -67,7 +86,7 @@ public class MessagesResource { final Account caller = optionalUserPrincipal(ctx); if (caller == null && !ArrayUtils.contains(PRE_AUTH_MESSAGE_GROUPS, group)) return forbidden(); - if (!ArrayUtils.contains(MessageService.ALL_MESSAGE_GROUPS, group)) return notFound(group); + if (!ArrayUtils.contains(ALL_MESSAGE_GROUPS, group)) return notFound(group); if (format == null) format = MessageResourceFormat.underscore; if (log.isDebugEnabled()) log.debug("loadMessagesByGroup: finding messages for group="+group+" among locales: "+StringUtil.toString(locales)); diff --git a/bubble-server/src/main/java/bubble/resources/notify/InboundNotifyResource.java b/bubble-server/src/main/java/bubble/resources/notify/InboundNotifyResource.java index 2a2a7b33..2339687f 100644 --- a/bubble-server/src/main/java/bubble/resources/notify/InboundNotifyResource.java +++ b/bubble-server/src/main/java/bubble/resources/notify/InboundNotifyResource.java @@ -12,6 +12,7 @@ import bubble.service.cloud.StorageStreamService; import bubble.service.notify.InboundNotification; import bubble.service.notify.NotificationReceiverService; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.Operation; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.security.RsaMessage; @@ -50,6 +51,10 @@ public class InboundNotifyResource { @Getter(lazy=true) private final Set localIps = configuredIpsAndExternalIp(); @POST + @Operation(tags=API_TAG_NODE, + summary="Receive notification", + description="Receive a notification from another node" + ) public Response receiveNotification(@Context Request req, @Context ContainerRequest ctx, JsonNode jsonNode) { @@ -117,6 +122,10 @@ public class InboundNotifyResource { } @GET @Path(EP_READ+"/{token}") + @Operation(tags=API_TAG_NODE, + summary="Read from storage", + description="Read from storage. Requires a read token." + ) public Response readStorage(@Context Request req, @Context ContainerRequest ctx, @PathParam("token") String token) { diff --git a/bubble-server/src/main/java/bubble/resources/stream/AppAssetsResource.java b/bubble-server/src/main/java/bubble/resources/stream/AppAssetsResource.java index a9a605cf..73e21465 100644 --- a/bubble-server/src/main/java/bubble/resources/stream/AppAssetsResource.java +++ b/bubble-server/src/main/java/bubble/resources/stream/AppAssetsResource.java @@ -7,6 +7,9 @@ package bubble.resources.stream; import bubble.dao.app.AppMessageDAO; import bubble.model.app.AppMessage; import bubble.model.app.BubbleApp; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.string.Base64; import org.cobbzilla.wizard.stream.DataUrlSendableResource; @@ -23,12 +26,14 @@ import javax.ws.rs.core.Response; import java.io.ByteArrayInputStream; import java.io.InputStream; +import static bubble.ApiConstants.API_TAG_APP_RUNTIME; import static org.cobbzilla.util.daemon.ZillaRuntime.CLASSPATH_PREFIX; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.util.http.HttpContentTypes.TEXT_PLAIN; import static org.cobbzilla.util.http.HttpContentTypes.contentType; import static org.cobbzilla.util.http.HttpSchemes.isHttpOrHttps; import static org.cobbzilla.util.http.HttpStatusCodes.OK; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; import static org.cobbzilla.util.io.StreamUtil.loadResourceAsBytesOrDie; import static org.cobbzilla.util.io.StreamUtil.loadResourceAsStream; import static org.cobbzilla.wizard.resources.ResourceUtil.*; @@ -50,6 +55,16 @@ public class AppAssetsResource { @GET @Path("/{assetId}") @Produces(MediaType.WILDCARD) + @Operation(tags=API_TAG_APP_RUNTIME, + summary="app runtime: read app resource", + description="Reads an app resource, for example a PNG image or something. Regarding the `raw` param: If true, bytes will be returned as-is. If false, bytes will be Base64-encoded", + parameters={ + @Parameter(name="assetId", description="The ID of the asset to read", required=true), + @Parameter(name="locale", description="The desired locale", required=true), + @Parameter(name="raw", description="If true, bytes will be returned as-is. If false, bytes will be Base64-encoded"), + }, + responses=@ApiResponse(responseCode=SC_OK, description="asset data, raw bytes or Base64-encoded") + ) public Response findAsset(@Context Request req, @Context ContainerRequest request, @PathParam("assetId") String assetId, diff --git a/bubble-server/src/main/java/bubble/resources/stream/FilterDataResource.java b/bubble-server/src/main/java/bubble/resources/stream/FilterDataResource.java index 21eab327..fa9afc0f 100644 --- a/bubble-server/src/main/java/bubble/resources/stream/FilterDataResource.java +++ b/bubble-server/src/main/java/bubble/resources/stream/FilterDataResource.java @@ -12,6 +12,9 @@ import bubble.model.account.Account; import bubble.model.app.*; import bubble.model.device.Device; import bubble.rule.AppRuleDriver; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.glassfish.grizzly.http.server.Request; @@ -28,6 +31,8 @@ import static bubble.ApiConstants.*; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_NOT_FOUND; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; import static org.cobbzilla.util.json.JsonUtil.COMPACT_MAPPER; import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.wizard.resources.ResourceUtil.*; @@ -62,6 +67,12 @@ public class FilterDataResource { } @GET @Path(EP_READ) + @Operation(tags=API_TAG_APP_RUNTIME, + summary="app runtime: read data", + description="Read app-specific data. If `value` is specified, only return data that matches that value. Otherwise return all data. The `format` param determines what to return. Formats are: `key` (array of key names), `value` (array of values), `key_value` (map of key->value), or `full` (array of AppData objects). The default format is `key`", + parameters=@Parameter(name="format", description="what to return. Formats are: `key` (default, array of key names), `value` (array of values), `key_value` (map of key->value), or `full` (array of AppData objects)"), + responses=@ApiResponse(responseCode=SC_OK, description="type depends on `format`") + ) public Response readData(@Context Request req, @Context ContainerRequest ctx, @QueryParam("format") AppDataFormat format, @@ -81,6 +92,11 @@ public class FilterDataResource { } @POST @Path(EP_WRITE) + @Operation(tags=API_TAG_APP_RUNTIME, + summary="app runtime: write data", + description="Write app-specific data.", + responses=@ApiResponse(responseCode=SC_OK, description="the AppData object that was written") + ) public Response writeData(@Context Request req, @Context ContainerRequest ctx, AppData data) { @@ -89,6 +105,15 @@ public class FilterDataResource { } @GET @Path(EP_WRITE) + @Operation(tags=API_TAG_APP_RUNTIME, + summary="app runtime: write data then redirect", + description="Write app-specific data. If `redirectLocation` param is set, return an HTTP redirect to that URL", + parameters={ + @Parameter(name=Q_DATA, description="the AppData object in JSON format", required=true), + @Parameter(name=Q_REDIRECT, description="the URL to redirect to") + }, + responses=@ApiResponse(responseCode=SC_OK, description="the AppData object that was written, or an HTTP redirect") + ) public Response writeData(@Context Request req, @Context ContainerRequest ctx, @QueryParam(Q_DATA) String dataJson, @@ -132,6 +157,15 @@ public class FilterDataResource { } @GET @Path(EP_READ+"/rule/{id}") + @Operation(tags=API_TAG_APP_RUNTIME, + summary="app runtime: read rule data", + description="Read rule data. ", + parameters=@Parameter(name="id", description="the ID of the data to read", required=true), + responses={ + @ApiResponse(responseCode=SC_OK, description="some object that was read"), + @ApiResponse(responseCode=SC_NOT_FOUND, description="no object found with the given id") + } + ) public Response readRuleData(@Context Request req, @Context ContainerRequest ctx, @PathParam("id") String id) { diff --git a/bubble-server/src/main/java/bubble/resources/stream/FilterHttpResource.java b/bubble-server/src/main/java/bubble/resources/stream/FilterHttpResource.java index f6b29ad1..654302e6 100644 --- a/bubble-server/src/main/java/bubble/resources/stream/FilterHttpResource.java +++ b/bubble-server/src/main/java/bubble/resources/stream/FilterHttpResource.java @@ -33,6 +33,8 @@ import bubble.service.stream.ConnectionCheckResponse; import bubble.service.stream.StandardRuleEngineService; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -70,6 +72,7 @@ import static org.cobbzilla.util.collection.ArrayUtil.arrayToString; import static org.cobbzilla.util.daemon.ZillaRuntime.*; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; import static org.cobbzilla.util.http.HttpContentTypes.TEXT_PLAIN; +import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK; import static org.cobbzilla.util.http.HttpUtil.applyRegexToUrl; import static org.cobbzilla.util.http.HttpUtil.chaseRedirects; import static org.cobbzilla.util.json.JsonUtil.*; @@ -157,6 +160,11 @@ public class FilterHttpResource { @POST @Path(EP_CHECK) @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) + @Operation(tags=API_TAG_MITMPROXY, + summary="mitmproxy: Check a connection", + description="Called by mitmproxy at the start of the TLS handshake, determines how Bubble will handle the connection. Caller must be from localhost.", + responses=@ApiResponse(responseCode=SC_OK, description="a ConnectionCheckResponse value") + ) public Response checkConnection(@Context Request req, @Context ContainerRequest request, FilterConnCheckRequest connCheckRequest) { @@ -274,6 +282,12 @@ public class FilterHttpResource { @POST @Path(EP_MATCHERS+"/{requestId}") @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) + @Operation(tags=API_TAG_MITMPROXY, + summary="mitmproxy: Determine matchers", + description="Called by mitmproxy after the request has been received but before the response. The matchers will determine which rules (from which apps) will apply to the request.", + parameters=@Parameter(name="requestId", description="A unique identifier for this request", required=true), + responses=@ApiResponse(responseCode=SC_OK, description="a FilterMatchersResponse object") + ) public Response selectMatchers(@Context Request req, @Context ContainerRequest request, @PathParam("requestId") String requestId, @@ -446,7 +460,12 @@ public class FilterHttpResource { @DELETE @Produces(APPLICATION_JSON) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags={API_TAG_MITMPROXY, API_TAG_DEVICES}, + summary="Flush caches", + description="Flushes caches of: connection decisions, matchers and rules", + responses=@ApiResponse(responseCode=SC_OK, description="a JSON object showing what was flushed") + ) public Response flushCaches(@Context ContainerRequest request) { final Account caller = userPrincipal(request); if (!caller.admin()) return forbidden(); @@ -465,7 +484,12 @@ public class FilterHttpResource { @DELETE @Path(EP_MATCHERS) @Produces(APPLICATION_JSON) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags={API_TAG_MITMPROXY, API_TAG_DEVICES}, + summary="Flush matchers", + description="Flushes matchers only", + responses=@ApiResponse(responseCode=SC_OK, description="an integer representing how many cache entries were flushed") + ) public Response flushMatchers(@Context ContainerRequest request) { final Account caller = userPrincipal(request); if (!caller.admin()) return forbidden(); @@ -475,6 +499,18 @@ public class FilterHttpResource { @POST @Path(EP_APPLY+"/{requestId}") @Consumes(MediaType.WILDCARD) @Produces(MediaType.WILDCARD) + @Operation(tags=API_TAG_MITMPROXY, + summary="mitmproxy: Filter response", + description="Called by mitmproxy while reading the response. As mitmproxy reads chunks of the response, it sends the bytes here for filtering, then relays the response to the device. The `encoding`, `type`, and `length` params are optional and are only used on the first call. When mitmproxy reaches the end of the response, it sends the `last` param with a value of `true` to indicate that no more data is coming. This allows Bubble to flush any caches and return any response data that might still be waiting.", + parameters={ + @Parameter(name="requestId", description="the unique identifier for the request", required=true), + @Parameter(name="encoding", description="the Content-Encoding of the data"), + @Parameter(name="type", description="the Content-Type of the data"), + @Parameter(name="length", description="the Content-Length of the data"), + @Parameter(name="last", description="true if this is the last chunk of bytes mitmproxy will be sending, false if there are more chunks still to send"), + }, + responses=@ApiResponse(responseCode=SC_OK, description="bytes of the response") + ) public Response filterHttp(@Context Request req, @Context ContainerRequest request, @PathParam("requestId") String requestId, @@ -654,6 +690,12 @@ public class FilterHttpResource { @GET @Path(EP_STATUS+"/{requestId}") @Produces(APPLICATION_JSON) + @Operation(tags=API_TAG_APP_RUNTIME, + summary="app runtime: Get a BlockStatsSummary for the current request", + description="Get a BlockStatsSummary for the current request", + parameters=@Parameter(name="requestId", description="The unique `requestId` for the request", required=true), + responses=@ApiResponse(responseCode=SC_OK, description="a BlockStatsSummary object") + ) public Response getRequestStatus(@Context Request req, @Context ContainerRequest ctx, @PathParam("requestId") String requestId) { @@ -665,6 +707,12 @@ public class FilterHttpResource { @GET @Path(EP_FLEX_ROUTERS+"/{fqdn}") @Produces(APPLICATION_JSON) + @Operation(tags=API_TAG_MITMPROXY, + summary="mitmproxy: Get a flex router", + description="Called by mitmproxty when a flex router is required. May return a FlexRouter object or, if no routers are available, a FlexRouterInfo object whose `errorHtml` property contains instructions on what to do next.", + parameters=@Parameter(name="fqdn", description="The hostname that requires a flex router", required=true), + responses=@ApiResponse(responseCode=SC_OK, description="a FlexRouter object or a FlexRouterInfo object whose `errorHtml` property contains instructions on what to do next.") + ) public Response getFlexRouter(@Context Request req, @Context ContainerRequest ctx, @PathParam("fqdn") String fqdn) { @@ -695,6 +743,12 @@ public class FilterHttpResource { @POST @Path(EP_LOGS+"/{requestId}") @Produces(APPLICATION_JSON) + @Operation(tags=API_TAG_APP_RUNTIME, + summary="app runtime: write to log file", + description="Useful when developing and debugging apps, your app can write to the server logfile using this API call", + parameters=@Parameter(name="requestId", description="The unique `requestId` for the request", required=true), + responses=@ApiResponse(responseCode=SC_OK, description="empty JSON object indicates success") + ) public Response requestLog(@Context Request req, @Context ContainerRequest ctx, @PathParam("requestId") String requestId, @@ -710,6 +764,12 @@ public class FilterHttpResource { @POST @Path(EP_FOLLOW+"/{requestId}") @Consumes(APPLICATION_JSON) @Produces(TEXT_PLAIN) + @Operation(tags=API_TAG_APP_RUNTIME, + summary="app runtime: chase redirects", + description="Apps can request that the Bubble server chase down redirects to find the real link. Bubble can then cache these so we avoid chasing the same link more than once.", + parameters=@Parameter(name="requestId", description="The unique `requestId` for the request", required=true), + responses=@ApiResponse(responseCode=SC_OK, description="a String representing the real URL to use") + ) public Response followLink(@Context Request req, @Context ContainerRequest ctx, @PathParam("requestId") String requestId, @@ -737,6 +797,12 @@ public class FilterHttpResource { @POST @Path(EP_FOLLOW_AND_APPLY_REGEX+"/{requestId}") @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) + @Operation(tags=API_TAG_APP_RUNTIME, + summary="app runtime: chase redirect then apply regex", + description="Some redirects land us on a page whose URL is not what we want (it is ugly in some way), but whose nicer URL is within the page itself. This method can follow redirects and apply a regex and determined by the FollowThenApplyRegex object in the request", + parameters=@Parameter(name="requestId", description="The unique `requestId` for the request", required=true), + responses=@ApiResponse(responseCode=SC_OK, description="a String representing the real URL to use") + ) public Response followLinkThenApplyRegex(@Context Request req, @Context ContainerRequest ctx, @PathParam("requestId") String requestId, diff --git a/bubble-server/src/main/java/bubble/resources/stream/ReverseProxyResource.java b/bubble-server/src/main/java/bubble/resources/stream/ReverseProxyResource.java index 334a01b0..ccc9a888 100644 --- a/bubble-server/src/main/java/bubble/resources/stream/ReverseProxyResource.java +++ b/bubble-server/src/main/java/bubble/resources/stream/ReverseProxyResource.java @@ -13,6 +13,7 @@ import bubble.server.BubbleConfiguration; import bubble.service.device.DeviceService; import bubble.service.stream.StandardRuleEngineService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -38,6 +39,7 @@ import static org.cobbzilla.util.http.HttpContentTypes.CONTENT_TYPE_ANY; import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; import static org.cobbzilla.wizard.resources.ResourceUtil.userPrincipal; +import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.API_TAG_UTILITY; import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY; @Path(PROXY_ENDPOINT) @@ -55,7 +57,12 @@ public class ReverseProxyResource { @GET @Path("/{path: .*}") @Consumes(CONTENT_TYPE_ANY) @Produces(CONTENT_TYPE_ANY) - @Operation(security=@SecurityRequirement(name=SEC_API_KEY)) + @Operation(security=@SecurityRequirement(name=SEC_API_KEY), + tags=API_TAG_UTILITY, + summary="Reverse proxy", + description="Reverse proxy a URL, applying matchers/rules", + parameters=@Parameter(name="path", description="the URL to reverse proxy") + ) public Response get(@Context Request req, @Context ContainerRequest request, @Context ContainerResponse response, diff --git a/bubble-server/src/main/java/bubble/service/boot/NodeManagerService.java b/bubble-server/src/main/java/bubble/service/boot/NodeManagerService.java index e59cceff..5c00c1a7 100644 --- a/bubble-server/src/main/java/bubble/service/boot/NodeManagerService.java +++ b/bubble-server/src/main/java/bubble/service/boot/NodeManagerService.java @@ -23,6 +23,8 @@ import static org.cobbzilla.util.io.FileUtil.toFileOrDie; @Service @Slf4j public class NodeManagerService { + public static final int NODEMANAGER_PASSWORD_MIN_LENGTH = 10; + public static final File NODEMANAGER_PASSWORD_FILE = new File("/home/bubble/.nodemanager_pass"); @Autowired private BubbleConfiguration configuration; diff --git a/bubble-server/src/main/java/bubble/service/boot/StandardSelfNodeService.java b/bubble-server/src/main/java/bubble/service/boot/StandardSelfNodeService.java index 10055b6f..c47434e0 100644 --- a/bubble-server/src/main/java/bubble/service/boot/StandardSelfNodeService.java +++ b/bubble-server/src/main/java/bubble/service/boot/StandardSelfNodeService.java @@ -73,9 +73,12 @@ public class StandardSelfNodeService implements SelfNodeService { public static final File SAGE_KEY_FILE = new File(HOME_DIR, SAGE_KEY_JSON); public static final long MIN_SAGE_KEY_TTL = MINUTES.toMillis(5); - private static final String REDIS_LOG_FLAG_KEY = "bubble_server_logs_enabled"; - private static final int TTL_LOG_FLAG_NODE = (int) DAYS.toSeconds(7); - private static final int TTL_LOG_FLAG_SAGE = (int) DAYS.toSeconds(30); + public static final String REDIS_LOG_FLAG_KEY = "bubble_server_logs_enabled"; + public static final int TTL_LOG_FLAG_NODE = (int) DAYS.toSeconds(7); + + public static final int MAX_LOG_TTL_DAYS = 30; + public static final int MAX_LOG_TTL = (int) DAYS.toSeconds(MAX_LOG_TTL_DAYS); + public static final int TTL_LOG_FLAG_SAGE = MAX_LOG_TTL; @Autowired private BubbleNodeDAO nodeDAO; @Autowired private BubbleNodeKeyDAO nodeKeyDAO; @@ -466,8 +469,8 @@ public class StandardSelfNodeService implements SelfNodeService { @Override public void setLogFlag(final boolean logFlag, @NonNull final Optional ttlInSeconds) { if (logFlag) { - getNodeConfig().set_plaintext(REDIS_LOG_FLAG_KEY, "true", EX, - ttlInSeconds.orElse(isSelfSage() ? TTL_LOG_FLAG_SAGE : TTL_LOG_FLAG_NODE)); + final int ttl = Math.min(ttlInSeconds.orElse(isSelfSage() ? TTL_LOG_FLAG_SAGE : TTL_LOG_FLAG_NODE), MAX_LOG_TTL); + getNodeConfig().set_plaintext(REDIS_LOG_FLAG_KEY, "true", EX, ttl); } else { // just (try to) remove the flag getNodeConfig().del(REDIS_LOG_FLAG_KEY); diff --git a/bubble-server/src/main/java/bubble/service/cloud/NodeProgressMeterTick.java b/bubble-server/src/main/java/bubble/service/cloud/NodeProgressMeterTick.java index 2e49236f..a0623a92 100644 --- a/bubble-server/src/main/java/bubble/service/cloud/NodeProgressMeterTick.java +++ b/bubble-server/src/main/java/bubble/service/cloud/NodeProgressMeterTick.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; +import org.cobbzilla.util.reflect.OpenApiSchema; import java.util.regex.Pattern; @@ -16,7 +17,7 @@ import static bubble.ApiConstants.enumFromString; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.util.daemon.ZillaRuntime.now; -@Accessors(chain=true) +@Accessors(chain=true) @OpenApiSchema public class NodeProgressMeterTick { public enum TickMatchType { diff --git a/bubble-server/src/main/java/bubble/service/device/StandardFlexRouterService.java b/bubble-server/src/main/java/bubble/service/device/StandardFlexRouterService.java index 56185a2b..def8ee29 100644 --- a/bubble-server/src/main/java/bubble/service/device/StandardFlexRouterService.java +++ b/bubble-server/src/main/java/bubble/service/device/StandardFlexRouterService.java @@ -138,7 +138,7 @@ public class StandardFlexRouterService extends SimpleDaemon implements FlexRoute private final Map statusMap = new ConcurrentHashMap<>(DEFAULT_MAX_TUNNELS); private final Map activeRouters = new ConcurrentHashMap<>(DEFAULT_MAX_TUNNELS); - private final int MAX_POLL_FAILURES = 3; + private static final int MAX_POLL_FAILURES = 3; private final Map pollFailures = new ConcurrentHashMap<>(DEFAULT_MAX_TUNNELS); public FlexRouterStatus status(String uuid) { diff --git a/bubble-server/src/main/java/bubble/service/packer/PackerJobSummary.java b/bubble-server/src/main/java/bubble/service/packer/PackerJobSummary.java index 6eca325b..1e02ec9b 100644 --- a/bubble-server/src/main/java/bubble/service/packer/PackerJobSummary.java +++ b/bubble-server/src/main/java/bubble/service/packer/PackerJobSummary.java @@ -9,12 +9,13 @@ import bubble.model.cloud.CloudService; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; +import org.cobbzilla.util.reflect.OpenApiSchema; import static org.cobbzilla.util.daemon.ZillaRuntime.now; import static org.cobbzilla.util.reflect.ReflectionUtil.copy; import static org.cobbzilla.util.time.TimeUtil.formatDuration; -@NoArgsConstructor @Accessors(chain=true) +@NoArgsConstructor @Accessors(chain=true) @OpenApiSchema public class PackerJobSummary { @Getter private CloudService cloud; diff --git a/bubble-server/src/main/resources/bubble-config.yml b/bubble-server/src/main/resources/bubble-config.yml index 5ed60e7a..ccf0759c 100644 --- a/bubble-server/src/main/resources/bubble-config.yml +++ b/bubble-server/src/main/resources/bubble-config.yml @@ -16,9 +16,11 @@ openApi: licenseName: Bubble License licenseUrl: https://getbubblenow.com/bubble-license/ additionalPackages: + - org.cobbzilla.util.dns - org.cobbzilla.wizard.model.search - org.cobbzilla.wizard.model.support - bubble.cloud + - bubble.service defaultLocale: {{#exists BUBBLE_DEFAULT_LOCALE}}{{BUBBLE_DEFAULT_LOCALE}}{{else}}en_US{{/exists}} testMode: {{#exists BUBBLE_TEST_MODE}}{{BUBBLE_TEST_MODE}}{{else}}false{{/exists}} diff --git a/utils/cobbzilla-utils b/utils/cobbzilla-utils index 2c3bfe64..a765cf0a 160000 --- a/utils/cobbzilla-utils +++ b/utils/cobbzilla-utils @@ -1 +1 @@ -Subproject commit 2c3bfe646107b1b0ff9a3c4b001f051e4bd81fa0 +Subproject commit a765cf0ac5e8000c381b5542d3b007600d2eda0a diff --git a/utils/cobbzilla-wizard b/utils/cobbzilla-wizard index f12add50..11e426b7 160000 --- a/utils/cobbzilla-wizard +++ b/utils/cobbzilla-wizard @@ -1 +1 @@ -Subproject commit f12add50c534143a443b09bf275211ee603563b4 +Subproject commit 11e426b77af3ad0f7227c5d50135d932c4295422