Przeglądaj źródła

WIP. nearing full coverage for openapi docs

tags/v1.4.37
Jonathan Cobb 4 lat temu
rodzic
commit
e28d96dcbd
56 zmienionych plików z 857 dodań i 166 usunięć
  1. +3
    -0
      bubble-server/src/main/java/bubble/ApiConstants.java
  2. +2
    -1
      bubble-server/src/main/java/bubble/cloud/CloudRegion.java
  3. +2
    -1
      bubble-server/src/main/java/bubble/cloud/CloudRegionRelative.java
  4. +1
    -1
      bubble-server/src/main/java/bubble/cloud/email/RenderedEmail.java
  5. +1
    -1
      bubble-server/src/main/java/bubble/cloud/geoCode/GeoCodeResult.java
  6. +1
    -1
      bubble-server/src/main/java/bubble/cloud/geoLocation/GeoLocation.java
  7. +1
    -1
      bubble-server/src/main/java/bubble/cloud/geoTime/GeoTimeZone.java
  8. +1
    -1
      bubble-server/src/main/java/bubble/model/AppLinks.java
  9. +1
    -1
      bubble-server/src/main/java/bubble/model/account/TrustedClientResponse.java
  10. +1
    -1
      bubble-server/src/main/java/bubble/model/app/config/AppConfigView.java
  11. +2
    -1
      bubble-server/src/main/java/bubble/notify/NewNodeNotification.java
  12. +2
    -1
      bubble-server/src/main/java/bubble/notify/storage/StorageListing.java
  13. +2
    -2
      bubble-server/src/main/java/bubble/resources/IdentityResource.java
  14. +2
    -0
      bubble-server/src/main/java/bubble/resources/TagsResource.java
  15. +2
    -2
      bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java
  16. +25
    -25
      bubble-server/src/main/java/bubble/resources/account/AccountsResource.java
  17. +1
    -1
      bubble-server/src/main/java/bubble/resources/account/AuthResource.java
  18. +6
    -1
      bubble-server/src/main/java/bubble/resources/account/MeResource.java
  19. +4
    -4
      bubble-server/src/main/java/bubble/resources/app/AppConfigResource.java
  20. +4
    -4
      bubble-server/src/main/java/bubble/resources/app/AppSitesResource.java
  21. +9
    -5
      bubble-server/src/main/java/bubble/resources/app/AppsResource.java
  22. +6
    -6
      bubble-server/src/main/java/bubble/resources/app/AppsResourceBase.java
  23. +25
    -0
      bubble-server/src/main/java/bubble/resources/app/DataResourceBase.java
  24. +13
    -0
      bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java
  25. +1
    -1
      bubble-server/src/main/java/bubble/resources/bill/AllPaymentMethodsResource.java
  26. +17
    -2
      bubble-server/src/main/java/bubble/resources/bill/BillsResource.java
  27. +14
    -11
      bubble-server/src/main/java/bubble/resources/bill/PromotionsResource.java
  28. +51
    -7
      bubble-server/src/main/java/bubble/resources/cloud/BackupsResource.java
  29. +1
    -1
      bubble-server/src/main/java/bubble/resources/cloud/CloudServiceDataResource.java
  30. +26
    -5
      bubble-server/src/main/java/bubble/resources/cloud/CloudServiceRegionsResource.java
  31. +22
    -1
      bubble-server/src/main/java/bubble/resources/cloud/ComputePackerResource.java
  32. +44
    -10
      bubble-server/src/main/java/bubble/resources/cloud/LogsResource.java
  33. +56
    -8
      bubble-server/src/main/java/bubble/resources/cloud/NetworkActionsResource.java
  34. +42
    -0
      bubble-server/src/main/java/bubble/resources/cloud/NetworkBackupKeysResource.java
  35. +44
    -8
      bubble-server/src/main/java/bubble/resources/cloud/NetworkDnsResource.java
  36. +15
    -1
      bubble-server/src/main/java/bubble/resources/cloud/NetworksResource.java
  37. +81
    -16
      bubble-server/src/main/java/bubble/resources/cloud/NodeManagerResource.java
  38. +30
    -3
      bubble-server/src/main/java/bubble/resources/cloud/PackerResource.java
  39. +0
    -4
      bubble-server/src/main/java/bubble/resources/cloud/PublicCloudServicesResource.java
  40. +84
    -9
      bubble-server/src/main/java/bubble/resources/cloud/StorageResource.java
  41. +27
    -2
      bubble-server/src/main/java/bubble/resources/device/DeviceTypesResource.java
  42. +11
    -1
      bubble-server/src/main/java/bubble/resources/device/FlexRoutersResource.java
  43. +21
    -2
      bubble-server/src/main/java/bubble/resources/message/MessagesResource.java
  44. +9
    -0
      bubble-server/src/main/java/bubble/resources/notify/InboundNotifyResource.java
  45. +15
    -0
      bubble-server/src/main/java/bubble/resources/stream/AppAssetsResource.java
  46. +34
    -0
      bubble-server/src/main/java/bubble/resources/stream/FilterDataResource.java
  47. +68
    -2
      bubble-server/src/main/java/bubble/resources/stream/FilterHttpResource.java
  48. +8
    -1
      bubble-server/src/main/java/bubble/resources/stream/ReverseProxyResource.java
  49. +2
    -0
      bubble-server/src/main/java/bubble/service/boot/NodeManagerService.java
  50. +8
    -5
      bubble-server/src/main/java/bubble/service/boot/StandardSelfNodeService.java
  51. +2
    -1
      bubble-server/src/main/java/bubble/service/cloud/NodeProgressMeterTick.java
  52. +1
    -1
      bubble-server/src/main/java/bubble/service/device/StandardFlexRouterService.java
  53. +2
    -1
      bubble-server/src/main/java/bubble/service/packer/PackerJobSummary.java
  54. +2
    -0
      bubble-server/src/main/resources/bubble-config.yml
  55. +1
    -1
      utils/cobbzilla-utils
  56. +1
    -1
      utils/cobbzilla-wizard

+ 3
- 0
bubble-server/src/main/java/bubble/ApiConstants.java Wyświetl plik

@@ -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";


+ 2
- 1
bubble-server/src/main/java/bubble/cloud/CloudRegion.java Wyświetl plik

@@ -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 {


+ 2
- 1
bubble-server/src/main/java/bubble/cloud/CloudRegionRelative.java Wyświetl plik

@@ -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); }


+ 1
- 1
bubble-server/src/main/java/bubble/cloud/email/RenderedEmail.java Wyświetl plik

@@ -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;


+ 1
- 1
bubble-server/src/main/java/bubble/cloud/geoCode/GeoCodeResult.java Wyświetl plik

@@ -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;



+ 1
- 1
bubble-server/src/main/java/bubble/cloud/geoLocation/GeoLocation.java Wyświetl plik

@@ -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;



+ 1
- 1
bubble-server/src/main/java/bubble/cloud/geoTime/GeoTimeZone.java Wyświetl plik

@@ -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 {


+ 1
- 1
bubble-server/src/main/java/bubble/model/AppLinks.java Wyświetl plik

@@ -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;


+ 1
- 1
bubble-server/src/main/java/bubble/model/account/TrustedClientResponse.java Wyświetl plik

@@ -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 {


+ 1
- 1
bubble-server/src/main/java/bubble/model/app/config/AppConfigView.java Wyświetl plik

@@ -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;



+ 2
- 1
bubble-server/src/main/java/bubble/notify/NewNodeNotification.java Wyświetl plik

@@ -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();


+ 2
- 1
bubble-server/src/main/java/bubble/notify/storage/StorageListing.java Wyświetl plik

@@ -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;


+ 2
- 2
bubble-server/src/main/java/bubble/resources/IdentityResource.java Wyświetl plik

@@ -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\"}}"),


+ 2
- 0
bubble-server/src/main/java/bubble/resources/TagsResource.java Wyświetl plik

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


+ 2
- 2
bubble-server/src/main/java/bubble/resources/account/AccountOwnedResource.java Wyświetl plik

@@ -211,7 +211,7 @@ public class AccountOwnedResource<E extends HasAccount, DAO extends AccountOwned
tags=API_TAG_ACCOUNT_OBJECTS,
summary="Update an existing object",
description="Update an new object. For many types, the object will be created if it does not exist. If validation errors occur, status "+SC_INVALID+" is returned and the response will contain an array of errors. Within each error, the `messageTemplate` field refers to messages that can be localized using the /messages resource",
parameters=@Parameter(name="id", description="the UUID (or name, if allowed) of the object to update"),
parameters=@Parameter(name="id", description="the UUID (or name, if allowed) of the object to update", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="the object that was updated"),
@ApiResponse(responseCode=SC_NOT_FOUND, description="no object exists with the given id"),
@@ -246,7 +246,7 @@ public class AccountOwnedResource<E extends HasAccount, DAO extends AccountOwned
tags=API_TAG_ACCOUNT_OBJECTS,
summary="Delete an existing object",
description="Delete an existing object",
parameters=@Parameter(name="id", description="the UUID (or name, if allowed) of the object to delete"),
parameters=@Parameter(name="id", description="the UUID (or name, if allowed) of the object to delete", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="the object that was deleted"),
@ApiResponse(responseCode=SC_NOT_FOUND, description="no object exists with the given id")


+ 25
- 25
bubble-server/src/main/java/bubble/resources/account/AccountsResource.java Wyświetl plik

@@ -176,7 +176,7 @@ public class AccountsResource {
tags=API_TAG_ACCOUNT,
summary="Download all data for an account",
description="Download all data for user. Must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="a Map<String, List<String>> of all user data"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="forbidden if not admin")
@@ -199,7 +199,7 @@ public class AccountsResource {
tags=API_TAG_ACCOUNT,
summary="Update an account",
description="Update an account. Caller must be the same account, or must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="the Account object that was updated"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="forbidden if caller is not admin and is updating any account Account other than themselves"),
@@ -233,7 +233,7 @@ public class AccountsResource {
tags=API_TAG_ACCOUNT,
summary="List all launch statuses for an account",
description="List all launch statuses for an account. Caller must be the same account, or must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="a List<NodeProgressMeterTick> representing the status of active launch operations"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="forbidden if caller is not admin and is accessing any account Account other than themselves")
@@ -259,7 +259,7 @@ public class AccountsResource {
tags=API_TAG_ACCOUNT,
summary="View the AccountPolicy for an account",
description="View the AccountPolicy for an account. Caller must be the same account, or must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="an AccountPolicy object"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="forbidden if caller is not admin and is accessing any account Account other than themselves")
@@ -278,7 +278,7 @@ public class AccountsResource {
tags=API_TAG_ACCOUNT,
summary="Update the AccountPolicy for an account",
description="Update the AccountPolicy for an account. Caller must be the same account, or must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="an AccountPolicy object"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="forbidden if caller is not admin and is accessing any account Account other than themselves"),
@@ -308,7 +308,7 @@ public class AccountsResource {
tags=API_TAG_ACCOUNT,
summary="Create or update an AccountContact in the AccountPolicy",
description="Create or update an AccountContact in the AccountPolicy. Caller must be the same account, or must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="the AccountContact object that was created or updated"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="forbidden if caller is not admin and is accessing any account Account other than themselves"),
@@ -349,7 +349,7 @@ public class AccountsResource {
tags=API_TAG_ACCOUNT,
summary="Send verification message for an AccountContact",
description="Send verification message for an AccountContact. Caller must be the same account, or must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="the AccountContact object that was created or updated"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="forbidden if caller is not admin and is accessing any account Account other than themselves")
@@ -377,9 +377,9 @@ public class AccountsResource {
summary="Find an AccountContact within an AccountPolicy",
description="Find an AccountContact within an AccountPolicy. Caller must be the same account, or must be admin.",
parameters={
@Parameter(name="id", description="UUID or email of the Account"),
@Parameter(name="type", description="the type of contact"),
@Parameter(name="info", description="the contact information, for example an email address or phone number")
@Parameter(name="id", description="UUID or email of the Account", required=true),
@Parameter(name="type", description="the type of contact", required=true),
@Parameter(name="info", description="the contact information, for example an email address or phone number", required=true)
},
responses={
@ApiResponse(responseCode=SC_OK, description="the AccountContact object"),
@@ -404,9 +404,9 @@ public class AccountsResource {
summary="Delete an AccountContact within an AccountPolicy",
description="Delete an AccountContact within an AccountPolicy. Caller must be the same account, or must be admin.",
parameters={
@Parameter(name="id", description="UUID or email of the Account"),
@Parameter(name="type", description="the type of contact"),
@Parameter(name="info", description="the contact information, for example an email address or phone number")
@Parameter(name="id", description="UUID or email of the Account", required=true),
@Parameter(name="type", description="the type of contact", required=true),
@Parameter(name="info", description="the contact information, for example an email address or phone number", required=true)
},
responses={
@ApiResponse(responseCode=SC_OK, description="the AccountContact object that was deleted"),
@@ -431,7 +431,7 @@ public class AccountsResource {
tags=API_TAG_ACCOUNT,
summary="Delete TOTP authenticator AccountContact from an AccountPolicy",
description="Delete TOTP authenticator AccountContact from an AccountPolicy. Caller must be the same account, or must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="the AccountPolicy object that was updated"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="forbidden if caller is not admin and is accessing any account Account other than themselves")
@@ -459,8 +459,8 @@ public class AccountsResource {
summary="Delete AccountContact from an AccountPolicy by UUID",
description="Delete AccountContact from an AccountPolicy by UUID. Caller must be the same account, or must be admin.",
parameters={
@Parameter(name="id", description="UUID or email of the Account"),
@Parameter(name="uuid", description="UUID of the AccountContact")
@Parameter(name="id", description="UUID or email of the Account", required=true),
@Parameter(name="uuid", description="UUID of the AccountContact", required=true)
},
responses={
@ApiResponse(responseCode=SC_OK, description="the AccountPolicy object that was updated"),
@@ -484,7 +484,7 @@ public class AccountsResource {
tags=API_TAG_ACCOUNT,
summary="Request deletion of an Account",
description="Request deletion of an Account. Caller must be the same account, or must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="the AccountMessage object that was sent"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="forbidden if caller is not admin and is accessing any account Account other than themselves")
@@ -514,7 +514,7 @@ public class AccountsResource {
tags=API_TAG_ACCOUNT,
summary="Change password for an account",
description="Change password for an account. Caller must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="the Account object that was updated"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="forbidden if caller is not admin and is accessing any account Account other than themselves"),
@@ -593,7 +593,7 @@ public class AccountsResource {
tags=API_TAG_ACCOUNT,
summary="Delete an Account",
description="Delete an Account. Caller must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="the Account object that was deleted"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="forbidden if caller is not admin and is accessing any account Account other than themselves")
@@ -632,7 +632,7 @@ public class AccountsResource {
tags=API_TAG_UTILITY,
summary="Get status of mitmproxy",
description="Get status of mitmproxy. Caller must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="returns true if mitmproxy is enabled, false otherwise",
content=@Content(mediaType=APPLICATION_JSON, examples={
@@ -655,7 +655,7 @@ public class AccountsResource {
tags=API_TAG_UTILITY,
summary="Enable mitmproxy",
description="Enable mitmproxy. Caller must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="returns true if mitmproxy is enabled, false otherwise",
content=@Content(mediaType=APPLICATION_JSON, examples={
@@ -678,7 +678,7 @@ public class AccountsResource {
tags=API_TAG_UTILITY,
summary="Disable mitmproxy",
description="Disable mitmproxy. Caller must be admin.",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="returns true if mitmproxy is enabled, false otherwise",
content=@Content(mediaType=APPLICATION_JSON, examples={
@@ -755,9 +755,9 @@ public class AccountsResource {
@GET @Path("/{id}"+EP_DEVICE_TYPES)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_UTILITY,
summary="List all device types",
description="List all device types",
parameters=@Parameter(name="id", description="UUID or email of the Account"),
summary="List selectable device types",
description="List selectable device types. This excludes pseudo-devices like Firefox and web-clients.",
parameters=@Parameter(name="id", description="UUID or email of the Account", required=true),
responses=@ApiResponse(responseCode=SC_OK, description="returns an array of Strings, each a BubbleDeviceType enum value")
)
public Response getDeviceTypes(@Context ContainerRequest ctx) {


+ 1
- 1
bubble-server/src/main/java/bubble/resources/account/AuthResource.java Wyświetl plik

@@ -973,7 +973,7 @@ public class AuthResource {
tags=API_TAG_AUTH,
summary="Logout a user everywhere",
description="Logout of the current session, or logout of all sessions everywhere if the `all` parameter is true.",
parameters=@Parameter(name="id", description="UUID or email of user to logout"),
parameters=@Parameter(name="id", description="UUID or email of user to logout", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="HTTP status 200 indicates success"),
@ApiResponse(responseCode=SC_INVALID, description="If there is no current session to log out of")


+ 6
- 1
bubble-server/src/main/java/bubble/resources/account/MeResource.java Wyświetl plik

@@ -576,7 +576,12 @@ public class MeResource {
}

@POST @Path(EP_UPGRADE)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_UTILITY,
summary="Upgrade Bubble",
description="Upgrade Bubble. Must be admin. Starts the upgrade process.",
responses=@ApiResponse(responseCode=SC_OK, description="the current public system configs")
)
public Response upgrade(@Context Request req,
@Context ContainerRequest ctx) {
final Account caller = userPrincipal(ctx);


+ 4
- 4
bubble-server/src/main/java/bubble/resources/app/AppConfigResource.java Wyświetl plik

@@ -68,7 +68,7 @@ public class AppConfigResource {
tags=API_TAG_APPS,
summary="Get a specific config view for Bubble app",
description="Get a specific config view for Bubble app. Returns a JSON object corresponding to the view",
parameters=@Parameter(name="view", description="config view name"),
parameters=@Parameter(name="view", description="config view name", required=true),
responses=@ApiResponse(responseCode=SC_OK, description="a JSON object")
)
public Response getConfigView(@Context Request req,
@@ -124,9 +124,9 @@ public class AppConfigResource {
summary="Take a config item action",
description="Take a config item action",
parameters={
@Parameter(name="view", description="config view name"),
@Parameter(name="action", description="action name"),
@Parameter(name="id", description="item id")
@Parameter(name="view", description="config view name", required=true),
@Parameter(name="action", description="action name", required=true),
@Parameter(name="id", description="item id", required=true)
},
responses=@ApiResponse(responseCode=SC_OK, description="a JSON object")
)


+ 4
- 4
bubble-server/src/main/java/bubble/resources/app/AppSitesResource.java Wyświetl plik

@@ -63,7 +63,7 @@ public class AppSitesResource extends AccountOwnedTemplateResource<AppSite, AppS
tags=API_TAG_APPS,
summary="Enable site",
description="Enable site",
parameters=@Parameter(name="id", description="UUID or name of site"),
parameters=@Parameter(name="id", description="UUID or name of site", required=true),
responses=@ApiResponse(responseCode=SC_OK, description="Site successfully enabled")
)
public Response enable(@Context ContainerRequest ctx,
@@ -79,7 +79,7 @@ public class AppSitesResource extends AccountOwnedTemplateResource<AppSite, AppS
tags=API_TAG_APPS,
summary="Disable site",
description="Disable site",
parameters=@Parameter(name="id", description="UUID or name of site"),
parameters=@Parameter(name="id", description="UUID or name of site", required=true),
responses=@ApiResponse(responseCode=SC_OK, description="Site successfully disabled")
)
public Response disable(@Context ContainerRequest ctx,
@@ -112,8 +112,8 @@ public class AppSitesResource extends AccountOwnedTemplateResource<AppSite, AppS
summary="Search data view for site data",
description="Search data view for site data. This uses the AppDataDriver.",
parameters={
@Parameter(name="id", description="UUID or name of site"),
@Parameter(name="view", description="name of AppDataView to use")
@Parameter(name="id", description="UUID or name of site", required=true),
@Parameter(name="view", description="name of AppDataView to use", required=true)
},
responses=@ApiResponse(responseCode=SC_OK, description="SearchResults object with results")
)


+ 9
- 5
bubble-server/src/main/java/bubble/resources/app/AppsResource.java Wyświetl plik

@@ -32,6 +32,8 @@ import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KE
@Slf4j
public class AppsResource extends AppsResourceBase {

public static final int DEFAULT_PAGE_SIZE = 10;

public AppsResource(Account account) { super(account); }

@Autowired private DeviceService deviceService;
@@ -42,8 +44,10 @@ public class AppsResource extends AppsResourceBase {
summary="Search data view for app data",
description="Search data view for app data",
parameters={
@Parameter(name="id", description="UUID or name of the app"),
@Parameter(name="view", description="name of AppDataView to use")
@Parameter(name="id", description="UUID or name of the app", required=true),
@Parameter(name="view", description="name of AppDataView to use", required=true),
@Parameter(name="n", description="page number of results to return, default is first page"),
@Parameter(name="sz", description="number of results per page, default is "+DEFAULT_PAGE_SIZE)
},
responses={
@ApiResponse(responseCode=SC_OK, description="SearchResults object with results")
@@ -55,7 +59,7 @@ public class AppsResource extends AppsResourceBase {
@PathParam("view") String viewName,
@QueryParam("n") Integer pageNumber,
@QueryParam("sz") Integer pageSize) {
final SearchQuery query = new SearchQuery(pageNumber == null ? 1 : pageNumber, pageSize == null ? 10 : pageSize);
final SearchQuery query = new SearchQuery(pageNumber == null ? 1 : pageNumber, pageSize == null ? DEFAULT_PAGE_SIZE : pageSize);
return search(req, ctx, id, viewName, query);
}

@@ -65,8 +69,8 @@ public class AppsResource extends AppsResourceBase {
summary="Search data view for app data",
description="Search data view for app data. This uses the AppDataDriver.",
parameters={
@Parameter(name="id", description="UUID or name of the app"),
@Parameter(name="view", description="name of AppDataView to use")
@Parameter(name="id", description="UUID or name of the app", required=true),
@Parameter(name="view", description="name of AppDataView to use", required=true)
},
responses={
@ApiResponse(responseCode=SC_OK, description="SearchResults object with results")


+ 6
- 6
bubble-server/src/main/java/bubble/resources/app/AppsResourceBase.java Wyświetl plik

@@ -72,10 +72,10 @@ public abstract class AppsResourceBase extends AccountOwnedTemplateResource<Bubb

@DELETE @Path("/{id}")
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_AUTH,
tags=API_TAG_APPS,
summary="Delete Bubble app",
description="Delete Bubble app",
parameters=@Parameter(name="id", description="UUID or name of the app"),
parameters=@Parameter(name="id", description="UUID or name of the app", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="HTTP status 200 indicates success",
content=@Content(mediaType=APPLICATION_JSON, examples=@ExampleObject(name="returns an empty JSON object", value="{}"))),
@@ -97,10 +97,10 @@ public abstract class AppsResourceBase extends AccountOwnedTemplateResource<Bubb

@POST @Path("/{id}"+EP_ENABLE)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_AUTH,
tags=API_TAG_APPS,
summary="Enable Bubble app",
description="Enable Bubble app",
parameters=@Parameter(name="id", description="UUID or name of the app"),
parameters=@Parameter(name="id", description="UUID or name of the app", required=true),
responses=@ApiResponse(responseCode=SC_OK, description="App successfully enabled")
)
public Response enable(@Context ContainerRequest ctx,
@@ -113,10 +113,10 @@ public abstract class AppsResourceBase extends AccountOwnedTemplateResource<Bubb

@POST @Path("/{id}"+EP_DISABLE)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_AUTH,
tags=API_TAG_APPS,
summary="Disable Bubble app",
description="Disable Bubble app",
parameters=@Parameter(name="id", description="UUID or name of the app"),
parameters=@Parameter(name="id", description="UUID or name of the app", required=true),
responses=@ApiResponse(responseCode=SC_OK, description="App successfully disabled")
)
public Response disable(@Context ContainerRequest ctx,


+ 25
- 0
bubble-server/src/main/java/bubble/resources/app/DataResourceBase.java Wyświetl plik

@@ -15,6 +15,9 @@ import bubble.model.app.BubbleApp;
import bubble.model.device.Device;
import bubble.resources.account.AccountOwnedTemplateResource;
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 lombok.extern.slf4j.Slf4j;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.jersey.server.ContainerRequest;
@@ -28,6 +31,7 @@ import javax.ws.rs.core.Response;
import java.util.List;

import static bubble.ApiConstants.*;
import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK;
import static org.cobbzilla.util.reflect.ReflectionUtil.copy;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;

@@ -65,6 +69,12 @@ public abstract class DataResourceBase extends AccountOwnedTemplateResource<AppD
}

@POST @Path("/{id}"+EP_ENABLE)
@Operation(tags=API_TAG_APPS,
summary="enable AppData",
description="Mark AppData as enabled",
parameters=@Parameter(name="id", description="The key of the data to enable", required=true),
responses=@ApiResponse(responseCode=SC_OK, description="the updated AppData")
)
public Response enable(@Context ContainerRequest ctx,
@PathParam("id") String key) {
if (isReadOnly(ctx)) return forbidden();
@@ -76,6 +86,12 @@ public abstract class DataResourceBase extends AccountOwnedTemplateResource<AppD
}

@POST @Path("/{id}"+EP_DISABLE)
@Operation(tags=API_TAG_APPS,
summary="disable AppData",
description="Mark AppData as disabled",
parameters=@Parameter(name="id", description="The key of the data to disable", required=true),
responses=@ApiResponse(responseCode=SC_OK, description="the updated AppData")
)
public Response disable(@Context ContainerRequest ctx,
@PathParam("id") String key) {
if (isReadOnly(ctx)) return forbidden();
@@ -87,6 +103,15 @@ public abstract class DataResourceBase extends AccountOwnedTemplateResource<AppD
}

@POST @Path("/{id}"+EP_ACTIONS+"/{action}")
@Operation(tags=API_TAG_APPS,
summary="invoke AppData action",
description="Invoke AppData action. Standard actions are: `enable`, `disable, `delete`",
parameters={
@Parameter(name="id", description="The key of the data to disable", required=true),
@Parameter(name="action", description="The action to take. Standard actions are: `enable`, `disable, `delete`", required=true),
},
responses=@ApiResponse(responseCode=SC_OK, description="the updated AppData")
)
public Response takeAction(@Context Request req,
@Context ContainerRequest ctx,
@PathParam("id") String id,


+ 13
- 0
bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java Wyświetl plik

@@ -25,6 +25,10 @@ import bubble.resources.account.AccountOwnedResource;
import bubble.server.BubbleConfiguration;
import bubble.service.account.StandardAuthenticatorService;
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.cobbzilla.wizard.validation.ValidationResult;
import org.glassfish.grizzly.http.server.Request;
@@ -44,9 +48,11 @@ import static bubble.ApiConstants.*;
import static bubble.model.account.Account.ROOT_EMAIL;
import static bubble.model.cloud.BubbleNetwork.validateHostname;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK;
import static org.cobbzilla.util.string.ValidationRegexes.*;
import static org.cobbzilla.wizard.model.NamedEntity.NAME_MAXLEN;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;
import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY;

@Slf4j
public class AccountPlansResource extends AccountOwnedResource<AccountPlan, AccountPlanDAO> {
@@ -318,6 +324,13 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco

// If the accountPlan is not found, look for an orphaned network
@DELETE @Path("/{id}")
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_ACCOUNT,
summary="Delete AccountPlan",
description="Delete AccountPlan. If a Bubble is running on the plan, it will be stopped.",
parameters=@Parameter(name="id", description="uuid of the AccountPlan to delete"),
responses=@ApiResponse(responseCode=SC_OK, description="the deleted AccountPlan")
)
@Override public Response delete(@Context Request req,
@Context ContainerRequest ctx,
@PathParam("id") String id) {


+ 1
- 1
bubble-server/src/main/java/bubble/resources/bill/AllPaymentMethodsResource.java Wyświetl plik

@@ -80,7 +80,7 @@ public class AllPaymentMethodsResource {
tags=API_TAG_PAYMENT,
summary="Find a payment method",
description="Find a payment method",
parameters=@Parameter(name="id", description="UUID or name of CloudService, or name of driver class"),
parameters=@Parameter(name="id", description="UUID or name of CloudService, or name of driver class", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="a PaymentService object"),
@ApiResponse(responseCode=SC_NOT_FOUND, description="no payment method found with the given id")


+ 17
- 2
bubble-server/src/main/java/bubble/resources/bill/BillsResource.java Wyświetl plik

@@ -11,6 +11,10 @@ import bubble.model.account.Account;
import bubble.model.bill.*;
import bubble.model.cloud.CloudService;
import bubble.resources.account.ReadOnlyAccountOwnedResource;
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.cobbzilla.util.collection.ExpirationEvictionPolicy;
import org.cobbzilla.util.collection.ExpirationMap;
@@ -27,11 +31,12 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static bubble.ApiConstants.EP_PAY;
import static bubble.ApiConstants.EP_PAYMENTS;
import static bubble.ApiConstants.*;
import static org.cobbzilla.util.daemon.ZillaRuntime.now;
import static org.cobbzilla.util.http.HttpStatusCodes.*;
import static org.cobbzilla.util.http.URIUtil.queryParams;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;
import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY;

@Slf4j
public class BillsResource extends ReadOnlyAccountOwnedResource<Bill, BillDAO> {
@@ -100,6 +105,16 @@ public class BillsResource extends ReadOnlyAccountOwnedResource<Bill, BillDAO> {
}

@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) {


+ 14
- 11
bubble-server/src/main/java/bubble/resources/bill/PromotionsResource.java Wyświetl plik

@@ -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,


+ 51
- 7
bubble-server/src/main/java/bubble/resources/cloud/BackupsResource.java Wyświetl plik

@@ -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)) {


+ 1
- 1
bubble-server/src/main/java/bubble/resources/cloud/CloudServiceDataResource.java Wyświetl plik

@@ -15,7 +15,7 @@ import java.util.List;

public class CloudServiceDataResource extends AccountOwnedResource<CloudServiceData, CloudServiceDataDAO> {

private CloudService cloud;
private final CloudService cloud;

public CloudServiceDataResource(Account account, CloudService cloud) {
super(account);


+ 26
- 5
bubble-server/src/main/java/bubble/resources/cloud/CloudServiceRegionsResource.java Wyświetl plik

@@ -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) {


+ 22
- 1
bubble-server/src/main/java/bubble/resources/cloud/ComputePackerResource.java Wyświetl plik

@@ -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) {


+ 44
- 10
bubble-server/src/main/java/bubble/resources/cloud/LogsResource.java Wyświetl plik

@@ -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<String, Object>(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<Byte> 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();
}
}

+ 56
- 8
bubble-server/src/main/java/bubble/resources/cloud/NetworkActionsResource.java Wyświetl plik

@@ -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) {


+ 42
- 0
bubble-server/src/main/java/bubble/resources/cloud/NetworkBackupKeysResource.java Wyświetl plik

@@ -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);


+ 44
- 8
bubble-server/src/main/java/bubble/resources/cloud/NetworkDnsResource.java Wyświetl plik

@@ -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);


+ 15
- 1
bubble-server/src/main/java/bubble/resources/cloud/NetworksResource.java Wyświetl plik

@@ -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<BubbleNetwork, Bubble
}

@GET @Path("/{id}"+EP_CLOSEST)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_CLOUDS,
summary="Find nearest regions for a given cloud service type",
description="Find nearest regions. If lat/lon are specified, we find the nearest regions to that lat/lon, otherwise attempt to discern caller's location using geo-location.",
parameters={
@Parameter(name="id", description="UUID or name of the network"),
@Parameter(name="type", description="the CloudServiceType to search for. usually `compute`"),
@Parameter(name="lat", description="latitude. if specified, `lon` must also be provided"),
@Parameter(name="lon", description="longitude. if specified, `lat` must also be provided")
},
responses=@ApiResponse(responseCode=SC_OK, description="the updated DnsRecord object")
)
public Response findClosest(@Context Request req,
@Context ContainerRequest ctx,
@PathParam("id") String id,


+ 81
- 16
bubble-server/src/main/java/bubble/resources/cloud/NodeManagerResource.java Wyświetl plik

@@ -12,6 +12,8 @@ import bubble.service.boot.NodeManagerService;
import bubble.service.boot.SelfNodeService;
import bubble.service.notify.NotificationService;
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.cobbzilla.util.http.*;
@@ -36,10 +38,14 @@ import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import static bubble.ApiConstants.API_TAG_NODE_MANAGER;
import static bubble.ApiConstants.EP_DISABLE;
import static bubble.model.cloud.notify.NotificationType.hello_to_sage;
import static bubble.service.boot.NodeManagerService.NODEMANAGER_PASSWORD_MIN_LENGTH;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.MULTIPART_FORM_DATA;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.http.HttpStatusCodes.*;
import static org.cobbzilla.util.io.FileUtil.*;
import static org.cobbzilla.util.system.CommandShell.execScript;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;
@@ -70,7 +76,17 @@ public class NodeManagerResource {
@Autowired private NotificationService notificationService;

@POST @Path("/set_password")
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_NODE_MANAGER,
summary="Set nodemanager password",
description="Set nodemanager password. Must be admin. Can only be performed on the current node.",
parameters=@Parameter(name="notify", description="If true, notify the sage of the name change. Default is true"),
responses={
@ApiResponse(responseCode=SC_OK, description="an empty JSON object indicates success"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller is not admin"),
@ApiResponse(responseCode=SC_INVALID, description="validation failure. password may have been empty or too short (min "+NODEMANAGER_PASSWORD_MIN_LENGTH+" chars), or node specified is not the current node")
}
)
public Response setPassword (@Context ContainerRequest ctx,
LoginRequest request,
@QueryParam("notify") Boolean notify) {
@@ -79,7 +95,7 @@ public class NodeManagerResource {

final String password = request.getPassword();
if (empty(password)) return invalid("err.password.required");
if (password.length() < 10) return invalid("err.password.tooShort");
if (password.length() < NODEMANAGER_PASSWORD_MIN_LENGTH) return invalid("err.password.tooShort");

if (!node.getUuid().equals(selfNodeService.getThisNode().getUuid())) {
return invalid("err.nodemanager.nodeNotLocal");
@@ -107,8 +123,17 @@ public class NodeManagerResource {
return ok_empty();
}

@POST @Path("/disable")
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@POST @Path(EP_DISABLE)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_NODE_MANAGER,
summary="Disable nodemanager",
description="Disable nodemanager. Must be admin. Can only be performed on the current node.",
responses={
@ApiResponse(responseCode=SC_OK, description="an empty JSON object indicates success"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller is not admin"),
@ApiResponse(responseCode=SC_INVALID, description="node specified is not the current node")
}
)
public Response disable (@Context ContainerRequest ctx) {
final Account caller = userPrincipal(ctx);
if (!caller.admin()) return forbidden();
@@ -161,7 +186,17 @@ public class NodeManagerResource {
}

@GET @Path("/stats/{stat}")
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_NODE_MANAGER,
summary="Get stat from nodemanager",
description="Get stat from nodemanager. Must be admin or owner of node. Valid stats are: `uptime`, `mem`, `net`, `disk`",
parameters=@Parameter(name="stat", description="name of the stat. can be `uptime`, `mem`, `net` or `disk`"),
responses={
@ApiResponse(responseCode=SC_OK, description="a string representing the stat"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller is not admin or owner of node"),
@ApiResponse(responseCode=SC_INVALID, description="error occurred calling nodemanager")
}
)
public Response getStats (@Context ContainerRequest ctx,
@PathParam("stat") String stat) {
final HttpRequestBean request = validateNodeManagerRequest(ctx, "stats/"+stat);
@@ -169,7 +204,17 @@ public class NodeManagerResource {
}

@POST @Path("/cmd/{command}")
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_NODE_MANAGER,
summary="Send command to nodemanager",
description="Send command to nodemanager. Must be admin or owner of node. The only valid command is `reboot`",
parameters=@Parameter(name="command", description="name of the command to send. only `reboot` is supported."),
responses={
@ApiResponse(responseCode=SC_OK, description="a string representing the stat"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller is not admin or owner of node"),
@ApiResponse(responseCode=SC_INVALID, description="error occurred calling nodemanager")
}
)
public Response runCommand (@Context ContainerRequest ctx,
@PathParam("command") String command) {
final HttpRequestBean request = validateNodeManagerRequest(ctx, "cmd/"+command)
@@ -178,7 +223,20 @@ public class NodeManagerResource {
}

@POST @Path("/service/{service}/{action}")
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_NODE_MANAGER,
summary="Send service action to nodemanager",
description="Send service action to nodemanager. Must be admin or owner of node. Services are: `supervisor`, `bubble`, `mitm`, `nginx`, `postgresql`, `nodemanager`. Actions are dependent on the service: `supervisor` supports `reload` and `status` actions. `nodemanager` supports `start`, `restart` and `status`. All other services support `start`, `stop`, `restart` and `status`.",
parameters={
@Parameter(name="service", description="Name of the service. Service are: `supervisor`, `bubble`, `mitm`, `nginx`, `postgresql`, `nodemanager`"),
@Parameter(name="action", description="Action to perform. Actions are dependent on the service: `supervisor` supports `reload` and `status` actions. `nodemanager` supports `start`, `restart` and `status`. All other services support `start`, `stop`, `restart` and `status`.")
},
responses={
@ApiResponse(responseCode=SC_OK, description="a string representing the stat"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller is not admin or owner of node"),
@ApiResponse(responseCode=SC_INVALID, description="error occurred calling nodemanager")
}
)
public Response service (@Context ContainerRequest ctx,
@PathParam("service") String service,
@PathParam("action") String action) {
@@ -187,17 +245,24 @@ public class NodeManagerResource {
return callNodeManager(request, "service");
}

@POST @Path("/redis/{key}")
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
public Response readRedisKey (@Context ContainerRequest ctx,
@PathParam("key") String key) {
final HttpRequestBean request = validateNodeManagerRequest(ctx, "redis/"+key);
return callNodeManager(request, "redis");
}

@POST @Path("/patch/file/{component}/{path : .+}")
@Consumes(MULTIPART_FORM_DATA)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_NODE_MANAGER,
summary="Send a patch file to nodemanager",
description="Send a patch file to nodemanager. Must be admin or owner of node. The patch file is actually hosted here in the API, and the nodemanager is sent a URL where it can download it. `component` specifies where to apply the patch. Components are: `root`, `bubble`, `mitmproxy`. `path` specified where to apply the patch within the component.",
parameters={
@Parameter(name="component", description="Name of the component to patch. Components are: `root`, `bubble`, `mitmproxy`"),
@Parameter(name="path", description="Where to apply the patch within the component"),
@Parameter(name="file", description="stream of bytes representing the patch"),
@Parameter(name="name", description="name of the patch file. ignored, can be anything.")
},
responses={
@ApiResponse(responseCode=SC_OK, description="a string representing the stat"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller is not admin or owner of node"),
@ApiResponse(responseCode=SC_INVALID, description="error occurred calling nodemanager")
}
)
public Response patchFile (@Context ContainerRequest ctx,
@PathParam("component") String component,
@PathParam("path") String path,


+ 30
- 3
bubble-server/src/main/java/bubble/resources/cloud/PackerResource.java Wyświetl plik

@@ -7,6 +7,7 @@ package bubble.resources.cloud;
import bubble.model.account.Account;
import bubble.service.packer.PackerService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.collection.MapBuilder;
@@ -21,7 +22,9 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;

import static bubble.ApiConstants.API_TAG_ACTIVATION;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.cobbzilla.util.http.HttpStatusCodes.*;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;
import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY;

@@ -40,7 +43,15 @@ public class PackerResource {
@Autowired private PackerService packerService;

@GET
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_ACTIVATION,
summary="List status for all packer jobs",
description="List status for all packer jobs. Must be admin. Returns a JSON object with two properties: `"+STATUS_RUNNING+"` is an array of PackerJobSummary objects. `"+STATUS_COMPLETED+"`, is JSON object whose properties are compute clouds, the value of each being an array of PackerImages.",
responses={
@ApiResponse(responseCode=SC_OK, description="status JSON object"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller is not admin")
}
)
public Response listAllStatus(@Context Request req,
@Context ContainerRequest ctx) {
if (packerNotAllowedForUser(ctx)) return forbidden();
@@ -51,7 +62,15 @@ public class PackerResource {
}

@GET @Path(STATUS_RUNNING)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_ACTIVATION,
summary="List status for running packer jobs",
description="List status for running packer jobs. Must be admin. Returns an array of PackerJobSummary objects",
responses={
@ApiResponse(responseCode=SC_OK, description="PackerJobSummary object"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller is not admin")
}
)
public Response listRunningBuilds(@Context Request req,
@Context ContainerRequest ctx) {
if (packerNotAllowedForUser(ctx)) return forbidden();
@@ -59,7 +78,15 @@ public class PackerResource {
}

@GET @Path(STATUS_COMPLETED)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_ACTIVATION,
summary="List completed packer jobs",
description="List completed packer jobs. Must be admin. Returns a JSON object whose properties are compute clouds, the value of each being an array of PackerImages.",
responses={
@ApiResponse(responseCode=SC_OK, description="Map of Cloud -> PackerImage[]"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller is not admin")
}
)
public Response listCompletedBuilds(@Context Request req,
@Context ContainerRequest ctx) {
if (packerNotAllowedForUser(ctx)) return forbidden();


+ 0
- 4
bubble-server/src/main/java/bubble/resources/cloud/PublicCloudServicesResource.java Wyświetl plik

@@ -9,8 +9,6 @@ import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.account.Account;
import bubble.model.cloud.CloudService;
import bubble.resources.account.AccountOwnedTemplateResource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.extern.slf4j.Slf4j;
import org.glassfish.jersey.server.ContainerRequest;
import org.springframework.stereotype.Service;
@@ -25,7 +23,6 @@ import java.util.List;
import static bubble.ApiConstants.CLOUDS_ENDPOINT;
import static bubble.ApiConstants.EP_DATA;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;
import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY;

@Path(CLOUDS_ENDPOINT)
@Service @Slf4j
@@ -34,7 +31,6 @@ public class PublicCloudServicesResource extends AccountOwnedTemplateResource<Cl
public PublicCloudServicesResource() { super(null); }

@Path("/{id}"+EP_DATA)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
public CloudServiceDataResource getData(@Context ContainerRequest ctx,
@PathParam("id") String id) {
final Account caller = userPrincipal(ctx);


+ 84
- 9
bubble-server/src/main/java/bubble/resources/cloud/StorageResource.java Wyświetl plik

@@ -12,6 +12,8 @@ import bubble.model.cloud.RekeyRequest;
import bubble.model.cloud.StorageMetadata;
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 lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@@ -30,6 +32,7 @@ import static bubble.ApiConstants.*;
import static javax.ws.rs.core.MediaType.MULTIPART_FORM_DATA;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.http.HttpContentTypes.*;
import static org.cobbzilla.util.http.HttpStatusCodes.*;
import static org.cobbzilla.util.io.FileUtil.basename;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;
import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KEY;
@@ -37,8 +40,8 @@ import static org.cobbzilla.wizard.server.config.OpenApiConfiguration.SEC_API_KE
@Slf4j
public class StorageResource {

private Account account;
@Getter private CloudService cloud;
private final Account account;
@Getter private final CloudService cloud;

@Autowired private CloudServiceDAO cloudDAO;
@Autowired private BubbleConfiguration configuration;
@@ -55,7 +58,17 @@ public class StorageResource {

@GET @Path(EP_READ_METADATA+"/{key : .+}")
@Produces(APPLICATION_JSON)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_CLOUDS,
summary="Read metadata for key from storage",
description="Read metadata for key from storage. Caller must own the underlying storage.",
parameters=@Parameter(name="key", description="key to read metadata from"),
responses={
@ApiResponse(responseCode=SC_OK, description="a StorageMetadata object"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller does not own the storage"),
@ApiResponse(responseCode=SC_NOT_FOUND, description="key not found")
}
)
public Response meta(@Context ContainerRequest ctx,
@PathParam("key") String key) {
final Account caller = getCaller(ctx);
@@ -65,7 +78,17 @@ public class StorageResource {

@GET @Path(EP_READ+"/{key : .+}")
@Produces(CONTENT_TYPE_ANY)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_CLOUDS,
summary="Read from storage",
description="Read from storage. Caller must own the underlying storage.",
parameters=@Parameter(name="key", description="key to read from"),
responses={
@ApiResponse(responseCode=SC_OK, description="response will be a stream of data. Content-Type is set based on the key suffix"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller does not own the storage"),
@ApiResponse(responseCode=SC_NOT_FOUND, description="key not found")
}
)
public Response read(@Context ContainerRequest ctx,
@PathParam("key") String key) {
final Account caller = getCaller(ctx);
@@ -87,7 +110,17 @@ public class StorageResource {

@GET @Path(EP_LIST+"/{key : .+}")
@Produces(APPLICATION_JSON)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_CLOUDS,
summary="List keys in storage matching a prefix",
description="List keys in storage matching a prefix. Caller must own the underlying storage. Returns a StorageListing object which contains the first page of results and an listingId that can be used to get more pages of results",
parameters=@Parameter(name="key", description="list keys with this prefix"),
responses={
@ApiResponse(responseCode=SC_OK, description="a StorageListing object"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller does not own the storage"),
@ApiResponse(responseCode=SC_INVALID, description="an error occurred listing keys")
}
)
public Response list(@Context ContainerRequest ctx,
@PathParam("key") String key) {
final Account caller = getCaller(ctx);
@@ -101,7 +134,17 @@ public class StorageResource {

@GET @Path(EP_LIST_NEXT+"/{id}")
@Produces(APPLICATION_JSON)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_CLOUDS,
summary="List more keys in storage matching a prefix",
description="List more keys in storage matching a prefix. Caller must own the underlying storage. Returns a StorageListing object which contains the first page of results and an listingId that can be used to get more pages of results",
parameters=@Parameter(name="id", description="the `listingId` from a `StorageListing` object from an initial listing"),
responses={
@ApiResponse(responseCode=SC_OK, description="a StorageListing object"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller does not own the storage"),
@ApiResponse(responseCode=SC_INVALID, description="an error occurred listing keys")
}
)
public Response listNext(@Context ContainerRequest ctx,
@PathParam("id") String id) {
final Account caller = getCaller(ctx);
@@ -116,7 +159,21 @@ public class StorageResource {
@POST @Path(EP_WRITE+"/{key : .+}")
@Consumes(MULTIPART_FORM_DATA)
@Produces(APPLICATION_JSON)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_CLOUDS,
summary="Write to storage",
description="Write to storage. Caller must own the underlying storage. Returns a StorageMetadata object for the data written.",
parameters={
@Parameter(name="key", description="write to this key", required=true),
@Parameter(name="sha256", description="SHA-256 sum of the data"),
@Parameter(name="file", description="stream of bytes to write to storage")
},
responses={
@ApiResponse(responseCode=SC_OK, description="a StorageMetadata object"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller does not own the storage"),
@ApiResponse(responseCode=SC_INVALID, description="an error occurred writing to storage")
}
)
public Response write(@Context ContainerRequest ctx,
@PathParam("key") String key,
@QueryParam("sha256") String sha256,
@@ -134,7 +191,17 @@ public class StorageResource {

@DELETE @Path(EP_DELETE+"/{key : .+}")
@Produces(APPLICATION_JSON)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_CLOUDS,
summary="Delete from storage",
description="Delete from storage. Caller must own the underlying storage. Returns a true upon successful deletion.",
parameters=@Parameter(name="key", description="delete this key and its data", required=true),
responses={
@ApiResponse(responseCode=SC_OK, description="true"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller does not own the storage"),
@ApiResponse(responseCode=SC_INVALID, description="an error occurred deleting from storage")
}
)
public Response delete(@Context ContainerRequest ctx,
@PathParam("key") String key) {
final Account caller = getCaller(ctx);
@@ -149,7 +216,15 @@ public class StorageResource {
@POST @Path(EP_REKEY)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_CLOUDS,
summary="Re-key storage",
description="Re-key storage. Generates a new encryption key and re-encrypts all data stored with the new key. Caller must own the underlying storage. Returns a true upon successful re-key.",
responses={
@ApiResponse(responseCode=SC_OK, description="true"),
@ApiResponse(responseCode=SC_FORBIDDEN, description="caller does not own the storage")
}
)
public Response rekey(@Context ContainerRequest ctx,
RekeyRequest request) {
final Account caller = getCaller(ctx);


+ 27
- 2
bubble-server/src/main/java/bubble/resources/device/DeviceTypesResource.java Wyświetl plik

@@ -9,6 +9,8 @@ import bubble.model.device.BubbleDeviceType;
import bubble.model.device.DeviceSecurityLevel;
import bubble.service.device.DeviceService;
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.jersey.server.ContainerRequest;
@@ -20,8 +22,10 @@ import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.Map;

import static bubble.ApiConstants.API_TAG_DEVICES;
import static bubble.ApiConstants.EP_DEFAULT_SECURITY_LEVEL;
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;

@@ -32,16 +36,28 @@ public class DeviceTypesResource {

@Autowired private DeviceService deviceService;

private Account account;
private final Account account;

public DeviceTypesResource (Account account) { this.account = account; }

@GET
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_DEVICES,
summary="Get device types",
description="Get device types. Only returns selectable device types. Pseudo-devices like `firefox` and `web_client` are not returned.",
responses=@ApiResponse(responseCode=SC_OK, description="array of device types")
)
public Response getDeviceTypes (@Context ContainerRequest ctx) {
return ok(BubbleDeviceType.getSelectableTypes());
}

@GET @Path(EP_DEFAULT_SECURITY_LEVEL)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_DEVICES,
summary="Get default security levels",
description="Get default security levels by device type",
responses=@ApiResponse(responseCode=SC_OK, description="Map of BubbleDeviceType -> DeviceSecurityLevel")
)
public Response getDefaultSecurityLevels (@Context ContainerRequest ctx) {
return ok(getDefaultSecurityLevels());
}
@@ -55,7 +71,16 @@ public class DeviceTypesResource {
}

@POST @Path(EP_DEFAULT_SECURITY_LEVEL+"/{deviceType}/{level}")
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_DEVICES,
summary="Set default security level",
description="Set default security level for a device type. Device types are: `ios`, `android`, `windows`, `macos`, `linux`. Levels are `maximum`, `strict`, `standard`, `basic` and `disabled`",
parameters={
@Parameter(name="deviceType", description="The BubbleDeviceType to set the default security level. Device types are: `ios`, `android`, `windows`, `macos`, `linux`."),
@Parameter(name="level", description="The DeviceSecurityLevel to set for the device type. Levels are `maximum`, `strict`, `standard`, `basic` and `disabled`")
},
responses=@ApiResponse(responseCode=SC_OK, description="Map of BubbleDeviceType -> DeviceSecurityLevel")
)
public Response setDefaultSecurityLevel (@Context ContainerRequest ctx,
@PathParam("deviceType") BubbleDeviceType type,
@PathParam("level") DeviceSecurityLevel level) {


+ 11
- 1
bubble-server/src/main/java/bubble/resources/device/FlexRoutersResource.java Wyświetl plik

@@ -13,6 +13,8 @@ import bubble.service.device.DeviceService;
import bubble.service.device.FlexRouterStatus;
import bubble.service.device.StandardFlexRouterService;
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.cobbzilla.util.network.PortPicker;
@@ -28,8 +30,10 @@ import javax.ws.rs.core.Response;
import java.math.BigInteger;
import java.net.InetAddress;

import static bubble.ApiConstants.API_TAG_DEVICES;
import static bubble.ApiConstants.EP_STATUS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.cobbzilla.util.http.HttpStatusCodes.SC_OK;
import static org.cobbzilla.util.network.PortPicker.portIsAvailable;
import static org.cobbzilla.util.system.Sleep.sleep;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;
@@ -53,7 +57,13 @@ public class FlexRoutersResource extends AccountOwnedResource<FlexRouter, FlexRo
}

@GET @Path("/{id}"+EP_STATUS)
@Operation(security=@SecurityRequirement(name=SEC_API_KEY))
@Operation(security=@SecurityRequirement(name=SEC_API_KEY),
tags=API_TAG_DEVICES,
summary="Get flex router status",
description="Get flex router status. The `id` param can be the UUID or IP address of the flex router. Returns the status of the router, which can be one of: `none`, `active`, `unreachable`, `deleted`",
parameters=@Parameter(name="id", description="The UUID or IP address of the flex router"),
responses=@ApiResponse(responseCode=SC_OK, description="status of the router, which can be one of: `none`, `active`, `unreachable`, `deleted`")
)
public Response getStatus(@Context Request req,
@Context ContainerRequest ctx,
@PathParam("id") String id) {


+ 21
- 2
bubble-server/src/main/java/bubble/resources/message/MessagesResource.java Wyświetl plik

@@ -10,6 +10,8 @@ import bubble.service.message.AppMessageService;
import bubble.service.message.MessageResourceFormat;
import bubble.service.message.MessageService;
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.apache.commons.lang.ArrayUtils;
@@ -27,7 +29,9 @@ import java.util.concurrent.ConcurrentHashMap;
import static bubble.ApiConstants.*;
import static bubble.service.message.MessageService.*;
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;

@Path(MESSAGES_ENDPOINT)
@@ -43,7 +47,12 @@ public class MessagesResource {
private final Map<String, Map<String, String>> 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));


+ 9
- 0
bubble-server/src/main/java/bubble/resources/notify/InboundNotifyResource.java Wyświetl plik

@@ -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<String> 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) {


+ 15
- 0
bubble-server/src/main/java/bubble/resources/stream/AppAssetsResource.java Wyświetl plik

@@ -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,


+ 34
- 0
bubble-server/src/main/java/bubble/resources/stream/FilterDataResource.java Wyświetl plik

@@ -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) {


+ 68
- 2
bubble-server/src/main/java/bubble/resources/stream/FilterHttpResource.java Wyświetl plik

@@ -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,


+ 8
- 1
bubble-server/src/main/java/bubble/resources/stream/ReverseProxyResource.java Wyświetl plik

@@ -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,


+ 2
- 0
bubble-server/src/main/java/bubble/service/boot/NodeManagerService.java Wyświetl plik

@@ -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;


+ 8
- 5
bubble-server/src/main/java/bubble/service/boot/StandardSelfNodeService.java Wyświetl plik

@@ -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<Integer> 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);


+ 2
- 1
bubble-server/src/main/java/bubble/service/cloud/NodeProgressMeterTick.java Wyświetl plik

@@ -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 {


+ 1
- 1
bubble-server/src/main/java/bubble/service/device/StandardFlexRouterService.java Wyświetl plik

@@ -138,7 +138,7 @@ public class StandardFlexRouterService extends SimpleDaemon implements FlexRoute
private final Map<String, FlexRouterStatus> statusMap = new ConcurrentHashMap<>(DEFAULT_MAX_TUNNELS);
private final Map<String, FlexRouterInfo> activeRouters = new ConcurrentHashMap<>(DEFAULT_MAX_TUNNELS);

private final int MAX_POLL_FAILURES = 3;
private static final int MAX_POLL_FAILURES = 3;
private final Map<String, AtomicInteger> pollFailures = new ConcurrentHashMap<>(DEFAULT_MAX_TUNNELS);

public FlexRouterStatus status(String uuid) {


+ 2
- 1
bubble-server/src/main/java/bubble/service/packer/PackerJobSummary.java Wyświetl plik

@@ -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;


+ 2
- 0
bubble-server/src/main/resources/bubble-config.yml Wyświetl plik

@@ -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}}


+ 1
- 1
utils/cobbzilla-utils

@@ -1 +1 @@
Subproject commit 2c3bfe646107b1b0ff9a3c4b001f051e4bd81fa0
Subproject commit a765cf0ac5e8000c381b5542d3b007600d2eda0a

+ 1
- 1
utils/cobbzilla-wizard

@@ -1 +1 @@
Subproject commit f12add50c534143a443b09bf275211ee603563b4
Subproject commit 11e426b77af3ad0f7227c5d50135d932c4295422

Ładowanie…
Anuluj
Zapisz