findByIdOptional(UUID.fromString(unveridifedVaultId)).orElseThrow(NotFoundException::new);
- var algorithm = Algorithm.ECDSA384(decodePublicKey(vault.authenticationPublicKey));
- verify(buildVerifier(algorithm), vaultAdminAuthorizationJWT);
- } else {
- throw new VaultAdminValidationFailedException("Other vaultId provided");
- }
- }
-
- //visible for testing
- void verify(JWTVerifier verifier, DecodedJWT vaultAdminAuthorizationJWT) {
- try {
- verifier.verify(vaultAdminAuthorizationJWT);
- } catch (IncorrectClaimException e) {
- if (e.getClaimName().equals(RegisteredClaims.ISSUED_AT)) {
- throw new VaultAdminTokenIAPNotValidException("ISSUED_AT claim of VaultAdminAuthorizationJWT not provided or not yet or no longer valid");
- } else {
- throw new VaultAdminValidationFailedException("Incorrect claim exception");
- }
- } catch (JWTVerificationException e) {
- throw new VaultAdminValidationFailedException("Different key used to sign the VaultAdminAuthorizationJWT");
- }
- }
-
- //visible for testing
- JWTVerifier buildVerifier(Algorithm algorithm) {
- return verification(algorithm).build();
- }
-
- private Verification verification(Algorithm algorithm) {
- return verification(algorithm, Instant.now());
- }
-
- //visible for testing
- Verification verification(Algorithm algorithm, Instant now) {
- return JWT.require(algorithm) //
- .withClaim(RegisteredClaims.ISSUED_AT, (claim, jwt) -> jwt.getIssuedAt() != null //
- && !jwt.getIssuedAt().before(Date.from(now.minus(REQUEST_LEEWAY_IN_SECONDS, ChronoUnit.SECONDS))) //
- && !jwt.getIssuedAt().after(Date.from(now.plus(REQUEST_LEEWAY_IN_SECONDS, ChronoUnit.SECONDS)))) //
- .ignoreIssuedAt();
- }
-
- //visible for testing
- String getVaultIdQueryParameter(ContainerRequestContext containerRequestContext) {
- var vauldIdQueryParameters = containerRequestContext.getUriInfo().getPathParameters().get(VAULT_ID);
- if (vauldIdQueryParameters == null || vauldIdQueryParameters.size() != 1) {
- throw new VaultAdminValidationFailedException("VaultId not provided");
- }
- return vauldIdQueryParameters.get(0);
- }
-
- //visible for testing
- DecodedJWT getUnverifiedvaultAdminAuthorizationJWT(ContainerRequestContext containerRequestContext) {
- var clientJwt = containerRequestContext.getHeaderString(VAULT_ADMIN_AUTHORIZATION);
- if (clientJwt != null) {
- try {
- return JWT.decode(clientJwt);
- } catch (JWTDecodeException e) {
- throw new VaultAdminValidationFailedException("Malformed VaultAdminAuthorizationJWT provided");
- }
- } else {
- throw new VaultAdminNotProvidedException("VaultAdminAuthorizationJWT not provided");
- }
- }
-
- //visible for testing
- String getUnverifiedVaultId(DecodedJWT vaultAdminAuthorizationJWT) {
- var unveridifedVaultId = vaultAdminAuthorizationJWT.getHeaderClaim(VAULT_ID);
- if (!unveridifedVaultId.isNull() && unveridifedVaultId.asString() != null) { // TODO should verify uuid as well
- return unveridifedVaultId.asString();
- } else {
- throw new VaultAdminValidationFailedException("No VaultAdminAuthorizationJWT provided");
- }
- }
-
- //visible for testing
- static ECPublicKey decodePublicKey(String pemEncodedPublicKey) {
- try {
- var publicKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(pemEncodedPublicKey));
-
- var keyFactory = KeyFactory.getInstance("EC");
- var key = keyFactory.generatePublic(publicKeySpec);
-
- if (key instanceof ECPublicKey k) {
- return k;
- } else {
- throw new IllegalStateException("Key not an EC public key.");
- }
- } catch (InvalidKeySpecException e) {
- throw new VaultAdminValidationFailedException("Wrong key provided", e);
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException(e);
- }
- }
-
-}
diff --git a/backend/src/main/java/org/cryptomator/hub/filters/VaultAdminTokenIAPNotValidException.java b/backend/src/main/java/org/cryptomator/hub/filters/VaultAdminTokenIAPNotValidException.java
deleted file mode 100644
index 5147f0bdc..000000000
--- a/backend/src/main/java/org/cryptomator/hub/filters/VaultAdminTokenIAPNotValidException.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.cryptomator.hub.filters;
-
-import jakarta.ws.rs.ForbiddenException;
-
-public class VaultAdminTokenIAPNotValidException extends ForbiddenException {
-
- public VaultAdminTokenIAPNotValidException(String message) {
- super(message);
- }
-
- public VaultAdminTokenIAPNotValidException(String message, Throwable cause) {
- super(message, cause);
- }
-}
diff --git a/backend/src/main/java/org/cryptomator/hub/filters/VaultAdminValidationFailedException.java b/backend/src/main/java/org/cryptomator/hub/filters/VaultAdminValidationFailedException.java
deleted file mode 100644
index 39a6e824d..000000000
--- a/backend/src/main/java/org/cryptomator/hub/filters/VaultAdminValidationFailedException.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.cryptomator.hub.filters;
-
-import jakarta.ws.rs.BadRequestException;
-
-public class VaultAdminValidationFailedException extends BadRequestException {
-
- public VaultAdminValidationFailedException(String message) {
- super(message);
- }
-
- public VaultAdminValidationFailedException(String message, Throwable cause) {
- super(message, cause);
- }
-}
diff --git a/backend/src/main/java/org/cryptomator/hub/filters/VaultRole.java b/backend/src/main/java/org/cryptomator/hub/filters/VaultRole.java
new file mode 100644
index 000000000..0dad890db
--- /dev/null
+++ b/backend/src/main/java/org/cryptomator/hub/filters/VaultRole.java
@@ -0,0 +1,35 @@
+package org.cryptomator.hub.filters;
+
+import jakarta.ws.rs.NameBinding;
+import org.cryptomator.hub.entities.VaultAccess;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to add the {@link VaultRoleFilter} request filter to annotated service.
+ */
+@NameBinding
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(value = RetentionPolicy.RUNTIME)
+public @interface VaultRole {
+ String DEFAULT_VAULT_ID_PARAM = "vaultId";
+
+ /**
+ * @return Roles required to access the annotated resource. Access is granted if _any_ role is present.
+ */
+ VaultAccess.Role[] value() default { VaultAccess.Role.MEMBER };
+
+ /**
+ * @return Name of the path parameter containing the {@link org.cryptomator.hub.entities.Vault#id vault id}.
+ */
+ String vaultIdParam() default DEFAULT_VAULT_ID_PARAM;
+
+ /**
+ * @return How to treat the case when a vault does not exist.
+ */
+ OnMissingVault onMissingVault() default OnMissingVault.FORBIDDEN;
+ enum OnMissingVault { FORBIDDEN, NOT_FOUND, PASS }
+}
diff --git a/backend/src/main/java/org/cryptomator/hub/filters/VaultRoleFilter.java b/backend/src/main/java/org/cryptomator/hub/filters/VaultRoleFilter.java
new file mode 100644
index 000000000..6e8c3b759
--- /dev/null
+++ b/backend/src/main/java/org/cryptomator/hub/filters/VaultRoleFilter.java
@@ -0,0 +1,68 @@
+package org.cryptomator.hub.filters;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.ForbiddenException;
+import jakarta.ws.rs.NotAuthorizedException;
+import jakarta.ws.rs.NotFoundException;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.ResourceInfo;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.ext.Provider;
+import org.cryptomator.hub.entities.EffectiveVaultAccess;
+import org.cryptomator.hub.entities.Vault;
+import org.cryptomator.hub.entities.VaultAccess;
+import org.eclipse.microprofile.jwt.JsonWebToken;
+
+import java.util.Arrays;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * Request filter which checks if the user's {@link org.cryptomator.hub.entities.VaultAccess.Role Role} on a {@link org.cryptomator.hub.entities.Vault Vault}.
+ *
+ * Applied to all methods annotated with {@link VaultRole}.
+ */
+@Provider
+@VaultRole
+public class VaultRoleFilter implements ContainerRequestFilter {
+
+ @Inject
+ JsonWebToken jwt;
+
+ @Context
+ ResourceInfo resourceInfo;
+
+ @Override
+ public void filter(ContainerRequestContext requestContext) throws NotFoundException, ForbiddenException, NotAuthorizedException {
+ var annotation = resourceInfo.getResourceMethod().getAnnotation(VaultRole.class);
+ var vaultIdStr = requestContext.getUriInfo().getPathParameters().getFirst(annotation.vaultIdParam());
+ final UUID vaultId;
+ try {
+ vaultId = UUID.fromString(vaultIdStr);
+ } catch (NullPointerException | IllegalArgumentException e) {
+ throw new ForbiddenException("@VaultRole not set up correctly (unknown vault id)", e);
+ }
+
+ var userId = jwt.getSubject();
+ if (userId == null) {
+ throw new NotAuthorizedException("No JWT supplied in request header");
+ }
+
+ var forbiddenMsg = "Vault role required: " + Arrays.stream(annotation.value()).map(VaultAccess.Role::name).collect(Collectors.joining(", "));
+ if (Vault.findByIdOptional(vaultId).isPresent()) {
+ // check permissions for existing vault:
+ var effectiveRoles = EffectiveVaultAccess.listRoles(vaultId, userId);
+ if (Arrays.stream(annotation.value()).noneMatch(effectiveRoles::contains)) {
+ throw new ForbiddenException(forbiddenMsg);
+ }
+ } else {
+ // how to treat non-existing vault:
+ switch (annotation.onMissingVault()) {
+ case FORBIDDEN -> throw new ForbiddenException(forbiddenMsg);
+ case NOT_FOUND -> throw new NotFoundException("Vault not found");
+ case PASS -> {}
+ }
+ }
+ }
+}
diff --git a/backend/src/main/java/org/cryptomator/hub/license/LicenseHolder.java b/backend/src/main/java/org/cryptomator/hub/license/LicenseHolder.java
index cc0a06a35..a837e2776 100644
--- a/backend/src/main/java/org/cryptomator/hub/license/LicenseHolder.java
+++ b/backend/src/main/java/org/cryptomator/hub/license/LicenseHolder.java
@@ -3,6 +3,7 @@
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
+import io.quarkus.scheduler.Scheduled;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@@ -11,9 +12,18 @@
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
import java.time.Instant;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.stream.Collectors;
@ApplicationScoped
public class LicenseHolder {
@@ -22,28 +32,58 @@ public class LicenseHolder {
@ConfigProperty(name = "hub.managed-instance", defaultValue = "false")
Boolean managedInstance;
+ @Inject
+ @ConfigProperty(name = "hub.initial-id")
+ Optional initialId;
+
+ @Inject
+ @ConfigProperty(name = "hub.initial-license")
+ Optional initialLicense;
+
+ @Inject
+ LicenseValidator licenseValidator;
+
+ @Inject
+ RandomMinuteSleeper randomMinuteSleeper;
+
private static final Logger LOG = Logger.getLogger(LicenseHolder.class);
- private final LicenseValidator licenseValidator;
private DecodedJWT license;
- LicenseHolder(LicenseValidator licenseValidator) {
- this.licenseValidator = licenseValidator;
- }
-
/**
- * Loads the license from the database, if present
+ * Loads the license from the database or from init props, if present
*/
@PostConstruct
void init() {
var settings = Settings.get();
if (settings.licenseKey != null) {
- try {
- this.license = licenseValidator.validate(settings.licenseKey, settings.hubId);
- } catch (JWTVerificationException e) {
- LOG.warn("License in database is invalid. Deleting entry. Please add the license over the REST API again.");
- settings.licenseKey = null;
- settings.persist();
- }
+ validateLicense(settings.licenseKey, settings.hubId);
+ } else if (initialId.isPresent() && initialLicense.isPresent()) {
+ applyInitialHubIdAndLicense(initialId.get(), initialLicense.get());
+ }
+ }
+
+ @Transactional
+ void validateLicense(String licenseKey, String hubId) {
+ try {
+ this.license = licenseValidator.validate(licenseKey, hubId);
+ } catch (JWTVerificationException e) {
+ LOG.warn("Provided license is invalid. Deleting entry. Please add the license over the REST API again.");
+ var settings = Settings.get();
+ settings.licenseKey = null;
+ settings.persistAndFlush();
+ }
+ }
+
+ @Transactional
+ void applyInitialHubIdAndLicense(String initialId, String initialLicense) {
+ try {
+ this.license = licenseValidator.validate(initialLicense, initialId);
+ var settings = Settings.get();
+ settings.licenseKey = initialLicense;
+ settings.hubId = initialId;
+ settings.persistAndFlush();
+ } catch (JWTVerificationException e) {
+ LOG.warn("Provided initial license is invalid.");
}
}
@@ -60,7 +100,47 @@ public void set(String token) throws JWTVerificationException {
var settings = Settings.get();
this.license = licenseValidator.validate(token, settings.hubId);
settings.licenseKey = token;
- settings.persist();
+ settings.persistAndFlush();
+ }
+
+ /**
+ * Attempts to refresh the Hub licence every day between 01:00:00 and 02:00:00 AM UTC if claim refreshURL is present.
+ */
+ @Scheduled(cron = "0 0 1 * * ?", timeZone = "UTC", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)
+ void refreshLicenseScheduler() throws InterruptedException {
+ if (license != null) {
+ randomMinuteSleeper.sleep(); // add random sleep between [0,59]min to reduce infrastructure load
+ var refreshUrl = licenseValidator.refreshUrl(license.getToken());
+ if (refreshUrl.isPresent()) {
+ var client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build();
+ refreshLicense(refreshUrl.get(), license.getToken(), client);
+ }
+ }
+ }
+
+ //visible for testing
+ void refreshLicense(String refreshUrl, String license, HttpClient client) throws InterruptedException {
+ var parameters = Map.of("token", license);
+ var body = parameters.entrySet() //
+ .stream() //
+ .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) //
+ .collect(Collectors.joining("&"));
+ var request = HttpRequest.newBuilder() //
+ .uri(URI.create(refreshUrl)) //
+ .headers("Content-Type", "application/x-www-form-urlencoded") //
+ .POST(HttpRequest.BodyPublishers.ofString(body)) //
+ .version(HttpClient.Version.HTTP_1_1) //
+ .build();
+ try {
+ var response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ if (response.statusCode() == 200 && !response.body().isEmpty()) {
+ set(response.body());
+ } else {
+ LOG.error("Failed to refresh license token with response code: " + response.statusCode());
+ }
+ } catch (IOException | JWTVerificationException e) {
+ LOG.error("Failed to refresh license token", e);
+ }
}
public DecodedJWT get() {
diff --git a/backend/src/main/java/org/cryptomator/hub/license/LicenseValidator.java b/backend/src/main/java/org/cryptomator/hub/license/LicenseValidator.java
index 90d6ba213..ff19aba58 100644
--- a/backend/src/main/java/org/cryptomator/hub/license/LicenseValidator.java
+++ b/backend/src/main/java/org/cryptomator/hub/license/LicenseValidator.java
@@ -16,6 +16,7 @@
import java.security.spec.X509EncodedKeySpec;
import java.time.Instant;
import java.util.Objects;
+import java.util.Optional;
@ApplicationScoped
public class LicenseValidator {
@@ -33,8 +34,9 @@ public class LicenseValidator {
public LicenseValidator() {
var algorithm = Algorithm.ECDSA512(decodePublicKey(LICENSE_PUBLIC_KEY), null);
- var leeway = Instant.now().getEpochSecond(); // this will make sure to accept tokens that expired in the past (beginning from 1970)
- this.verifier = JWT.require(algorithm).acceptExpiresAt(leeway).build();
+ var expiresleeway = Instant.now().getEpochSecond(); // this will make sure to accept tokens that expired in the past (beginning from 1970)
+ // ignoring issued at will make sure to accept tokens that are issued "in the future" e.g. when the hub time is behind the store time
+ this.verifier = JWT.require(algorithm).acceptExpiresAt(expiresleeway).ignoreIssuedAt().build();
}
private static ECPublicKey decodePublicKey(String pemEncodedPublicKey) {
@@ -66,4 +68,9 @@ public DecodedJWT validate(String token, String expectedHubId) throws JWTVerific
return jwt;
}
+ public Optional refreshUrl(String token) throws JWTVerificationException {
+ var jwt = verifier.verify(token);
+ return Optional.ofNullable(jwt.getClaim("refreshUrl").asString());
+ }
+
}
diff --git a/backend/src/main/java/org/cryptomator/hub/license/RandomMinuteSleeper.java b/backend/src/main/java/org/cryptomator/hub/license/RandomMinuteSleeper.java
new file mode 100644
index 000000000..f5b005e75
--- /dev/null
+++ b/backend/src/main/java/org/cryptomator/hub/license/RandomMinuteSleeper.java
@@ -0,0 +1,21 @@
+package org.cryptomator.hub.license;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import java.util.Random;
+
+@ApplicationScoped
+public class RandomMinuteSleeper {
+
+ private static final long MINUTE_IN_MILLIS = 60 * 1000L;
+ private final Random rng;
+
+ public RandomMinuteSleeper() {
+ this.rng = new Random();
+ }
+
+ void sleep() throws InterruptedException {
+ Thread.sleep(rng.nextInt(0, 60) * MINUTE_IN_MILLIS);
+ }
+
+}
diff --git a/backend/src/main/java/org/cryptomator/hub/validation/OnlyBase64Chars.java b/backend/src/main/java/org/cryptomator/hub/validation/OnlyBase64Chars.java
index 237a412f8..8cd68fdfe 100644
--- a/backend/src/main/java/org/cryptomator/hub/validation/OnlyBase64Chars.java
+++ b/backend/src/main/java/org/cryptomator/hub/validation/OnlyBase64Chars.java
@@ -2,7 +2,6 @@
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
-import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import java.lang.annotation.Documented;
@@ -13,7 +12,6 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Pattern(regexp = "[+/A-Za-z0-9]+=*")
-@NotNull
@Target({METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = {})
diff --git a/backend/src/main/java/org/cryptomator/hub/validation/OnlyBase64UrlChars.java b/backend/src/main/java/org/cryptomator/hub/validation/OnlyBase64UrlChars.java
deleted file mode 100644
index 0c28c9052..000000000
--- a/backend/src/main/java/org/cryptomator/hub/validation/OnlyBase64UrlChars.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.cryptomator.hub.validation;
-
-import jakarta.validation.Constraint;
-import jakarta.validation.Payload;
-import jakarta.validation.constraints.NotNull;
-import jakarta.validation.constraints.Pattern;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-import static java.lang.annotation.ElementType.*;
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-@Pattern(regexp = "[-_A-Za-z0-9]+=*")
-@NotNull
-@Target({METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE, PARAMETER})
-@Retention(RUNTIME)
-@Constraint(validatedBy = {})
-@Documented
-public @interface OnlyBase64UrlChars {
- String message() default "Input is not a valid base64url encoded string";
-
- Class>[] groups() default {};
-
- Class extends Payload>[] payload() default {};
-}
diff --git a/backend/src/main/java/org/cryptomator/hub/validation/ValidJWE.java b/backend/src/main/java/org/cryptomator/hub/validation/ValidJWE.java
index 97bede999..e902eb0e7 100644
--- a/backend/src/main/java/org/cryptomator/hub/validation/ValidJWE.java
+++ b/backend/src/main/java/org/cryptomator/hub/validation/ValidJWE.java
@@ -2,7 +2,6 @@
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
-import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import java.lang.annotation.Documented;
@@ -13,7 +12,6 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Pattern(regexp = "[-_A-Za-z0-9]+=*\\.[-_A-Za-z0-9]*=*\\.[-_A-Za-z0-9]*=*\\.[-_A-Za-z0-9]+=*\\.[-_A-Za-z0-9]*=*")
-@NotNull
@Target({METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = {})
diff --git a/backend/src/main/java/org/cryptomator/hub/validation/ValidJWS.java b/backend/src/main/java/org/cryptomator/hub/validation/ValidJWS.java
index 0a24b53a6..1226eb4b9 100644
--- a/backend/src/main/java/org/cryptomator/hub/validation/ValidJWS.java
+++ b/backend/src/main/java/org/cryptomator/hub/validation/ValidJWS.java
@@ -2,7 +2,6 @@
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
-import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import java.lang.annotation.Documented;
@@ -13,7 +12,6 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Pattern(regexp = "[-_A-Za-z0-9]+=*\\.[-_A-Za-z0-9]*=*\\.[-_A-Za-z0-9]*=*")
-@NotNull
@Target({METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = {})
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
index 751bb4d93..15a752d71 100644
--- a/backend/src/main/resources/application.properties
+++ b/backend/src/main/resources/application.properties
@@ -32,7 +32,7 @@ hub.keycloak.oidc.cryptomator-client-id=cryptomator
%dev.quarkus.keycloak.devservices.realm-name=cryptomator
%dev.quarkus.keycloak.devservices.port=8180
%dev.quarkus.keycloak.devservices.service-name=quarkus-cryptomator-hub
-%dev.quarkus.keycloak.devservices.image-name=ghcr.io/cryptomator/keycloak:22.0.1
+%dev.quarkus.keycloak.devservices.image-name=ghcr.io/cryptomator/keycloak:22.0.5
%dev.quarkus.oidc.devui.grant.type=code
# OIDC will be mocked during unit tests. Use fake auth url to prevent dev services to start:
%test.quarkus.oidc.auth-server-url=http://localhost:43210/dev/null
@@ -77,6 +77,8 @@ quarkus.flyway.locations=classpath:org/cryptomator/hub/flyway
# HTTP Security Headers see e.g. https://owasp.org/www-project-secure-headers/#div-bestpractices
quarkus.http.header."Content-Security-Policy".value=default-src 'self'; connect-src 'self' api.cryptomator.org; object-src 'none'; child-src 'self'; img-src * data:; frame-ancestors 'none'
%dev.quarkus.http.header."Content-Security-Policy".value=default-src 'self'; connect-src 'self' api.cryptomator.org localhost:8180; object-src 'none'; child-src 'self'; img-src * data:; frame-ancestors 'none'
+# dev-ui needs very permissive CSP:
+# %dev.quarkus.http.header."Content-Security-Policy".value=default-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data:; connect-src 'self' api.cryptomator.org localhost:8180;
quarkus.http.header."Referrer-Policy".value=no-referrer
quarkus.http.header."Strict-Transport-Security".value=max-age=31536000; includeSubDomains
quarkus.http.header."X-Content-Type-Options".value=nosniff
diff --git a/backend/src/main/resources/dev-realm.json b/backend/src/main/resources/dev-realm.json
index bbfe8e1ab..76d75ff90 100644
--- a/backend/src/main/resources/dev-realm.json
+++ b/backend/src/main/resources/dev-realm.json
@@ -38,7 +38,8 @@
"composites": {
"client": {
"realm-management": [
- "view-users"
+ "view-users",
+ "view-clients"
]
}
}
@@ -79,6 +80,21 @@
"realmRoles": [
"syncer"
]
+ },
+ {
+ "username": "cli",
+ "email": "cli@localhost",
+ "enabled": true,
+ "serviceAccountClientId": "cryptomatorhub-cli",
+ "attributes": {
+ "picture": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJ5ZXMiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj4KICAgIDxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9ImJsYWNrIi8+CiAgICA8cGF0aCBzdHJva2Utd2lkdGg9IjUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0iTTMwIDM3LjUgbDE1IDEyLjUgbC0xNSAxMi41ICBtMjAgMCBoMjAiIC8+Cjwvc3ZnPgo="
+ },
+ "realmRoles": [
+ "user"
+ ],
+ "clientRoles" : {
+ "realm-management" : [ "manage-users", "view-users" ]
+ }
}
],
"scopeMappings": [
@@ -156,6 +172,16 @@
"attributes": {
"pkce.code.challenge.method": "S256"
}
+ },
+ {
+ "clientId": "cryptomatorhub-cli",
+ "name": "Cryptomator Hub CLI",
+ "clientAuthenticatorType": "client-secret",
+ "secret": "top-secret",
+ "standardFlowEnabled": false,
+ "serviceAccountsEnabled": true,
+ "publicClient": false,
+ "enabled": true
}
],
"browserSecurityHeaders": {
diff --git a/backend/src/main/resources/org/cryptomator/hub/flyway/B14__Hub_1.3.0.sql b/backend/src/main/resources/org/cryptomator/hub/flyway/B14__Hub_1.3.0.sql
new file mode 100644
index 000000000..3fed56430
--- /dev/null
+++ b/backend/src/main/resources/org/cryptomator/hub/flyway/B14__Hub_1.3.0.sql
@@ -0,0 +1,262 @@
+-- noinspection SqlNoDataSourceInspectionForFile
+
+CREATE TABLE "settings"
+(
+ "id" INT4 NOT NULL,
+ "hub_id" VARCHAR(255) NOT NULL,
+ "license_key" VARCHAR,
+ CONSTRAINT "SETTINGS_PK" PRIMARY KEY ("id")
+);
+
+INSERT INTO "settings" ("id", "hub_id") VALUES (0, gen_random_uuid());
+
+CREATE TABLE "authority"
+(
+ "id" VARCHAR(255) COLLATE "C" NOT NULL,
+ "type" VARCHAR(5) NOT NULL,
+ "name" VARCHAR NOT NULL,
+ CONSTRAINT "AUTHORITY_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "AUTHORITY_CHK_TYPE" CHECK ("type" = 'USER' OR "type" = 'GROUP')
+);
+
+CREATE TABLE "group_membership"
+(
+ "group_id" VARCHAR(255) COLLATE "C" NOT NULL,
+ "member_id" VARCHAR(255) COLLATE "C" NOT NULL,
+ CONSTRAINT "GROUP_MEMBERSHIP_PK" PRIMARY KEY ("group_id", "member_id"),
+ CONSTRAINT "GROUP_MEMBERSHIP_FK_GROUP" FOREIGN KEY ("group_id") REFERENCES "authority" ("id") ON DELETE CASCADE,
+ CONSTRAINT "GROUP_MEMBERSHIP_FK_MEMBER" FOREIGN KEY ("member_id") REFERENCES "authority" ("id") ON DELETE CASCADE,
+ CONSTRAINT "GROUP_MEMBERSHIP_CHK_NOTSAME" CHECK ("group_id" <> "member_id")
+);
+
+-- @formatter:off
+CREATE VIEW "effective_group_membership" ("group_id", "member_id", "path") AS
+WITH RECURSIVE "members" ("root", "member_id", "depth", "path") AS (
+ SELECT "group_id", "member_id", 0, '/' || "group_id" || '/' || "member_id"
+ FROM "group_membership"
+ UNION
+ SELECT "parent"."root", "child"."member_id", "parent"."depth" + 1, "parent"."path" || '/' || "child"."member_id"
+ FROM "group_membership" "child"
+ INNER JOIN "members" "parent" ON "child"."group_id" = "parent"."member_id"
+ WHERE "parent"."depth" < 10
+) SELECT "root", "member_id", "path" FROM "members";
+-- @formatter:on
+
+CREATE TABLE "user_details"
+(
+ "id" VARCHAR(255) COLLATE "C" NOT NULL,
+ "picture_url" VARCHAR,
+ "email" VARCHAR,
+ "publickey" VARCHAR, -- base64-encoded SPKI DER (RFC 5280, 4.1.2.7)
+ "privatekey" VARCHAR, -- private key, encrypted using setup code (JWE PBES2)
+ "setupcode" VARCHAR, -- setup code, encrypted using user's public key (JWE ECDH-ES)
+ CONSTRAINT "USER_DETAIL_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "USER_DETAIL_FK_USER" FOREIGN KEY ("id") REFERENCES "authority" ("id") ON DELETE CASCADE
+);
+
+CREATE TABLE "group_details"
+(
+ "id" VARCHAR(255) COLLATE "C" NOT NULL,
+ CONSTRAINT "GROUP_DETAIL_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "GROUP_DETAIL_FK_GROUP" FOREIGN KEY ("id") REFERENCES "authority" ("id") ON DELETE CASCADE
+);
+
+CREATE TABLE "vault"
+(
+ "id" UUID NOT NULL,
+ "name" VARCHAR NOT NULL,
+ "description" VARCHAR,
+ "creation_time" TIMESTAMP WITH TIME ZONE NOT NULL,
+ "archived" BOOLEAN NOT NULL DEFAULT false,
+ "salt" VARCHAR(255), -- deprecated ("vault admin password")
+ "iterations" INTEGER, -- deprecated ("vault admin password")
+ "masterkey" VARCHAR(255), -- deprecated ("vault admin password")
+ "auth_pubkey" VARCHAR, -- deprecated ("vault admin password")
+ "auth_prvkey" VARCHAR, -- deprecated ("vault admin password")
+ CONSTRAINT "VAULT_PK" PRIMARY KEY ("id")
+);
+
+CREATE TABLE "vault_access"
+(
+ "vault_id" UUID NOT NULL,
+ "authority_id" VARCHAR(255) COLLATE "C" NOT NULL,
+ "role" VARCHAR(50) NOT NULL DEFAULT 'MEMBER',
+ CONSTRAINT "VAULT_ACCESS_PK" PRIMARY KEY ("vault_id", "authority_id"),
+ CONSTRAINT "VAULT_ACCESS_FK_VAULT" FOREIGN KEY ("vault_id") REFERENCES "vault" ("id") ON DELETE CASCADE,
+ CONSTRAINT "VAULT_ACCESS_FK_AUTHORITY" FOREIGN KEY ("authority_id") REFERENCES "authority" ("id") ON DELETE CASCADE
+);
+
+-- @formatter:off
+CREATE VIEW "effective_vault_access" ("vault_id", "authority_id", "role") AS
+ SELECT "va"."vault_id", "va"."authority_id", "va"."role" FROM "vault_access" "va"
+ UNION
+ SELECT "va"."vault_id", "gm"."member_id", "va"."role" FROM "vault_access" "va"
+ INNER JOIN "effective_group_membership" "gm" ON "va"."authority_id" = "gm"."group_id";
+-- @formatter:on
+
+CREATE TABLE "device"
+(
+ "id" VARCHAR(255) NOT NULL,
+ "owner_id" VARCHAR(255) NOT NULL,
+ "name" VARCHAR NOT NULL,
+ "type" VARCHAR(50) NOT NULL DEFAULT 'DESKTOP',
+ "publickey" VARCHAR NOT NULL, -- base64-encoded SPKI DER (RFC 5280, 4.1.2.7)
+ "user_privatekey" VARCHAR NOT NULL UNIQUE, -- private key, encrypted using device's public key (JWE ECDH-ES)
+ "creation_time" TIMESTAMP WITH TIME ZONE NOT NULL,
+ CONSTRAINT "DEVICE_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "DEVICE_FK_USER" FOREIGN KEY ("owner_id") REFERENCES "user_details" ("id") ON DELETE CASCADE
+);
+
+CREATE TABLE "access_token"
+(
+ "user_id" VARCHAR(255) COLLATE "C" NOT NULL,
+ "vault_id" UUID NOT NULL,
+ "vault_masterkey" VARCHAR NOT NULL UNIQUE, -- private key, encrypted using user's public key (JWE ECDH-ES)
+ CONSTRAINT "ACCESS_PK" PRIMARY KEY ("user_id", "vault_id"),
+ CONSTRAINT "ACCESS_FK_USER" FOREIGN KEY ("user_id") REFERENCES "user_details" ("id") ON DELETE CASCADE,
+ CONSTRAINT "ACCESS_FK_VAULT" FOREIGN KEY ("vault_id") REFERENCES "vault" ("id") ON DELETE CASCADE
+);
+
+-- ------------- --
+-- LEGACY TABLES --
+-- ------------- --
+CREATE TABLE "device_legacy"
+(
+ "id" VARCHAR(255) COLLATE "C" NOT NULL,
+ "owner_id" VARCHAR(255) COLLATE "C" NOT NULL,
+ "name" VARCHAR NOT NULL,
+ "type" VARCHAR(50) NOT NULL DEFAULT 'DESKTOP',
+ "publickey" VARCHAR NOT NULL,
+ "creation_time" TIMESTAMP WITH TIME ZONE NOT NULL,
+ CONSTRAINT "DEVICE_LEGACY_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "DEVICE_LEGACY_FK_USER" FOREIGN KEY ("owner_id") REFERENCES "authority" ("id") ON DELETE CASCADE,
+ CONSTRAINT "DEVICE_LEGACY_UNIQUE_NAME_PER_OWNER" UNIQUE ("owner_id", "name")
+);
+COMMENT ON COLUMN "device_legacy"."publickey" IS 'Note: This contains base64url-encoded data for historic reasons.';
+
+CREATE TABLE "access_token_legacy"
+(
+ "device_id" VARCHAR(255) COLLATE "C" NOT NULL,
+ "vault_id" UUID NOT NULL,
+ "jwe" VARCHAR NOT NULL UNIQUE,
+ CONSTRAINT "ACCESS_LEGACY_PK" PRIMARY KEY ("device_id", "vault_id"),
+ CONSTRAINT "ACCESS_LEGACY_FK_DEVICE" FOREIGN KEY ("device_id") REFERENCES "device_legacy" ("id") ON DELETE CASCADE,
+ CONSTRAINT "ACCESS_LEGACY_FK_VAULT" FOREIGN KEY ("vault_id") REFERENCES "vault" ("id") ON DELETE CASCADE
+);
+
+-- --------- --
+-- AUDIT LOG --
+-- --------- --
+CREATE SEQUENCE audit_event_id_seq AS BIGINT;
+CREATE TABLE "audit_event"
+(
+ "id" BIGINT NOT NULL DEFAULT nextval('audit_event_id_seq'),
+ "type" VARCHAR(50) NOT NULL,
+ "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL,
+ CONSTRAINT "AUDIT_EVENT_PK" PRIMARY KEY ("id")
+);
+ALTER SEQUENCE audit_event_id_seq OWNED BY audit_event.id;
+
+CREATE TABLE "audit_event_vault_create"
+(
+ "id" BIGINT NOT NULL,
+ "created_by" VARCHAR(255) COLLATE "C" NOT NULL,
+ "vault_id" UUID NOT NULL,
+ "vault_name" VARCHAR NOT NULL,
+ "vault_description" VARCHAR,
+ CONSTRAINT "AUDIT_EVENT_VAULT_CREATE_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "AUDIT_EVENT_VAULT_CREATE_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
+);
+
+CREATE TABLE "audit_event_vault_key_retrieve"
+(
+ "id" BIGINT NOT NULL,
+ "retrieved_by" VARCHAR(255) COLLATE "C" NOT NULL,
+ "vault_id" UUID NOT NULL,
+ "result" VARCHAR(50) NOT NULL,
+ CONSTRAINT "AUDIT_EVENT_VAULT_UNLOCK_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "AUDIT_EVENT_VAULT_UNLOCK_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
+);
+
+CREATE TABLE "audit_event_vault_member_add"
+(
+ "id" BIGINT NOT NULL,
+ "added_by" VARCHAR(255) COLLATE "C" NOT NULL,
+ "vault_id" UUID NOT NULL,
+ "authority_id" VARCHAR(255) COLLATE "C" NOT NULL,
+ "role" VARCHAR(50) NOT NULL DEFAULT 'MEMBER',
+ CONSTRAINT "AUDIT_EVENT_VAULT_MEMBER_ADD_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "AUDIT_EVENT_VAULT_MEMBER_ADD_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
+);
+
+CREATE TABLE "audit_event_vault_member_remove"
+(
+ "id" BIGINT NOT NULL,
+ "removed_by" VARCHAR(255) COLLATE "C" NOT NULL,
+ "vault_id" UUID NOT NULL,
+ "authority_id" VARCHAR(255) COLLATE "C" NOT NULL,
+ CONSTRAINT "AUDIT_EVENT_VAULT_MEMBER_REMOVE_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "AUDIT_EVENT_VAULT_MEMBER_REMOVE_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
+);
+
+CREATE TABLE "audit_event_vault_member_update"
+(
+ "id" BIGINT NOT NULL,
+ "updated_by" VARCHAR(255) COLLATE "C" NOT NULL,
+ "vault_id" UUID NOT NULL,
+ "authority_id" VARCHAR(255) COLLATE "C" NOT NULL,
+ "role" VARCHAR(50) NOT NULL,
+ CONSTRAINT "AUDIT_EVENT_VAULT_MEMBER_UPDATE_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "AUDIT_EVENT_VAULT_MEMBER_UPDATE_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
+);
+
+CREATE TABLE "audit_event_vault_ownership_claim"
+(
+ "id" BIGINT NOT NULL,
+ "claimed_by" VARCHAR(255) COLLATE "C" NOT NULL,
+ "vault_id" UUID NOT NULL,
+ CONSTRAINT "AUDIT_EVENT_VAULT_OWNERSHIP_CLAIM_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "AUDIT_EVENT_VAULT_OWNERSGIP_CLAIM_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
+);
+
+CREATE TABLE "audit_event_device_register"
+(
+ "id" BIGINT NOT NULL,
+ "registered_by" VARCHAR(255) COLLATE "C" NOT NULL,
+ "device_id" VARCHAR(64) COLLATE "C" NOT NULL,
+ "device_name" VARCHAR NOT NULL,
+ "device_type" VARCHAR(50) NOT NULL,
+ CONSTRAINT "AUDIT_EVENT_DEVICE_REGISTER_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "AUDIT_EVENT_DEVICE_REGISTER_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
+);
+
+CREATE TABLE "audit_event_device_remove"
+(
+ "id" BIGINT NOT NULL,
+ "removed_by" VARCHAR(255) COLLATE "C" NOT NULL,
+ "device_id" VARCHAR(64) COLLATE "C" NOT NULL,
+ CONSTRAINT "AUDIT_EVENT_DEVICE_REMOVE_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "AUDIT_EVENT_DEVICE_REMOVE_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
+);
+
+CREATE TABLE "audit_event_vault_update"
+(
+ "id" BIGINT NOT NULL,
+ "updated_by" VARCHAR(255) COLLATE "C" NOT NULL,
+ "vault_id" UUID NOT NULL,
+ "vault_name" VARCHAR NOT NULL,
+ "vault_description" VARCHAR,
+ "vault_archived" BOOLEAN NOT NULL,
+ CONSTRAINT "AUDIT_EVENT_VAULT_UPDATE_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "AUDIT_EVENT_VAULT_UPDATE_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
+);
+
+CREATE TABLE "audit_event_vault_access_grant"
+(
+ "id" BIGINT NOT NULL,
+ "granted_by" VARCHAR(255) COLLATE "C" NOT NULL,
+ "vault_id" UUID NOT NULL,
+ "authority_id" VARCHAR(255) COLLATE "C" NOT NULL,
+ CONSTRAINT "AUDIT_EVENT_VAULT_ACCESS_GRANT_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "AUDIT_EVENT_VAULT_ACCESS_GRANT_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/backend/src/main/resources/org/cryptomator/hub/flyway/ERM.png b/backend/src/main/resources/org/cryptomator/hub/flyway/ERM.png
index 1ef70a42b..c19e352b3 100644
Binary files a/backend/src/main/resources/org/cryptomator/hub/flyway/ERM.png and b/backend/src/main/resources/org/cryptomator/hub/flyway/ERM.png differ
diff --git a/backend/src/main/resources/org/cryptomator/hub/flyway/V14__User_Keys.sql b/backend/src/main/resources/org/cryptomator/hub/flyway/V14__User_Keys.sql
new file mode 100644
index 000000000..99d1584bb
--- /dev/null
+++ b/backend/src/main/resources/org/cryptomator/hub/flyway/V14__User_Keys.sql
@@ -0,0 +1,94 @@
+-- remove varchar length restrictions, see https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_varchar.28n.29_by_default
+ALTER TABLE "settings" ALTER COLUMN "license_key" SET DATA TYPE VARCHAR;
+ALTER TABLE "authority" ALTER COLUMN "name" SET DATA TYPE VARCHAR;
+ALTER TABLE "user_details" ALTER COLUMN "picture_url" SET DATA TYPE VARCHAR;
+ALTER TABLE "user_details" ALTER COLUMN "email" SET DATA TYPE VARCHAR;
+ALTER TABLE "vault" ALTER COLUMN "name" SET DATA TYPE VARCHAR;
+ALTER TABLE "vault" ALTER COLUMN "description" SET DATA TYPE TEXT;
+ALTER TABLE "vault" ALTER COLUMN "auth_pubkey" SET DATA TYPE VARCHAR;
+ALTER TABLE "vault" ALTER COLUMN "auth_prvkey" SET DATA TYPE VARCHAR;
+ALTER TABLE "device" ALTER COLUMN "name" SET DATA TYPE VARCHAR;
+ALTER TABLE "device" ALTER COLUMN "publickey" SET DATA TYPE VARCHAR;
+ALTER TABLE "access_token" ALTER COLUMN "jwe" SET DATA TYPE VARCHAR;
+ALTER TABLE "audit_event_vault_create" ALTER COLUMN "vault_name" SET DATA TYPE VARCHAR;
+ALTER TABLE "audit_event_vault_create" ALTER COLUMN "vault_description" SET DATA TYPE VARCHAR;
+ALTER TABLE "audit_event_vault_update" ALTER COLUMN "vault_name" SET DATA TYPE VARCHAR;
+ALTER TABLE "audit_event_vault_update" ALTER COLUMN "vault_description" SET DATA TYPE VARCHAR;
+ALTER TABLE "audit_event_device_register" ALTER COLUMN "device_name" SET DATA TYPE VARCHAR;
+
+-- users will generate a new key pair during first login in the browser:
+ALTER TABLE "user_details" ADD "publickey" VARCHAR; -- base64-encoded SPKI DER (RFC 5280, 4.1.2.7)
+ALTER TABLE "user_details" ADD "privatekey" VARCHAR; -- private key, encrypted using setup code (JWE PBES2)
+ALTER TABLE "user_details" ADD "setupcode" VARCHAR; -- setup code, encrypted using user's public key (JWE ECDH-ES)
+
+-- keep existing device-based access tokens for continuous unlock from old clients.
+ALTER TABLE "access_token" RENAME TO "access_token_legacy";
+ALTER TABLE "access_token_legacy" RENAME CONSTRAINT "ACCESS_PK" TO "ACCESS_LEGACY_PK";
+ALTER TABLE "access_token_legacy" RENAME CONSTRAINT "ACCESS_FK_DEVICE" TO "ACCESS_LEGACY_FK_DEVICE";
+ALTER TABLE "access_token_legacy" RENAME CONSTRAINT "ACCESS_FK_VAULT" TO "ACCESS_LEGACY_FK_VAULT";
+ALTER TABLE "device" RENAME TO "device_legacy";
+ALTER TABLE "device_legacy" RENAME CONSTRAINT "DEVICE_PK" TO "DEVICE_LEGACY_PK";
+ALTER TABLE "device_legacy" RENAME CONSTRAINT "DEVICE_FK_USER" TO "DEVICE_LEGACY_FK_USER";
+ALTER TABLE "device_legacy" RENAME CONSTRAINT "DEVICE_UNIQUE_NAME_PER_OWNER" TO "DEVICE_LEGACY_UNIQUE_NAME_PER_OWNER";
+COMMENT ON COLUMN "device_legacy"."publickey" IS 'Note: This contains base64url-encoded data for historic reasons.';
+
+-- new device table with non-null user_privatekey:
+CREATE TABLE "device"
+(
+ "id" VARCHAR(255) NOT NULL,
+ "owner_id" VARCHAR(255) NOT NULL,
+ "name" VARCHAR NOT NULL,
+ "type" VARCHAR(50) NOT NULL DEFAULT 'DESKTOP',
+ "publickey" VARCHAR NOT NULL,
+ "user_privatekey" VARCHAR NOT NULL UNIQUE, -- private key, encrypted using device's public key (JWE ECDH-ES)
+ "creation_time" TIMESTAMP WITH TIME ZONE NOT NULL,
+ CONSTRAINT "DEVICE_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "DEVICE_FK_USER" FOREIGN KEY ("owner_id") REFERENCES "user_details" ("id") ON DELETE CASCADE
+);
+
+-- new access tokens will be issued for users (not devices):
+CREATE TABLE "access_token"
+(
+ "user_id" VARCHAR(255) COLLATE "C" NOT NULL,
+ "vault_id" UUID NOT NULL,
+ "vault_masterkey" VARCHAR NOT NULL UNIQUE, -- private key, encrypted using user's public key (JWE ECDH-ES)
+ CONSTRAINT "ACCESS_PK" PRIMARY KEY ("user_id", "vault_id"),
+ CONSTRAINT "ACCESS_FK_USER" FOREIGN KEY ("user_id") REFERENCES "user_details" ("id") ON DELETE CASCADE,
+ CONSTRAINT "ACCESS_FK_VAULT" FOREIGN KEY ("vault_id") REFERENCES "vault" ("id") ON DELETE CASCADE
+);
+
+ALTER TABLE "vault_access" ADD "role" VARCHAR(50) NOT NULL DEFAULT 'MEMBER';
+ALTER TABLE "audit_event_vault_member_add" ADD "role" VARCHAR(50) NOT NULL DEFAULT 'MEMBER';
+CREATE TABLE "audit_event_vault_member_update"
+(
+ "id" BIGINT NOT NULL,
+ "updated_by" VARCHAR(255) COLLATE "C" NOT NULL,
+ "vault_id" UUID NOT NULL,
+ "authority_id" VARCHAR(255) COLLATE "C" NOT NULL,
+ "role" VARCHAR(50) NOT NULL,
+ CONSTRAINT "AUDIT_EVENT_VAULT_MEMBER_UPDATE_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "AUDIT_EVENT_VAULT_MEMBER_UPDATE_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
+);
+CREATE TABLE "audit_event_vault_ownership_claim"
+(
+ "id" BIGINT NOT NULL,
+ "claimed_by" VARCHAR(255) COLLATE "C" NOT NULL,
+ "vault_id" UUID NOT NULL,
+ CONSTRAINT "AUDIT_EVENT_VAULT_OWNERSHIP_CLAIM_PK" PRIMARY KEY ("id"),
+ CONSTRAINT "AUDIT_EVENT_VAULT_OWNERSGIP_CLAIM_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
+);
+
+-- @formatter:off
+CREATE OR REPLACE VIEW "effective_vault_access" ("vault_id", "authority_id", "role") AS
+ SELECT "va"."vault_id", "va"."authority_id", "va"."role" FROM "vault_access" "va"
+ UNION
+ SELECT "va"."vault_id", "gm"."member_id", "va"."role" FROM "vault_access" "va"
+ INNER JOIN "effective_group_membership" "gm" ON "va"."authority_id" = "gm"."group_id";
+-- @formatter:on
+
+-- deprecate vault admin password
+ALTER TABLE "vault" ALTER COLUMN "salt" DROP NOT NULL;
+ALTER TABLE "vault" ALTER COLUMN "iterations" DROP NOT NULL;
+ALTER TABLE "vault" ALTER COLUMN "masterkey" DROP NOT NULL;
+ALTER TABLE "vault" ALTER COLUMN "auth_pubkey" DROP NOT NULL;
+ALTER TABLE "vault" ALTER COLUMN "auth_prvkey" DROP NOT NULL;
diff --git a/backend/src/test/java/org/cryptomator/hub/KeycloakRemoteUserProviderTest.java b/backend/src/test/java/org/cryptomator/hub/KeycloakRemoteUserProviderTest.java
index 2d12f18c1..f8b85c530 100644
--- a/backend/src/test/java/org/cryptomator/hub/KeycloakRemoteUserProviderTest.java
+++ b/backend/src/test/java/org/cryptomator/hub/KeycloakRemoteUserProviderTest.java
@@ -7,14 +7,18 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.GroupResource;
import org.keycloak.admin.client.resource.GroupsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.mockito.Mockito;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
@@ -26,12 +30,25 @@ class KeycloakRemoteUserProviderTest {
private KeycloakRemoteUserProvider keycloakRemoteUserProvider;
private UserRepresentation user1 = Mockito.mock(UserRepresentation.class);
private UserRepresentation user2 = Mockito.mock(UserRepresentation.class);
+
private UserRepresentation syncer = Mockito.mock(UserRepresentation.class);
+ private UserRepresentation hubCliUser = Mockito.mock(UserRepresentation.class);
+
+ private ClientsResource hubCliClientsResource = Mockito.mock(ClientsResource.class);
+
+ private ClientRepresentation hubCliClientRepresentation = Mockito.mock(ClientRepresentation.class);
+
+ private ClientResource hubCliClientResource = Mockito.mock(ClientResource.class);
+
+
@BeforeEach
void setUp() {
var synerConfig = Mockito.mock(SyncerConfig.class);
+ Mockito.when(realm.clients()).thenReturn(hubCliClientsResource);
+ Mockito.when(realm.clients().findByClientId("cryptomatorhub-cli")).thenReturn(List.of());
+
Mockito.when(realm.users()).thenReturn(usersResource);
Mockito.when(user1.getId()).thenReturn("id3000");
@@ -53,7 +70,7 @@ void setUp() {
@Test
@DisplayName("test user listing excludes syncer and returns two users")
- public void testListUser() {
+ void testListUser() {
Mockito.when(usersResource.list(0, KeycloakRemoteUserProvider.MAX_COUNT_PER_REQUEST)).thenReturn(List.of(user1, user2, syncer));
var result = keycloakRemoteUserProvider.users(realm);
@@ -74,9 +91,51 @@ public void testListUser() {
Assertions.assertNull(resultUser2.pictureUrl);
}
+ @Test
+ @DisplayName("test user listing excludes syncer, includes Hub CLI user and returns two users")
+ void testListUserIncludingHubCliUser() {
+ Mockito.when(usersResource.list(0, KeycloakRemoteUserProvider.MAX_COUNT_PER_REQUEST)).thenReturn(List.of(user1, user2, syncer));
+
+ Mockito.when(realm.clients()).thenReturn(hubCliClientsResource);
+
+ List clientRepresentations = Mockito.mock(List.class);
+ Mockito.when(realm.clients().findByClientId("cryptomatorhub-cli")).thenReturn(clientRepresentations);
+ Mockito.when(clientRepresentations.get(0)).thenReturn(hubCliClientRepresentation);
+
+ Mockito.when(hubCliClientRepresentation.getId()).thenReturn("cryptomatorHubCliClientId");
+
+ Mockito.when(realm.clients().get(Mockito.anyString())).thenReturn(hubCliClientResource);
+
+ Mockito.when(hubCliUser.getId()).thenReturn("cryptomatorHubCliUserId");
+ Mockito.when(hubCliUser.getUsername()).thenReturn("cryptomatorHubCliUserUsername");
+ Mockito.when(hubCliClientResource.getServiceAccountUser()).thenReturn(hubCliUser);
+
+ var result = keycloakRemoteUserProvider.users(realm);
+
+ Assertions.assertEquals(3, result.size());
+
+ var resultUser1 = result.get(0);
+ var resultUser2 = result.get(1);
+ var resultUser3 = result.get(2);
+
+ Assertions.assertEquals("id3000", resultUser1.id);
+ Assertions.assertEquals("username3000", resultUser1.name);
+ Assertions.assertEquals("email3000", resultUser1.email);
+ Assertions.assertEquals("picture3000", resultUser1.pictureUrl);
+
+ Assertions.assertEquals("id3001", resultUser2.id);
+ Assertions.assertEquals("username3001", resultUser2.name);
+ Assertions.assertEquals("email3001", resultUser2.email);
+ Assertions.assertNull(resultUser2.pictureUrl);
+
+ Assertions.assertEquals("cryptomatorHubCliUserId", resultUser3.id);
+ Assertions.assertEquals("cryptomatorHubCliUserUsername", resultUser3.name);
+ }
+
+
@Nested
@DisplayName("Test groups")
- public class Groups {
+ class Groups {
private GroupsResource groupsResource = Mockito.mock(GroupsResource.class);
private GroupRepresentation group1 = Mockito.mock(GroupRepresentation.class);
@@ -121,9 +180,9 @@ public void testListGroups() {
Assertions.assertEquals("grpName3001", resultGroup2.name);
Assertions.assertEquals(2, resultGroup2.members.size());
- var membersGroup2 = resultGroup2.members.stream().toList();
- var member1Group2 = (User) membersGroup2.get(1);
- var member2Group2 = (User) membersGroup2.get(0);
+ var membersGroup2 = resultGroup2.members.stream().sorted(Comparator.comparing(a -> a.id)).toList();
+ var member1Group2 = (User) membersGroup2.get(0);
+ var member2Group2 = (User) membersGroup2.get(1);
Assertions.assertEquals("id3000", member1Group2.id);
Assertions.assertEquals("username3000", member1Group2.name);
diff --git a/backend/src/test/java/org/cryptomator/hub/api/AuditLogResourceTest.java b/backend/src/test/java/org/cryptomator/hub/api/AuditLogResourceTest.java
index a783b1de7..c128f55ef 100644
--- a/backend/src/test/java/org/cryptomator/hub/api/AuditLogResourceTest.java
+++ b/backend/src/test/java/org/cryptomator/hub/api/AuditLogResourceTest.java
@@ -30,7 +30,7 @@ public void testGetAuditLogEntries() {
.param("paginationId", 9999L)
.when().get("/auditlog")
.then().statusCode(200)
- .body("id", Matchers.containsInRelativeOrder(comparesEqualTo(4242), comparesEqualTo(3000), comparesEqualTo(2003), comparesEqualTo(2002), comparesEqualTo(2001), comparesEqualTo(2000), comparesEqualTo(1111), comparesEqualTo(201), comparesEqualTo(200), comparesEqualTo(102), comparesEqualTo(101), comparesEqualTo(100), comparesEqualTo(31), comparesEqualTo(30), comparesEqualTo(23), comparesEqualTo(22), comparesEqualTo(21), comparesEqualTo(20), comparesEqualTo(12), comparesEqualTo(11)));
+ .body("id", Matchers.containsInRelativeOrder(comparesEqualTo(4242), comparesEqualTo(3000), comparesEqualTo(2003), comparesEqualTo(2002), comparesEqualTo(2001), comparesEqualTo(2000), comparesEqualTo(1111), comparesEqualTo(201), comparesEqualTo(200), comparesEqualTo(102), comparesEqualTo(101), comparesEqualTo(100), comparesEqualTo(31), comparesEqualTo(30), comparesEqualTo(25), comparesEqualTo(24), comparesEqualTo(23), comparesEqualTo(22), comparesEqualTo(21), comparesEqualTo(20)));
}
@Test
diff --git a/backend/src/test/java/org/cryptomator/hub/api/AuthorityResourceTest.java b/backend/src/test/java/org/cryptomator/hub/api/AuthorityResourceTest.java
index a66e75f71..129a3bf58 100644
--- a/backend/src/test/java/org/cryptomator/hub/api/AuthorityResourceTest.java
+++ b/backend/src/test/java/org/cryptomator/hub/api/AuthorityResourceTest.java
@@ -23,9 +23,6 @@
@DisplayName("Resource /authorities")
public class AuthorityResourceTest {
- @Inject
- AgroalDataSource dataSource;
-
@BeforeAll
public static void beforeAll() {
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
@@ -121,17 +118,5 @@ public void testGetSome() {
.then().statusCode(200)
.body("id", containsInAnyOrder("user1", "group2"));
}
-
- @Test
- @DisplayName("GET /authorities?ids=user1&ids=group2 as user returns 403")
- @TestSecurity(user = "User Name 1", roles = {"user"})
- @OidcSecurity(claims = {
- @Claim(key = "sub", value = "user1")
- })
- public void testGetSomeAsUser() {
- given().param("ids", "user1", "group2")
- .when().get("/authorities")
- .then().statusCode(403);
- }
}
}
\ No newline at end of file
diff --git a/backend/src/test/java/org/cryptomator/hub/api/BillingResourceManagedInstanceTest.java b/backend/src/test/java/org/cryptomator/hub/api/BillingResourceManagedInstanceTest.java
index f978cad0b..c4a85708b 100644
--- a/backend/src/test/java/org/cryptomator/hub/api/BillingResourceManagedInstanceTest.java
+++ b/backend/src/test/java/org/cryptomator/hub/api/BillingResourceManagedInstanceTest.java
@@ -1,6 +1,7 @@
package org.cryptomator.hub.api;
import io.agroal.api.AgroalDataSource;
+import io.quarkus.arc.Arc;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
@@ -9,6 +10,7 @@
import io.quarkus.test.security.oidc.OidcSecurity;
import io.restassured.RestAssured;
import jakarta.inject.Inject;
+import org.cryptomator.hub.license.LicenseHolder;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -35,6 +37,7 @@ public class BillingResourceManagedInstanceTest {
@BeforeAll
public static void beforeAll() {
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
+ Arc.container().instance(LicenseHolder.class).destroy();
}
public static class ManagedInstanceTestProfile implements QuarkusTestProfile {
@@ -47,7 +50,7 @@ public Map getConfigOverrides() {
@Test
@DisplayName("GET /billing returns 401 with empty license managed instance")
public void testGetEmptyManagedInstance() throws SQLException {
- try (var s = dataSource.getConnection().createStatement()) {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
s.execute("""
UPDATE "settings"
SET "hub_id" = '42', "license_key" = null
diff --git a/backend/src/test/java/org/cryptomator/hub/api/BillingResourceTest.java b/backend/src/test/java/org/cryptomator/hub/api/BillingResourceTest.java
index fe56b6b92..dbea7086b 100644
--- a/backend/src/test/java/org/cryptomator/hub/api/BillingResourceTest.java
+++ b/backend/src/test/java/org/cryptomator/hub/api/BillingResourceTest.java
@@ -3,7 +3,7 @@
import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTVerificationException;
import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.mockito.InjectMock;
+import io.quarkus.test.InjectMock;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.oidc.Claim;
import io.quarkus.test.security.oidc.OidcSecurity;
diff --git a/backend/src/test/java/org/cryptomator/hub/api/DeviceResourceTest.java b/backend/src/test/java/org/cryptomator/hub/api/DeviceResourceTest.java
index ddb3bbaba..afd40c3de 100644
--- a/backend/src/test/java/org/cryptomator/hub/api/DeviceResourceTest.java
+++ b/backend/src/test/java/org/cryptomator/hub/api/DeviceResourceTest.java
@@ -9,17 +9,21 @@
import io.restassured.http.ContentType;
import jakarta.inject.Inject;
import org.cryptomator.hub.entities.Device;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
import java.sql.SQLException;
import java.time.Instant;
-import java.util.Set;
import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.when;
+import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
@@ -41,9 +45,11 @@ public static void beforeAll() {
@OidcSecurity(claims = {
@Claim(key = "sub", value = "user1")
})
+ @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AsAuthorzedUser1 {
@Test
+ @Order(1)
@DisplayName("PUT /devices/device1 without DTO returns 400")
public void testCreateNoDeviceDto() {
given().contentType(ContentType.JSON).body("")
@@ -52,58 +58,131 @@ public void testCreateNoDeviceDto() {
}
@Test
- @DisplayName("PUT /devices/ with DTO returns 400")
+ @Order(1)
+ @DisplayName("PUT /devices/ with DTO returns 400")
public void testCreateNoDeviceId() {
- var deviceDto = new DeviceResource.DeviceDto("device1", "Computer 1", Device.Type.DESKTOP, "publickey1", "", Set.of(), Instant.parse("2020-02-20T20:20:20Z"));
+ var deviceDto = new DeviceResource.DeviceDto("device1", "Computer 1", Device.Type.DESKTOP, "publickey1", "jwe.jwe.jwe.user1.device1", "user1", Instant.parse("2020-02-20T20:20:20Z"));
given().contentType(ContentType.JSON).body(deviceDto)
- .when().put("/devices/{deviceId}", "\u0020") //a whitespace
+ .when().put("/devices/{deviceId}", " ") //a whitespace
.then().statusCode(400);
}
@Test
- @DisplayName("PUT /devices/device1 returns 409")
- public void testCreate1() {
- var deviceDto = new DeviceResource.DeviceDto("device1", "Computer 1", Device.Type.DESKTOP, "publickey1", "owner1", Set.of(), Instant.parse("2020-02-20T20:20:20Z"));
+ @Order(1)
+ @DisplayName("GET /devices/device1 returns 200")
+ public void testGet1() {
+ given().when().get("/devices/{deviceId}", "device1")
+ .then().statusCode(200)
+ .body("id", is("device1"))
+ .body("name", is("Computer 1"))
+ .body("userPrivateKey", is("jwe.jwe.jwe.user1.device1"));
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("GET /devices/device2 returns 404 (owned by other user)")
+ public void testGet2() {
+ given().when().get("/devices/{deviceId}", "device2")
+ .then().statusCode(404);
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("GET /devices/noSuchDevice returns 404 (no such device)")
+ public void testGetNonExistingDeviceToken() {
+ when().get("/devices/{deviceId}", "noSuchDevice")
+ .then().statusCode(404);
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("PUT /devices/device999 returns 201 (creating new device)")
+ public void testCreate999() throws SQLException {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ s.execute("""
+ INSERT INTO "device_legacy" ("id", "owner_id", "name", "type", "publickey", "creation_time")
+ VALUES
+ ('device999', 'user1', 'Computer 999', 'DESKTOP', 'publickey999', '2020-02-20 20:20:20')
+ """);
+ }
+
+ var deviceDto = new DeviceResource.DeviceDto("device999", "Computer 999", Device.Type.DESKTOP, "publickey999", "jwe.jwe.jwe.user1.device999", "user1", Instant.parse("2020-02-20T20:20:20Z"));
given().contentType(ContentType.JSON).body(deviceDto)
- .when().put("/devices/{deviceId}", "device1")
- .then().statusCode(409);
+ .when().put("/devices/{deviceId}", "device999")
+ .then().statusCode(201);
+
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ var rs = s.executeQuery("""
+ SELECT * FROM "device_legacy" WHERE "id" = 'device999';
+ """);
+ Assertions.assertFalse(rs.next());
+ }
}
@Test
- @DisplayName("PUT /devices/deviceX returns 409 due to non-unique name")
+ @Order(2)
+ @DisplayName("PUT /devices/deviceX returns 201 (creating new device with same name as device1)")
public void testCreateX() {
- var deviceDto = new DeviceResource.DeviceDto("deviceX", "Computer 1", Device.Type.DESKTOP, "publickey1", "owner1", Set.of(), Instant.parse("2020-02-20T20:20:20Z"));
+ var deviceDto = new DeviceResource.DeviceDto("deviceX", "Computer 1", Device.Type.DESKTOP, "publickey1", "jwe.jwe.jwe.user1.deviceX", "user1", Instant.parse("2020-02-20T20:20:20Z"));
given().contentType(ContentType.JSON).body(deviceDto)
.when().put("/devices/{deviceId}", "deviceX")
+ .then().statusCode(201);
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("PUT /devices/deviceY returns 409 (creating new device with the key of deviceX conflicts)")
+ public void testCreateYWithKeyOfDeviceX() {
+ var deviceDto = new DeviceResource.DeviceDto("deviceY", "Computer 2", Device.Type.DESKTOP, "publickey1", "jwe.jwe.jwe.user1.deviceX", "user1", Instant.parse("2020-02-20T20:20:20Z"));
+
+ given().contentType(ContentType.JSON).body(deviceDto)
+ .when().put("/devices/{deviceId}", "deviceY")
.then().statusCode(409);
}
@Test
- @DisplayName("PUT /devices/device999 returns 201")
- public void testCreate2() throws SQLException {
- var deviceDto = new DeviceResource.DeviceDto("device999", "Computer 999", Device.Type.DESKTOP, "publickey999", "owner1", Set.of(), Instant.parse("2020-02-20T20:20:20Z"));
+ @Order(4)
+ @DisplayName("GET /devices/device999 returns 200")
+ public void testGet999AfterCreate() {
+ given().when().get("/devices/{deviceId}", "device999")
+ .then().statusCode(200)
+ .body("id", is("device999"))
+ .body("name", is("Computer 999"));
+ }
+
+ @Test
+ @Order(5)
+ @DisplayName("PUT /devices/device999 returns 201 (updating existing device)")
+ public void testUpdate1() {
+ var deviceDto = new DeviceResource.DeviceDto("device999", "Computer 999 got a new name", Device.Type.DESKTOP, "publickey999", "jwe.jwe.jwe.user1.device999", "user1", Instant.parse("2020-02-20T20:20:20Z"));
given().contentType(ContentType.JSON).body(deviceDto)
.when().put("/devices/{deviceId}", "device999")
.then().statusCode(201);
+ }
- try (var s = dataSource.getConnection().createStatement()) {
- s.execute("""
- DELETE FROM "device" WHERE "id" = 'device999';
- """);
- }
+ @Test
+ @Order(6)
+ @DisplayName("GET /devices/device999 returns 200 (with updated name)")
+ public void testGet999AfterUpdate() {
+ given().when().get("/devices/{deviceId}", "device999")
+ .then().statusCode(200)
+ .body("id", is("device999"))
+ .body("name", is("Computer 999 got a new name"));
}
@Test
- @DisplayName("DELETE /devices/ returns 400")
+ @Order(7)
+ @DisplayName("DELETE /devices/ returns 400")
public void testDeleteNoDeviceId() {
- when().delete("/devices/{deviceId}", "\u0020") //a whitespace
+ when().delete("/devices/{deviceId}", " ") //a whitespace
.then().statusCode(400);
}
@Test
+ @Order(7)
@DisplayName("DELETE /devices/device0 returns 404")
public void testDeleteNotExisting() {
when().delete("/devices/{deviceId}", "device0") //
@@ -111,6 +190,7 @@ public void testDeleteNotExisting() {
}
@Test
+ @Order(7)
@DisplayName("DELETE /devices/device2 returns 404")
public void testDeleteNotOwner() {
when().delete("/devices/{deviceId}", "device2") //
@@ -118,15 +198,9 @@ public void testDeleteNotOwner() {
}
@Test
+ @Order(7)
@DisplayName("DELETE /devices/device999 returns 204")
- public void testDeleteValid() throws SQLException {
- try (var s = dataSource.getConnection().createStatement()) {
- s.execute("""
- INSERT INTO "device" ("id", "owner_id", "name", "type", "publickey", "creation_time")
- VALUES ('device999', 'user1', 'To Be Deleted', 'DESKTOP', 'publickey1', '2020-02-20 20:20:20');
- """);
- }
-
+ public void testDeleteValid() {
when().delete("/devices/{deviceId}", "device999") //
.then().statusCode(204);
}
@@ -141,7 +215,7 @@ public class AsAnonymous {
@Test
@DisplayName("PUT /devices/device1 returns 401")
public void testCreate1() {
- var deviceDto = new DeviceResource.DeviceDto("device1", "Computer 1", Device.Type.DESKTOP, "publickey1", "user1", Set.of(), Instant.parse("2020-02-20T20:20:20Z"));
+ var deviceDto = new DeviceResource.DeviceDto("device1", "Device 1", Device.Type.BROWSER, "publickey1", "jwe.jwe.jwe.user1.device1", "user1", Instant.parse("2020-02-20T20:20:20Z"));
given().contentType(ContentType.JSON).body(deviceDto)
.when().put("/devices/{deviceId}", "device1")
diff --git a/backend/src/test/java/org/cryptomator/hub/api/GroupsResourceTest.java b/backend/src/test/java/org/cryptomator/hub/api/GroupsResourceTest.java
new file mode 100644
index 000000000..2ef886ceb
--- /dev/null
+++ b/backend/src/test/java/org/cryptomator/hub/api/GroupsResourceTest.java
@@ -0,0 +1,103 @@
+package org.cryptomator.hub.api;
+
+import io.agroal.api.AgroalDataSource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
+import io.quarkus.test.security.oidc.Claim;
+import io.quarkus.test.security.oidc.OidcSecurity;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.sql.SQLException;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.RestAssured.when;
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.empty;
+
+@QuarkusTest
+@DisplayName("Resource /groups")
+public class GroupsResourceTest {
+
+ @Inject
+ AgroalDataSource dataSource;
+
+ @BeforeAll
+ public static void beforeAll() {
+ RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
+ }
+
+ @Nested
+ @DisplayName("As user1")
+ @TestSecurity(user = "User Name 1", roles = {"user"})
+ @OidcSecurity(claims = {
+ @Claim(key = "sub", value = "user1")
+ })
+ public class AsAuthorzedUser1 {
+
+ @Test
+ @DisplayName("GET /groups returns 200")
+ public void testGetAll() {
+ when().get("/groups")
+ .then().statusCode(200)
+ .body("id", hasItems("group1", "group2"));
+ }
+
+ @Test
+ @DisplayName("GET /groups/group1/effective-members contains direct and subgroup members")
+ public void testGetEffectiveUsers() throws SQLException {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ s.execute("""
+ INSERT INTO "authority" ("id", "type", "name")
+ VALUES
+ ('user999', 'USER', 'User 999'),
+ ('group999', 'GROUP', 'Group 999');
+
+ INSERT INTO "user_details" ("id") VALUES ('user999');
+ INSERT INTO "group_details" ("id") VALUES ('group999');
+
+ INSERT INTO "group_membership" ("group_id", "member_id")
+ VALUES
+ ('group999', 'user999'),
+ ('group1', 'group999');
+ """);
+ }
+
+ when().get("/groups/{groupId}/effective-members", "group1")
+ .then().statusCode(200)
+ .body("id", hasItems("user1", "user999"));
+
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ s.execute("""
+ DELETE FROM "authority" WHERE "id" = 'user999' OR "id" = 'group999';
+ """);
+ }
+ }
+
+ }
+
+ @Nested
+ @DisplayName("As unauthenticated user")
+ public class AsAnonymous {
+
+ @DisplayName("401 Unauthorized")
+ @ParameterizedTest(name = "{0} {1}")
+ @CsvSource(value = {
+ "GET, /users"
+ })
+ public void testGet(String method, String path) {
+ when().request(method, path)
+ .then().statusCode(401);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/backend/src/test/java/org/cryptomator/hub/api/UsersResourceTest.java b/backend/src/test/java/org/cryptomator/hub/api/UsersResourceTest.java
index 66128368c..150623494 100644
--- a/backend/src/test/java/org/cryptomator/hub/api/UsersResourceTest.java
+++ b/backend/src/test/java/org/cryptomator/hub/api/UsersResourceTest.java
@@ -59,18 +59,7 @@ public void testGetMe2() {
when().get("/users/me?withDevices=true")
.then().statusCode(200)
.body("id", is("user1"))
- .body("devices.id", hasItems("device1"))
- .body("devices.accessTo.flatten()", empty());
- }
-
- @Test
- @DisplayName("GET /users/me?withDevices=true&withAccessibleVaults=true returns 200")
- public void testGetMe3() {
- when().get("/users/me?withDevices=true&withAccessibleVaults=true")
- .then().statusCode(200)
- .body("id", is("user1"))
- .body("devices.id", hasItems("device1"))
- .body("devices.accessTo.id.flatten()", hasItems(equalToIgnoringCase("7E57C0DE-0000-4000-8000-000100001111")));
+ .body("devices.id", hasItems("device1"));
}
@Test
diff --git a/backend/src/test/java/org/cryptomator/hub/api/VaultResourceTest.java b/backend/src/test/java/org/cryptomator/hub/api/VaultResourceTest.java
index 7dde2b88d..4383143f3 100644
--- a/backend/src/test/java/org/cryptomator/hub/api/VaultResourceTest.java
+++ b/backend/src/test/java/org/cryptomator/hub/api/VaultResourceTest.java
@@ -10,9 +10,9 @@
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import jakarta.inject.Inject;
+import jakarta.transaction.Transactional;
import jakarta.validation.Validator;
-import org.cryptomator.hub.entities.Device;
-import org.cryptomator.hub.filters.VaultAdminOnlyFilterProvider;
+import org.cryptomator.hub.entities.Vault;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterAll;
@@ -28,17 +28,19 @@
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
+import java.security.GeneralSecurityException;
import java.security.KeyFactory;
+import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.interfaces.ECPrivateKey;
+import java.security.spec.ECGenParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.sql.SQLException;
import java.time.Instant;
import java.util.Base64;
import java.util.Map;
-import java.util.Set;
import java.util.UUID;
import static io.restassured.RestAssured.given;
@@ -55,10 +57,6 @@
@DisplayName("Resource /vaults")
public class VaultResourceTest {
- private static String vault1AdminJWT;
- private static String vault2AdminJWT;
- private static String vaultArchivedAdminJWT;
-
@Inject
AgroalDataSource dataSource;
@@ -66,16 +64,8 @@ public class VaultResourceTest {
Validator validator;
@BeforeAll
- public static void beforeAll() throws NoSuchAlgorithmException, InvalidKeySpecException {
+ public static void beforeAll() {
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
-
- var algorithmVault1 = Algorithm.ECDSA384((ECPrivateKey) getPrivateKey("MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAa57e0Q/KAqmIVOVcWX7b+Sm5YVNRUx8W7nc4wk1IBj2QJmsj+MeShQRHG4ozTE9KhZANiAASVL4lbdVoG9Wv0YpkafXf31YNN3rVD1/BAyZm4EYBg92X+taTvTlBjpaGWZuiSYRW9r+YQdKg1D3zAWb0UEKrOHjkgZ38MbBnTheGLlqH7VspuRWG12zydm0dF1ImiRik="));
- var algorithmVault2 = Algorithm.ECDSA384((ECPrivateKey) getPrivateKey("MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCAHpFQ62QnGCEvYh/pE9QmR1C9aLcDItRbslbmhen/h1tt8AyMhskeenT+rAyyPhGhZANiAAQLW5ZJePZzMIPAxMtZXkEWbDF0zo9f2n4+T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw8lE5IPUWpgu553SteKigiKLUPeNpbqmYZUkWGh3MLfVzLmx85ii2vMU="));
- var algorithmVaultArchived = Algorithm.ECDSA384((ECPrivateKey) getPrivateKey("MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCAHpFQ62QnGCEvYh/pE9QmR1C9aLcDItRbslbmhen/h1tt8AyMhskeenT+rAyyPhGhZANiAAQLW5ZJePZzMIPAxMtZXkEWbDF0zo9f2n4+T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw8lE5IPUWpgu553SteKigiKLUPeNpbqmYZUkWGh3MLfVzLmx85ii2vMU="));
-
- vault1AdminJWT = JWT.create().withHeader(Map.of("vaultId", "7E57C0DE-0000-4000-8000-000100001111")).withIssuedAt(Instant.now()).sign(algorithmVault1);
- vault2AdminJWT = JWT.create().withHeader(Map.of("vaultId", "7E57C0DE-0000-4000-8000-000100002222")).withIssuedAt(Instant.now()).sign(algorithmVault2);
- vaultArchivedAdminJWT = JWT.create().withHeader(Map.of("vaultId", "7E57C0DE-0000-4000-8000-0001AAAAAAAA")).withIssuedAt(Instant.now()).sign(algorithmVaultArchived);
}
private static PrivateKey getPrivateKey(String keyBytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
@@ -111,9 +101,9 @@ public void testValidDto() {
public class AsAuthorizedUser1 {
@Test
- @DisplayName("GET /vaults returns 200")
+ @DisplayName("GET /vaults/accessible returns 200")
public void testGetSharedOrOwnedNotArchived() {
- when().get("/vaults")
+ when().get("/vaults/accessible")
.then().statusCode(200)
.body("id", hasItems(equalToIgnoringCase("7E57C0DE-0000-4000-8000-000100001111"), equalToIgnoringCase("7E57C0DE-0000-4000-8000-000100002222")));
}
@@ -134,42 +124,113 @@ public void testGetVault2() {
}
@Test
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/members returns 401")
- public void testGetAccess() {
- when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100001111")
- .then().statusCode(401);
- }
-
- @Test
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/keys/device1 returns 200 using user access")
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/access-token returns 200 using user access")
public void testUnlock1() {
- when().get("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100001111", "device1")
+ when().get("/vaults/{vaultId}/access-token", "7E57C0DE-0000-4000-8000-000100001111")
.then().statusCode(200)
- .body(is("jwe1"));
+ .body(is("jwe.jwe.jwe.vault1.user1"));
}
@Test
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/keys/device3 returns 200 using group access")
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/access-token returns 200 using group access")
public void testUnlock2() {
- when().get("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100002222", "device3")
+ when().get("/vaults/{vaultId}/access-token", "7E57C0DE-0000-4000-8000-000100002222")
.then().statusCode(200)
- .body(is("jwe3"));
+ .body(is("jwe.jwe.jwe.vault2.user1"));
}
@Test
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/keys/noSuchDevice returns 404")
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/access-token returns 200 using user access with evenIfArchived set")
public void testUnlock3() {
- when().get("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100001111", "noSuchDevice")
- .then().statusCode(404);
+ when().get("/vaults/{vaultId}/access-token?evenIfArchived=true", "7E57C0DE-0000-4000-8000-000100001111")
+ .then().statusCode(200)
+ .body(is("jwe.jwe.jwe.vault1.user1"));
+ }
+
+ @Test
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-00010000AAAA/access-token returns 410 for archived vaults")
+ public void testUnlockArchived1() {
+ when().get("/vaults/{vaultId}/access-token", "7E57C0DE-0000-4000-8000-00010000AAAA")
+ .then().statusCode(410);
+ }
+
+ @Test
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-00010000AAAA/access-token returns 410 for archived vaults with evenIfArchived set to false")
+ public void testUnlockArchived2() {
+ when().get("/vaults/{vaultId}/access-token?evenIfArchived=false", "7E57C0DE-0000-4000-8000-00010000AAAA")
+ .then().statusCode(410);
}
@Test
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/keys/device2 returns 403")
- public void testUnlock4() {
- when().get("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100001111", "device2")
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-00010000AAAA/access-token returns 403 for archived vaults with evenIfArchived set to true")
+ public void testUnlockArchived3() throws SQLException {
+ when().get("/vaults/{vaultId}/access-token?evenIfArchived=true", "7E57C0DE-0000-4000-8000-00010000AAAA")
.then().statusCode(403);
}
+ @Nested
+ @DisplayName("legacy unlock")
+ @TestSecurity(user = "User Name 1", roles = {"user"})
+ @OidcSecurity(claims = {
+ @Claim(key = "sub", value = "user1")
+ })
+ public class LegacyUnlock {
+
+ @Test
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/keys/legacyDevice1 returns 200 using user access")
+ public void testUnlock1() {
+ when().get("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100001111", "legacyDevice1")
+ .then().statusCode(200)
+ .body(is("legacy.jwe.jwe.vault1.device1"));
+ }
+
+ @Test
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/keys/legacyDevice3 returns 200 using group access")
+ public void testUnlock2() {
+ when().get("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100002222", "legacyDevice3")
+ .then().statusCode(200)
+ .body(is("legacy.jwe.jwe.vault2.device3"));
+ }
+
+ @Test
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/keys/noSuchDevice returns 403") // legacy unlock must not encourage to register a legacy device by responding with 404 here
+ public void testUnlock3() {
+ when().get("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100001111", "noSuchDevice")
+ .then().statusCode(403);
+ }
+
+ @Test
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/keys/legacyDevice2 returns 403")
+ public void testUnlock4() {
+ when().get("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100001111", "legacyDevice2")
+ .then().statusCode(403);
+ }
+
+ @Test
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-00010000AAAA/keys/someDevice returns 410 for archived vaults")
+ public void testUnlockArchived() {
+ when().get("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-00010000AAAA", "legacyDevice1")
+ .then().statusCode(410);
+ }
+
+ }
+
+ }
+
+ @Nested
+ @DisplayName("As user2")
+ @TestSecurity(user = "User Name 2", roles = {"user"})
+ @OidcSecurity(claims = {
+ @Claim(key = "sub", value = "user2")
+ })
+ public class AsAuthorizedUser2 {
+
+ @Test
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/access-token returns 449, because user2 is not initialized")
+ public void testUnlock() {
+ when().get("/vaults/{vaultId}/access-token", "7E57C0DE-0000-4000-8000-000100001111")
+ .then().statusCode(449);
+ }
}
@Nested
@@ -178,26 +239,67 @@ public void testUnlock4() {
@OidcSecurity(claims = {
@Claim(key = "sub", value = "user1")
})
- public class GetMembers {
+ @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+ public class CreateVaults {
@Test
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/members returns 200")
- public void testGetAccess1() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100001111")
- .then().statusCode(200)
- .body("id", hasItems("user1", "user2"));
+ @Order(1)
+ @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100003333 returns 201")
+ public void testCreateVault1() {
+ var uuid = UUID.fromString("7E57C0DE-0000-4000-8000-000100003333");
+ var vaultDto = new VaultResource.VaultDto(uuid, "My Vault", "Test vault 3", false, Instant.parse("2112-12-21T21:12:21Z"), "masterkey3", 42, "NaCl", "authPubKey3", "authPrvKey3");
+
+ given().contentType(ContentType.JSON).body(vaultDto)
+ .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-000100003333")
+ .then().statusCode(201)
+ .body("id", equalToIgnoringCase("7E57C0DE-0000-4000-8000-000100003333"))
+ .body("name", equalTo("My Vault"))
+ .body("description", equalTo("Test vault 3"))
+ .body("archived", equalTo(false));
}
@Test
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/members returns 400 when using incorrect 'Cryptomator-Vault-Admin-Authorization' header")
- public void testGetAccess2() {
- var vault2ButWrongKeyAdminJWT = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzM4NCIsInZhdWx0SWQiOiJ2YXVsdDIifQ.e30.cGZDCqzJQgcBHNVPcmBc8JfeGzUf3CHUrwSAMwOA0Dcy9aUZvsAm1dr1MKzuPW_UFHRfMnNi2EwASOA6t-vPWvPFolAHFn5REt2Y9Aw9mIz-qxSBLpz6OMZD16tysQcd";
-
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2ButWrongKeyAdminJWT)
- .when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100002222")
+ @Order(1)
+ @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-BADBADBADBAD returns 400 due to malformed request body")
+ public void testCreateVault2() {
+ given().contentType(ContentType.JSON)
+ .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-BADBADBADBAD") // invalid body (expected json)
.then().statusCode(400);
}
+
+ @Test
+ @Order(1)
+ @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100004444 returns 201 ignoring archived flag")
+ public void testCreateVault3() {
+ var uuid = UUID.fromString("7E57C0DE-0000-4000-8000-000100004444");
+ var vaultDto = new VaultResource.VaultDto(uuid, "My Vault", "Test vault 4", true, Instant.parse("2112-12-21T21:12:21Z"), "masterkey4", 42, "NaCl", "authPubKey4", "authPrvKey4");
+
+ given().contentType(ContentType.JSON).body(vaultDto)
+ .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-000100004444")
+ .then().statusCode(201)
+ .body("id", equalToIgnoringCase("7E57C0DE-0000-4000-8000-000100004444"))
+ .body("name", equalTo("My Vault"))
+ .body("description", equalTo("Test vault 4"))
+ .body("archived", equalTo(false));
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100003333 returns 200, updating only name, description and archive flag")
+ public void testUpdateVault() {
+ var uuid = UUID.fromString("7E57C0DE-0000-4000-8000-000100003333");
+ var vaultDto = new VaultResource.VaultDto(uuid, "VaultUpdated", "Vault updated.", true, Instant.parse("2222-11-11T11:11:11Z"), "doNotUpdate", 27, "doNotUpdate", "doNotUpdate", "doNotUpdate");
+ given().contentType(ContentType.JSON)
+ .body(vaultDto)
+ .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-000100003333")
+ .then().statusCode(200)
+ .body("id", equalToIgnoringCase("7E57C0DE-0000-4000-8000-000100003333"))
+ .body("name", equalTo("VaultUpdated"))
+ .body("description", equalTo("Vault updated."))
+ .body("archived", equalTo(true))
+ .body("creationTime", not("2222-11-11T11:11:11Z"));
+ }
+
}
@Nested
@@ -210,66 +312,95 @@ public void testGetAccess2() {
public class GrantAccess {
@Test
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100001111/keys/device3 returns 201")
- public void testGrantAccess1() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .contentType(ContentType.TEXT).body("jwe.jwe.jwe.vault1.device3")
- .when().put("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100001111", "device3")
- .then().statusCode(201);
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100001111/access-tokens returns 404 for [user998, user999, user666]")
+ public void testGrantAccess0() throws SQLException {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ s.execute("""
+ INSERT INTO "authority" ("id", "type", "name") VALUES ('user998', 'USER', 'User 998');
+ INSERT INTO "authority" ("id", "type", "name") VALUES ('user999', 'USER', 'User 999');
+ INSERT INTO "user_details" ("id") VALUES ('user998');
+ INSERT INTO "user_details" ("id") VALUES ('user999');
+ INSERT INTO "group_membership" ("group_id", "member_id") VALUES ('group2', 'user998');
+ INSERT INTO "group_membership" ("group_id", "member_id") VALUES ('group2', 'user999');
+ """);
+ }
+
+ given().contentType(ContentType.JSON).body(Map.of("user998", "jwe.jwe.jwe.vault1.user998", "user999", "jwe.jwe.jwe.vault1.user999", "user666", "jwe.jwe.jwe.vault1.user666"))
+ .when().post("/vaults/{vaultId}/access-tokens/", "7E57C0DE-0000-4000-8000-000100001111")
+ .then().statusCode(404);
+
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ s.execute("""
+ DELETE FROM "authority" WHERE "id" = 'user998';
+ DELETE FROM "authority" WHERE "id" = 'user999';
+ """);
+ }
+ }
+
+ @Test
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100001111/access-tokens returns 200 for [user998, user999]")
+ public void testGrantAccess1() throws SQLException {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ s.execute("""
+ INSERT INTO "authority" ("id", "type", "name") VALUES ('user998', 'USER', 'User 998');
+ INSERT INTO "authority" ("id", "type", "name") VALUES ('user999', 'USER', 'User 999');
+ INSERT INTO "user_details" ("id") VALUES ('user998');
+ INSERT INTO "user_details" ("id") VALUES ('user999');
+ INSERT INTO "group_membership" ("group_id", "member_id") VALUES ('group2', 'user998');
+ INSERT INTO "group_membership" ("group_id", "member_id") VALUES ('group2', 'user999');
+ """);
+ }
+
+ given().contentType(ContentType.JSON).body(Map.of("user998", "jwe.jwe.jwe.vault1.user998", "user999", "jwe.jwe.jwe.vault1.user999"))
+ .when().post("/vaults/{vaultId}/access-tokens/", "7E57C0DE-0000-4000-8000-000100001111")
+ .then().statusCode(200);
+
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ s.execute("""
+ DELETE FROM "authority" WHERE "id" = 'user998';
+ DELETE FROM "authority" WHERE "id" = 'user999';
+ """);
+ }
}
@Test
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100001111/keys/device1 returns 409 due to user access already granted")
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100001111/access-tokens returns 200 for user1")
public void testGrantAccess2() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .contentType(ContentType.TEXT).body("jwe1.jwe1.jwe1.jwe1.jwe1")
- .when().put("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100001111", "device1")
- .then().statusCode(409);
+ given().contentType(ContentType.JSON).body(Map.of("user1", "jwe.jwe.jwe.vault1.user1"))
+ .when().post("/vaults/{vaultId}/access-tokens/", "7E57C0DE-0000-4000-8000-000100001111")
+ .then().statusCode(200);
}
@Test
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100002222/keys/device3 returns 409 due to group access already granted")
- @TestSecurity(user = "User Name 2", roles = {"user"}) //we switch here for easy usage
- @OidcSecurity(claims = {
- @Claim(key = "sub", value = "user2")
- })
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-BADBADBADBAD/access-tokens returns 403 (not owning this vault)")
public void testGrantAccess3() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .contentType(ContentType.TEXT).body("jwe3.jwe3.jwe3.jwe3.jwe3")
- .when().put("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100002222", "device3")
- .then().statusCode(409);
+ given().contentType(ContentType.JSON).body(Map.of("user1", "jwe.jwe.jwe.vault666.user1"))
+ .when().post("/vaults/{vaultId}/access-tokens/", "7E57C0DE-0000-4000-8000-BADBADBADBAD")
+ .then().statusCode(403);
}
@Test
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100001111/keys/nonExistingDevice returns 404")
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100001111/access-tokens returns 404 for nonExistingUser")
public void testGrantAccess4() {
- given()
- .header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .contentType(ContentType.TEXT).body("jwe3.jwe3.jwe3.jwe3.jwe3")
- .when().put("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100001111", "nonExistingDevice")
+ given().contentType(ContentType.JSON).body(Map.of("user666", "jwe.jwe.jwe.vault1.user666"))
+ .when().post("/vaults/{vaultId}/access-tokens/", "7E57C0DE-0000-4000-8000-000100001111")
.then().statusCode(404);
}
@Test
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-0001AAAAAAAA/keys/someDevice returns 410")
- public void testGrantAccessArchived() {
- given()
- .header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vaultArchivedAdminJWT)
- .contentType(ContentType.TEXT).body("jwe3.jwe3.jwe3.jwe3.jwe3")
- .when().put("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-0001AAAAAAAA", "someDevice")
- .then().statusCode(410);
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100001111/access-tokens returns 400 for empty body")
+ public void testGrantAccess5() {
+ given().contentType(ContentType.JSON).body(Map.of())
+ .when().post("/vaults/{vaultId}/access-tokens/", "7E57C0DE-0000-4000-8000-000100001111")
+ .then().statusCode(400);
}
-
- @AfterAll
- public void deleteData() throws SQLException {
- try (var s = dataSource.getConnection().createStatement()) {
- s.execute("""
- DELETE FROM "access_token"
- WHERE "device_id" = 'device3'
- AND "vault_id" = '7E57C0DE-0000-4000-8000-000100001111';
- """);
- }
+ @Test
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-00010000AAAA/access-tokens returns 410")
+ public void testGrantAccessArchived() {
+ given().contentType(ContentType.JSON).body(Map.of("user1", "jwe.jwe.jwe.vaultAAA.user1"))
+ .when().post("/vaults/{vaultId}/access-tokens/", "7E57C0DE-0000-4000-8000-00010000AAAA")
+ .then().statusCode(410);
}
}
@@ -287,155 +418,133 @@ public class ManageAccessAsUser2 {
@Order(1)
@DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100002222/users/user9999 returns 404 - no such user")
public void addNonExistingUser() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .when().put("/vaults/{vaultId}/users/{userId}", "7E57C0DE-0000-4000-8000-000100002222", "user9999")
+ given().when().put("/vaults/{vaultId}/users/{userId}", "7E57C0DE-0000-4000-8000-000100002222", "user9999")
.then().statusCode(404);
}
@Test
@Order(2)
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-BADBADBADBAD/users/user2 returns 404 - no such vault")
- public void addUserToNonExistingVault() throws NoSuchAlgorithmException, InvalidKeySpecException {
- var algorithmVault = Algorithm.ECDSA384((ECPrivateKey) getPrivateKey("MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCAHpFQ62QnGCEvYh/pE9QmR1C9aLcDItRbslbmhen/h1tt8AyMhskeenT+rAyyPhGhZANiAAQLW5ZJePZzMIPAxMtZXkEWbDF0zo9f2n4+T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw8lE5IPUWpgu553SteKigiKLUPeNpbqmYZUkWGh3MLfVzLmx85ii2vMU="));
- var vaultAdminJWT = JWT.create().withHeader(Map.of("vaultId", "7E57C0DE-0000-4000-8000-BADBADBADBAD")).withIssuedAt(Instant.now()).sign(algorithmVault);
-
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vaultAdminJWT)
- .when().put("/vaults/{vaultId}/users/{userId}", "7E57C0DE-0000-4000-8000-BADBADBADBAD", "user2")
- .then().statusCode(404);
- }
-
- @Test
- @Order(3)
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100002222/users/user9999 returns 401 - unauthenticated")
- public void addNonExistingUserUnauthenticated() {
- when().put("/vaults/{vaultId}/users/{userId}", "7E57C0DE-0000-4000-8000-000100002222", "user9999")
- .then().statusCode(401);
+ @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-BADBADBADBAD/users/user2 returns 403 - not owning a nonexisting vault")
+ public void addUserToNonExistingVault() {
+ given().when().put("/vaults/{vaultId}/users/{userId}", "7E57C0DE-0000-4000-8000-BADBADBADBAD", "user2")
+ .then().statusCode(403);
}
@Test
@Order(4)
@DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/members does not contain user2")
- public void getAccess1() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100002222")
+ public void getMembersOfVault2a() {
+ given().when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100002222")
.then().statusCode(200)
.body("users.id", not(hasItems("user2")));
}
+ @Test
+ @Order(4)
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/members returns 403")
+ public void getMembersOfVault1() {
+ when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100001111")
+ .then().statusCode(403);
+ }
+
@Test
@Order(5)
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/devices-requiring-access-grant does not contains device2")
- public void testGetDevicesRequiringAccess1() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .when().get("/vaults/{vaultId}/devices-requiring-access-grant", "7E57C0DE-0000-4000-8000-000100002222")
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/users-requiring-access-grant does contains user2 via group membership")
+ public void testGetUsersRequiringAccess1() throws SQLException {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ s.execute("""
+ UPDATE
+ "user_details" SET publickey='public2', privatekey='private2', setupcode='setup2'
+ WHERE id='user2';
+ """);
+ }
+
+ given().when().get("/vaults/{vaultId}/users-requiring-access-grant", "7E57C0DE-0000-4000-8000-000100002222")
.then().statusCode(200)
- .body("id", not(hasItems("device2")));
+ .body("id", hasItems("user2"));
+
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ s.execute("""
+ UPDATE
+ "user_details" SET publickey=NULL, privatekey=NULL, setupcode=NULL
+ WHERE id='user2';
+ """);
+ }
}
@Test
@Order(6)
@DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100002222/members/user2 returns 201")
- public void testGrantAccess() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .when().put("/vaults/{vaultId}/users/{userId}", "7E57C0DE-0000-4000-8000-000100002222", "user2")
+ public void testGrantDirectAccessToSelf() {
+ given().when().put("/vaults/{vaultId}/users/{userId}", "7E57C0DE-0000-4000-8000-000100002222", "user2")
.then().statusCode(201);
}
@Test
@Order(7)
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/members does contain user2")
- public void getMembers2() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100002222")
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/members does contain user2 directly")
+ public void getMembersOfVault2b() {
+ given().when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100002222")
.then().statusCode(200)
.body("id", hasItems("user2"));
}
- @Test
- @Order(8)
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100002222/users/user2 returns 409 - user2 already direct member of vault2")
- public void testGrantAccessAgain() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .when().put("/vaults/{vaultId}/users/{usersId}", "7E57C0DE-0000-4000-8000-000100002222", "user2")
- .then().statusCode(409);
- }
-
- @Test
- @Order(9)
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100002222/groups/group1 returns 409 - group1 already direct member of vault2")
- public void testAddingMemberAgainFails() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .when().put("/vaults/{vaultId}/groups/{groupId}", "7E57C0DE-0000-4000-8000-000100002222", "group1")
- .then().statusCode(409);
- }
-
@Test
@Order(10)
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/devices-requiring-access-grant contains device2")
- public void testGetDevicesRequiringAccess2() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .when().get("/vaults/{vaultId}/devices-requiring-access-grant", "7E57C0DE-0000-4000-8000-000100002222")
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/users-requiring-access-grant contains user2")
+ public void testGetUsersRequiringAccess2() throws SQLException {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ s.execute("""
+ UPDATE
+ "user_details" SET publickey='public2', privatekey='private2', setupcode='setup2'
+ WHERE id='user2';
+ """);
+ }
+
+ given().when().get("/vaults/{vaultId}/users-requiring-access-grant", "7E57C0DE-0000-4000-8000-000100002222")
.then().statusCode(200)
- .body("id", hasItems("device2"));
+ .body("id", hasItems("user2"));
+
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ s.execute("""
+ UPDATE
+ "user_details" SET publickey=NULL, privatekey=NULL, setupcode=NULL
+ WHERE id='user2';
+ """);
+ }
}
@Test
@Order(11)
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100002222/keys/device2 returns 201")
- public void testGrantAccess1() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .given().contentType(ContentType.TEXT).body("jwe.jwe.jwe.vault2.device2")
- .when().put("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100002222", "device2")
- .then().statusCode(201);
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100002222/access-tokens for user2 returns 200")
+ public void testGrantAccess() {
+ given().contentType(ContentType.JSON).body(Map.of("user2", "jwe.jwe.jwe.vault2.user2"))
+ .when().post("/vaults/{vaultId}/access-tokens/", "7E57C0DE-0000-4000-8000-000100002222")
+ .then().statusCode(200);
}
@Test
@Order(12)
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/devices-requiring-access-grant contains not device2")
- public void testGetDevicesRequiringAccess3() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .when().get("/vaults/{vaultId}/devices-requiring-access-grant", "7E57C0DE-0000-4000-8000-000100002222")
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/users-requiring-access-grant contains not user2")
+ public void testGetUsersRequiringAccess3() {
+ given().when().get("/vaults/{vaultId}/users-requiring-access-grant", "7E57C0DE-0000-4000-8000-000100002222")
.then().statusCode(200)
- .body("id", not(hasItems("device2")));
+ .body("id", not(hasItems("user2")));
}
@Test
@Order(13)
- @DisplayName("PUT /devices/device9999 returns 201")
- public void testCreateDevice2() {
- var deviceDto = new DeviceResource.DeviceDto("device9999", "Computer 9999", Device.Type.DESKTOP, "publickey9999", "user2", Set.of(), Instant.parse("2020-02-20T20:20:20Z"));
-
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .given().contentType(ContentType.JSON).body(deviceDto)
- .when().put("/devices/{deviceId}", "device9999")
- .then().statusCode(201);
- }
-
- @Test
- @Order(14)
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/devices-requiring-access-grant contains not device9999")
- public void testGetDevicesRequiringAccess4() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .when().get("/vaults/{vaultId}/devices-requiring-access-grant", "7E57C0DE-0000-4000-8000-000100002222")
- .then().statusCode(200)
- .body("id", hasItems("device9999"));
- }
-
- @Test
- @Order(15)
@DisplayName("DELETE /vaults/7E57C0DE-0000-4000-8000-000100002222/members/user2 returns 204")
public void testRevokeAccess() { // previously added in testGrantAccess()
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .when().delete("/vaults/{vaultId}/users/{userId}", "7E57C0DE-0000-4000-8000-000100002222", "user2")
+ given().when().delete("/vaults/{vaultId}/authority/{userId}", "7E57C0DE-0000-4000-8000-000100002222", "user2")
.then().statusCode(204);
}
@Test
- @Order(16)
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/access does not contain user2")
- public void getMembers3() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault2AdminJWT)
- .when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100002222")
+ @Order(14)
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/members does not contain user2")
+ public void getMembersOfVault2c() {
+ given().when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100002222")
.then().statusCode(200)
.body("id", not(hasItems("user2")));
}
@@ -451,12 +560,23 @@ public void getMembers3() {
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ManageAccessAsUser1 {
+ @BeforeAll
+ public void setup() throws SQLException {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ // user999 will be deleted in #cleanup()
+ s.execute("""
+ INSERT INTO "authority" ("id", "type", "name") VALUES ('user999', 'USER', 'User 999');
+ INSERT INTO "user_details" ("id", "publickey", "privatekey", "setupcode") VALUES ('user999', 'public999', 'private999', 'setup999');
+ INSERT INTO "group_membership" ("group_id", "member_id") VALUES ('group2', 'user999')
+ """);
+ }
+ }
+
@Test
@Order(1)
@DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100001111/groups/group3000 returns 404")
public void addNonExistingGroup() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .when().put("/vaults/{vaultId}/groups/{groupId}", "7E57C0DE-0000-4000-8000-000100001111", "group3000")
+ given().when().put("/vaults/{vaultId}/groups/{groupId}", "7E57C0DE-0000-4000-8000-000100001111", "group3000")
.then().statusCode(404);
}
@@ -464,84 +584,76 @@ public void addNonExistingGroup() {
@Order(2)
@DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100001111/groups/group2 returns 201")
public void addGroupToVault() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .when().put("/vaults/{vaultId}/groups/{groupId}", "7E57C0DE-0000-4000-8000-000100001111", "group2")
+ given().when().put("/vaults/{vaultId}/groups/{groupId}", "7E57C0DE-0000-4000-8000-000100001111", "group2")
.then().statusCode(201);
}
@Test
@Order(3)
@DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/members does contain group2")
- public void getMembers1() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100001111")
+ public void getMembersOfVault1a() {
+ given().when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100001111")
.then().statusCode(200)
.body("id", hasItems("group2"));
}
@Test
- @Order(4)
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/devices-requiring-access-grant contains device999")
- public void testGetDevicesRequiringAccess3() throws SQLException {
- try (var s = dataSource.getConnection().createStatement()) {
- // device999 will be deleted in #cleanup()
- s.execute("""
- INSERT INTO "device" ("id", "owner_id", "name", "publickey", "creation_time")
- VALUES
- ('device999', 'user2', 'Computer 999', 'publickey90', '2020-02-20 20:20:20');
- """);
- }
+ @Order(3)
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100002222/members returns 403")
+ public void getMembersOfVault2() {
+ given().when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100002222")
+ .then().statusCode(403);
+ }
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .when().get("/vaults/{vaultId}/devices-requiring-access-grant", "7E57C0DE-0000-4000-8000-000100001111")
+ @Test
+ @Order(4)
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/users-requiring-access-grant contains user999")
+ public void testGetUsersRequiringAccess3() {
+ given().when().get("/vaults/{vaultId}/users-requiring-access-grant", "7E57C0DE-0000-4000-8000-000100001111")
.then().statusCode(200)
- .body("id", hasItems("device999"));
+ .body("id", hasItems("user999"));
}
@Test
@Order(5)
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100001111/keys/device999 returns 201")
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100001111/access-tokens for user999 returns 200")
public void testGrantAccess2() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .contentType(ContentType.TEXT).body("jwe.jwe.jwe.vault2.device93")
- .when().put("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100001111", "device999")
- .then().statusCode(201);
+ given().contentType(ContentType.JSON).body(Map.of("user999", "jwe.jwe.jwe.vault2.user999"))
+ .when().post("/vaults/{vaultId}/access-tokens/", "7E57C0DE-0000-4000-8000-000100001111")
+ .then().statusCode(200);
}
@Test
@Order(6)
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/devices-requiring-access-grant contains not device999")
- public void testGetDevicesRequiringAccess4() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .when().get("/vaults/{vaultId}/devices-requiring-access-grant", "7E57C0DE-0000-4000-8000-000100001111")
+ @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/users-requiring-access-grant does no longer contain user999")
+ public void testGetUsersRequiringAccess4() {
+ given().when().get("/vaults/{vaultId}/users-requiring-access-grant", "7E57C0DE-0000-4000-8000-000100001111")
.then().statusCode(200)
- .body("id", not(hasItems("device999")));
+ .body("id", not(hasItems("user999")));
}
@Test
@Order(7)
@DisplayName("DELETE /vaults/7E57C0DE-0000-4000-8000-000100001111/groups/group2 returns 204")
public void removeGroup2() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .when().delete("/vaults/{vaultId}/groups/{groupId}", "7E57C0DE-0000-4000-8000-000100001111", "group2")
+ given().when().delete("/vaults/{vaultId}/authority/{groupId}", "7E57C0DE-0000-4000-8000-000100001111", "group2")
.then().statusCode(204);
}
@Test
@Order(8)
@DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/members does not contain group2")
- public void getMembers2() {
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100001111")
+ public void getMembersOfVault1b() {
+ given().when().get("/vaults/{vaultId}/members", "7E57C0DE-0000-4000-8000-000100001111")
.then().statusCode(200)
.body("id", not(hasItems("group2")));
}
@AfterAll
public void cleanup() throws SQLException {
- try (var s = dataSource.getConnection().createStatement()) {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
s.execute("""
- DELETE FROM "device" WHERE ID = 'device999';
+ DELETE FROM "authority" WHERE ID = 'user999';
""");
}
}
@@ -561,7 +673,7 @@ public class ExceedingLicenseLimits {
@BeforeAll
public void setup() throws SQLException {
//Assumptions.assumeTrue(EffectiveVaultAccess.countEffectiveVaultUsers() == 2);
- try (var s = dataSource.getConnection().createStatement()) {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
s.execute("""
INSERT INTO "authority" ("id", "type", "name")
VALUES
@@ -576,7 +688,6 @@ public void setup() throws SQLException {
VALUES
('group91');
-
INSERT INTO "user_details" ("id")
VALUES
('user91'),
@@ -594,7 +705,7 @@ public void setup() throws SQLException {
INSERT INTO "vault_access" ("vault_id", "authority_id")
VALUES
- ('7E57C0DE-0000-4000-8000-0001AAAAAAAA', 'user95_A');
+ ('7E57C0DE-0000-4000-8000-00010000AAAA', 'user95_A');
""");
}
}
@@ -605,20 +716,18 @@ public void setup() throws SQLException {
public void addGroupToVaultExceedingSeats() {
//Assumptions.assumeTrue(EffectiveVaultAccess.countEffectiveVaultUsers() == 2);
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .when().put("/vaults/{vaultId}/groups/{groupId}", "7E57C0DE-0000-4000-8000-000100001111", "group91")
+ given().when().put("/vaults/{vaultId}/groups/{groupId}", "7E57C0DE-0000-4000-8000-000100001111", "group91")
.then().statusCode(402);
}
@Order(2)
@DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-000100001111/users/userXX returns 201")
- @ParameterizedTest(name = "Adding user {index} succeeds")
+ @ParameterizedTest(name = "Adding user {0} succeeds")
@ValueSource(strings = {"user91", "user92", "user93"})
public void addUserToVaultNotExceedingSeats(String userId) {
//Assumptions.assumeTrue(EffectiveVaultAccess.countEffectiveVaultUsers() == 2);
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .when().put("/vaults/{vaultId}/users/{usersId}", "7E57C0DE-0000-4000-8000-000100001111", userId)
+ given().when().put("/vaults/{vaultId}/users/{usersId}", "7E57C0DE-0000-4000-8000-000100001111", userId)
.then().statusCode(201);
}
@@ -628,16 +737,67 @@ public void addUserToVaultNotExceedingSeats(String userId) {
public void addUserToVaultExceedingSeats() {
//Assumptions.assumeTrue(EffectiveVaultAccess.countEffectiveVaultUsers() == 5);
- given().header(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION, vault1AdminJWT)
- .when().put("/vaults/{vaultId}/users/{usersId}", "7E57C0DE-0000-4000-8000-000100001111", "user94")
+ given().when().put("/vaults/{vaultId}/users/{usersId}", "7E57C0DE-0000-4000-8000-000100001111", "user94")
.then().statusCode(402);
}
@Test
+ @TestSecurity(user = "User 94", roles = {"user"})
+ @OidcSecurity(claims = {
+ @Claim(key = "sub", value = "user94")
+ })
@Order(4)
+ @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-0001FFFF3333 (as user94) exceeding the license returns 402")
+ public void testCreateVaultExceedingSeats() {
+ //Assumptions.assumeTrue(EffectiveVaultAccess.countEffectiveVaultUsers() > 5);
+
+ var uuid = UUID.fromString("7E57C0DE-0000-4000-8000-0001FFFF3333");
+ var vaultDto = new VaultResource.VaultDto(uuid, "My Vault", "Test vault 4", false, Instant.parse("2112-12-21T21:12:21Z"), "masterkey3", 42, "NaCl", "authPubKey3", "authPrvKey3");
+ given().contentType(ContentType.JSON).body(vaultDto)
+ .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-0001FFFF3333")
+ .then().statusCode(402);
+ }
+
+ @Test
+ @Order(5)
+ @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-0001FFFF3333 (as user1) returns 201 not exceeding seats because user already has access to an existing vault")
+ public void testCreateVaultNotExceedingSeats() {
+ //Assumptions.assumeTrue(EffectiveVaultAccess.countEffectiveVaultUsers() > 5);
+
+ var uuid = UUID.fromString("7E57C0DE-0000-4000-8000-0001FFFF3333");
+ var vaultDto = new VaultResource.VaultDto(uuid, "My Vault", "Test vault 3", false, Instant.parse("2112-12-21T21:12:21Z"), "masterkey3", 42, "NaCl", "authPubKey3", "authPrvKey3");
+ given().contentType(ContentType.JSON).body(vaultDto)
+ .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-0001FFFF3333")
+ .then().statusCode(201)
+ .body("id", equalToIgnoringCase("7E57C0DE-0000-4000-8000-0001FFFF3333"))
+ .body("name", equalTo("My Vault"))
+ .body("description", equalTo("Test vault 3"))
+ .body("archived", equalTo(false));
+ }
+
+ @Test
+ @Order(6)
+ @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-0001FFFF3333 (as user1) returns 200 with only updated name, description and archive flag, despite exceeding license")
+ public void testUpdateVaultDespiteLicenseExceeded() {
+ //Assumptions.assumeTrue(EffectiveVaultAccess.countEffectiveVaultUsers() > 5);
+
+ var uuid = UUID.fromString("7E57C0DE-0000-4000-8000-0001FFFF3333");
+ var vaultDto = new VaultResource.VaultDto(uuid, "VaultUpdated", "Vault updated.", true, Instant.parse("2222-11-11T11:11:11Z"), "someVaule", -1, "doNotUpdate", "doNotUpdate", "doNotUpdate");
+ given().contentType(ContentType.JSON)
+ .body(vaultDto)
+ .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-0001FFFF3333")
+ .then().statusCode(200)
+ .body("id", equalToIgnoringCase("7E57C0DE-0000-4000-8000-0001FFFF3333"))
+ .body("name", equalTo("VaultUpdated"))
+ .body("description", equalTo("Vault updated."))
+ .body("archived", equalTo(true));
+ }
+
+ @Test
+ @Order(7)
@DisplayName("Unlock is blocked if exceeding license seats")
public void testUnlockBlockedIfLicenseExceeded() throws SQLException {
- try (var s = dataSource.getConnection().createStatement()) {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
s.execute("""
INSERT INTO "vault_access" ("vault_id", "authority_id")
VALUES ('7E57C0DE-0000-4000-8000-000100001111', 'group91');
@@ -645,13 +805,13 @@ public void testUnlockBlockedIfLicenseExceeded() throws SQLException {
}
//Assumptions.assumeTrue(EffectiveVaultAccess.countEffectiveVaultUsers() > 5);
- when().get("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-000100001111", "device1")
+ when().get("/vaults/{vaultId}/access-token", "7E57C0DE-0000-4000-8000-000100001111")
.then().statusCode(402);
}
@AfterAll
public void reset() throws SQLException {
- try (var s = dataSource.getConnection().createStatement()) {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
s.execute("""
DELETE FROM "authority"
WHERE "id" IN ('user91', 'user92', 'user93', 'user94', 'user95_A', 'group91');
@@ -661,6 +821,208 @@ public void reset() throws SQLException {
}
+ @Nested
+ @DisplayName("Claim Ownership")
+ @TestSecurity(user = "User Name 1", roles = {"user"})
+ @OidcSecurity(claims = {
+ @Claim(key = "sub", value = "user1")
+ })
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+ public class ClaimOwnership {
+
+ private static Algorithm JWT_ALG;
+
+ @BeforeAll
+ @Transactional
+ public static void setup() throws GeneralSecurityException {
+ var keyPairGen = KeyPairGenerator.getInstance("EC");
+ keyPairGen.initialize(new ECGenParameterSpec("secp384r1"));
+ var keyPair = keyPairGen.generateKeyPair();
+ JWT_ALG = Algorithm.ECDSA384((ECPrivateKey) keyPair.getPrivate());
+
+ Vault v = new Vault();
+ v.id = UUID.fromString("7E57C0DE-0000-4000-8000-000100009999");
+ v.name = "ownership-test-vault";
+ v.creationTime = Instant.now();
+ v.authenticationPublicKey = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
+ v.persist();
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100009999/claim-ownership returns 400 - JWT has wrong SUB")
+ public void testClaimOwnershipIncorrectJWT1() {
+ var proof = JWT.create()
+ .withNotBefore(Instant.now().minusSeconds(10))
+ .withExpiresAt(Instant.now().plusSeconds(10))
+ .withSubject("userBAD")
+ .withClaim("vaultId", "7E57C0DE-0000-4000-8000-000100009999".toLowerCase())
+ .sign(JWT_ALG);
+
+ given().param("proof", proof)
+ .when().post("/vaults/{vaultId}/claim-ownership", "7E57C0DE-0000-4000-8000-000100009999")
+ .then().statusCode(400);
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100009999/claim-ownership returns 400 - JWT missing NBF")
+ public void testClaimOwnershipIncorrectJWT2() {
+ var proof = JWT.create()
+ .withExpiresAt(Instant.now().plusSeconds(10))
+ .withSubject("user1")
+ .withClaim("vaultId", "7E57C0DE-0000-4000-8000-000100009999".toLowerCase())
+ .sign(JWT_ALG);
+
+ given().param("proof", proof)
+ .when().post("/vaults/{vaultId}/claim-ownership", "7E57C0DE-0000-4000-8000-000100009999")
+ .then().statusCode(400);
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100009999/claim-ownership returns 400 - JWT not yet valid")
+ public void testClaimOwnershipIncorrectJWT3() {
+ var proof = JWT.create()
+ .withNotBefore(Instant.now().plusSeconds(60))
+ .withExpiresAt(Instant.now().plusSeconds(10))
+ .withSubject("user1")
+ .withClaim("vaultId", "7E57C0DE-0000-4000-8000-000100009999".toLowerCase())
+ .sign(JWT_ALG);
+
+ given().param("proof", proof)
+ .when().post("/vaults/{vaultId}/claim-ownership", "7E57C0DE-0000-4000-8000-000100009999")
+ .then().statusCode(400);
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100009999/claim-ownership returns 400 - JWT missing EXP")
+ public void testClaimOwnershipIncorrectJWT4() {
+ var proof = JWT.create()
+ .withNotBefore(Instant.now().minusSeconds(10))
+ .withSubject("user1")
+ .withClaim("vaultId", "7E57C0DE-0000-4000-8000-000100009999".toLowerCase())
+ .sign(JWT_ALG);
+
+ given().param("proof", proof)
+ .when().post("/vaults/{vaultId}/claim-ownership", "7E57C0DE-0000-4000-8000-000100009999")
+ .then().statusCode(400);
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100009999/claim-ownership returns 400 - JWT expired")
+ public void testClaimOwnershipIncorrectJWT5() {
+ var proof = JWT.create()
+ .withNotBefore(Instant.now().minusSeconds(10))
+ .withExpiresAt(Instant.now().minusSeconds(60))
+ .withSubject("user1")
+ .withClaim("vaultId", "7E57C0DE-0000-4000-8000-000100009999".toLowerCase())
+ .sign(JWT_ALG);
+
+ given().param("proof", proof)
+ .when().post("/vaults/{vaultId}/claim-ownership", "7E57C0DE-0000-4000-8000-000100009999")
+ .then().statusCode(400);
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100009999/claim-ownership returns 400 - JWT wrong vaultId")
+ public void testClaimOwnershipIncorrectJWT6() {
+ var proof = JWT.create()
+ .withNotBefore(Instant.now().minusSeconds(10))
+ .withExpiresAt(Instant.now().plusSeconds(10))
+ .withSubject("user1")
+ .withClaim("vaultId", "wrong")
+ .sign(JWT_ALG);
+
+ given().param("proof", proof)
+ .when().post("/vaults/{vaultId}/claim-ownership", "7E57C0DE-0000-4000-8000-000100009999")
+ .then().statusCode(400);
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100009999/claim-ownership returns 400 - JWT wrong signature")
+ public void testClaimOwnershipIncorrectJWT7() throws GeneralSecurityException {
+ var keyPairGen = KeyPairGenerator.getInstance("EC");
+ keyPairGen.initialize(new ECGenParameterSpec("secp384r1"));
+ var differentKey = keyPairGen.generateKeyPair();
+ var alg = Algorithm.ECDSA384((ECPrivateKey) differentKey.getPrivate());
+
+ var proof = JWT.create()
+ .withNotBefore(Instant.now().minusSeconds(10))
+ .withExpiresAt(Instant.now().plusSeconds(10))
+ .withSubject("user1")
+ .withClaim("vaultId", "7E57C0DE-0000-4000-8000-000100009999".toLowerCase())
+ .sign(alg);
+
+ given().param("proof", proof)
+ .when().post("/vaults/{vaultId}/claim-ownership", "7E57C0DE-0000-4000-8000-000100009999")
+ .then().statusCode(400);
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-BADBADBADBAD/claim-ownership returns 404")
+ public void testClaimOwnershipNoSuchVault() {
+ var proof = JWT.create()
+ .withJWTId(UUID.randomUUID().toString())
+ .withSubject("user1")
+ .withClaim("vaultId", "7E57C0DE-0000-4000-8000-000100009999".toLowerCase())
+ .withIssuedAt(Instant.now())
+ .sign(JWT_ALG);
+
+ given().param("proof", proof)
+ .when().post("/vaults/{vaultId}/claim-ownership", "7E57C0DE-0000-4000-8000-BADBADBADBAD")
+ .then().statusCode(404);
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100009999/claim-ownership returns 200")
+ public void testClaimOwnershipSuccess() {
+ var proof = JWT.create()
+ .withNotBefore(Instant.now().minusSeconds(10))
+ .withExpiresAt(Instant.now().plusSeconds(10))
+ .withSubject("user1")
+ .withClaim("vaultId", "7E57C0DE-0000-4000-8000-000100009999".toLowerCase())
+ .sign(JWT_ALG);
+
+ given().param("proof", proof)
+ .when().post("/vaults/{vaultId}/claim-ownership", "7E57C0DE-0000-4000-8000-000100009999")
+ .then().statusCode(200);
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("POST /vaults/7E57C0DE-0000-4000-8000-000100009999/claim-ownership returns 409")
+ public void testClaimOwnershipAlreadyClaimed() {
+ var proof = JWT.create()
+ .withNotBefore(Instant.now().minusSeconds(10))
+ .withExpiresAt(Instant.now().plusSeconds(10))
+ .withSubject("user1")
+ .withClaim("vaultId", "7E57C0DE-0000-4000-8000-000100009999".toLowerCase())
+ .sign(JWT_ALG);
+
+ given().param("proof", proof)
+ .when().post("/vaults/{vaultId}/claim-ownership", "7E57C0DE-0000-4000-8000-000100009999")
+ .then().statusCode(409);
+ }
+
+ @AfterAll
+ public void reset() throws SQLException {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
+ s.execute("""
+ DELETE FROM "vault" WHERE "id" = '7E57C0DE-0000-4000-8000-000100009999';
+ """);
+ }
+ }
+
+ }
+
@Nested
@DisplayName("As unauthenticated user")
public class AsAnonymous {
@@ -668,13 +1030,13 @@ public class AsAnonymous {
@DisplayName("401 Unauthorized")
@ParameterizedTest(name = "{0} {1}")
@CsvSource(value = {
- "GET, /vaults",
+ "GET, /vaults/accessible",
"GET, /vaults/7E57C0DE-0000-4000-8000-000100001111",
"GET, /vaults/7E57C0DE-0000-4000-8000-000100001111/members",
"PUT, /vaults/7E57C0DE-0000-4000-8000-000100001111/users/user1",
- "DELETE, /vaults/7E57C0DE-0000-4000-8000-000100001111/users/user1",
- "GET, /vaults/7E57C0DE-0000-4000-8000-000100001111/devices-requiring-access-grant",
- "GET, /vaults/7E57C0DE-0000-4000-8000-000100001111/keys/device1"
+ "DELETE, /vaults/7E57C0DE-0000-4000-8000-000100001111/authority/user1",
+ "GET, /vaults/7E57C0DE-0000-4000-8000-000100001111/users-requiring-access-grant",
+ "GET, /vaults/7E57C0DE-0000-4000-8000-000100001111/access-token"
})
public void testGet(String method, String path) {
when().request(method, path)
@@ -707,41 +1069,10 @@ public void testGetAllVaultsAsUser() {
public void testGetAllVaultsAsAdmin() {
when().get("/vaults/all")
.then().statusCode(200)
- .body("id", hasItems(equalToIgnoringCase("7E57C0DE-0000-4000-8000-000100001111"), equalToIgnoringCase("7E57C0DE-0000-4000-8000-000100002222"), equalToIgnoringCase("7E57C0DE-0000-4000-8000-0001AAAAAAAA")));
+ .body("id", hasItems(equalToIgnoringCase("7E57C0DE-0000-4000-8000-000100001111"), equalToIgnoringCase("7E57C0DE-0000-4000-8000-000100002222"), equalToIgnoringCase("7E57C0DE-0000-4000-8000-00010000AAAA")));
}
}
- @Nested
- @DisplayName("GET /vaults/{vaultid}/keys/{deviceId}")
- @TestSecurity(user = "User Name 1", roles = {"user"})
- @OidcSecurity(claims = {
- @Claim(key = "sub", value = "user1")
- })
- public class Unlock {
-
- @Test
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-000100001111/keys/iDoNotExist returns 404 for not-existing device")
- public void testUnlockNotExistingDevice() {
- when().get("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-BADBADBADBAD", "iDoNotExist")
- .then().statusCode(404);
- }
-
- @Test
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-BADBADBADBAD/keys/someDevice returns 404 for not-existing vaults")
- public void testUnlockNotExistingVault() {
- when().get("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-BADBADBADBAD", "someDevice")
- .then().statusCode(404);
- }
-
- @Test
- @DisplayName("GET /vaults/7E57C0DE-0000-4000-8000-0001AAAAAAAA/keys/someDevice returns 410 for archived vaults")
- public void testUnlockArchived() {
- when().get("/vaults/{vaultId}/keys/{deviceId}", "7E57C0DE-0000-4000-8000-0001AAAAAAAA", "someDevice")
- .then().statusCode(410);
- }
-
- }
-
@Nested
@DisplayName("/vaults/some")
public class GetSomeVaults {
@@ -793,266 +1124,4 @@ public void testListSomeVaultsAsUser() {
.then().statusCode(403);
}
}
-
- @Nested
- @DisplayName("PUT /vaults/{vaultid}")
- @TestSecurity(user = "User Name 1", roles = {"user"})
- @OidcSecurity(claims = {
- @Claim(key = "sub", value = "user1")
- })
- @TestInstance(TestInstance.Lifecycle.PER_CLASS)
- public class CreateOrUpdate {
-
- @BeforeAll
- public void insertData() throws SQLException {
- try (var s = dataSource.getConnection().createStatement()) {
- s.execute("""
- INSERT INTO "vault" ("id", "name", "description", "creation_time", "salt", "iterations", "masterkey", "auth_pubkey", "auth_prvkey", "archived")
- VALUES
- ('7E57C0DE-0000-4000-8000-0001FFFF1111', 'Vault U', 'Vault to update.',
- '2020-02-20T20:20:20Z', 'saltU', 42, 'masterkeyU', 'authPubKeyU', 'authPrvKeyU', FALSE);
-
- INSERT INTO "authority" ("id", "type", "name")
- VALUES
- ('user96', 'USER', 'user name 96'),
- ('user97', 'USER', 'user name 97');
-
- INSERT INTO "user_details" ("id")
- VALUES
- ('user96'),
- ('user97');
- """);
-
- }
- }
-
- @Test
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-0001FFFF2222 returns 201")
- public void testCreateVault1() {
- var uuid = UUID.fromString("7E57C0DE-0000-4000-8000-0001FFFF2222");
- var vaultDto = new VaultResource.VaultDto(uuid, "Test Vault", "Vault to create", false, Instant.parse("2112-12-21T21:12:21Z"), "masterkeyC", 42, "saltC", "authPubKeyC", "authPrvKeyC");
- given().contentType(ContentType.JSON).body(vaultDto)
- .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-0001FFFF2222")
- .then().statusCode(201)
- .body("id", equalToIgnoringCase("7E57C0DE-0000-4000-8000-0001FFFF2222"))
- .body("name", equalTo("Test Vault"))
- .body("description", equalTo("Vault to create"))
- .body("masterkey", equalTo("masterkeyC"))
- .body("salt", equalTo("saltC"))
- .body("iterations", equalTo(42))
- .body("authPublicKey", equalTo("authPubKeyC"))
- .body("authPrivateKey", equalTo("authPrvKeyC"))
- .body("archived", equalTo(false));
- }
-
- @Test
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-BADBADBADBAD returns 400")
- public void testCreateVault2() {
- given().contentType(ContentType.JSON)
- .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-BADBADBADBAD")
- .then().statusCode(400);
- }
-
- @Test
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-0001FFFF3333 returns 201 not exceeding seats but user does not have a vault yet")
- @TestSecurity(user = "User Name 96", roles = {"user"})
- @OidcSecurity(claims = {
- @Claim(key = "sub", value = "user96")
- })
- public void testCreateVault3() {
- var uuid = UUID.fromString("7E57C0DE-0000-4000-8000-0001FFFF3333");
- var vaultDto = new VaultResource.VaultDto(uuid, "Test Vault", "Vault to create", false, Instant.parse("2112-12-21T21:12:21Z"), "masterkeyC", 42, "saltC", "authPubKeyC", "authPrvKeyC");
- given().contentType(ContentType.JSON).body(vaultDto)
- .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-0001FFFF3333")
- .then().statusCode(201)
- .body("id", equalToIgnoringCase("7E57C0DE-0000-4000-8000-0001FFFF3333"))
- .body("name", equalTo("Test Vault"))
- .body("description", equalTo("Vault to create"))
- .body("masterkey", equalTo("masterkeyC"))
- .body("salt", equalTo("saltC"))
- .body("iterations", equalTo(42))
- .body("authPublicKey", equalTo("authPubKeyC"))
- .body("authPrivateKey", equalTo("authPrvKeyC"))
- .body("archived", equalTo(false));
- }
-
- @Test
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-0001FFFF4444 exceeding the license returns 402")
- @TestSecurity(user = "User Name 97", roles = {"user"})
- @OidcSecurity(claims = {
- @Claim(key = "sub", value = "user97")
- })
- public void testCreateVault4() throws SQLException {
- try (var s = dataSource.getConnection().createStatement()) {
- s.execute("""
- INSERT INTO "authority" ("id", "type", "name")
- VALUES
- ('user91', 'USER', 'user name 91'),
- ('user92', 'USER', 'user name 92'),
- ('user93', 'USER', 'user name 93'),
- ('user94', 'USER', 'user name 94'),
- ('user95_A', 'USER', 'user name Archived'),
- ('group91', 'GROUP', 'group name 91');
-
- INSERT INTO "group_details" ("id")
- VALUES
- ('group91');
-
- INSERT INTO "user_details" ("id")
- VALUES
- ('user91'),
- ('user92'),
- ('user93'),
- ('user94'),
- ('user95_A');
-
- INSERT INTO "group_membership" ("group_id", "member_id")
- VALUES
- ('group91', 'user91'),
- ('group91', 'user92'),
- ('group91', 'user93'),
- ('group91', 'user94');
-
- INSERT INTO "vault_access" ("vault_id", "authority_id")
- VALUES
- ('7E57C0DE-0000-4000-8000-0001AAAAAAAA', 'user95_A'),
- ('7E57C0DE-0000-4000-8000-000100001111', 'group91');
- """);
- }
- //Assumptions.assumeTrue(EffectiveVaultAccess.countEffectiveVaultUsers() > 5);
-
- var uuid = UUID.fromString("7E57C0DE-0000-4000-8000-0001FFFF4444");
- var vaultDto = new VaultResource.VaultDto(uuid, "Test Vault", "Vault to create", false, Instant.parse("2112-12-21T21:12:21Z"), "masterkeyC", 42, "saltC", "authPubKeyC", "authPrvKeyC");
- given().contentType(ContentType.JSON).body(vaultDto)
- .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-0001FFFF4444")
- .then().statusCode(402);
-
- try (var s = dataSource.getConnection().createStatement()) {
- s.execute("""
- DELETE FROM "authority"
- WHERE "id" IN ('user91', 'user92', 'user93', 'user94', 'user95_A', 'group91');
- """);
- }
- }
-
- @Test
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-0001FFFFAAAA returns 201 ignoring archived flag")
- public void testCreateVaultIgnoringArchived() {
- var uuid = UUID.fromString("7E57C0DE-0000-4000-8000-0001FFFFAAAA");
- var vaultDto = new VaultResource.VaultDto(uuid, "Test Vault", "Vault to create", true, Instant.parse("2112-12-21T21:12:21Z"), "masterkeyC", 42, "saltC", "authPubKeyC", "authPrvKeyC");
- given().contentType(ContentType.JSON).body(vaultDto)
- .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-0001FFFFAAAA")
- .then().statusCode(201)
- .body("id", equalToIgnoringCase("7E57C0DE-0000-4000-8000-0001FFFFAAAA"))
- .body("name", equalTo("Test Vault"))
- .body("description", equalTo("Vault to create"))
- .body("masterkey", equalTo("masterkeyC"))
- .body("salt", equalTo("saltC"))
- .body("iterations", equalTo(42))
- .body("authPublicKey", equalTo("authPubKeyC"))
- .body("authPrivateKey", equalTo("authPrvKeyC"))
- .body("archived", equalTo(false));
- }
-
- @Test
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-0001FFFF1111 returns 200 with only updated name, description and archive flag")
- public void testUpdateVault() {
- var uuid = UUID.fromString("7E57C0DE-0000-4000-8000-0001FFFF1111");
- var vaultDto = new VaultResource.VaultDto(uuid, "VaultUpdated", "Vault updated.", true, Instant.parse("2112-12-21T21:12:21Z"), "someVaule", -1, "someVaule", "someValue", "someValue");
- given().contentType(ContentType.JSON)
- .body(vaultDto)
- .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-0001FFFF1111")
- .then().statusCode(200)
- .body("id", equalToIgnoringCase("7E57C0DE-0000-4000-8000-0001FFFF1111"))
- .body("name", equalTo("VaultUpdated"))
- .body("description", equalTo("Vault updated."))
- .body("creationTime", equalTo("2020-02-20T20:20:20Z"))
- .body("masterkey", equalTo("masterkeyU"))
- .body("salt", equalTo("saltU"))
- .body("iterations", equalTo(42))
- .body("authPublicKey", equalTo("authPubKeyU"))
- .body("authPrivateKey", equalTo("authPrvKeyU"))
- .body("archived", equalTo(true));
- }
-
- @Test
- @DisplayName("PUT /vaults/7E57C0DE-0000-4000-8000-0001FFFF1111 returns 200 with only updated name, description and archive flag, exceeding license")
- public void testUpdateVault1() throws SQLException {
- try (var s = dataSource.getConnection().createStatement()) {
- s.execute("""
- INSERT INTO "authority" ("id", "type", "name")
- VALUES
- ('user91', 'USER', 'user name 91'),
- ('user92', 'USER', 'user name 92'),
- ('user93', 'USER', 'user name 93'),
- ('user94', 'USER', 'user name 94'),
- ('user95_A', 'USER', 'user name Archived'),
- ('group91', 'GROUP', 'group name 91');
-
- INSERT INTO "group_details" ("id")
- VALUES
- ('group91');
-
- INSERT INTO "user_details" ("id")
- VALUES
- ('user91'),
- ('user92'),
- ('user93'),
- ('user94'),
- ('user95_A');
-
- INSERT INTO "group_membership" ("group_id", "member_id")
- VALUES
- ('group91', 'user91'),
- ('group91', 'user92'),
- ('group91', 'user93'),
- ('group91', 'user94');
-
- INSERT INTO "vault_access" ("vault_id", "authority_id")
- VALUES
- ('7E57C0DE-0000-4000-8000-0001AAAAAAAA', 'user95_A'),
- ('7E57C0DE-0000-4000-8000-000100001111', 'group91');
- """);
- }
- //Assumptions.assumeTrue(EffectiveVaultAccess.countEffectiveVaultUsers() > 5);
-
- var uuid = UUID.fromString("7E57C0DE-0000-4000-8000-0001FFFF1111");
- var vaultDto = new VaultResource.VaultDto(uuid, "VaultUpdated", "Vault updated.", true, Instant.parse("2112-12-21T21:12:21Z"), "someVaule", -1, "someVaule", "someValue", "someValue");
- given().contentType(ContentType.JSON)
- .body(vaultDto)
- .when().put("/vaults/{vaultId}", "7E57C0DE-0000-4000-8000-0001FFFF1111")
- .then().statusCode(200)
- .body("id", equalToIgnoringCase("7E57C0DE-0000-4000-8000-0001FFFF1111"))
- .body("name", equalTo("VaultUpdated"))
- .body("description", equalTo("Vault updated."))
- .body("creationTime", equalTo("2020-02-20T20:20:20Z"))
- .body("masterkey", equalTo("masterkeyU"))
- .body("salt", equalTo("saltU"))
- .body("iterations", equalTo(42))
- .body("authPublicKey", equalTo("authPubKeyU"))
- .body("authPrivateKey", equalTo("authPrvKeyU"))
- .body("archived", equalTo(true));
-
- try (var s = dataSource.getConnection().createStatement()) {
- s.execute("""
- DELETE FROM "authority"
- WHERE "id" IN ('user91', 'user92', 'user93', 'user94', 'user95_A', 'group91');
- """);
- }
- }
-
- @AfterAll
- public void deleteData() throws SQLException {
- try (var s = dataSource.getConnection().createStatement()) {
- s.execute("""
- DELETE FROM "vault"
- WHERE "id" IN ('7E57C0DE-0000-4000-8000-0001FFFF1111','7E57C0DE-0000-4000-8000-0001FFFF2222');
-
- DELETE FROM "authority"
- WHERE "id" IN ('user96', 'user97');
- """);
- }
- }
-
- }
-}
\ No newline at end of file
+}
diff --git a/backend/src/test/java/org/cryptomator/hub/entities/EntityIntegrationTest.java b/backend/src/test/java/org/cryptomator/hub/entities/EntityIntegrationTest.java
index 6c3d224f0..a54560dee 100644
--- a/backend/src/test/java/org/cryptomator/hub/entities/EntityIntegrationTest.java
+++ b/backend/src/test/java/org/cryptomator/hub/entities/EntityIntegrationTest.java
@@ -1,11 +1,9 @@
package org.cryptomator.hub.entities;
import io.agroal.api.AgroalDataSource;
-import io.quarkus.panache.common.Parameters;
import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
-import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceException;
import org.hibernate.exception.ConstraintViolationException;
import org.junit.jupiter.api.Assertions;
@@ -14,7 +12,6 @@
import java.sql.SQLException;
import java.time.Instant;
-import java.util.List;
import java.util.UUID;
@QuarkusTest
@@ -24,69 +21,32 @@ public class EntityIntegrationTest {
@Inject
AgroalDataSource dataSource;
- @Inject
- EntityManager entityManager;
-
@Test
@TestTransaction
- @DisplayName("Removing a Device cascades to Access")
- public void removingDeviceCascadesToAccess() throws SQLException {
- try (var s = dataSource.getConnection().createStatement()) {
+ @DisplayName("Removing a User cascades to Access")
+ public void removingUserCascadesToAccess() throws SQLException {
+ try (var c = dataSource.getConnection(); var s = c.createStatement()) {
// test data will be removed via @TestTransaction
s.execute("""
- INSERT INTO "device" ("id", "owner_id", "name", "publickey", "creation_time")
- VALUES ('device999', 'user1', 'Computer 999', 'publickey999', '2020-02-20 20:20:20');
- INSERT INTO "access_token" ("device_id", "vault_id", "jwe") VALUES ('device999', '7E57C0DE-0000-4000-8000-000100001111', 'jwe4');
+ INSERT INTO "authority" ("id", "type", "name") VALUES ('user999', 'USER', 'User 999');
+ INSERT INTO "user_details" ("id") VALUES ('user999');
+ INSERT INTO "access_token" ("user_id", "vault_id", "vault_masterkey") VALUES ('user999', '7E57C0DE-0000-4000-8000-000100001111', 'jwe.jwe.jwe.vault1.user999');
""");
}
- var deleted = Device.deleteById("device999");
- var matchAfter = AccessToken.findAll().stream().anyMatch(a -> "device999".equals(a.device.id));
+ var deleted = User.deleteById("user999");
+ var matchAfter = AccessToken.findAll().stream().anyMatch(a -> "user999".equals(a.user.id));
Assertions.assertTrue(deleted);
Assertions.assertFalse(matchAfter);
}
@Test
@TestTransaction
- @DisplayName("User's device names need to be unique")
- public void testAddNonUniqueDeviceName() {
- Device existingDevice = Device.findById("device1");
- Device conflictingDevice = new Device();
- conflictingDevice.id = "deviceX";
- conflictingDevice.name = existingDevice.name;
- conflictingDevice.owner = existingDevice.owner;
- conflictingDevice.publickey = "XYZ";
- conflictingDevice.creationTime = Instant.parse("2020-02-20T20:20:20Z");
-
- PersistenceException thrown = Assertions.assertThrows(PersistenceException.class, conflictingDevice::persistAndFlush);
- Assertions.assertInstanceOf(ConstraintViolationException.class, thrown);
- }
-
- @Test
- @TestTransaction
- @DisplayName("Retrieve the correct token when a device has access to multiple vaults")
- public void testGetCorrectTokenForDeviceWithAcessToMultipleVaults() throws SQLException {
- try (var s = dataSource.getConnection().createStatement()) {
- // test data will be removed via @TestTransaction
- s.execute("""
- INSERT INTO "device" ("id", "owner_id", "name", "publickey", "creation_time")
- VALUES ('device999', 'user1', 'Computer 999', 'publickey999', '2020-02-20 20:20:20');
- INSERT INTO "access_token" ("device_id", "vault_id", "jwe") VALUES ('device999', '7E57C0DE-0000-4000-8000-000100001111', 'jwe4');
- INSERT INTO "access_token" ("device_id", "vault_id", "jwe") VALUES ('device999', '7E57C0DE-0000-4000-8000-000100002222', 'jwe5');
- """);
- }
-
- List tokens = AccessToken
- .find("#AccessToken.get", Parameters.with("deviceId", "device999")
- .and("vaultId", UUID.fromString("7E57C0DE-0000-4000-8000-000100001111"))
- .and("userId", "user1"))
- .stream().toList();
-
- var token = tokens.get(0);
-
- Assertions.assertEquals(1, tokens.size());
+ @DisplayName("Retrieve the correct token when a user has access to multiple vaults")
+ public void testGetCorrectTokenForDeviceWithAcessToMultipleVaults() {
+ var token = AccessToken.unlock(UUID.fromString("7E57C0DE-0000-4000-8000-000100001111"), "user1");
Assertions.assertEquals(UUID.fromString("7E57C0DE-0000-4000-8000-000100001111"), token.vault.id);
- Assertions.assertEquals("device999", token.device.id);
- Assertions.assertEquals("jwe4", token.jwe);
+ Assertions.assertEquals("user1", token.user.id);
+ Assertions.assertEquals("jwe.jwe.jwe.vault1.user1", token.vaultKey);
}
}
\ No newline at end of file
diff --git a/backend/src/test/java/org/cryptomator/hub/filters/VaultAdminOnlyFilterProviderTest.java b/backend/src/test/java/org/cryptomator/hub/filters/VaultAdminOnlyFilterProviderTest.java
deleted file mode 100644
index 12d4187af..000000000
--- a/backend/src/test/java/org/cryptomator/hub/filters/VaultAdminOnlyFilterProviderTest.java
+++ /dev/null
@@ -1,193 +0,0 @@
-package org.cryptomator.hub.filters;
-
-import com.auth0.jwt.JWT;
-import com.auth0.jwt.JWTVerifier;
-import com.auth0.jwt.algorithms.Algorithm;
-import com.auth0.jwt.interfaces.DecodedJWT;
-import io.quarkus.test.junit.QuarkusTest;
-import jakarta.ws.rs.container.ContainerRequestContext;
-import jakarta.ws.rs.core.MultivaluedHashMap;
-import jakarta.ws.rs.core.UriInfo;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-
-import java.time.Clock;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.temporal.ChronoUnit;
-
-@QuarkusTest
-class VaultAdminOnlyFilterProviderTest {
-
- private static final String NO_VAULT_ID_TOKEN = "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.e30.vT0jNCotwtzr37JNM_C6uZFCw3GvVjcikn-CVrDociILPiXBXA8i7dWFwBnUQkDBcFbouh-eUB_wEWgqe9WTG2rT66_c1G2LZUQcCsKdWJdTyK4ZxLXLYOYhNOHtqShI";
- private static final String INVALID_VAULT_ID_TOKEN = "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsInZhdWx0SWQiOjI1fQ.e30.YxqmX5xeOviP9WldQV870zhPEF4PRaZrW0TaoWzm4lvkEmacIUt3OIoH0grAeh_gtJNRg4WfnqFNTgUx40-yDOtBLzyoeubfrMgb0-agN1898Mbr4ZhD1xqor0lBDrmc";
- private static final String NO_IAT_TOKEN_VAULT_2 = "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsInZhdWx0SWQiOiI3RTU3QzBERS0wMDAwLTQwMDAtODAwMC0wMDAxMDAwMDIyMjIifQ.e30.etWcY66h7FAaiBo8SY6t1CLP4ivrYj8ld5OhJn1y3tZ71-EhedIryaDt8nty0DGsm6OFlKd0gUaz_OvU1TuJHArbzLg7uX3Ss-2-v4Kjx7K4E-UEqc4MMP0oCJa0g2re";
- private VaultAdminOnlyFilterProvider vaultAdminOnlyFilterProvider;
- private ContainerRequestContext context;
- private UriInfo uriInfo;
-
- @BeforeEach
- void setUp() {
- vaultAdminOnlyFilterProvider = Mockito.spy(new VaultAdminOnlyFilterProvider());
- context = Mockito.mock(ContainerRequestContext.class);
- uriInfo = Mockito.mock(UriInfo.class);
- }
-
- @Nested
- @DisplayName("Test JWT verification")
- public class TestJWTVerification {
-
- private static final String PUBLIC_KEY_VAULT_2 = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii1D3jaW6pmGVJFhodzC31cy5sfOYotrzF";
-
- private static final DecodedJWT VALID_VAULT2 = JWT.decode(VaultAdminOnlyFilterProviderTestConstants.VALID_TOKEN_VAULT_2);
-
- @Test
- @DisplayName("validate future IAT in leeway vaultAdminAuthorizationJWT")
- public void testFutureIATInLeewayVaultAdminAuthorizationJWT() {
- vaultAdminOnlyFilterProviderVerifierFor(VaultAdminOnlyFilterProviderTestConstants.NOW.plus(VaultAdminOnlyFilterProvider.REQUEST_LEEWAY_IN_SECONDS - 1, ChronoUnit.SECONDS));
- vaultAdminOnlyFilterProvider.verify(verifier(), VALID_VAULT2);
- }
-
- @Test
- @DisplayName("validate after IAT in leeway vaultAdminAuthorizationJWT")
- public void testAfterIATInLeewayVaultAdminAuthorizationJWT() {
- vaultAdminOnlyFilterProviderVerifierFor(VaultAdminOnlyFilterProviderTestConstants.NOW.minus(VaultAdminOnlyFilterProvider.REQUEST_LEEWAY_IN_SECONDS - 1, ChronoUnit.SECONDS));
- vaultAdminOnlyFilterProvider.verify(verifier(), VALID_VAULT2);
- }
-
- @Test
- @DisplayName("validate future IAT out leeway vaultAdminAuthorizationJWT")
- public void testFutureIATOutLeewayVaultAdminAuthorizationJWT() {
- vaultAdminOnlyFilterProviderVerifierFor(VaultAdminOnlyFilterProviderTestConstants.NOW.plus(VaultAdminOnlyFilterProvider.REQUEST_LEEWAY_IN_SECONDS + 1, ChronoUnit.SECONDS));
- var verifier = verifier();
- Assertions.assertThrows(VaultAdminTokenIAPNotValidException.class, () -> vaultAdminOnlyFilterProvider.verify(verifier, VALID_VAULT2));
- }
-
- @Test
- @DisplayName("validate after IAT out leeway vaultAdminAuthorizationJWT")
- public void testAfterIATOutLeewayAdminAuthorizationJWT() {
- vaultAdminOnlyFilterProviderVerifierFor(VaultAdminOnlyFilterProviderTestConstants.NOW.minus(VaultAdminOnlyFilterProvider.REQUEST_LEEWAY_IN_SECONDS + 1, ChronoUnit.SECONDS));
- var verifier = verifier();
- Assertions.assertThrows(VaultAdminTokenIAPNotValidException.class, () -> vaultAdminOnlyFilterProvider.verify(verifier, VALID_VAULT2));
- }
-
- @Test
- @DisplayName("validate no IAT in vaultAdminAuthorizationJWT")
- public void testMalformedVaultAdminAuthorizationJWTNoDates() {
- var verifier = verifier();
- var jwt = JWT.decode(NO_IAT_TOKEN_VAULT_2);
- Assertions.assertThrows(VaultAdminValidationFailedException.class, () -> vaultAdminOnlyFilterProvider.verify(verifier, jwt));
- }
-
- private com.auth0.jwt.interfaces.JWTVerifier verifier() {
- return vaultAdminOnlyFilterProvider.buildVerifier(Algorithm.ECDSA384(VaultAdminOnlyFilterProvider.decodePublicKey(PUBLIC_KEY_VAULT_2), null));
- }
-
- private void vaultAdminOnlyFilterProviderVerifierFor(Instant instant) {
- // Decorate verifier to use fixed time
- Mockito.doAnswer(invocationOnMock -> {
- Algorithm algorithm = invocationOnMock.getArgument(0);
- var verifier = (JWTVerifier.BaseVerification) vaultAdminOnlyFilterProvider.verification(algorithm, instant);
- return verifier.build(Clock.fixed(instant, ZoneId.of("UTC")));
- }).when(vaultAdminOnlyFilterProvider).buildVerifier(Mockito.any());
- }
- }
-
- @Nested
- @DisplayName("Test vaultId qurey parameter")
- public class TestVaultIdQueryParameter {
-
- @Test
- @DisplayName("validate valid vaultId in query")
- public void testGetValidVaultIdQueryParameter() {
- var pathParams = new MultivaluedHashMap();
- pathParams.add(VaultAdminOnlyFilterProvider.VAULT_ID, "7E57C0DE-0000-4000-8000-000100002222");
-
- Mockito.when(context.getUriInfo()).thenReturn(uriInfo);
- Mockito.when(context.getUriInfo().getPathParameters()).thenReturn(pathParams);
-
- String result = vaultAdminOnlyFilterProvider.getVaultIdQueryParameter(context);
- Assertions.assertEquals("7E57C0DE-0000-4000-8000-000100002222", result);
- }
-
- @Test
- @DisplayName("validate no vaultId in query")
- public void testNoVaultIdQueryParameter() {
- var pathParams = new MultivaluedHashMap();
-
- Mockito.when(context.getUriInfo()).thenReturn(uriInfo);
- Mockito.when(context.getUriInfo().getPathParameters()).thenReturn(pathParams);
-
- Assertions.assertThrows(VaultAdminValidationFailedException.class, () -> vaultAdminOnlyFilterProvider.getVaultIdQueryParameter(context));
- }
-
- @Test
- @DisplayName("validate multiple vaultId in query")
- public void testMultipleVaultIdQueryParameter() {
- var pathParams = new MultivaluedHashMap();
- pathParams.add(VaultAdminOnlyFilterProvider.VAULT_ID, "7E57C0DE-0000-4000-8000-000100002222");
- pathParams.add(VaultAdminOnlyFilterProvider.VAULT_ID, "7E57C0DE-0000-4000-8000-000100003333");
-
- Mockito.when(context.getUriInfo()).thenReturn(uriInfo);
- Mockito.when(context.getUriInfo().getPathParameters()).thenReturn(pathParams);
-
- Assertions.assertThrows(VaultAdminValidationFailedException.class, () -> vaultAdminOnlyFilterProvider.getVaultIdQueryParameter(context));
- }
- }
-
- @Nested
- @DisplayName("Test vaultAdminAuthorizationJWT decoding")
- public class TestVaultAdminAuthorizationJWTDecoding {
- @Test
- @DisplayName("validate valid VAULT_ADMIN_AUTHORIZATION")
- public void testValidVaultAdminAuthorizationJWTProvided() {
- Mockito.when(context.getHeaderString(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION)).thenReturn(VaultAdminOnlyFilterProviderTestConstants.VALID_TOKEN_VAULT_2);
- DecodedJWT result = vaultAdminOnlyFilterProvider.getUnverifiedvaultAdminAuthorizationJWT(context);
- Assertions.assertEquals(JWT.decode(VaultAdminOnlyFilterProviderTestConstants.VALID_TOKEN_VAULT_2).getHeader(), result.getHeader());
- Assertions.assertEquals(JWT.decode(VaultAdminOnlyFilterProviderTestConstants.VALID_TOKEN_VAULT_2).getPayload(), result.getPayload());
- }
-
- @Test
- @DisplayName("validate no vaultAdminAuthorizationJWT")
- public void testNoVaultAdminAuthorizationJWTProvided() {
- Assertions.assertThrows(VaultAdminNotProvidedException.class, () -> vaultAdminOnlyFilterProvider.getUnverifiedvaultAdminAuthorizationJWT(context));
- }
-
- @Test
- @DisplayName("validate malformed vaultAdminAuthorizationJWT")
- public void testMalformedVaultAdminAuthorizationJWT() {
- Mockito.when(context.getHeaderString(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION)).thenReturn(VaultAdminOnlyFilterProviderTestConstants.MALFORMED_TOKEN);
- Assertions.assertThrows(VaultAdminValidationFailedException.class, () -> vaultAdminOnlyFilterProvider.getUnverifiedvaultAdminAuthorizationJWT(context));
- }
- }
-
- @Nested
- @DisplayName("Test VAULT_ADMIN_AUTHORIZATION header extraction")
- public class TestVaultAdminAuthorizationJWTHeaderExtraction {
-
- @Test
- @DisplayName("validate valid VAULT_ADMIN_AUTHORIZATION leads to valid vaultId")
- public void testValidVaultAdminAuthorizationJWTLeadsToValidVaultId() {
- String result = vaultAdminOnlyFilterProvider.getUnverifiedVaultId(JWT.decode(VaultAdminOnlyFilterProviderTestConstants.VALID_TOKEN_VAULT_2));
- Assertions.assertEquals("7E57C0DE-0000-4000-8000-000100002222", result);
- }
-
- @Test
- @DisplayName("validate no vaultId in VAULT_ADMIN_AUTHORIZATION")
- public void testNoVaultIdInJWT() {
- var jwt = JWT.decode(NO_VAULT_ID_TOKEN);
- Assertions.assertThrows(VaultAdminValidationFailedException.class, () -> vaultAdminOnlyFilterProvider.getUnverifiedVaultId(jwt));
- }
-
- @Test
- @DisplayName("validate invalid vaultId in VAULT_ADMIN_AUTHORIZATION")
- public void testInvalidVaultIdInJWT() {
- var jwt = JWT.decode(INVALID_VAULT_ID_TOKEN);
- Assertions.assertThrows(VaultAdminValidationFailedException.class, () -> vaultAdminOnlyFilterProvider.getUnverifiedVaultId(jwt));
- }
- }
-}
\ No newline at end of file
diff --git a/backend/src/test/java/org/cryptomator/hub/filters/VaultAdminOnlyFilterProviderTestConstants.java b/backend/src/test/java/org/cryptomator/hub/filters/VaultAdminOnlyFilterProviderTestConstants.java
deleted file mode 100644
index 9dfdf83c6..000000000
--- a/backend/src/test/java/org/cryptomator/hub/filters/VaultAdminOnlyFilterProviderTestConstants.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.cryptomator.hub.filters;
-
-import java.time.Instant;
-
-final class VaultAdminOnlyFilterProviderTestConstants {
-
- // { "alg": "ES384", "typ": "JWT", "vaultId": "7E57C0DE-0000-4000-8000-000100002222" } { "iat": 1516239015 (2018-01-18T01:30:15) }
- static final String VALID_TOKEN_VAULT_2 = "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsInZhdWx0SWQiOiI3RTU3QzBERS0wMDAwLTQwMDAtODAwMC0wMDAxMDAwMDIyMjIifQ.eyJpYXQiOjE1MTYyMzkwMTV9.x3-JltFRYrwC6fNBgtvCHyIh8HzmcS190GVSbKzhLROeIyYpvvWo9PH_nVHa_8p6xQoMrwf7-H5gQYVm3EhtHWO_2CZro55zdzFkLThU26ql6yWtGPNroTmOyUT1MSQs";
-
- // { "alg": "ES384", "typ": "JWT", "vaultId": "7E57C0DE-0000-4000-8000-000100003000" } { "iat": 1516239015 (2018-01-18T01:30:15) }
- static final String VALID_TOKEN_VAULT_3000 = "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsInZhdWx0SWQiOiI3RTU3QzBERS0wMDAwLTQwMDAtODAwMC0wMDAxMDAwMDMwMDAifQ.eyJpYXQiOjE1MTYyMzkwMTV9.MCI388EG6LAXuRLVm6_YFEP-Up8bYI2SBvCtIv3azrPtmNbidR5KxtSVoV_W3iFsG8AUj4G7JLxT8F-b4Dw1i3VBhPMVl4GlC_AN89yvp5SPgtfYmIUdHWvcugahayHh";
-
- // { "alg": "ES384", "typ": "JWT", "vaultId": "7E57C0DE-0000-4000-8000-000100002222" } { "iat": 1516239015 (2018-01-18T01:30:15) } but signed with key of vault1
- static final String INVALID_SIGNATURE_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzM4NCIsInZhdWx0SWQiOiI3RTU3QzBERS0wMDAwLTQwMDAtODAwMC0wMDAxMDAwMDIyMjIifQ.e30.9_5pMhgkn9iyOG01T82hB00tHEELwMX0BGIc2_DwzZSizJYNz312B5xWkI1TOwzteEpWO2ivdki3NfgJkRsNBOJ02H5QJ8Zg4qT5lCbWySdZpMeSODTjHRuN5lErwAR2";
- static final String MALFORMED_TOKEN = "hello world";
- static final Instant NOW = Instant.ofEpochSecond(1516239015); // 2018-01-18T01:30:15
-
-}
diff --git a/backend/src/test/java/org/cryptomator/hub/filters/VaultAdminOnlyFilterProviderTestIT.java b/backend/src/test/java/org/cryptomator/hub/filters/VaultAdminOnlyFilterProviderTestIT.java
deleted file mode 100644
index f6820abed..000000000
--- a/backend/src/test/java/org/cryptomator/hub/filters/VaultAdminOnlyFilterProviderTestIT.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package org.cryptomator.hub.filters;
-
-import com.auth0.jwt.JWTVerifier;
-import com.auth0.jwt.algorithms.Algorithm;
-import io.agroal.api.AgroalDataSource;
-import io.quarkus.test.junit.QuarkusTest;
-import jakarta.inject.Inject;
-import jakarta.ws.rs.container.ContainerRequestContext;
-import jakarta.ws.rs.core.MultivaluedHashMap;
-import jakarta.ws.rs.core.UriInfo;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-
-import java.sql.SQLException;
-import java.time.Clock;
-import java.time.ZoneId;
-
-@QuarkusTest
-class VaultAdminOnlyFilterProviderTestIT {
-
- private VaultAdminOnlyFilterProvider vaultAdminOnlyFilterProvider;
- private ContainerRequestContext context;
- private UriInfo uriInfo;
-
- @Inject
- AgroalDataSource dataSource;
-
- @BeforeEach
- void setUp() {
- vaultAdminOnlyFilterProvider = Mockito.spy(new VaultAdminOnlyFilterProvider());
- context = Mockito.mock(ContainerRequestContext.class);
- uriInfo = Mockito.mock(UriInfo.class);
-
- // Decorate verifier to use fixed time
- Mockito.doAnswer(invocationOnMock -> {
- Algorithm algorithm = invocationOnMock.getArgument(0);
- var verifier = (JWTVerifier.BaseVerification) vaultAdminOnlyFilterProvider.verification(algorithm, VaultAdminOnlyFilterProviderTestConstants.NOW);
- return verifier.build(Clock.fixed(VaultAdminOnlyFilterProviderTestConstants.NOW, ZoneId.of("UTC")));
- }).when(vaultAdminOnlyFilterProvider).buildVerifier(Mockito.any());
- }
-
- @Test
- @DisplayName("validate valid vaultAdminAuthorizationJWT header")
- public void testValidVaultAdminAuthorizationJWTHeader() {
- var pathParams = new MultivaluedHashMap();
- pathParams.add(VaultAdminOnlyFilterProvider.VAULT_ID, "7E57C0DE-0000-4000-8000-000100002222");
-
- Mockito.when(context.getHeaderString(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION)).thenReturn(VaultAdminOnlyFilterProviderTestConstants.VALID_TOKEN_VAULT_2);
- Mockito.when(context.getUriInfo()).thenReturn(uriInfo);
- Mockito.when(context.getUriInfo().getPathParameters()).thenReturn(pathParams);
-
- vaultAdminOnlyFilterProvider.filter(context);
- }
-
- @Test
- @DisplayName("validate no vaultAdminAuthorizationJWT header provided")
- public void testNoVaultAdminAuthorizationJWTHeader() {
- var pathParams = new MultivaluedHashMap();
- pathParams.add(VaultAdminOnlyFilterProvider.VAULT_ID, "7E57C0DE-0000-4000-8000-000100002222");
-
- Mockito.when(context.getUriInfo()).thenReturn(uriInfo);
- Mockito.when(context.getUriInfo().getPathParameters()).thenReturn(pathParams);
-
- Assertions.assertThrows(VaultAdminNotProvidedException.class, () -> vaultAdminOnlyFilterProvider.filter(context));
- }
-
- @Test
- @DisplayName("validate other path-param provided")
- public void testOtherPathParamProvided() {
- var pathParams = new MultivaluedHashMap();
- pathParams.add(VaultAdminOnlyFilterProvider.VAULT_ID, "7E57C0DE-0000-4000-8000-000100003000");
-
- Mockito.when(context.getHeaderString(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION)).thenReturn(VaultAdminOnlyFilterProviderTestConstants.VALID_TOKEN_VAULT_2);
- Mockito.when(context.getUriInfo()).thenReturn(uriInfo);
- Mockito.when(context.getUriInfo().getPathParameters()).thenReturn(pathParams);
-
- Assertions.assertThrows(VaultAdminValidationFailedException.class, () -> vaultAdminOnlyFilterProvider.filter(context));
- }
-
- @Test
- @DisplayName("validate vaultAdminAuthorizationJWT header signed by other key")
- public void testOtherKeyVaultAdminAuthorizationJWTHeader() {
- var pathParams = new MultivaluedHashMap();
- pathParams.add(VaultAdminOnlyFilterProvider.VAULT_ID, "7E57C0DE-0000-4000-8000-000100002222");
-
- Mockito.when(context.getHeaderString(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION)).thenReturn(VaultAdminOnlyFilterProviderTestConstants.INVALID_SIGNATURE_TOKEN);
- Mockito.when(context.getUriInfo()).thenReturn(uriInfo);
- Mockito.when(context.getUriInfo().getPathParameters()).thenReturn(pathParams);
-
- Assertions.assertThrows(VaultAdminValidationFailedException.class, () -> vaultAdminOnlyFilterProvider.filter(context));
- }
-
- @Test
- @DisplayName("validate malformed vaultAdminAuthorizationJWT header")
- public void testMalformedVaultAdminAuthorizationJWTHeader() {
- var pathParams = new MultivaluedHashMap();
- pathParams.add(VaultAdminOnlyFilterProvider.VAULT_ID, "7E57C0DE-0000-4000-8000-000100002222");
-
- Mockito.when(context.getHeaderString(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION)).thenReturn(VaultAdminOnlyFilterProviderTestConstants.MALFORMED_TOKEN);
- Mockito.when(context.getUriInfo()).thenReturn(uriInfo);
- Mockito.when(context.getUriInfo().getPathParameters()).thenReturn(pathParams);
-
- Assertions.assertThrows(VaultAdminValidationFailedException.class, () -> vaultAdminOnlyFilterProvider.filter(context));
- }
-
- @Test
- @DisplayName("validate malformed key in database")
- public void testMalformedKeyInDatabase() throws SQLException {
- var pathParams = new MultivaluedHashMap();
- pathParams.add(VaultAdminOnlyFilterProvider.VAULT_ID, "7E57C0DE-0000-4000-8000-000100003000");
-
- Mockito.when(context.getHeaderString(VaultAdminOnlyFilterProvider.VAULT_ADMIN_AUTHORIZATION)).thenReturn(VaultAdminOnlyFilterProviderTestConstants.VALID_TOKEN_VAULT_3000);
- Mockito.when(context.getUriInfo()).thenReturn(uriInfo);
- Mockito.when(context.getUriInfo().getPathParameters()).thenReturn(pathParams);
-
- try (var s = dataSource.getConnection().createStatement()) {
- s.execute("""
- INSERT INTO "vault" ("id", "name", "description", "creation_time", "salt", "iterations", "masterkey", "auth_pubkey", "auth_prvkey")
- VALUES ('7E57C0DE-0000-4000-8000-000100003000', 'Vault 1000', 'This is a testvault.', '2020-02-20 20:20:20', 'salt3000', 'iterations3000', 'masterkey3000', 'pubkey', 'prvkey')
- """);
-
- Assertions.assertThrows(VaultAdminValidationFailedException.class, () -> vaultAdminOnlyFilterProvider.filter(context));
- }
- }
-}
\ No newline at end of file
diff --git a/backend/src/test/java/org/cryptomator/hub/filters/VaultRoleFilterTest.java b/backend/src/test/java/org/cryptomator/hub/filters/VaultRoleFilterTest.java
new file mode 100644
index 000000000..a190985b6
--- /dev/null
+++ b/backend/src/test/java/org/cryptomator/hub/filters/VaultRoleFilterTest.java
@@ -0,0 +1,185 @@
+package org.cryptomator.hub.filters;
+
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.annotation.Nullable;
+import jakarta.ws.rs.ForbiddenException;
+import jakarta.ws.rs.NotAuthorizedException;
+import jakarta.ws.rs.NotFoundException;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ResourceInfo;
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.UriInfo;
+import org.cryptomator.hub.entities.VaultAccess;
+import org.eclipse.microprofile.jwt.JsonWebToken;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+
+@QuarkusTest
+public class VaultRoleFilterTest {
+
+ private final ResourceInfo resourceInfo = Mockito.mock(ResourceInfo.class);
+ private final UriInfo uriInfo = Mockito.mock(UriInfo.class);
+ private final ContainerRequestContext context = Mockito.mock(ContainerRequestContext.class);
+ private final JsonWebToken jwt = Mockito.mock(JsonWebToken.class);
+ private final VaultRoleFilter filter = new VaultRoleFilter();
+
+ @BeforeEach
+ public void setup() {
+ filter.resourceInfo = resourceInfo;
+ filter.jwt = jwt;
+
+ Mockito.doReturn(uriInfo).when(context).getUriInfo();
+ }
+
+ @Test
+ @DisplayName("error 403 if annotated resource has no vaultId path param")
+ public void testFilterWithMissingVaultId() throws NoSuchMethodException {
+ Mockito.doReturn(VaultRoleFilterTest.class.getMethod("allowMember")).when(resourceInfo).getResourceMethod();
+ Mockito.doReturn(new MultivaluedHashMap<>()).when(uriInfo).getPathParameters();
+
+ Assertions.assertThrows(ForbiddenException.class, () -> filter.filter(context));
+ }
+
+ @Test
+ @DisplayName("error 401 if JWT is missing")
+ public void testFilterWithMissingJWT() throws NoSuchMethodException {
+ Mockito.doReturn(VaultRoleFilterTest.class.getMethod("allowMember")).when(resourceInfo).getResourceMethod();
+ Mockito.doReturn(new MultivaluedHashMap<>(Map.of(VaultRole.DEFAULT_VAULT_ID_PARAM, "7E57C0DE-0000-4000-8000-000100001111"))).when(uriInfo).getPathParameters();
+
+ Assertions.assertThrows(NotAuthorizedException.class, () -> filter.filter(context));
+ }
+
+ @Test
+ @DisplayName("error 403 if user2 tries to access 7E57C0DE-0000-4000-8000-000100001111")
+ public void testFilterWithInsufficientPrivileges() throws NoSuchMethodException {
+ Mockito.doReturn(VaultRoleFilterTest.class.getMethod("allowOwner")).when(resourceInfo).getResourceMethod();
+ Mockito.doReturn(new MultivaluedHashMap<>(Map.of(VaultRole.DEFAULT_VAULT_ID_PARAM, "7E57C0DE-0000-4000-8000-000100001111"))).when(uriInfo).getPathParameters();
+ Mockito.doReturn("user2").when(jwt).getSubject();
+
+ var e = Assertions.assertThrows(ForbiddenException.class, () -> filter.filter(context));
+
+ Assertions.assertEquals("Vault role required: OWNER", e.getMessage());
+ }
+
+ @Test
+ @DisplayName("pass if user1 tries to access 7E57C0DE-0000-4000-8000-000100001111 (user1 is OWNER of vault)")
+ public void testFilterSuccess1() throws NoSuchMethodException {
+ Mockito.doReturn(VaultRoleFilterTest.class.getMethod("allowOwner")).when(resourceInfo).getResourceMethod();
+ Mockito.doReturn(new MultivaluedHashMap<>(Map.of(VaultRole.DEFAULT_VAULT_ID_PARAM, "7E57C0DE-0000-4000-8000-000100001111"))).when(uriInfo).getPathParameters();
+ Mockito.doReturn("user1").when(jwt).getSubject();
+
+ Assertions.assertDoesNotThrow(() -> filter.filter(context));
+ }
+
+ @Test
+ @DisplayName("pass if user2 tries to access 7E57C0DE-0000-4000-8000-000100002222 (user2 is member of group2, which is OWNER of the vault)")
+ public void testFilterSuccess2() throws NoSuchMethodException {
+ Mockito.doReturn(VaultRoleFilterTest.class.getMethod("allowOwner")).when(resourceInfo).getResourceMethod();
+ Mockito.doReturn(new MultivaluedHashMap<>(Map.of(VaultRole.DEFAULT_VAULT_ID_PARAM, "7E57C0DE-0000-4000-8000-000100002222"))).when(uriInfo).getPathParameters();
+ Mockito.doReturn("user2").when(jwt).getSubject();
+
+ Assertions.assertDoesNotThrow(() -> filter.filter(context));
+ }
+
+ @Nested
+ @DisplayName("when attempting to access archived vault")
+ public class OnArchivedVault {
+
+ @BeforeEach
+ public void setup() {
+ Mockito.doReturn(new MultivaluedHashMap<>(Map.of(VaultRole.DEFAULT_VAULT_ID_PARAM, "7E57C0DE-0000-4000-8000-00010000AAAA"))).when(uriInfo).getPathParameters();
+ }
+
+ @Test
+ @DisplayName("pass if user1 tries to access 7E57C0DE-0000-4000-8000-00010000AAAA (user1 is OWNER of vault)")
+ public void testFilterSuccess() throws NoSuchMethodException {
+ Mockito.doReturn(VaultRoleFilterTest.class.getMethod("allowOwner")).when(resourceInfo).getResourceMethod();
+ Mockito.doReturn("user1").when(jwt).getSubject();
+
+ Assertions.assertDoesNotThrow(() -> filter.filter(context));
+ }
+
+ @Test
+ @DisplayName("error 403 if user2 tries to access 7E57C0DE-0000-4000-8000-00010000AAAA")
+ public void testFilterWithInsufficientPrivileges() throws NoSuchMethodException {
+ Mockito.doReturn(VaultRoleFilterTest.class.getMethod("allowOwner")).when(resourceInfo).getResourceMethod();
+ Mockito.doReturn("user2").when(jwt).getSubject();
+
+ var e = Assertions.assertThrows(ForbiddenException.class, () -> filter.filter(context));
+
+ Assertions.assertEquals("Vault role required: OWNER", e.getMessage());
+ }
+
+ }
+
+ @Nested
+ @DisplayName("when attempting to access non-existing vault")
+ public class OnMissingVault {
+
+ @BeforeEach
+ public void setup() {
+ Mockito.doReturn(new MultivaluedHashMap<>(Map.of(VaultRole.DEFAULT_VAULT_ID_PARAM, "7E57C0DE-0000-4000-8000-BADBADBADBAD"))).when(uriInfo).getPathParameters();
+ Mockito.doReturn("user1").when(jwt).getSubject();
+ }
+
+ @Test
+ @DisplayName("error 403 if annotated with @VaultRole(onMissingVault = OnMissingVault.FORBIDDEN)")
+ public void testForbidden() throws NoSuchMethodException {
+ Mockito.doReturn(NonExistingVault.class.getMethod("forbidden")).when(resourceInfo).getResourceMethod();
+
+ var e = Assertions.assertThrows(ForbiddenException.class, () -> filter.filter(context));
+
+ Assertions.assertEquals("Vault role required: OWNER", e.getMessage());
+ }
+
+ @Test
+ @DisplayName("error 404 if annotated with @VaultRole(onMissingVault = OnMissingVault.NOT_FOUND)")
+ public void testNotFound() throws NoSuchMethodException {
+ Mockito.doReturn(NonExistingVault.class.getMethod("notFound")).when(resourceInfo).getResourceMethod();
+
+ var e = Assertions.assertThrows(NotFoundException.class, () -> filter.filter(context));
+
+ Assertions.assertEquals("Vault not found", e.getMessage());
+ }
+
+ @Test
+ @DisplayName("pass if annotated with @VaultRole(onMissingVault = OnMissingVault.PASS)")
+ public void testPass() throws NoSuchMethodException {
+ Mockito.doReturn(NonExistingVault.class.getMethod("pass")).when(resourceInfo).getResourceMethod();
+
+ Assertions.assertDoesNotThrow(() -> filter.filter(context));
+ }
+
+ }
+
+ /*
+ * "real" methods for testing below, as we can not mock Method.class without breaking Mockito
+ */
+
+ @VaultRole({VaultAccess.Role.MEMBER})
+ public void allowMember() {}
+
+ @VaultRole({VaultAccess.Role.OWNER})
+ public void allowOwner() {}
+
+ public static class NonExistingVault {
+ @VaultRole(value = {VaultAccess.Role.OWNER}, onMissingVault = VaultRole.OnMissingVault.FORBIDDEN)
+ public void forbidden() {}
+
+ @VaultRole(value = {VaultAccess.Role.OWNER}, onMissingVault = VaultRole.OnMissingVault.NOT_FOUND)
+ public void notFound() {}
+
+ @VaultRole(value = {VaultAccess.Role.OWNER}, onMissingVault = VaultRole.OnMissingVault.PASS)
+ public void pass() {}
+ }
+
+
+}
\ No newline at end of file
diff --git a/backend/src/test/java/org/cryptomator/hub/license/LicenseHolderTest.java b/backend/src/test/java/org/cryptomator/hub/license/LicenseHolderTest.java
index 5a162d7ce..005e32586 100644
--- a/backend/src/test/java/org/cryptomator/hub/license/LicenseHolderTest.java
+++ b/backend/src/test/java/org/cryptomator/hub/license/LicenseHolderTest.java
@@ -2,8 +2,12 @@
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
+import io.quarkus.arc.Arc;
+import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.mockito.InjectMock;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import io.quarkus.test.junit.TestProfile;
+import jakarta.inject.Inject;
import org.cryptomator.hub.entities.Settings;
import org.hibernate.Session;
import org.hibernate.query.Query;
@@ -13,13 +17,28 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicBoolean;
@QuarkusTest
public class LicenseHolderTest {
+ @Inject
LicenseHolder holder;
@Nested
@@ -29,20 +48,25 @@ class TestPostConstruct {
@InjectMock
Session session;
+ @InjectMock
LicenseValidator validator;
+ @InjectMock
+ RandomMinuteSleeper randomMinuteSleeper;
+
MockedStatic settingsClass;
@BeforeEach
- public void setup() {
+ public void setup() throws InterruptedException {
Query mockQuery = Mockito.mock(Query.class);
Mockito.doNothing().when(session).persist(Mockito.any());
Mockito.when(session.createQuery(Mockito.anyString())).thenReturn(mockQuery);
Mockito.when(mockQuery.getSingleResult()).thenReturn(0l);
+ Mockito.doNothing().when(randomMinuteSleeper).sleep();
+
+ Arc.container().instance(LicenseHolder.class).destroy();
- validator = Mockito.mock(LicenseValidator.class);
settingsClass = Mockito.mockStatic(Settings.class);
- holder = new LicenseHolder(validator);
}
@AfterEach
@@ -62,14 +86,18 @@ public void testValidDBTokenSet() {
holder.init();
- Mockito.verify(validator, Mockito.times(1)).validate("token", "42");
+ // init implicitly called due to @PostConstruct which increases the times to verify by 1
+ // See https://github.com/cryptomator/hub/pull/229#discussion_r1374694626 for further information
+ Mockito.verify(validator, Mockito.times(2)).validate("token", "42");
Assertions.assertEquals(decodedJWT, holder.get());
}
@Test
@DisplayName("If database token is invalid, do net set it in license holder and nullify db entry")
public void testDBTokenOnFailedValidationNotSet() {
- Mockito.when(validator.validate(Mockito.anyString(), Mockito.anyString())).thenThrow(JWTVerificationException.class);
+ Mockito.when(validator.validate(Mockito.anyString(), Mockito.anyString())).thenAnswer(invocationOnMock -> {
+ throw new JWTVerificationException("");
+ });
Settings settingsMock = new Settings();
settingsMock.licenseKey = "token";
settingsMock.hubId = "42";
@@ -78,7 +106,7 @@ public void testDBTokenOnFailedValidationNotSet() {
holder.init();
Mockito.verify(validator, Mockito.times(1)).validate("token", "42");
- Mockito.verify(session, Mockito.times(1)).persist(Mockito.any());
+ Mockito.verify(session, Mockito.times(1)).persist(Mockito.eq(settingsMock));
Assertions.assertNull(holder.get());
}
@@ -87,6 +115,7 @@ public void testDBTokenOnFailedValidationNotSet() {
public void testNullDBTokenNotSet() {
Settings settingsEntity = Mockito.mock(Settings.class);
settingsClass.when(Settings::get).thenReturn(settingsEntity);
+
holder.init();
Mockito.verify(validator, Mockito.never()).validate(Mockito.anyString(), Mockito.anyString());
@@ -103,20 +132,25 @@ class TestSetter {
@InjectMock
Session session;
+ @InjectMock
LicenseValidator validator;
+ @InjectMock
+ RandomMinuteSleeper randomMinuteSleeper;
+
MockedStatic settingsClass;
@BeforeEach
- public void setup() {
+ public void setup() throws InterruptedException {
Query mockQuery = Mockito.mock(Query.class);
Mockito.doNothing().when(session).persist(Mockito.any());
Mockito.when(session.createQuery(Mockito.anyString())).thenReturn(mockQuery);
Mockito.when(mockQuery.getSingleResult()).thenReturn(0l);
+ Mockito.doNothing().when(randomMinuteSleeper).sleep();
+
+ Arc.container().instance(LicenseHolder.class).destroy();
- validator = Mockito.mock(LicenseValidator.class);
settingsClass = Mockito.mockStatic(Settings.class);
- holder = new LicenseHolder(validator);
}
@AfterEach
@@ -136,7 +170,7 @@ public void testSetValidToken() {
holder.set("token");
Mockito.verify(validator, Mockito.times(1)).validate("token", "42");
- Mockito.verify(session, Mockito.times(1)).persist(Mockito.any());
+ Mockito.verify(session, Mockito.times(1)).persist(Mockito.eq(settingsMock));
Assertions.assertEquals("token", settingsMock.licenseKey);
Assertions.assertEquals(decodedJWT, holder.get());
}
@@ -144,7 +178,9 @@ public void testSetValidToken() {
@Test
@DisplayName("Setting an invalid token fails with exception")
public void testSetInvalidToken() {
- Mockito.when(validator.validate("token", "42")).thenThrow(JWTVerificationException.class);
+ Mockito.when(validator.validate("token", "42")).thenAnswer(invocationOnMock -> {
+ throw new JWTVerificationException("");
+ });
Settings settingsMock = new Settings();
settingsMock.hubId = "42";
settingsClass.when(Settings::get).thenReturn(settingsMock);
@@ -157,6 +193,359 @@ public void testSetInvalidToken() {
}
}
+ @Nested
+ @DisplayName("Testing refreshLicense() method of LicenseHolder")
+ class TestRefreshLicense {
+
+ private final String refreshURL = "https://foo.bar.baz/";
+
+ private final HttpRequest refreshRequst = HttpRequest.newBuilder() //
+ .uri(URI.create(refreshURL)) //
+ .headers("Content-Type", "application/x-www-form-urlencoded") //
+ .POST(HttpRequest.BodyPublishers.ofString("token=token")) //
+ .build();
+
+ @InjectMock
+ Session session;
+
+ @InjectMock
+ LicenseValidator validator;
+
+ @InjectMock
+ RandomMinuteSleeper randomMinuteSleeper;
+
+ MockedStatic settingsClass;
+
+ @BeforeEach
+ public void setup() throws InterruptedException {
+ Query mockQuery = Mockito.mock(Query.class);
+ Mockito.doNothing().when(session).persist(Mockito.any());
+ Mockito.when(session.createQuery(Mockito.anyString())).thenReturn(mockQuery);
+ Mockito.when(mockQuery.getSingleResult()).thenReturn(0l);
+ Mockito.doNothing().when(randomMinuteSleeper).sleep();
+
+ Arc.container().instance(LicenseHolder.class).destroy();
+
+ settingsClass = Mockito.mockStatic(Settings.class);
+ }
+
+ @AfterEach
+ public void teardown() {
+ settingsClass.close();
+ }
+
+ @Test
+ @DisplayName("Refreshing a valid token validates and persists it to db")
+ public void testRefreshingExistingValidTokenInculdingRefreshURL() throws IOException, InterruptedException {
+ var existingJWT = Mockito.mock(DecodedJWT.class);
+ var receivedJWT = Mockito.mock(DecodedJWT.class);
+ Mockito.when(existingJWT.getToken()).thenReturn("token&foo=bar");
+ Mockito.when(validator.validate("token", "42")).thenReturn(receivedJWT);
+ Mockito.when(validator.validate("token&foo=bar", "42")).thenReturn(existingJWT);
+ Settings settingsMock = new Settings();
+ settingsMock.hubId = "42";
+ settingsMock.licenseKey = "token&foo=bar";
+ settingsClass.when(Settings::get).thenReturn(settingsMock);
+
+ var refreshTokenContainingSpecialChars = HttpRequest.newBuilder() //
+ .uri(URI.create(refreshURL)) //
+ .headers("Content-Type", "application/x-www-form-urlencoded") //
+ .POST(HttpRequest.BodyPublishers.ofString("token=token%26foo%3Dbar")) //
+ .build();
+
+ var httpClient = Mockito.mock(HttpClient.class);
+ var response = Mockito.mock(HttpResponse.class);
+ Mockito.doAnswer(invocation -> {
+ HttpRequest httpRequest = invocation.getArgument(0);
+ Assertions.assertEquals(refreshTokenContainingSpecialChars, httpRequest);
+ Assertions.assertEquals("token=token%26foo%3Dbar", getResponseFromRequest(httpRequest));
+ return response;
+ }).when(httpClient).send(Mockito.any(), Mockito.eq(HttpResponse.BodyHandlers.ofString()));
+ Mockito.when(response.body()).thenReturn("token");
+ Mockito.when(response.statusCode()).thenReturn(200);
+
+ holder.refreshLicense(refreshURL, existingJWT.getToken(), httpClient);
+
+ Mockito.verify(validator, Mockito.times(1)).validate("token", "42");
+ Mockito.verify(session, Mockito.times(1)).persist(Mockito.eq(settingsMock));
+ Assertions.assertEquals(receivedJWT, holder.get());
+ }
+
+ @ParameterizedTest(name = "Refreshing a valid token but receiving \"{0}\" with status code does \"{1}\" not persists it to db")
+ @CsvSource(value = {"invalidToken,200", "'',200", "validToken,500"})
+ public void testInvalidTokenReceivedLeadsToNoOp(String receivedToken, int receivedCode) throws IOException, InterruptedException {
+ var existingJWT = Mockito.mock(DecodedJWT.class);
+ var receivedJWT = Mockito.mock(DecodedJWT.class);
+ Mockito.when(existingJWT.getToken()).thenReturn("token");
+ Mockito.when(validator.validate("token", "42")).thenReturn(existingJWT);
+ if (receivedToken.equals("validToken")) {
+ Mockito.when(validator.validate(receivedToken, "42")).thenReturn(receivedJWT);
+ } else {
+ Mockito.when(validator.validate(receivedToken, "42")).thenAnswer(invocationOnMock -> {
+ throw new JWTVerificationException("");
+ });
+ }
+ Settings settingsMock = new Settings();
+ settingsMock.hubId = "42";
+ settingsMock.licenseKey = "token";
+ settingsClass.when(Settings::get).thenReturn(settingsMock);
+
+ var httpClient = Mockito.mock(HttpClient.class);
+ var response = Mockito.mock(HttpResponse.class);
+ Mockito.doAnswer(invocation -> {
+ HttpRequest httpRequest = invocation.getArgument(0);
+ Assertions.assertEquals(refreshRequst, httpRequest);
+ Assertions.assertEquals("token=token", getResponseFromRequest(httpRequest));
+ return response;
+ }).when(httpClient).send(Mockito.any(), Mockito.eq(HttpResponse.BodyHandlers.ofString()));
+ Mockito.when(response.body()).thenReturn(receivedToken);
+ Mockito.when(response.statusCode()).thenReturn(receivedCode);
+
+ holder.refreshLicense(refreshURL, existingJWT.getToken(), httpClient);
+
+ // init implicitly called due to @PostConstruct which increases the times to verify by 1
+ // See https://github.com/cryptomator/hub/pull/229#discussion_r1374694626 for further information
+ Mockito.verify(validator, Mockito.times(1)).validate("token", "42");
+ Mockito.verify(session, Mockito.never()).persist(Mockito.any());
+ Assertions.assertEquals(existingJWT, holder.get());
+ }
+
+ @Test
+ @DisplayName("Refreshing a valid token but IOException thrown does not persists it to db")
+ public void testCommunicationProblemLeadsToNoOp() throws IOException, InterruptedException {
+ var existingJWT = Mockito.mock(DecodedJWT.class);
+ Mockito.when(existingJWT.getToken()).thenReturn("token");
+ Mockito.when(validator.validate("token", "42")).thenReturn(existingJWT);
+ Settings settingsMock = new Settings();
+ settingsMock.hubId = "42";
+ settingsMock.licenseKey = "token";
+ settingsClass.when(Settings::get).thenReturn(settingsMock);
+
+ var httpClient = Mockito.mock(HttpClient.class);
+ Mockito.doAnswer(invocation -> {
+ HttpRequest httpRequest = invocation.getArgument(0);
+ Assertions.assertEquals(refreshRequst, httpRequest);
+ throw new IOException("Problem during communication");
+ }).when(httpClient).send(Mockito.any(), Mockito.eq(HttpResponse.BodyHandlers.ofString()));
+
+ holder.refreshLicense(refreshURL, existingJWT.getToken(), httpClient);
+
+ // init implicitly called due to @PostConstruct which increases the times to verify by 1
+ // See https://github.com/cryptomator/hub/pull/229#discussion_r1374694626 for further information
+ Mockito.verify(validator, Mockito.times(1)).validate("token", "42");
+ Mockito.verify(session, Mockito.never()).persist(Mockito.any());
+ Assertions.assertEquals(existingJWT, holder.get());
+ }
+
+ @Test
+ @DisplayName("Refreshing a valid token without refresh URL does not execute refreshLicense")
+ public void testNoOpExistingValidTokenExculdingRefreshURL() throws InterruptedException {
+ var existingJWT = Mockito.mock(DecodedJWT.class);
+ Mockito.when(validator.validate("token", "42")).thenReturn(existingJWT);
+ Mockito.when(validator.refreshUrl(existingJWT.getToken())).thenReturn(Optional.empty());
+ Settings settingsMock = new Settings();
+ settingsMock.hubId = "42";
+ settingsMock.licenseKey = "token";
+ settingsClass.when(Settings::get).thenReturn(settingsMock);
+
+ holder.refreshLicenseScheduler();
+
+ // init implicitly called due to @PostConstruct which increases the times to verify by 1
+ // See https://github.com/cryptomator/hub/pull/229#discussion_r1374694626 for further information
+ Mockito.verify(validator, Mockito.times(1)).validate("token", "42");
+ Mockito.verify(session, Mockito.never()).persist(Mockito.any());
+ Assertions.assertEquals(existingJWT, holder.get());
+ }
+
+ private String getResponseFromRequest(HttpRequest httpRequest) {
+ return httpRequest.bodyPublisher().map(p -> {
+ var bodySubscriber = HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8);
+ var flowSubscriber = new StringSubscriber(bodySubscriber);
+ p.subscribe(flowSubscriber);
+ return bodySubscriber.getBody().toCompletableFuture().join();
+ }).get();
+ }
+
+ private record StringSubscriber(HttpResponse.BodySubscriber wrapped) implements Flow.Subscriber {
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ wrapped.onSubscribe(subscription);
+ }
+
+ @Override
+ public void onNext(ByteBuffer item) {
+ wrapped.onNext(List.of(item));
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ wrapped.onError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ wrapped.onComplete();
+ }
+ }
+ }
+
+ @Nested
+ @TestProfile(LicenseHolderInitPropsTest.ValidInitPropsInstanceTestProfile.class)
+ @DisplayName("Testing LicenseHolder methods using InitProps")
+ class LicenseHolderInitPropsTest {
+
+ @InjectMock
+ Session session;
+
+ @InjectMock
+ LicenseValidator validator;
+
+ @InjectMock
+ RandomMinuteSleeper randomMinuteSleeper;
+
+ MockedStatic settingsClass;
+
+ public static class ValidInitPropsInstanceTestProfile implements QuarkusTestProfile {
+ @Override
+ public Map getConfigOverrides() {
+ return Map.of("hub.initial-id", "42", "hub.initial-license", "token");
+ }
+ }
+
+ @BeforeEach
+ public void setup() throws InterruptedException {
+ Query mockQuery = Mockito.mock(Query.class);
+ Mockito.doNothing().when(session).persist(Mockito.any());
+ Mockito.when(session.createQuery(Mockito.anyString())).thenReturn(mockQuery);
+ Mockito.when(mockQuery.getSingleResult()).thenReturn(0l);
+ Mockito.doNothing().when(randomMinuteSleeper).sleep();
+
+ Arc.container().instance(LicenseHolder.class).destroy();
+
+ settingsClass = Mockito.mockStatic(Settings.class);
+ }
+
+ @AfterEach
+ public void teardown() {
+ settingsClass.close();
+ }
+
+ @Test
+ @DisplayName("If init token is valid, set it in license holder")
+ public void testValidInitTokenSet() {
+ var decodedJWT = Mockito.mock(DecodedJWT.class);
+ Mockito.when(validator.validate("token", "42")).thenReturn(decodedJWT);
+ Settings settingsMock = new Settings();
+ settingsMock.hubId = "42";
+ settingsClass.when(Settings::get).thenReturn(settingsMock);
+
+ var newLicensePersisted = new AtomicBoolean(false);
+ Mockito.doAnswer(invocation -> {
+ Settings settings = invocation.getArgument(0);
+ if (settings.hubId.equals("42") && settings.licenseKey.equals("token")) {
+ newLicensePersisted.set(true);
+ }
+ return null;
+ }).when(session).persist(Mockito.any());
+
+ holder.init();
+
+ // init implicitly called due to @PostConstruct which increases the times to verify by 1
+ // See https://github.com/cryptomator/hub/pull/229#discussion_r1374694626 for further information
+ Mockito.verify(validator, Mockito.times(2)).validate("token", "42");
+ Assertions.assertTrue(newLicensePersisted.get());
+ Assertions.assertEquals(decodedJWT, holder.get());
+ }
+
+ @Test
+ @DisplayName("If init token is invalid and no token is set in db, do not modify db")
+ public void testInitTokenOnFailedValidationNotSet() {
+ Mockito.when(validator.validate("token", "42")).thenAnswer(invocationOnMock -> {
+ throw new JWTVerificationException("");
+ });
+ Settings settingsMock = new Settings();
+ settingsMock.hubId = "42";
+ settingsClass.when(Settings::get).thenReturn(settingsMock);
+
+ holder.init();
+
+ // init implicitly called due to @PostConstruct which increases the times to verify by 1
+ // See https://github.com/cryptomator/hub/pull/229#discussion_r1374694626 for further information
+ Mockito.verify(validator, Mockito.times(2)).validate("token", "42");
+ Mockito.verify(session, Mockito.never()).persist(Mockito.eq(settingsMock));
+ Assertions.assertNull(holder.get());
+ }
+
+ @Test
+ @DisplayName("If token is set in DB, ignore valid init token")
+ public void testValidDBTokenIgnoresValidInitToken() {
+ var decodedJWT = Mockito.mock(DecodedJWT.class);
+ Mockito.when(validator.validate("token3000", "3000")).thenReturn(decodedJWT);
+ Settings settingsMock = new Settings();
+ settingsMock.hubId = "3000";
+ settingsMock.licenseKey = "token3000";
+ settingsClass.when(Settings::get).thenReturn(settingsMock);
+
+ holder.init();
+
+ // init implicitly called due to @PostConstruct which increases the times to verify by 1
+ // See https://github.com/cryptomator/hub/pull/229#discussion_r1374694626 for further information
+ Mockito.verify(validator, Mockito.times(2)).validate("token3000", "3000");
+ Mockito.verify(session, Mockito.never()).persist(Mockito.any());
+ Assertions.assertEquals(decodedJWT, holder.get());
+ }
+
+ @Test
+ @DisplayName("If token is set in DB, ignore invalid init token")
+ public void testValidDBTokenIgnoresInvalidInitToken() {
+ Mockito.when(validator.validate("token", "42")).thenAnswer(invocationOnMock -> {
+ throw new JWTVerificationException("");
+ });
+
+ var decodedJWT = Mockito.mock(DecodedJWT.class);
+ Mockito.when(validator.validate("token3000", "42")).thenReturn(decodedJWT);
+ Settings settingsMock = new Settings();
+ settingsMock.hubId = "42";
+ settingsMock.licenseKey = "token3000";
+ settingsClass.when(Settings::get).thenReturn(settingsMock);
+
+ holder.init();
+
+ // init implicitly called due to @PostConstruct which increases the times to verify by 1
+ // See https://github.com/cryptomator/hub/pull/229#discussion_r1374694626 for further information
+ Mockito.verify(validator, Mockito.times(2)).validate("token3000", "42");
+ Mockito.verify(session, Mockito.never()).persist(Mockito.any());
+ Assertions.assertEquals(decodedJWT, holder.get());
+ }
+
+ @Test
+ @DisplayName("Setting a valid token validates and overwrites the init token")
+ public void testSetValidToken() {
+ var decodedJWT = Mockito.mock(DecodedJWT.class);
+ Mockito.when(validator.validate("token3000", "42")).thenReturn(decodedJWT);
+
+ Settings initSettingsMock = new Settings();
+ initSettingsMock.hubId = "42";
+ settingsClass.when(Settings::get).thenReturn(initSettingsMock);
+
+ var newLicensePersisted = new AtomicBoolean(false);
+ Mockito.doAnswer(invocation -> {
+ Settings settings = invocation.getArgument(0);
+ if (settings.hubId.equals("42") && settings.licenseKey.equals("token3000")) {
+ newLicensePersisted.set(true);
+ }
+ return null;
+ }).when(session).persist(Mockito.any());
+
+ holder.set("token3000");
+
+ Mockito.verify(validator, Mockito.times(1)).validate("token3000", "42");
+ Assertions.assertTrue(newLicensePersisted.get());
+ Assertions.assertEquals(decodedJWT, holder.get());
+ }
+
+ }
}
diff --git a/backend/src/test/java/org/cryptomator/hub/license/LicenseValidatorTest.java b/backend/src/test/java/org/cryptomator/hub/license/LicenseValidatorTest.java
index 31223bfd8..f409fab8f 100644
--- a/backend/src/test/java/org/cryptomator/hub/license/LicenseValidatorTest.java
+++ b/backend/src/test/java/org/cryptomator/hub/license/LicenseValidatorTest.java
@@ -7,10 +7,13 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+import java.util.Optional;
+
public class LicenseValidatorTest {
private static final String VALID_TOKEN = "eyJhbGciOiJFUzUxMiJ9.eyJqdGkiOiI0MiIsImlhdCI6MTY0ODA0OTM2MCwiaXNzIjoiU2t5bWF0aWMiLCJhdWQiOiJDcnlwdG9tYXRvciBIdWIiLCJzdWIiOiJodWJAY3J5cHRvbWF0b3Iub3JnIiwic2VhdHMiOjUsImV4cCI6MjUzNDAyMjE0NDAwLCJyZWZyZXNoVXJsIjoiaHR0cDovL2xvY2FsaG9zdDo4Nzg3L2h1Yi9zdWJzY3JpcHRpb24_aHViX2lkPTQyIn0.AKyoZ0WQ8xhs8vPymWPHCsc6ch6pZpfxBcrF5QjVLSQVnYz2s5QF3nnkwn4AGR7V14TuhkJMZLUZxMdQAYLyL95sAV2Fu0E4-e1v3IVKlNKtze89eqYvEs6Ak9jWjtecOgPWNWjz2itI4MfJBDmbFtTnehOtqRqUdsDoC9NFik2C7tHm";
private static final String EXPIRED_TOKEN = "eyJhbGciOiJFUzUxMiJ9.eyJqdGkiOiI0MiIsImlhdCI6MTY3NzA4MzI1OSwiaXNzIjoiU2t5bWF0aWMiLCJhdWQiOiJDcnlwdG9tYXRvciBIdWIiLCJzdWIiOiJodWJAY3J5cHRvbWF0b3Iub3JnIiwic2VhdHMiOjUsImV4cCI6OTQ2Njg0ODAwLCJyZWZyZXNoVXJsIjoiaHR0cDovL2xvY2FsaG9zdDo4Nzg3L2h1Yi9zdWJzY3JpcHRpb24_aHViX2lkPTQyIn0.APQnWig9ZyT6_xRviPVs3YPTaP1w_YXTpWULgvsUpCGmGQwEmT6nl0x2jNB_jkQi93E7tr9WvipvX5DkXUOYJP3OAJjzPdN7rTX2tnXTKO8irshkcqmvt79v1E4k50YLkwP-1NIwiO_ltp5sezhLbzOVPXRag6mQfc0KvS6PiZTYGYQh";
+ private static final String FUTURE_TOKEN = "eyJhbGciOiJFUzUxMiJ9.eyJqdGkiOjQyLCJpYXQiOjE3MDEyNDkzMzEzMSwiaXNzIjoiU2t5bWF0aWMiLCJhdWQiOiJDcnlwdG9tYXRvciBIdWIiLCJzdWIiOiJ0b2JpYXMuaGFnZW1hbm5Ac2t5bWF0aWMuZGUiLCJzZWF0cyI6NSwiZXhwIjoxNzIyMzg0MDAwLCJyZWZyZXNoVXJsIjoiaHR0cDovL2xvY2FsaG9zdDo4Nzg3L2h1Yi9zdWJzY3JpcHRpb24_aHViX2lkPTQyIn0.ALd0oyPR3kgntysXp8TZ1LvmHYDiDIGlbmaq52d5wAE1V8MZ1asWvufXgL9YExXvJhFbGCnLu66XgA387rxjrxKeASL_q43ZZUEDxtm8aa7uH2VMOvdM3gXEibSHUzNwO0MRWFbeYWOc8daRNWdxgOcrpX6NcMV7vPZH7yZSEct_cqf5";
private static final String TOKEN_WITH_INVALID_SIGNATURE = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.AbVUinMiT3J_03je8WTOIl-VdggzvoFgnOsdouAs-DLOtQzau9valrq-S6pETyi9Q18HH-EuwX49Q7m3KC0GuNBJAc9Tksulgsdq8GqwIqZqDKmG7hNmDzaQG1Dpdezn2qzv-otf3ZZe-qNOXUMRImGekfQFIuH_MjD2e8RZyww6lbZk";
private static final String MALFORMED_TOKEN = "hello world";
@@ -45,6 +48,13 @@ public void testValidateExpiredToken() {
validator.validate(EXPIRED_TOKEN, "42");
}
+ @Test
+ @DisplayName("validate future token")
+ public void testValidateFutureToken() {
+ // this should not throw an exception and return a JWT with an issued at in the future
+ validator.validate(FUTURE_TOKEN, "42");
+ }
+
@Test
@DisplayName("validate token with invalid signature")
public void testValidateTokenWithInvalidSignature() {
@@ -61,4 +71,25 @@ public void testValidateMalformedToken() {
});
}
+ @Test
+ @DisplayName("validate token's refreshURL")
+ public void testGetTokensRefreshUrl() {
+ Assertions.assertEquals(Optional.of("http://localhost:8787/hub/subscription?hub_id=42"), validator.refreshUrl(VALID_TOKEN));
+ }
+
+ @Test
+ @DisplayName("validate expired token's refreshURL")
+ public void testGetExpiredTokensRefreshUrl() {
+ // this should not throw an exception and return a JWT with an expired date
+ Assertions.assertEquals(Optional.of("http://localhost:8787/hub/subscription?hub_id=42"), validator.refreshUrl(EXPIRED_TOKEN));
+ }
+
+ @Test
+ @DisplayName("validate expired token's refreshURL with invalid signature")
+ public void testInvalidSignatureTokensRefreshUrl() {
+ Assertions.assertThrows(SignatureVerificationException.class, () -> {
+ validator.refreshUrl(TOKEN_WITH_INVALID_SIGNATURE);
+ });
+ }
+
}
diff --git a/backend/src/test/java/org/cryptomator/hub/validation/ValidationTestResource.java b/backend/src/test/java/org/cryptomator/hub/validation/ValidationTestResource.java
index 855b65255..b54e2a071 100644
--- a/backend/src/test/java/org/cryptomator/hub/validation/ValidationTestResource.java
+++ b/backend/src/test/java/org/cryptomator/hub/validation/ValidationTestResource.java
@@ -28,12 +28,6 @@ public Response probeOnlyBase64Chars(@PathParam("b64string") @OnlyBase64Chars St
return Response.ok().build();
}
- @GET
- @Path("/onlybase64urlchars/{b64urlstring}")
- public Response probeOnlyBase64UrlChars(@PathParam("b64urlstring") @OnlyBase64UrlChars String base64UrlString) {
- return Response.ok().build();
- }
-
@GET
@Path("/validjwe/{jwe}")
public Response probeValidJWE(@PathParam("jwe") @ValidJWE String jwe) {
diff --git a/backend/src/test/java/org/cryptomator/hub/validation/ValidationTestResourceTest.java b/backend/src/test/java/org/cryptomator/hub/validation/ValidationTestResourceTest.java
index 4f8893576..33504ef23 100644
--- a/backend/src/test/java/org/cryptomator/hub/validation/ValidationTestResourceTest.java
+++ b/backend/src/test/java/org/cryptomator/hub/validation/ValidationTestResourceTest.java
@@ -100,28 +100,6 @@ public void testOnlyBase64CharsInvalid(String toTest) {
}
}
- @Nested
- @DisplayName("Test @OnlyBase64UrlChars")
- public class Base64UrlCharsTest {
-
- @DisplayName("Strings only containing base64url-chars are accepted")
- @ParameterizedTest
- @ValueSource(strings = {"abcdefghijklmnopqrstuvwxyz0123456789-_", "bGln-HQgd2_yaw==", "-======"})
- public void testOnlyBase64UrlCharsValid(String toTest) {
- when().get("/test/onlybase64urlchars/{b64String}", toTest)
- .then().statusCode(200);
- }
-
- @DisplayName("Strings containing not-base64url-chars (or wrong order) are rejected")
- @ParameterizedTest
- @ValueSource(strings = {"foo+/", "\u5207ä=", "abc==abc", "==="})
- @ArgumentsSource(MalicousStringsProvider.class)
- public void testOnlyBase64UrlCharsInvalid(String toTest) {
- when().get("/test/onlybase64urlchars/{b64String}", toTest)
- .then().statusCode(400);
- }
- }
-
@Nested
@DisplayName("Test @ValidJWE")
public class JWETest {
diff --git a/backend/src/test/resources/org/cryptomator/hub/flyway/V9999__Test_Data.sql b/backend/src/test/resources/org/cryptomator/hub/flyway/V9999__Test_Data.sql
index 1e106f0f4..3ac426e73 100644
--- a/backend/src/test/resources/org/cryptomator/hub/flyway/V9999__Test_Data.sql
+++ b/backend/src/test/resources/org/cryptomator/hub/flyway/V9999__Test_Data.sql
@@ -14,10 +14,10 @@ VALUES
('group1', 'GROUP', 'Group Name 1'),
('group2', 'GROUP', 'Group Name 2');
-INSERT INTO "user_details" ("id")
+INSERT INTO "user_details" ("id", "publickey", "privatekey", "setupcode")
VALUES
- ('user1'),
- ('user2');
+ ('user1', 'public1', 'private1', 'setup1'),
+ ('user2', NULL, NULL, NULL);
INSERT INTO "group_details" ("id")
VALUES
@@ -39,29 +39,43 @@ VALUES
'MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii1D3jaW6pmGVJFhodzC31cy5sfOYotrzF',
'MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCAHpFQ62QnGCEvYh/pE9QmR1C9aLcDItRbslbmhen/h1tt8AyMhskeenT+rAyyPhGhZANiAAQLW5ZJePZzMIPAxMtZXkEWbDF0zo9f2n4+T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw8lE5IPUWpgu553SteKigiKLUPeNpbqmYZUkWGh3MLfVzLmx85ii2vMU=',
FALSE),
- ('7E57C0DE-0000-4000-8000-0001AAAAAAAA', 'Vault Archived', 'This is a archived vault.', '2020-02-20 20:20:20', 'salt3', 42, 'masterkey3',
+ ('7E57C0DE-0000-4000-8000-00010000AAAA', 'Vault Archived', 'This is a archived vault.', '2020-02-20 20:20:20', 'salt3', 42, 'masterkey3',
'MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii1D3jaW6pmGVJFhodzC31cy5sfOYotrzF',
'MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCAHpFQ62QnGCEvYh/pE9QmR1C9aLcDItRbslbmhen/h1tt8AyMhskeenT+rAyyPhGhZANiAAQLW5ZJePZzMIPAxMtZXkEWbDF0zo9f2n4+T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw8lE5IPUWpgu553SteKigiKLUPeNpbqmYZUkWGh3MLfVzLmx85ii2vMU=',
TRUE);
-INSERT INTO "vault_access" ("vault_id", "authority_id")
+INSERT INTO "vault_access" ("vault_id", "authority_id", "role")
VALUES
- ('7E57C0DE-0000-4000-8000-000100001111', 'user1'),
- ('7E57C0DE-0000-4000-8000-000100001111', 'user2'),
- ('7E57C0DE-0000-4000-8000-0001AAAAAAAA', 'user1'),
- ('7E57C0DE-0000-4000-8000-000100002222', 'group1'); /* user1, part of group1, has access to vault2 */
+ ('7E57C0DE-0000-4000-8000-000100001111', 'user1', 'OWNER'),
+ ('7E57C0DE-0000-4000-8000-000100001111', 'user2', 'MEMBER'),
+ ('7E57C0DE-0000-4000-8000-00010000AAAA', 'user1', 'OWNER'),
+ ('7E57C0DE-0000-4000-8000-000100002222', 'group2', 'OWNER'),
+ ('7E57C0DE-0000-4000-8000-000100002222', 'group1', 'MEMBER');
-INSERT INTO "device" ("id", "owner_id", "name", "type", "publickey", "creation_time")
+INSERT INTO "device" ("id", "owner_id", "name", "type", "publickey", "creation_time", "user_privatekey")
VALUES
- ('device1', 'user1', 'Computer 1', 'DESKTOP', 'publickey1', '2020-02-20 20:20:20'),
- ('device2', 'user2', 'Computer 2', 'DESKTOP', 'publickey2', '2020-02-20 20:20:20'),
- ('device3', 'user1', 'Computer 3', 'DESKTOP', 'publickey3', '2020-02-20 20:20:20'); /* user1 is part of group1 */
+ ('device1', 'user1', 'Computer 1', 'DESKTOP', 'publickey1', '2020-02-20 20:20:20', 'jwe.jwe.jwe.user1.device1'),
+ ('device2', 'user2', 'Computer 2', 'DESKTOP', 'publickey2', '2020-02-20 20:20:20', 'jwe.jwe.jwe.user2.device2'),
+ ('device3', 'user1', 'Computer 3', 'DESKTOP', 'publickey3', '2020-02-20 20:20:20', 'jwe.jwe.jwe.user1.device3');
-INSERT INTO "access_token" ("device_id", "vault_id", "jwe")
+INSERT INTO "access_token" ("user_id", "vault_id", "vault_masterkey")
VALUES
- ('device1', '7E57C0DE-0000-4000-8000-000100001111', 'jwe1'),
- ('device2', '7E57C0DE-0000-4000-8000-000100001111', 'jwe2'),
- ('device3', '7E57C0DE-0000-4000-8000-000100002222', 'jwe3'); -- device3 of user1, part of group1, has access to vault2
+ ('user1', '7E57C0DE-0000-4000-8000-000100001111', 'jwe.jwe.jwe.vault1.user1'), -- direct access
+ ('user2', '7E57C0DE-0000-4000-8000-000100001111', 'jwe.jwe.jwe.vault1.user2'), -- direct access
+ ('user1', '7E57C0DE-0000-4000-8000-000100002222', 'jwe.jwe.jwe.vault2.user1'); -- access via group1
+
+-- DEPRECATED:
+INSERT INTO "device_legacy" ("id", "owner_id", "name", "type", "publickey", "creation_time")
+VALUES
+ ('legacyDevice1', 'user1', 'Computer 1', 'DESKTOP', 'publickey1', '2020-02-20 20:20:20'),
+ ('legacyDevice2', 'user2', 'Computer 2', 'DESKTOP', 'publickey2', '2020-02-20 20:20:20'),
+ ('legacyDevice3', 'user1', 'Computer 3', 'DESKTOP', 'publickey3', '2020-02-20 20:20:20');
+
+INSERT INTO "access_token_legacy" ("device_id", "vault_id", "jwe")
+VALUES
+ ('legacyDevice1', '7E57C0DE-0000-4000-8000-000100001111', 'legacy.jwe.jwe.vault1.device1'), -- direct access
+ ('legacyDevice2', '7E57C0DE-0000-4000-8000-000100001111', 'legacy.jwe.jwe.vault1.device2'), -- direct access
+ ('legacyDevice3', '7E57C0DE-0000-4000-8000-000100002222', 'legacy.jwe.jwe.vault2.device3'); -- access via group1
INSERT INTO "audit_event" ("id", "timestamp", "type")
VALUES
@@ -72,6 +86,8 @@ VALUES
(21, '2020-02-20T20:20:20.021Z', 'VAULT_MEMBER_ADD'),
(22, '2020-02-20T20:20:20.022Z', 'VAULT_MEMBER_ADD'),
(23, '2020-02-20T20:20:20.023Z', 'VAULT_MEMBER_REMOVE'),
+ (24, '2020-02-20T20:20:20.024Z', 'VAULT_MEMBER_UPDATE'),
+ (25, '2020-02-20T20:20:20.025Z', 'VAULT_MEMBER_UPDATE'),
(30, '2020-02-20T20:20:20.030Z', 'VAULT_CREATE'),
(31, '2020-02-20T20:20:20.031Z', 'VAULT_MEMBER_ADD'),
(100, '2020-02-20T20:20:20.100Z', 'DEVICE_REGISTER'),
@@ -93,20 +109,25 @@ INSERT INTO "audit_event_vault_create" ("id", "created_by", "vault_id", "vault_n
VALUES
(10, 'user1', '7E57C0DE-0000-4000-8000-000100001111', 'Vault 1', 'This is a testvault.'),
(20, 'user1', '7E57C0DE-0000-4000-8000-000100002222', 'Vault 2', 'This is a testvault.'),
- (30, 'user2', '7E57C0DE-0000-4000-8000-0001AAAAAAAA', 'Vault 3', 'This is a testvault.');
+ (30, 'user2', '7E57C0DE-0000-4000-8000-00010000AAAA', 'Vault 3', 'This is a testvault.');
-INSERT INTO "audit_event_vault_member_add" ("id", "added_by", "vault_id", "authority_id")
+INSERT INTO "audit_event_vault_member_add" ("id", "added_by", "vault_id", "authority_id", "role")
VALUES
- (11, 'user1', '7E57C0DE-0000-4000-8000-000100001111', 'user1'),
- (12, 'user1', '7E57C0DE-0000-4000-8000-000100001111', 'user2'),
- (21, 'user1', '7E57C0DE-0000-4000-8000-000100002222', 'user1'),
- (22, 'user1', '7E57C0DE-0000-4000-8000-000100002222', 'group1'),
- (31, 'user2', '7E57C0DE-0000-4000-8000-0001AAAAAAAA', 'user2');
+ (11, 'user1', '7E57C0DE-0000-4000-8000-000100001111', 'user1', 'OWNER'),
+ (12, 'user1', '7E57C0DE-0000-4000-8000-000100001111', 'user2', 'MEMBER'),
+ (21, 'user1', '7E57C0DE-0000-4000-8000-000100002222', 'user1', 'MEMBER'),
+ (22, 'user1', '7E57C0DE-0000-4000-8000-000100002222', 'group1', 'MEMBER'),
+ (31, 'user2', '7E57C0DE-0000-4000-8000-00010000AAAA', 'user1', 'MEMBER');
INSERT INTO "audit_event_vault_member_remove" ("id", "removed_by", "vault_id", "authority_id")
VALUES
(23, 'user1', '7E57C0DE-0000-4000-8000-000100002222', 'user1');
+INSERT INTO "audit_event_vault_member_update" ("id", "updated_by", "vault_id", "authority_id", "role")
+VALUES
+ (24, 'user1', '7E57C0DE-0000-4000-8000-000100001111', 'user2', 'OWNER'),
+ (25, 'user1', '7E57C0DE-0000-4000-8000-000100001111', 'user2', 'MEMBER');
+
INSERT INTO "audit_event_device_register" ("id", "registered_by", "device_id", "device_name", "device_type")
VALUES
(100, 'user1', 'device1', 'Computer 1', 'DESKTOP'),
@@ -127,9 +148,9 @@ INSERT INTO "audit_event_vault_access_grant" ("id", "granted_by", "vault_id", "a
VALUES
(2000, 'user1', '7E57C0DE-0000-4000-8000-000100001111', 'user1'),
(2001, 'user1', '7E57C0DE-0000-4000-8000-000100001111', 'user2'),
- (2002, 'user1', '7E57C0DE-0000-4000-8000-0001AAAAAAAA', 'user1'),
+ (2002, 'user1', '7E57C0DE-0000-4000-8000-00010000AAAA', 'user1'),
(2003, 'user1', '7E57C0DE-0000-4000-8000-000100002222', 'group1');
INSERT INTO "audit_event_vault_update" ("id", "updated_by", "vault_id", "vault_name", "vault_description", "vault_archived")
VALUES
- (3000, 'user1', '7E57C0DE-0000-4000-8000-0001AAAAAAAA', 'Vault Archived', 'This is a archived vault.', TRUE);
+ (3000, 'user1', '7E57C0DE-0000-4000-8000-00010000AAAA', 'Vault Archived', 'This is a archived vault.', TRUE);
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 16140c265..968cae889 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,57 +1,57 @@
{
"name": "cryptomator-hub",
- "version": "1.2.2",
+ "version": "1.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cryptomator-hub",
- "version": "1.2.2",
+ "version": "1.3.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
- "@headlessui/tailwindcss": "^0.1.3",
- "@headlessui/vue": "^1.7.14",
+ "@headlessui/tailwindcss": "^0.2.0",
+ "@headlessui/vue": "^1.7.16",
"@heroicons/vue": "^2.0.18",
- "axios": "^1.4.0",
+ "axios": "^1.6.0",
"file-saver": "^2.0.5",
"jdenticon": "^3.2.0",
"jszip": "^3.10.1",
- "keycloak-js": "^22.0.1",
+ "keycloak-js": "^22.0.5",
"miscreant": "^0.3.2",
- "rfc4648": "^1.5.2",
- "semver": "^7.5.3",
+ "rfc4648": "^1.5.3",
+ "semver": "^7.5.4",
"vue": "^3.3.4",
- "vue-i18n": "^9.2.2",
- "vue-router": "^4.2.2"
+ "vue-i18n": "^9.6.2",
+ "vue-router": "^4.2.5"
},
"devDependencies": {
- "@intlify/unplugin-vue-i18n": "^0.11.0",
- "@tailwindcss/forms": "^0.5.3",
- "@types/blueimp-md5": "^2.18.0",
- "@types/chai": "^4.3.5",
- "@types/chai-as-promised": "^7.1.5",
- "@types/file-saver": "^2.0.5",
- "@types/mocha": "^10.0.1",
- "@types/node": "^20.3.2",
- "@types/semver": "^7.5.0",
- "@typescript-eslint/eslint-plugin": "^5.60.1",
- "@typescript-eslint/parser": "^5.60.1",
- "@vitejs/plugin-vue": "^4.2.3",
+ "@intlify/unplugin-vue-i18n": "^1.4.0",
+ "@tailwindcss/forms": "^0.5.6",
+ "@types/blueimp-md5": "^2.18.1",
+ "@types/chai": "^4.3.9",
+ "@types/chai-as-promised": "^7.1.7",
+ "@types/file-saver": "^2.0.6",
+ "@types/mocha": "^10.0.3",
+ "@types/node": "^20.8.10",
+ "@types/semver": "^7.5.4",
+ "@typescript-eslint/eslint-plugin": "^6.9.1",
+ "@typescript-eslint/parser": "^6.9.1",
+ "@vitejs/plugin-vue": "^4.4.0",
"@vue/compiler-sfc": "^3.3.4",
- "autoprefixer": "^10.4.14",
- "chai": "^4.3.7",
+ "autoprefixer": "^10.4.16",
+ "chai": "^4.3.10",
"chai-as-promised": "^7.1.1",
"chai-bytes": "^0.1.2",
- "eslint": "^8.43.0",
- "eslint-plugin-vue": "^9.15.1",
+ "eslint": "^8.52.0",
+ "eslint-plugin-vue": "^9.18.1",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
- "postcss": "^8.4.24",
- "tailwindcss": "^3.3.2",
+ "postcss": "^8.4.31",
+ "tailwindcss": "^3.3.5",
"ts-node": "^10.9.1",
- "typescript": "^5.1.6",
- "vite": "^4.3.9",
- "vue-tsc": "^1.8.3"
+ "typescript": "^5.2.2",
+ "vite": "^4.5.0",
+ "vue-tsc": "^1.8.22"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -88,47 +88,119 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz",
- "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==",
+ "version": "7.22.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"dev": true,
"dependencies": {
- "@babel/highlight": "^7.22.5"
+ "@babel/highlight": "^7.22.13",
+ "chalk": "^2.4.2"
},
"engines": {
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/code-frame/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/@babel/compat-data": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz",
- "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz",
+ "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz",
- "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
+ "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.22.5",
- "@babel/generator": "^7.22.5",
- "@babel/helper-compilation-targets": "^7.22.5",
- "@babel/helper-module-transforms": "^7.22.5",
- "@babel/helpers": "^7.22.5",
- "@babel/parser": "^7.22.5",
- "@babel/template": "^7.22.5",
- "@babel/traverse": "^7.22.5",
- "@babel/types": "^7.22.5",
- "convert-source-map": "^1.7.0",
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-compilation-targets": "^7.22.15",
+ "@babel/helper-module-transforms": "^7.23.0",
+ "@babel/helpers": "^7.23.2",
+ "@babel/parser": "^7.23.0",
+ "@babel/template": "^7.22.15",
+ "@babel/traverse": "^7.23.2",
+ "@babel/types": "^7.23.0",
+ "convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
- "json5": "^2.2.2",
- "semver": "^6.3.0"
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
@@ -138,22 +210,28 @@
"url": "https://opencollective.com/babel"
}
},
+ "node_modules/@babel/core/node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true
+ },
"node_modules/@babel/core/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/generator": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz",
- "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dev": true,
"dependencies": {
- "@babel/types": "^7.22.5",
+ "@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -163,50 +241,47 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz",
- "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==",
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
+ "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==",
"dev": true,
"dependencies": {
- "@babel/compat-data": "^7.22.5",
- "@babel/helper-validator-option": "^7.22.5",
- "browserslist": "^4.21.3",
+ "@babel/compat-data": "^7.22.9",
+ "@babel/helper-validator-option": "^7.22.15",
+ "browserslist": "^4.21.9",
"lru-cache": "^5.1.1",
- "semver": "^6.3.0"
+ "semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-environment-visitor": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz",
- "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-function-name": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz",
- "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"dependencies": {
- "@babel/template": "^7.22.5",
- "@babel/types": "^7.22.5"
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
@@ -225,34 +300,34 @@
}
},
"node_modules/@babel/helper-module-imports": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz",
- "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==",
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
+ "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
"dev": true,
"dependencies": {
- "@babel/types": "^7.22.5"
+ "@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz",
- "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz",
+ "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==",
"dev": true,
"dependencies": {
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-module-imports": "^7.22.5",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-module-imports": "^7.22.15",
"@babel/helper-simple-access": "^7.22.5",
- "@babel/helper-split-export-declaration": "^7.22.5",
- "@babel/helper-validator-identifier": "^7.22.5",
- "@babel/template": "^7.22.5",
- "@babel/traverse": "^7.22.5",
- "@babel/types": "^7.22.5"
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/helper-validator-identifier": "^7.22.20"
},
"engines": {
"node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-simple-access": {
@@ -268,9 +343,9 @@
}
},
"node_modules/@babel/helper-split-export-declaration": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz",
- "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==",
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dev": true,
"dependencies": {
"@babel/types": "^7.22.5"
@@ -289,45 +364,45 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz",
- "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz",
- "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==",
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
+ "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz",
- "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz",
+ "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==",
"dev": true,
"dependencies": {
- "@babel/template": "^7.22.5",
- "@babel/traverse": "^7.22.5",
- "@babel/types": "^7.22.5"
+ "@babel/template": "^7.22.15",
+ "@babel/traverse": "^7.23.2",
+ "@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz",
- "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"dev": true,
"dependencies": {
- "@babel/helper-validator-identifier": "^7.22.5",
- "chalk": "^2.0.0",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
@@ -406,9 +481,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz",
- "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -417,33 +492,33 @@
}
},
"node_modules/@babel/template": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
- "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==",
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dev": true,
"dependencies": {
- "@babel/code-frame": "^7.22.5",
- "@babel/parser": "^7.22.5",
- "@babel/types": "^7.22.5"
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz",
- "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
"dev": true,
"dependencies": {
- "@babel/code-frame": "^7.22.5",
- "@babel/generator": "^7.22.5",
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
- "@babel/helper-split-export-declaration": "^7.22.5",
- "@babel/parser": "^7.22.5",
- "@babel/types": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/parser": "^7.23.0",
+ "@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -461,13 +536,13 @@
}
},
"node_modules/@babel/types": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz",
- "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.22.5",
- "@babel/helper-validator-identifier": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
@@ -497,9 +572,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
- "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+ "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
@@ -513,9 +588,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
- "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+ "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
@@ -529,9 +604,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
- "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+ "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
@@ -545,9 +620,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
- "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+ "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
@@ -561,9 +636,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
- "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+ "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
@@ -577,9 +652,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
- "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+ "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
@@ -593,9 +668,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
- "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+ "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
@@ -609,9 +684,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
- "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+ "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
@@ -625,9 +700,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
- "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+ "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
@@ -641,9 +716,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
- "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+ "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
@@ -657,9 +732,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
- "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+ "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
@@ -673,9 +748,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
- "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+ "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
@@ -689,9 +764,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
- "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+ "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
@@ -705,9 +780,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
- "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+ "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
@@ -721,9 +796,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
- "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+ "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
@@ -737,9 +812,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
- "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+ "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
@@ -753,9 +828,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
- "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
@@ -769,9 +844,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
- "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
@@ -785,9 +860,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
- "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+ "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
@@ -801,9 +876,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
- "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+ "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
@@ -817,9 +892,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
- "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+ "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
@@ -833,9 +908,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
- "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+ "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
@@ -864,23 +939,23 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.5.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
- "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
+ "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
"dev": true,
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
"node_modules/@eslint/eslintrc": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz",
- "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
+ "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
- "espree": "^9.5.2",
+ "espree": "^9.6.0",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
@@ -896,18 +971,18 @@
}
},
"node_modules/@eslint/js": {
- "version": "8.43.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz",
- "integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==",
+ "version": "8.52.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz",
+ "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@headlessui/tailwindcss": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.1.3.tgz",
- "integrity": "sha512-3aMdDyYZx9A15euRehpppSyQnb2gIw2s/Uccn2ELIoLQ9oDy0+9oRygNWNjXCD5Dt+w1pxo7C+XoiYvGcqA4Kg==",
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.0.tgz",
+ "integrity": "sha512-fpL830Fln1SykOCboExsWr3JIVeQKieLJ3XytLe/tt1A0XzqUthOftDmjcCYLW62w7mQI7wXcoPXr3tZ9QfGxw==",
"engines": {
"node": ">=10"
},
@@ -916,9 +991,9 @@
}
},
"node_modules/@headlessui/vue": {
- "version": "1.7.14",
- "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.14.tgz",
- "integrity": "sha512-aL9U9Sa7wdOzlrfjx6EjMIYNRCma5mngWcWzQBcHFwznpRZ8g/QZ/AYFtRDrZZUw22Ttttja4D7ZRXFwhONewA==",
+ "version": "1.7.16",
+ "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.16.tgz",
+ "integrity": "sha512-nKT+nf/q6x198SsyK54mSszaQl/z+QxtASmgMEJtpxSX2Q0OPJX0upS/9daDyiECpeAsvjkoOrm2O/6PyBQ+Qg==",
"engines": {
"node": ">=10"
},
@@ -935,12 +1010,12 @@
}
},
"node_modules/@humanwhocodes/config-array": {
- "version": "0.11.10",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
- "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
+ "version": "0.11.13",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
+ "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
"dev": true,
"dependencies": {
- "@humanwhocodes/object-schema": "^1.2.1",
+ "@humanwhocodes/object-schema": "^2.0.1",
"debug": "^4.1.1",
"minimatch": "^3.0.5"
},
@@ -962,27 +1037,27 @@
}
},
"node_modules/@humanwhocodes/object-schema": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
- "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
+ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"dev": true
},
"node_modules/@intlify/bundle-utils": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-6.0.1.tgz",
- "integrity": "sha512-BkeZNKZiC0B7K3OYMwiPLoAqsZmKH3SxTL75vYAkuQ//XWR8WO0NpfjXhTxgLTVFHxMcNb2agAopC0DP6fqDrg==",
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-7.4.0.tgz",
+ "integrity": "sha512-AQfjBe2HUxzyN8ignIk3WhhSuVcSuirgzOzkd17nb337rCbI4Gv/t1R60UUyIqFoFdviLb/wLcDUzTD/xXjv9w==",
"dev": true,
"dependencies": {
- "@intlify/message-compiler": "9.3.0-beta.17",
- "@intlify/shared": "9.3.0-beta.17",
+ "@intlify/message-compiler": "^9.4.0",
+ "@intlify/shared": "^9.4.0",
"acorn": "^8.8.2",
"escodegen": "^2.0.0",
"estree-walker": "^2.0.2",
- "jsonc-eslint-parser": "^1.0.1",
+ "jsonc-eslint-parser": "^2.3.0",
"magic-string": "^0.30.0",
"mlly": "^1.2.0",
- "source-map": "0.6.1",
- "yaml-eslint-parser": "^0.3.2"
+ "source-map-js": "^1.0.1",
+ "yaml-eslint-parser": "^1.2.2"
},
"engines": {
"node": ">= 14.16"
@@ -997,94 +1072,54 @@
}
},
"node_modules/@intlify/core-base": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.2.2.tgz",
- "integrity": "sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==",
+ "version": "9.6.2",
+ "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.6.2.tgz",
+ "integrity": "sha512-ci0j2nbEL/pamvqgcCqyIVeQ3LS41F1IRqI5rCBNnpSp0FjNnH8bpha8R3OifkhqatzlP4wGOuN/UqfLYVDv7g==",
"dependencies": {
- "@intlify/devtools-if": "9.2.2",
- "@intlify/message-compiler": "9.2.2",
- "@intlify/shared": "9.2.2",
- "@intlify/vue-devtools": "9.2.2"
+ "@intlify/message-compiler": "9.6.2",
+ "@intlify/shared": "9.6.2"
},
"engines": {
- "node": ">= 14"
- }
- },
- "node_modules/@intlify/core-base/node_modules/@intlify/message-compiler": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz",
- "integrity": "sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==",
- "dependencies": {
- "@intlify/shared": "9.2.2",
- "source-map": "0.6.1"
- },
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/@intlify/core-base/node_modules/@intlify/shared": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
- "integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==",
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/@intlify/devtools-if": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.2.2.tgz",
- "integrity": "sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==",
- "dependencies": {
- "@intlify/shared": "9.2.2"
+ "node": ">= 16"
},
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/@intlify/devtools-if/node_modules/@intlify/shared": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
- "integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==",
- "engines": {
- "node": ">= 14"
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
- "version": "9.3.0-beta.17",
- "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.3.0-beta.17.tgz",
- "integrity": "sha512-i7hvVIRk1Ax2uKa9xLRJCT57to08OhFMhFXXjWN07rmx5pWQYQ23MfX1xgggv9drnWTNhqEiD+u4EJeHoS5+Ww==",
- "dev": true,
+ "version": "9.6.2",
+ "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.6.2.tgz",
+ "integrity": "sha512-kgZQL9zeJDeEB5vvD93Y++HvFUELnT48PjnpfCcF3EJaLLVs9he8IzODiNK42Z40lWbFyja0SXJZjsalybQygA==",
"dependencies": {
- "@intlify/shared": "9.3.0-beta.17",
- "source-map": "0.6.1"
+ "@intlify/shared": "9.6.2",
+ "source-map-js": "^1.0.2"
},
"engines": {
- "node": ">= 14"
+ "node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
- "version": "9.3.0-beta.17",
- "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.3.0-beta.17.tgz",
- "integrity": "sha512-mscf7RQsUTOil35jTij4KGW1RC9SWQjYScwLxP53Ns6g24iEd5HN7ksbt9O6FvTmlQuX77u+MXpBdfJsGqizLQ==",
- "dev": true,
+ "version": "9.6.2",
+ "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.6.2.tgz",
+ "integrity": "sha512-9KBcXmJNxElp7QMnU8V0/tScTOitDqyFi4HceEZqJyyDkMi8K5DBPMTIuXIAMmtMlXpe/nj5pke7tRw97VeQRA==",
"engines": {
- "node": ">= 14"
+ "node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/unplugin-vue-i18n": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-0.11.0.tgz",
- "integrity": "sha512-ivcLZo08fvepHWV8o5lcKfhcKFSWqhwrqIAU6pUIbvq2ICo9fnXnIPYIZj7FeuHDLW1G3ADm44ZhQC3nYmvDlg==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-1.4.0.tgz",
+ "integrity": "sha512-RGDchCRBlDTyVVFgPA1C1XC1uD4xYN81Ma+3EnU6GQ8pBEreraX/PWdPXXzOB6k9GWCQHuqii3atYXhcH3rpSg==",
"dev": true,
"dependencies": {
- "@intlify/bundle-utils": "^6.0.1",
- "@intlify/shared": "9.3.0-beta.17",
+ "@intlify/bundle-utils": "^7.4.0",
+ "@intlify/shared": "^9.4.0",
"@rollup/pluginutils": "^5.0.2",
"@vue/compiler-sfc": "^3.2.47",
"debug": "^4.3.3",
@@ -1093,7 +1128,7 @@
"json5": "^2.2.3",
"pathe": "^1.0.0",
"picocolors": "^1.0.0",
- "source-map": "0.6.1",
+ "source-map-js": "^1.0.2",
"unplugin": "^1.1.0"
},
"engines": {
@@ -1116,26 +1151,6 @@
}
}
},
- "node_modules/@intlify/vue-devtools": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz",
- "integrity": "sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==",
- "dependencies": {
- "@intlify/core-base": "9.2.2",
- "@intlify/shared": "9.2.2"
- },
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/@intlify/vue-devtools/node_modules/@intlify/shared": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
- "integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==",
- "engines": {
- "node": ">= 14"
- }
- },
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -1258,9 +1273,9 @@
}
},
"node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
- "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
+ "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
"engines": {
"node": ">=6.0.0"
}
@@ -1279,19 +1294,14 @@
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.18",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz",
- "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==",
+ "version": "0.3.20",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
+ "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
"dependencies": {
- "@jridgewell/resolve-uri": "3.1.0",
- "@jridgewell/sourcemap-codec": "1.4.14"
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
}
},
- "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.4.14",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
- "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
- },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1325,9 +1335,9 @@
}
},
"node_modules/@rollup/pluginutils": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
- "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz",
+ "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0",
@@ -1338,7 +1348,7 @@
"node": ">=14.0.0"
},
"peerDependencies": {
- "rollup": "^1.20.0||^2.0.0||^3.0.0"
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
@@ -1347,9 +1357,9 @@
}
},
"node_modules/@tailwindcss/forms": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.3.tgz",
- "integrity": "sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==",
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.6.tgz",
+ "integrity": "sha512-Fw+2BJ0tmAwK/w01tEFL5TiaJBX1NLT1/YbWgvm7ws3Qcn11kiXxzNTEQDMs5V3mQemhB56l3u0i9dwdzSQldA==",
"dev": true,
"dependencies": {
"mini-svg-data-uri": "^1.2.3"
@@ -1383,88 +1393,92 @@
"devOptional": true
},
"node_modules/@types/blueimp-md5": {
- "version": "2.18.0",
- "resolved": "https://registry.npmjs.org/@types/blueimp-md5/-/blueimp-md5-2.18.0.tgz",
- "integrity": "sha512-f4A+++lGZGJvVSgeyMkqA7BEf2BVQli6F+qEykKb49c5ieWQBkfpn6CP5c1IZr2Yi2Ofl6Fj+v0e1fN18Z8Cnw==",
+ "version": "2.18.1",
+ "resolved": "https://registry.npmjs.org/@types/blueimp-md5/-/blueimp-md5-2.18.1.tgz",
+ "integrity": "sha512-0Ud2ef0DMurdLPj63BmiB01E98F1UNrwdBEflTpZtE3Z4jhhCclCiS88XDWXagiOaG+RjfliyvOBTAMUMw7jnw==",
"dev": true
},
"node_modules/@types/chai": {
- "version": "4.3.5",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz",
- "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==",
+ "version": "4.3.9",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.9.tgz",
+ "integrity": "sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg==",
"dev": true
},
"node_modules/@types/chai-as-promised": {
- "version": "7.1.5",
- "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.5.tgz",
- "integrity": "sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ==",
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.7.tgz",
+ "integrity": "sha512-APucaP5rlmTRYKtRA6FE5QPP87x76ejw5t5guRJ4y5OgMnwtsvigw7HHhKZlx2MGXLeZd6R/GNZR/IqDHcbtQw==",
"dev": true,
"dependencies": {
"@types/chai": "*"
}
},
"node_modules/@types/estree": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
- "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.4.tgz",
+ "integrity": "sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==",
"dev": true
},
"node_modules/@types/file-saver": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz",
- "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==",
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.6.tgz",
+ "integrity": "sha512-Mw671DVqoMHbjw0w4v2iiOro01dlT/WhWp5uwecBa0Wg8c+bcZOjgF1ndBnlaxhtvFCgTRBtsGivSVhrK/vnag==",
"dev": true
},
"node_modules/@types/json-schema": {
- "version": "7.0.12",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
- "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz",
+ "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==",
"dev": true
},
"node_modules/@types/mocha": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz",
- "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==",
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.3.tgz",
+ "integrity": "sha512-RsOPImTriV/OE4A9qKjMtk2MnXiuLLbcO3nCXK+kvq4nr0iMfFgpjaX3MPLb6f7+EL1FGSelYvuJMV6REH+ZPQ==",
"dev": true
},
"node_modules/@types/node": {
- "version": "20.3.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz",
- "integrity": "sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw=="
+ "version": "20.8.10",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
+ "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
},
"node_modules/@types/semver": {
- "version": "7.5.0",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz",
- "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "5.60.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.1.tgz",
- "integrity": "sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw==",
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz",
+ "integrity": "sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==",
"dev": true,
"dependencies": {
- "@eslint-community/regexpp": "^4.4.0",
- "@typescript-eslint/scope-manager": "5.60.1",
- "@typescript-eslint/type-utils": "5.60.1",
- "@typescript-eslint/utils": "5.60.1",
+ "@eslint-community/regexpp": "^4.5.1",
+ "@typescript-eslint/scope-manager": "6.9.1",
+ "@typescript-eslint/type-utils": "6.9.1",
+ "@typescript-eslint/utils": "6.9.1",
+ "@typescript-eslint/visitor-keys": "6.9.1",
"debug": "^4.3.4",
- "grapheme-splitter": "^1.0.4",
- "ignore": "^5.2.0",
- "natural-compare-lite": "^1.4.0",
- "semver": "^7.3.7",
- "tsutils": "^3.21.0"
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.4",
+ "natural-compare": "^1.4.0",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^5.0.0",
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+ "eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -1473,25 +1487,26 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "5.60.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz",
- "integrity": "sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==",
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.1.tgz",
+ "integrity": "sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==",
"dev": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "5.60.1",
- "@typescript-eslint/types": "5.60.1",
- "@typescript-eslint/typescript-estree": "5.60.1",
+ "@typescript-eslint/scope-manager": "6.9.1",
+ "@typescript-eslint/types": "6.9.1",
+ "@typescript-eslint/typescript-estree": "6.9.1",
+ "@typescript-eslint/visitor-keys": "6.9.1",
"debug": "^4.3.4"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -1500,16 +1515,16 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "5.60.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz",
- "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==",
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.1.tgz",
+ "integrity": "sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "5.60.1",
- "@typescript-eslint/visitor-keys": "5.60.1"
+ "@typescript-eslint/types": "6.9.1",
+ "@typescript-eslint/visitor-keys": "6.9.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
@@ -1517,25 +1532,25 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "5.60.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.60.1.tgz",
- "integrity": "sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A==",
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.1.tgz",
+ "integrity": "sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==",
"dev": true,
"dependencies": {
- "@typescript-eslint/typescript-estree": "5.60.1",
- "@typescript-eslint/utils": "5.60.1",
+ "@typescript-eslint/typescript-estree": "6.9.1",
+ "@typescript-eslint/utils": "6.9.1",
"debug": "^4.3.4",
- "tsutils": "^3.21.0"
+ "ts-api-utils": "^1.0.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "*"
+ "eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -1544,12 +1559,12 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "5.60.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz",
- "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==",
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.1.tgz",
+ "integrity": "sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==",
"dev": true,
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
@@ -1557,21 +1572,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "5.60.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz",
- "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==",
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.1.tgz",
+ "integrity": "sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "5.60.1",
- "@typescript-eslint/visitor-keys": "5.60.1",
+ "@typescript-eslint/types": "6.9.1",
+ "@typescript-eslint/visitor-keys": "6.9.1",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
- "semver": "^7.3.7",
- "tsutils": "^3.21.0"
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
@@ -1584,52 +1599,57 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "5.60.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.60.1.tgz",
- "integrity": "sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==",
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.1.tgz",
+ "integrity": "sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==",
"dev": true,
"dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@types/json-schema": "^7.0.9",
- "@types/semver": "^7.3.12",
- "@typescript-eslint/scope-manager": "5.60.1",
- "@typescript-eslint/types": "5.60.1",
- "@typescript-eslint/typescript-estree": "5.60.1",
- "eslint-scope": "^5.1.1",
- "semver": "^7.3.7"
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.9.1",
+ "@typescript-eslint/types": "6.9.1",
+ "@typescript-eslint/typescript-estree": "6.9.1",
+ "semver": "^7.5.4"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "eslint": "^7.0.0 || ^8.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "5.60.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz",
- "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==",
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.1.tgz",
+ "integrity": "sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "5.60.1",
- "eslint-visitor-keys": "^3.3.0"
+ "@typescript-eslint/types": "6.9.1",
+ "eslint-visitor-keys": "^3.4.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+ "dev": true
+ },
"node_modules/@vitejs/plugin-vue": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz",
- "integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz",
+ "integrity": "sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==",
"dev": true,
"engines": {
"node": "^14.18.0 || >=16.0.0"
@@ -1640,95 +1660,96 @@
}
},
"node_modules/@volar/language-core": {
- "version": "1.7.10",
- "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.7.10.tgz",
- "integrity": "sha512-18Gmth5M0UI3hDDqhZngjMnb6WCslcfglkOdepRIhGxRYe7xR7DRRzciisYDMZsvOQxDYme+uaohg0dKUxLV2Q==",
+ "version": "1.10.9",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.10.9.tgz",
+ "integrity": "sha512-QXHMX7CeXLqXwvC7nbr6iZ3zrqgKdJ9f6g1B211eZBnvaBki2ds0+Kz8cprUiulVuMQEPJNhDfuh8Vym1gxHRQ==",
"dev": true,
"dependencies": {
- "@volar/source-map": "1.7.10"
+ "@volar/source-map": "1.10.9"
}
},
"node_modules/@volar/source-map": {
- "version": "1.7.10",
- "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.7.10.tgz",
- "integrity": "sha512-FBpLEOKJpRxeh2nYbw1mTI5sZOPXYU8LlsCz6xuBY3yNtAizDTTIZtBHe1V8BaMpoSMgRysZe4gVxMEi3rDGVA==",
+ "version": "1.10.9",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.10.9.tgz",
+ "integrity": "sha512-ul8yGO9nCxy6UedVuo0VsfKMLZzr39N1rgbtnYTGP5C554EDcUix6K/HDurhVdPHEDIw1yhXltLZZQKi3NrTvA==",
"dev": true,
"dependencies": {
"muggle-string": "^0.3.1"
}
},
"node_modules/@volar/typescript": {
- "version": "1.7.10",
- "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.7.10.tgz",
- "integrity": "sha512-yqIov4wndLU3GE1iE25bU5W6T+P+exPePcE1dFPPBKzQIBki1KvmdQN5jBlJp3Wo+wp7UIxa/RsdNkXT+iFBjg==",
+ "version": "1.10.9",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.10.9.tgz",
+ "integrity": "sha512-5jLB46mCQLJqLII/qDLgfyHSq1cesjwuJQIa2GNWd7LPLSpX5vzo3jfQLWc/gyo3up2fQFrlRJK2kgY5REtwuQ==",
"dev": true,
"dependencies": {
- "@volar/language-core": "1.7.10"
+ "@volar/language-core": "1.10.9",
+ "path-browserify": "^1.0.1"
}
},
"node_modules/@vue/compiler-core": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
- "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.7.tgz",
+ "integrity": "sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==",
"dependencies": {
- "@babel/parser": "^7.21.3",
- "@vue/shared": "3.3.4",
+ "@babel/parser": "^7.23.0",
+ "@vue/shared": "3.3.7",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
- "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.7.tgz",
+ "integrity": "sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==",
"dependencies": {
- "@vue/compiler-core": "3.3.4",
- "@vue/shared": "3.3.4"
+ "@vue/compiler-core": "3.3.7",
+ "@vue/shared": "3.3.7"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
- "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
- "dependencies": {
- "@babel/parser": "^7.20.15",
- "@vue/compiler-core": "3.3.4",
- "@vue/compiler-dom": "3.3.4",
- "@vue/compiler-ssr": "3.3.4",
- "@vue/reactivity-transform": "3.3.4",
- "@vue/shared": "3.3.4",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.7.tgz",
+ "integrity": "sha512-7pfldWy/J75U/ZyYIXRVqvLRw3vmfxDo2YLMwVtWVNew8Sm8d6wodM+OYFq4ll/UxfqVr0XKiVwti32PCrruAw==",
+ "dependencies": {
+ "@babel/parser": "^7.23.0",
+ "@vue/compiler-core": "3.3.7",
+ "@vue/compiler-dom": "3.3.7",
+ "@vue/compiler-ssr": "3.3.7",
+ "@vue/reactivity-transform": "3.3.7",
+ "@vue/shared": "3.3.7",
"estree-walker": "^2.0.2",
- "magic-string": "^0.30.0",
- "postcss": "^8.1.10",
+ "magic-string": "^0.30.5",
+ "postcss": "^8.4.31",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
- "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.7.tgz",
+ "integrity": "sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==",
"dependencies": {
- "@vue/compiler-dom": "3.3.4",
- "@vue/shared": "3.3.4"
+ "@vue/compiler-dom": "3.3.7",
+ "@vue/shared": "3.3.7"
}
},
"node_modules/@vue/devtools-api": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz",
- "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q=="
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.1.tgz",
+ "integrity": "sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA=="
},
"node_modules/@vue/language-core": {
- "version": "1.8.3",
- "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.3.tgz",
- "integrity": "sha512-AzhvMYoQkK/tg8CpAAttO19kx1zjS3+weYIr2AhlH/M5HebVzfftQoq4jZNFifjq+hyLKi8j9FiDMS8oqA89+A==",
+ "version": "1.8.22",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.22.tgz",
+ "integrity": "sha512-bsMoJzCrXZqGsxawtUea1cLjUT9dZnDsy5TuZ+l1fxRMzUGQUG9+Ypq4w//CqpWmrx7nIAJpw2JVF/t258miRw==",
"dev": true,
"dependencies": {
- "@volar/language-core": "1.7.10",
- "@volar/source-map": "1.7.10",
+ "@volar/language-core": "~1.10.5",
+ "@volar/source-map": "~1.10.5",
"@vue/compiler-dom": "^3.3.0",
- "@vue/reactivity": "^3.3.0",
"@vue/shared": "^3.3.0",
- "minimatch": "^9.0.0",
+ "computeds": "^0.0.1",
+ "minimatch": "^9.0.3",
"muggle-string": "^0.3.1",
"vue-template-compiler": "^2.7.14"
},
@@ -1751,9 +1772,9 @@
}
},
"node_modules/@vue/language-core/node_modules/minimatch": {
- "version": "9.0.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.2.tgz",
- "integrity": "sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==",
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -1766,75 +1787,65 @@
}
},
"node_modules/@vue/reactivity": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz",
- "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.7.tgz",
+ "integrity": "sha512-cZNVjWiw00708WqT0zRpyAgduG79dScKEPYJXq2xj/aMtk3SKvL3FBt2QKUlh6EHBJ1m8RhBY+ikBUzwc7/khg==",
"dependencies": {
- "@vue/shared": "3.3.4"
+ "@vue/shared": "3.3.7"
}
},
"node_modules/@vue/reactivity-transform": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz",
- "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.7.tgz",
+ "integrity": "sha512-APhRmLVbgE1VPGtoLQoWBJEaQk4V8JUsqrQihImVqKT+8U6Qi3t5ATcg4Y9wGAPb3kIhetpufyZ1RhwbZCIdDA==",
"dependencies": {
- "@babel/parser": "^7.20.15",
- "@vue/compiler-core": "3.3.4",
- "@vue/shared": "3.3.4",
+ "@babel/parser": "^7.23.0",
+ "@vue/compiler-core": "3.3.7",
+ "@vue/shared": "3.3.7",
"estree-walker": "^2.0.2",
- "magic-string": "^0.30.0"
+ "magic-string": "^0.30.5"
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz",
- "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.7.tgz",
+ "integrity": "sha512-LHq9du3ubLZFdK/BP0Ysy3zhHqRfBn80Uc+T5Hz3maFJBGhci1MafccnL3rpd5/3wVfRHAe6c+PnlO2PAavPTQ==",
"dependencies": {
- "@vue/reactivity": "3.3.4",
- "@vue/shared": "3.3.4"
+ "@vue/reactivity": "3.3.7",
+ "@vue/shared": "3.3.7"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz",
- "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.7.tgz",
+ "integrity": "sha512-PFQU1oeJxikdDmrfoNQay5nD4tcPNYixUBruZzVX/l0eyZvFKElZUjW4KctCcs52nnpMGO6UDK+jF5oV4GT5Lw==",
"dependencies": {
- "@vue/runtime-core": "3.3.4",
- "@vue/shared": "3.3.4",
- "csstype": "^3.1.1"
+ "@vue/runtime-core": "3.3.7",
+ "@vue/shared": "3.3.7",
+ "csstype": "^3.1.2"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
- "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.7.tgz",
+ "integrity": "sha512-UlpKDInd1hIZiNuVVVvLgxpfnSouxKQOSE2bOfQpBuGwxRV/JqqTCyyjXUWiwtVMyeRaZhOYYqntxElk8FhBhw==",
"dependencies": {
- "@vue/compiler-ssr": "3.3.4",
- "@vue/shared": "3.3.4"
+ "@vue/compiler-ssr": "3.3.7",
+ "@vue/shared": "3.3.7"
},
"peerDependencies": {
- "vue": "3.3.4"
+ "vue": "3.3.7"
}
},
"node_modules/@vue/shared": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
- "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
- },
- "node_modules/@vue/typescript": {
- "version": "1.8.3",
- "resolved": "https://registry.npmjs.org/@vue/typescript/-/typescript-1.8.3.tgz",
- "integrity": "sha512-6bdgSnIFpRYHlt70pHmnmNksPU00bfXgqAISeaNz3W6d2cH0OTfH8h/IhligQ82sJIhsuyfftQJ5518ZuKIhtA==",
- "dev": true,
- "dependencies": {
- "@volar/typescript": "1.7.10",
- "@vue/language-core": "1.8.3"
- }
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.7.tgz",
+ "integrity": "sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg=="
},
"node_modules/acorn": {
- "version": "8.9.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
- "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
+ "version": "8.11.2",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
+ "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
"devOptional": true,
"bin": {
"acorn": "bin/acorn"
@@ -1853,9 +1864,9 @@
}
},
"node_modules/acorn-walk": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
- "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz",
+ "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==",
"devOptional": true,
"engines": {
"node": ">=0.4.0"
@@ -1993,9 +2004,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/autoprefixer": {
- "version": "10.4.14",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
- "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==",
+ "version": "10.4.16",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
+ "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==",
"dev": true,
"funding": [
{
@@ -2005,12 +2016,16 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
- "browserslist": "^4.21.5",
- "caniuse-lite": "^1.0.30001464",
- "fraction.js": "^4.2.0",
+ "browserslist": "^4.21.10",
+ "caniuse-lite": "^1.0.30001538",
+ "fraction.js": "^4.3.6",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"postcss-value-parser": "^4.2.0"
@@ -2026,9 +2041,9 @@
}
},
"node_modules/axios": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
- "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
+ "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -2100,9 +2115,9 @@
"dev": true
},
"node_modules/browserslist": {
- "version": "4.21.9",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
- "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
+ "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
"dev": true,
"funding": [
{
@@ -2119,10 +2134,10 @@
}
],
"dependencies": {
- "caniuse-lite": "^1.0.30001503",
- "electron-to-chromium": "^1.4.431",
- "node-releases": "^2.0.12",
- "update-browserslist-db": "^1.0.11"
+ "caniuse-lite": "^1.0.30001541",
+ "electron-to-chromium": "^1.4.535",
+ "node-releases": "^2.0.13",
+ "update-browserslist-db": "^1.0.13"
},
"bin": {
"browserslist": "cli.js"
@@ -2173,9 +2188,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001509",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001509.tgz",
- "integrity": "sha512-2uDDk+TRiTX5hMcUYT/7CSyzMZxjfGu0vAUjS2g0LSD8UoXOv0LtpH4LxGMemsiPq6LCVIUjNwVM0erkOkGCDA==",
+ "version": "1.0.30001559",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz",
+ "integrity": "sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==",
"dev": true,
"funding": [
{
@@ -2201,18 +2216,18 @@
}
},
"node_modules/chai": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz",
- "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==",
+ "version": "4.3.10",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
+ "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==",
"dev": true,
"dependencies": {
"assertion-error": "^1.1.0",
- "check-error": "^1.0.2",
- "deep-eql": "^4.1.2",
- "get-func-name": "^2.0.0",
- "loupe": "^2.3.1",
+ "check-error": "^1.0.3",
+ "deep-eql": "^4.1.3",
+ "get-func-name": "^2.0.2",
+ "loupe": "^2.3.6",
"pathval": "^1.1.1",
- "type-detect": "^4.0.5"
+ "type-detect": "^4.0.8"
},
"engines": {
"node": ">=4"
@@ -2259,10 +2274,13 @@
}
},
"node_modules/check-error": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
- "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
+ "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
"dev": true,
+ "dependencies": {
+ "get-func-name": "^2.0.2"
+ },
"engines": {
"node": "*"
}
@@ -2367,6 +2385,12 @@
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true
},
+ "node_modules/computeds": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
+ "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
+ "dev": true
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2536,9 +2560,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.4.445",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.445.tgz",
- "integrity": "sha512-++DB+9VK8SBJwC+X1zlMfJ1tMA3F0ipi39GdEp+x3cV2TyBihqAgad8cNMWtLDEkbH39nlDQP7PfGrDr3Dr7HA==",
+ "version": "1.4.571",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.571.tgz",
+ "integrity": "sha512-Sc+VtKwKCDj3f/kLBjdyjMpNzoZsU6WuL/wFb6EH8USmHEcebxRXcRrVpOpayxd52tuey4RUDpUsw5OS5LhJqg==",
"dev": true
},
"node_modules/emoji-regex": {
@@ -2554,9 +2578,9 @@
"dev": true
},
"node_modules/esbuild": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
- "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+ "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
"bin": {
@@ -2566,28 +2590,28 @@
"node": ">=12"
},
"optionalDependencies": {
- "@esbuild/android-arm": "0.17.19",
- "@esbuild/android-arm64": "0.17.19",
- "@esbuild/android-x64": "0.17.19",
- "@esbuild/darwin-arm64": "0.17.19",
- "@esbuild/darwin-x64": "0.17.19",
- "@esbuild/freebsd-arm64": "0.17.19",
- "@esbuild/freebsd-x64": "0.17.19",
- "@esbuild/linux-arm": "0.17.19",
- "@esbuild/linux-arm64": "0.17.19",
- "@esbuild/linux-ia32": "0.17.19",
- "@esbuild/linux-loong64": "0.17.19",
- "@esbuild/linux-mips64el": "0.17.19",
- "@esbuild/linux-ppc64": "0.17.19",
- "@esbuild/linux-riscv64": "0.17.19",
- "@esbuild/linux-s390x": "0.17.19",
- "@esbuild/linux-x64": "0.17.19",
- "@esbuild/netbsd-x64": "0.17.19",
- "@esbuild/openbsd-x64": "0.17.19",
- "@esbuild/sunos-x64": "0.17.19",
- "@esbuild/win32-arm64": "0.17.19",
- "@esbuild/win32-ia32": "0.17.19",
- "@esbuild/win32-x64": "0.17.19"
+ "@esbuild/android-arm": "0.18.20",
+ "@esbuild/android-arm64": "0.18.20",
+ "@esbuild/android-x64": "0.18.20",
+ "@esbuild/darwin-arm64": "0.18.20",
+ "@esbuild/darwin-x64": "0.18.20",
+ "@esbuild/freebsd-arm64": "0.18.20",
+ "@esbuild/freebsd-x64": "0.18.20",
+ "@esbuild/linux-arm": "0.18.20",
+ "@esbuild/linux-arm64": "0.18.20",
+ "@esbuild/linux-ia32": "0.18.20",
+ "@esbuild/linux-loong64": "0.18.20",
+ "@esbuild/linux-mips64el": "0.18.20",
+ "@esbuild/linux-ppc64": "0.18.20",
+ "@esbuild/linux-riscv64": "0.18.20",
+ "@esbuild/linux-s390x": "0.18.20",
+ "@esbuild/linux-x64": "0.18.20",
+ "@esbuild/netbsd-x64": "0.18.20",
+ "@esbuild/openbsd-x64": "0.18.20",
+ "@esbuild/sunos-x64": "0.18.20",
+ "@esbuild/win32-arm64": "0.18.20",
+ "@esbuild/win32-ia32": "0.18.20",
+ "@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/escalade": {
@@ -2633,27 +2657,28 @@
}
},
"node_modules/eslint": {
- "version": "8.43.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz",
- "integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==",
+ "version": "8.52.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz",
+ "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.4.0",
- "@eslint/eslintrc": "^2.0.3",
- "@eslint/js": "8.43.0",
- "@humanwhocodes/config-array": "^0.11.10",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.2",
+ "@eslint/js": "8.52.0",
+ "@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
- "ajv": "^6.10.0",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^7.2.0",
- "eslint-visitor-keys": "^3.4.1",
- "espree": "^9.5.2",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
"esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
@@ -2663,7 +2688,6 @@
"globals": "^13.19.0",
"graphemer": "^1.4.0",
"ignore": "^5.2.0",
- "import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
@@ -2673,9 +2697,8 @@
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
- "optionator": "^0.9.1",
+ "optionator": "^0.9.3",
"strip-ansi": "^6.0.1",
- "strip-json-comments": "^3.1.0",
"text-table": "^0.2.0"
},
"bin": {
@@ -2689,17 +2712,17 @@
}
},
"node_modules/eslint-plugin-vue": {
- "version": "9.15.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.15.1.tgz",
- "integrity": "sha512-CJE/oZOslvmAR9hf8SClTdQ9JLweghT6JCBQNrT2Iel1uVw0W0OLJxzvPd6CxmABKCvLrtyDnqGV37O7KQv6+A==",
+ "version": "9.18.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.18.1.tgz",
+ "integrity": "sha512-7hZFlrEgg9NIzuVik2I9xSnJA5RsmOfueYgsUGUokEDLJ1LHtxO0Pl4duje1BriZ/jDWb+44tcIlC3yi0tdlZg==",
"dev": true,
"dependencies": {
- "@eslint-community/eslint-utils": "^4.3.0",
+ "@eslint-community/eslint-utils": "^4.4.0",
"natural-compare": "^1.4.0",
- "nth-check": "^2.0.1",
- "postcss-selector-parser": "^6.0.9",
- "semver": "^7.3.5",
- "vue-eslint-parser": "^9.3.0",
+ "nth-check": "^2.1.1",
+ "postcss-selector-parser": "^6.0.13",
+ "semver": "^7.5.4",
+ "vue-eslint-parser": "^9.3.1",
"xml-name-validator": "^4.0.0"
},
"engines": {
@@ -2710,56 +2733,14 @@
}
},
"node_modules/eslint-scope": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
- "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
"dev": true,
"dependencies": {
"esrecurse": "^4.3.0",
- "estraverse": "^4.1.1"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/eslint-scope/node_modules/estraverse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
- "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
- "dev": true,
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/eslint-utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
- "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
- "dev": true,
- "dependencies": {
- "eslint-visitor-keys": "^1.1.0"
- },
- "engines": {
- "node": ">=6"
+ "estraverse": "^5.2.0"
},
- "funding": {
- "url": "https://github.com/sponsors/mysticatea"
- }
- },
- "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
- "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/eslint-visitor-keys": {
- "version": "3.4.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
- "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
- "dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -2767,15 +2748,11 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/eslint-scope": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
- "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -2783,30 +2760,13 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/optionator": {
- "version": "0.9.3",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
- "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
- "dev": true,
- "dependencies": {
- "@aashutoshrathi/word-wrap": "^1.2.3",
- "deep-is": "^0.1.3",
- "fast-levenshtein": "^2.0.6",
- "levn": "^0.4.1",
- "prelude-ls": "^1.2.1",
- "type-check": "^0.4.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
"node_modules/espree": {
- "version": "9.5.2",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz",
- "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==",
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
"dependencies": {
- "acorn": "^8.8.0",
+ "acorn": "^8.9.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.4.1"
},
@@ -2884,9 +2844,9 @@
"dev": true
},
"node_modules/fast-glob": {
- "version": "3.2.12",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
- "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
+ "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
@@ -3000,28 +2960,29 @@
}
},
"node_modules/flat-cache": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
- "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz",
+ "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==",
"dev": true,
"dependencies": {
- "flatted": "^3.1.0",
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
"rimraf": "^3.0.2"
},
"engines": {
- "node": "^10.12.0 || >=12.0.0"
+ "node": ">=12.0.0"
}
},
"node_modules/flatted": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
- "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+ "version": "3.2.9",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
+ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
"dev": true
},
"node_modules/follow-redirects": {
- "version": "1.15.2",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
- "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+ "version": "1.15.3",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
+ "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"funding": [
{
"type": "individual",
@@ -3064,16 +3025,16 @@
}
},
"node_modules/fraction.js": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
- "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"dev": true,
"engines": {
"node": "*"
},
"funding": {
"type": "patreon",
- "url": "https://www.patreon.com/infusion"
+ "url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fromentries": {
@@ -3102,9 +3063,9 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
- "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"optional": true,
"os": [
@@ -3115,9 +3076,12 @@
}
},
"node_modules/function-bind": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
@@ -3138,9 +3102,9 @@
}
},
"node_modules/get-func-name": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
- "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
+ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
"dev": true,
"engines": {
"node": "*"
@@ -3187,9 +3151,9 @@
}
},
"node_modules/globals": {
- "version": "13.20.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
- "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+ "version": "13.23.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
+ "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
"dev": true,
"dependencies": {
"type-fest": "^0.20.2"
@@ -3227,29 +3191,12 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
- "node_modules/grapheme-splitter": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
- "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
- "dev": true
- },
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
- "node_modules/has": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
- "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dependencies": {
- "function-bind": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4.0"
- }
- },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -3284,6 +3231,17 @@
"node": ">=8"
}
},
+ "node_modules/hasown": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
+ "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -3373,11 +3331,11 @@
}
},
"node_modules/is-core-module": {
- "version": "2.12.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
- "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
+ "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"dependencies": {
- "has": "^1.0.3"
+ "hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3524,9 +3482,9 @@
}
},
"node_modules/istanbul-lib-instrument/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
@@ -3550,17 +3508,32 @@
}
},
"node_modules/istanbul-lib-report": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
- "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
"dev": true,
"dependencies": {
"istanbul-lib-coverage": "^3.0.0",
- "make-dir": "^3.0.0",
+ "make-dir": "^4.0.0",
"supports-color": "^7.1.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/istanbul-lib-source-maps": {
@@ -3578,9 +3551,9 @@
}
},
"node_modules/istanbul-reports": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz",
- "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==",
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz",
+ "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==",
"dev": true,
"dependencies": {
"html-escaper": "^2.0.0",
@@ -3605,9 +3578,9 @@
}
},
"node_modules/jiti": {
- "version": "1.18.2",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
- "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
+ "version": "1.21.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
+ "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
"bin": {
"jiti": "bin/jiti.js"
}
@@ -3647,6 +3620,12 @@
"node": ">=4"
}
},
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -3672,63 +3651,21 @@
}
},
"node_modules/jsonc-eslint-parser": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-1.4.1.tgz",
- "integrity": "sha512-hXBrvsR1rdjmB2kQmUjf1rEIa+TqHBGMge8pwi++C+Si1ad7EjZrJcpgwym+QGK/pqTx+K7keFAtLlVNdLRJOg==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz",
+ "integrity": "sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==",
"dev": true,
"dependencies": {
- "acorn": "^7.4.1",
- "eslint-utils": "^2.1.0",
- "eslint-visitor-keys": "^1.3.0",
- "espree": "^6.0.0",
- "semver": "^6.3.0"
- },
- "engines": {
- "node": ">=8.10.0"
- }
- },
- "node_modules/jsonc-eslint-parser/node_modules/acorn": {
- "version": "7.4.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
- "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
- "dev": true,
- "bin": {
- "acorn": "bin/acorn"
+ "acorn": "^8.5.0",
+ "eslint-visitor-keys": "^3.0.0",
+ "espree": "^9.0.0",
+ "semver": "^7.3.5"
},
"engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/jsonc-eslint-parser/node_modules/eslint-visitor-keys": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
- "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/jsonc-eslint-parser/node_modules/espree": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz",
- "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==",
- "dev": true,
- "dependencies": {
- "acorn": "^7.1.1",
- "acorn-jsx": "^5.2.0",
- "eslint-visitor-keys": "^1.1.0"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/jsonc-eslint-parser/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true,
- "bin": {
- "semver": "bin/semver.js"
+ "funding": {
+ "url": "https://github.com/sponsors/ota-meshi"
}
},
"node_modules/jsonc-parser": {
@@ -3749,14 +3686,23 @@
}
},
"node_modules/keycloak-js": {
- "version": "22.0.1",
- "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-22.0.1.tgz",
- "integrity": "sha512-5cwOzMTMW2HuKGaIHv50BJHz2o8ID+YgzaaXKNwOk0XqD6ZOPD/jQXvqTz+Z8ID5cP46zVWnNiTouFK41NbPOQ==",
+ "version": "22.0.5",
+ "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-22.0.5.tgz",
+ "integrity": "sha512-a7ZwCZeHl8tpeJBy102tZtAnHslDUOA1Nf/sHNF3HYLchKpwoDuaitwIUiS2GnNUe+tlNKLlCqZS+Mi5K79m1w==",
"dependencies": {
"base64-js": "^1.5.1",
"js-sha256": "^0.9.0"
}
},
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -3841,12 +3787,12 @@
}
},
"node_modules/loupe": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
- "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==",
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
+ "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
"dev": true,
"dependencies": {
- "get-func-name": "^2.0.0"
+ "get-func-name": "^2.0.1"
}
},
"node_modules/lru-cache": {
@@ -3859,11 +3805,11 @@
}
},
"node_modules/magic-string": {
- "version": "0.30.0",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz",
- "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==",
+ "version": "0.30.5",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
+ "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
"dependencies": {
- "@jridgewell/sourcemap-codec": "^1.4.13"
+ "@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
@@ -3885,9 +3831,9 @@
}
},
"node_modules/make-dir/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
@@ -3964,15 +3910,15 @@
"integrity": "sha512-fL9KxsQz9BJB2KGPMHFrReioywkiomBiuaLk6EuChijK0BsJsIKJXdVomR+/bPj5mvbFD6wM0CM3bZio9g7OHA=="
},
"node_modules/mlly": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.0.tgz",
- "integrity": "sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==",
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz",
+ "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==",
"dev": true,
"dependencies": {
- "acorn": "^8.9.0",
+ "acorn": "^8.10.0",
"pathe": "^1.1.1",
"pkg-types": "^1.0.3",
- "ufo": "^1.1.2"
+ "ufo": "^1.3.0"
}
},
"node_modules/mocha": {
@@ -4097,12 +4043,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
- "node_modules/natural-compare-lite": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
- "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
- "dev": true
- },
"node_modules/node-preload": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz",
@@ -4116,9 +4056,9 @@
}
},
"node_modules/node-releases": {
- "version": "2.0.12",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz",
- "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==",
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
+ "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
"dev": true
},
"node_modules/normalize-path": {
@@ -4342,6 +4282,23 @@
"wrappy": "1"
}
},
+ "node_modules/optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -4425,6 +4382,12 @@
"node": ">=6"
}
},
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4588,9 +4551,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.24",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
- "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
@@ -4760,9 +4723,9 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/punycode": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
- "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"engines": {
"node": ">=6"
@@ -4857,11 +4820,11 @@
"dev": true
},
"node_modules/resolve": {
- "version": "1.22.2",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
- "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
+ "version": "1.22.8",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+ "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dependencies": {
- "is-core-module": "^2.11.0",
+ "is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
@@ -4891,9 +4854,9 @@
}
},
"node_modules/rfc4648": {
- "version": "1.5.2",
- "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.2.tgz",
- "integrity": "sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg=="
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.3.tgz",
+ "integrity": "sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ=="
},
"node_modules/rimraf": {
"version": "3.0.2",
@@ -4911,9 +4874,9 @@
}
},
"node_modules/rollup": {
- "version": "3.25.3",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.3.tgz",
- "integrity": "sha512-ZT279hx8gszBj9uy5FfhoG4bZx8c+0A1sbqtr7Q3KNWIizpTdDEPZbV2xcbvHsnFp4MavCQYZyzApJ+virB8Yw==",
+ "version": "3.29.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
+ "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
@@ -4954,9 +4917,9 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/semver": {
- "version": "7.5.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
- "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -5043,6 +5006,7 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5134,9 +5098,9 @@
}
},
"node_modules/sucrase": {
- "version": "3.32.0",
- "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz",
- "integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==",
+ "version": "3.34.0",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
+ "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"commander": "^4.0.0",
@@ -5197,19 +5161,19 @@
}
},
"node_modules/tailwindcss": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz",
- "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==",
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
+ "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
- "fast-glob": "^3.2.12",
+ "fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
- "jiti": "^1.18.2",
+ "jiti": "^1.19.1",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
@@ -5221,7 +5185,6 @@
"postcss-load-config": "^4.0.1",
"postcss-nested": "^6.0.1",
"postcss-selector-parser": "^6.0.11",
- "postcss-value-parser": "^4.2.0",
"resolve": "^1.22.2",
"sucrase": "^3.32.0"
},
@@ -5292,6 +5255,18 @@
"node": ">=8.0"
}
},
+ "node_modules/ts-api-utils": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
+ "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
+ "dev": true,
+ "engines": {
+ "node": ">=16.13.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@@ -5355,27 +5330,6 @@
"node": ">=0.3.1"
}
},
- "node_modules/tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "dev": true
- },
- "node_modules/tsutils": {
- "version": "3.21.0",
- "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
- "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
- "dev": true,
- "dependencies": {
- "tslib": "^1.8.1"
- },
- "engines": {
- "node": ">= 6"
- },
- "peerDependencies": {
- "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
- }
- },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -5419,9 +5373,9 @@
}
},
"node_modules/typescript": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
- "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+ "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
@@ -5432,27 +5386,32 @@
}
},
"node_modules/ufo": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz",
- "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz",
+ "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==",
"dev": true
},
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+ },
"node_modules/unplugin": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz",
- "integrity": "sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==",
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.5.0.tgz",
+ "integrity": "sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==",
"dev": true,
"dependencies": {
- "acorn": "^8.8.2",
+ "acorn": "^8.10.0",
"chokidar": "^3.5.3",
"webpack-sources": "^3.2.3",
"webpack-virtual-modules": "^0.5.0"
}
},
"node_modules/update-browserslist-db": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
- "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
+ "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"dev": true,
"funding": [
{
@@ -5509,14 +5468,14 @@
"devOptional": true
},
"node_modules/vite": {
- "version": "4.3.9",
- "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
- "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
+ "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
"dev": true,
"dependencies": {
- "esbuild": "^0.17.5",
- "postcss": "^8.4.23",
- "rollup": "^3.21.0"
+ "esbuild": "^0.18.10",
+ "postcss": "^8.4.27",
+ "rollup": "^3.27.1"
},
"bin": {
"vite": "bin/vite.js"
@@ -5524,12 +5483,16 @@
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@types/node": ">= 14",
"less": "*",
+ "lightningcss": "^1.21.0",
"sass": "*",
"stylus": "*",
"sugarss": "*",
@@ -5542,6 +5505,9 @@
"less": {
"optional": true
},
+ "lightningcss": {
+ "optional": true
+ },
"sass": {
"optional": true
},
@@ -5557,21 +5523,29 @@
}
},
"node_modules/vue": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz",
- "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.7.tgz",
+ "integrity": "sha512-YEMDia1ZTv1TeBbnu6VybatmSteGOS3A3YgfINOfraCbf85wdKHzscD6HSS/vB4GAtI7sa1XPX7HcQaJ1l24zA==",
"dependencies": {
- "@vue/compiler-dom": "3.3.4",
- "@vue/compiler-sfc": "3.3.4",
- "@vue/runtime-dom": "3.3.4",
- "@vue/server-renderer": "3.3.4",
- "@vue/shared": "3.3.4"
+ "@vue/compiler-dom": "3.3.7",
+ "@vue/compiler-sfc": "3.3.7",
+ "@vue/runtime-dom": "3.3.7",
+ "@vue/server-renderer": "3.3.7",
+ "@vue/shared": "3.3.7"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
}
},
"node_modules/vue-eslint-parser": {
- "version": "9.3.1",
- "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz",
- "integrity": "sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==",
+ "version": "9.3.2",
+ "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.2.tgz",
+ "integrity": "sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
@@ -5592,51 +5566,29 @@
"eslint": ">=6.0.0"
}
},
- "node_modules/vue-eslint-parser/node_modules/eslint-scope": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
- "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
- "dev": true,
+ "node_modules/vue-i18n": {
+ "version": "9.6.2",
+ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.6.2.tgz",
+ "integrity": "sha512-J43grTQjPR8LCUxvx3mkoM+11xhTnej1Al4lvJCEeKmQqf8eqbuYPQb54HXnEg/UzZyaxLBAwPAUTbrZ8V7hcg==",
"dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
+ "@intlify/core-base": "9.6.2",
+ "@intlify/shared": "9.6.2",
+ "@vue/devtools-api": "^6.5.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": ">= 16"
},
"funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/vue-i18n": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.2.2.tgz",
- "integrity": "sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==",
- "dependencies": {
- "@intlify/core-base": "9.2.2",
- "@intlify/shared": "9.2.2",
- "@intlify/vue-devtools": "9.2.2",
- "@vue/devtools-api": "^6.2.1"
- },
- "engines": {
- "node": ">= 14"
+ "url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
- "node_modules/vue-i18n/node_modules/@intlify/shared": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
- "integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==",
- "engines": {
- "node": ">= 14"
- }
- },
"node_modules/vue-router": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.2.tgz",
- "integrity": "sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==",
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz",
+ "integrity": "sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==",
"dependencies": {
"@vue/devtools-api": "^6.5.0"
},
@@ -5648,9 +5600,9 @@
}
},
"node_modules/vue-template-compiler": {
- "version": "2.7.14",
- "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz",
- "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==",
+ "version": "2.7.15",
+ "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.15.tgz",
+ "integrity": "sha512-yQxjxMptBL7UAog00O8sANud99C6wJF+7kgbcwqkvA38vCGF7HWE66w0ZFnS/kX5gSoJr/PQ4/oS3Ne2pW37Og==",
"dev": true,
"dependencies": {
"de-indent": "^1.0.2",
@@ -5658,14 +5610,14 @@
}
},
"node_modules/vue-tsc": {
- "version": "1.8.3",
- "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.3.tgz",
- "integrity": "sha512-Ua4DHuYxjudlhCW2nRZtaXbhIDVncRGIbDjZhHpF8Z8vklct/G/35/kAPuGNSOmq0JcvhPAe28Oa7LWaUerZVA==",
+ "version": "1.8.22",
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.22.tgz",
+ "integrity": "sha512-j9P4kHtW6eEE08aS5McFZE/ivmipXy0JzrnTgbomfABMaVKx37kNBw//irL3+LlE3kOo63XpnRigyPC3w7+z+A==",
"dev": true,
"dependencies": {
- "@vue/language-core": "1.8.3",
- "@vue/typescript": "1.8.3",
- "semver": "^7.3.8"
+ "@volar/typescript": "~1.10.5",
+ "@vue/language-core": "1.8.22",
+ "semver": "^7.5.4"
},
"bin": {
"vue-tsc": "bin/vue-tsc.js"
@@ -5775,40 +5727,28 @@
"dev": true
},
"node_modules/yaml": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
- "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==",
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.3.tgz",
+ "integrity": "sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==",
"engines": {
"node": ">= 14"
}
},
"node_modules/yaml-eslint-parser": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-0.3.2.tgz",
- "integrity": "sha512-32kYO6kJUuZzqte82t4M/gB6/+11WAuHiEnK7FreMo20xsCKPeFH5tDBU7iWxR7zeJpNnMXfJyXwne48D0hGrg==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-1.2.2.tgz",
+ "integrity": "sha512-pEwzfsKbTrB8G3xc/sN7aw1v6A6c/pKxLAkjclnAyo5g5qOh6eL9WGu0o3cSDQZKrTNk4KL4lQSwZW+nBkANEg==",
"dev": true,
"dependencies": {
- "eslint-visitor-keys": "^1.3.0",
- "lodash": "^4.17.20",
- "yaml": "^1.10.0"
- }
- },
- "node_modules/yaml-eslint-parser/node_modules/eslint-visitor-keys": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
- "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/yaml-eslint-parser/node_modules/yaml": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
- "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
- "dev": true,
+ "eslint-visitor-keys": "^3.0.0",
+ "lodash": "^4.17.21",
+ "yaml": "^2.0.0"
+ },
"engines": {
- "node": ">= 6"
+ "node": "^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ota-meshi"
}
},
"node_modules/yargs": {
diff --git a/frontend/package.json b/frontend/package.json
index 98d2cb70d..48c865a69 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "cryptomator-hub",
- "version": "1.2.2",
+ "version": "1.3.0",
"description": "Web-Frontend for Cryptomator Hub",
"author": "Skymatic GmbH",
"license": "AGPL-3.0-or-later",
@@ -19,48 +19,48 @@
"test": "./test"
},
"devDependencies": {
- "@intlify/unplugin-vue-i18n": "^0.11.0",
- "@tailwindcss/forms": "^0.5.3",
- "@types/blueimp-md5": "^2.18.0",
- "@types/chai": "^4.3.5",
- "@types/chai-as-promised": "^7.1.5",
- "@types/file-saver": "^2.0.5",
- "@types/mocha": "^10.0.1",
- "@types/node": "^20.3.2",
- "@types/semver": "^7.5.0",
- "@typescript-eslint/eslint-plugin": "^5.60.1",
- "@typescript-eslint/parser": "^5.60.1",
- "@vitejs/plugin-vue": "^4.2.3",
+ "@intlify/unplugin-vue-i18n": "^1.4.0",
+ "@tailwindcss/forms": "^0.5.6",
+ "@types/blueimp-md5": "^2.18.1",
+ "@types/chai": "^4.3.9",
+ "@types/chai-as-promised": "^7.1.7",
+ "@types/file-saver": "^2.0.6",
+ "@types/mocha": "^10.0.3",
+ "@types/node": "^20.8.10",
+ "@types/semver": "^7.5.4",
+ "@typescript-eslint/eslint-plugin": "^6.9.1",
+ "@typescript-eslint/parser": "^6.9.1",
+ "@vitejs/plugin-vue": "^4.4.0",
"@vue/compiler-sfc": "^3.3.4",
- "autoprefixer": "^10.4.14",
- "chai": "^4.3.7",
+ "autoprefixer": "^10.4.16",
+ "chai": "^4.3.10",
"chai-as-promised": "^7.1.1",
"chai-bytes": "^0.1.2",
- "eslint": "^8.43.0",
- "eslint-plugin-vue": "^9.15.1",
+ "eslint": "^8.52.0",
+ "eslint-plugin-vue": "^9.18.1",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
- "postcss": "^8.4.24",
- "tailwindcss": "^3.3.2",
+ "postcss": "^8.4.31",
+ "tailwindcss": "^3.3.5",
"ts-node": "^10.9.1",
- "typescript": "^5.1.6",
- "vite": "^4.3.9",
- "vue-tsc": "^1.8.3"
+ "typescript": "^5.2.2",
+ "vite": "^4.5.0",
+ "vue-tsc": "^1.8.22"
},
"dependencies": {
- "@headlessui/tailwindcss": "^0.1.3",
- "@headlessui/vue": "^1.7.14",
+ "@headlessui/tailwindcss": "^0.2.0",
+ "@headlessui/vue": "^1.7.16",
"@heroicons/vue": "^2.0.18",
- "axios": "^1.4.0",
+ "axios": "^1.6.0",
"file-saver": "^2.0.5",
"jdenticon": "^3.2.0",
"jszip": "^3.10.1",
- "keycloak-js": "^22.0.1",
+ "keycloak-js": "^22.0.5",
"miscreant": "^0.3.2",
- "rfc4648": "^1.5.2",
- "semver": "^7.5.3",
+ "rfc4648": "^1.5.3",
+ "semver": "^7.5.4",
"vue": "^3.3.4",
- "vue-i18n": "^9.2.2",
- "vue-router": "^4.2.2"
+ "vue-i18n": "^9.6.2",
+ "vue-router": "^4.2.5"
}
}
diff --git a/frontend/src/common/auditlog.ts b/frontend/src/common/auditlog.ts
index 3c3b1c3e2..e6c83dcc6 100644
--- a/frontend/src/common/auditlog.ts
+++ b/frontend/src/common/auditlog.ts
@@ -6,7 +6,7 @@ import { Deferred, debounce } from './util';
export type AuditEventDto = {
id: number;
timestamp: Date;
- type: 'DEVICE_REGISTER' | 'DEVICE_REMOVE' | 'VAULT_CREATE' | 'VAULT_UPDATE' | 'VAULT_ACCESS_GRANT' | 'VAULT_KEY_RETRIEVE' | 'VAULT_MEMBER_ADD' | 'VAULT_MEMBER_REMOVE';
+ type: 'DEVICE_REGISTER' | 'DEVICE_REMOVE' | 'VAULT_CREATE' | 'VAULT_UPDATE' | 'VAULT_ACCESS_GRANT' | 'VAULT_KEY_RETRIEVE' | 'VAULT_MEMBER_ADD' | 'VAULT_MEMBER_REMOVE' | 'VAULT_MEMBER_UPDATE' | 'VAULT_OWNERSHIP_CLAIM';
}
export type AuditEventDeviceRegisterDto = AuditEventDto & {
@@ -52,6 +52,7 @@ export type AuditEventVaultMemberAddDto = AuditEventDto & {
addedBy: string;
vaultId: string;
authorityId: string;
+ role: 'MEMBER' | 'OWNER';
}
export type AuditEventVaultMemberRemoveDto = AuditEventDto & {
@@ -60,6 +61,18 @@ export type AuditEventVaultMemberRemoveDto = AuditEventDto & {
authorityId: string;
}
+export type AuditEventVaultMemberUpdateDto = AuditEventDto & {
+ updatedBy: string;
+ vaultId: string;
+ authorityId: string;
+ role: 'MEMBER' | 'OWNER';
+}
+
+export type AuditEventVaultOwnershipClaimDto = AuditEventDto & {
+ claimedBy: string;
+ vaultId: string;
+}
+
/* Entity Cache */
export class AuditLogEntityCache {
diff --git a/frontend/src/common/backend.ts b/frontend/src/common/backend.ts
index b21b082cd..3ca473623 100644
--- a/frontend/src/common/backend.ts
+++ b/frontend/src/common/backend.ts
@@ -1,9 +1,8 @@
-import AxiosStatic, { AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse } from 'axios';
+import AxiosStatic, { AxiosHeaders, AxiosRequestConfig, AxiosResponse } from 'axios';
import { JdenticonConfig, toSvg } from 'jdenticon';
import { base64 } from 'rfc4648';
import authPromise from './auth';
-import config, { backendBaseURL } from './config';
-import { VaultKeys } from './crypto';
+import { backendBaseURL } from './config';
import { JWTHeader } from './jwt';
const axiosBaseCfg: AxiosRequestConfig = {
@@ -23,9 +22,9 @@ axiosAuth.interceptors.request.use(async request => {
try {
const token = await authPromise.then(auth => auth.bearerToken());
if (request.headers) {
- request.headers['Authorization'] = `Bearer ${token}`;
+ request.headers.setAuthorization(`Bearer ${token}`);
} else {
- request.headers = { 'Authorization': `Bearer ${token}` } as AxiosRequestHeaders;
+ request.headers = AxiosHeaders.from({ 'Authorization': `Bearer ${token}` });
}
return request;
} catch (err: unknown) {
@@ -39,14 +38,14 @@ axiosAuth.interceptors.request.use(async request => {
export type VaultDto = {
id: string;
name: string;
- description: string;
+ description?: string;
archived: boolean;
creationTime: Date;
- masterkey: string;
- iterations: number;
- salt: string;
- authPublicKey: string;
- authPrivateKey: string;
+ masterkey?: string;
+ iterations?: number;
+ salt?: string;
+ authPublicKey?: string;
+ authPrivateKey?: string;
};
export type DeviceDto = {
@@ -54,10 +53,17 @@ export type DeviceDto = {
name: string;
type: 'BROWSER' | 'DESKTOP' | 'MOBILE';
publicKey: string;
- accessTo: VaultDto[];
+ userPrivateKey: string;
creationTime: Date;
};
+export type VaultRole = 'MEMBER' | 'OWNER';
+
+export type AccessGrant = {
+ userId: string,
+ token: string
+};
+
enum AuthorityType {
User = 'USER',
Group = 'GROUP'
@@ -84,11 +90,12 @@ export abstract class AuthorityDto {
}
export class UserDto extends AuthorityDto {
- constructor(public id: string, public name: string, public type: AuthorityType, public email: string, public devices: DeviceDto[], pictureUrl?: string) {
+ constructor(public id: string, public name: string, public type: AuthorityType, public email: string, public devices: DeviceDto[], public accessibleVaults: VaultDto[], pictureUrl?: string,
+ public publicKey?: string, public privateKey?: string, public setupCode?: string) {
super(id, name, type, pictureUrl);
}
- getIdenticonConfig(): JdenticonConfig {
+ static getIdenticonConfig(): JdenticonConfig {
return {
hues: [6, 28, 48, 121, 283],
saturation: {
@@ -103,6 +110,10 @@ export class UserDto extends AuthorityDto {
};
}
+ getIdenticonConfig(): JdenticonConfig {
+ return UserDto.getIdenticonConfig();
+ }
+
static typeOf(obj: any): obj is UserDto {
const userDto = obj as UserDto;
return typeof userDto.id === 'string'
@@ -112,7 +123,7 @@ export class UserDto extends AuthorityDto {
}
static copy(obj: UserDto): UserDto {
- return new UserDto(obj.id, obj.name, obj.type, obj.email, obj.devices, obj.pictureUrl);
+ return new UserDto(obj.id, obj.name, obj.type, obj.email, obj.devices, obj.accessibleVaults, obj.pictureUrl, obj.publicKey, obj.privateKey, obj.setupCode);
}
}
@@ -121,7 +132,7 @@ export class GroupDto extends AuthorityDto {
super(id, name, type, pictureUrl);
}
- getIdenticonConfig(): JdenticonConfig {
+ static getIdenticonConfig(): JdenticonConfig {
return {
hues: [190],
saturation: {
@@ -136,6 +147,10 @@ export class GroupDto extends AuthorityDto {
};
}
+ getIdenticonConfig(): JdenticonConfig {
+ return GroupDto.getIdenticonConfig();
+ }
+
static typeOf(obj: any): obj is GroupDto {
const groupDto = obj as GroupDto;
return typeof groupDto.id === 'string'
@@ -149,6 +164,21 @@ export class GroupDto extends AuthorityDto {
}
}
+export class MemberDto extends AuthorityDto {
+ constructor(public id: string, public name: string, public type: AuthorityType, public role: VaultRole, pictureUrl: string) {
+ super(id, name, type, pictureUrl);
+ }
+
+ getIdenticonConfig(): JdenticonConfig {
+ switch (this.type) {
+ case AuthorityType.User:
+ return UserDto.getIdenticonConfig();
+ case AuthorityType.Group:
+ return GroupDto.getIdenticonConfig();
+ }
+ }
+}
+
export type BillingDto = {
hubId: string;
hasLicense: boolean;
@@ -172,8 +202,9 @@ export interface VaultIdHeader extends JWTHeader {
}
class VaultService {
- public async listAccessible(): Promise {
- return axiosAuth.get('/vaults').then(response => response.data);
+ public async listAccessible(role?: 'MEMBER' | 'OWNER'): Promise {
+ const queryParams = role ? { role: role } : {};
+ return axiosAuth.get('/vaults/accessible', { params: queryParams }).then(response => response.data);
}
public async listSome(vaultsIds: string[]): Promise {
@@ -195,66 +226,62 @@ class VaultService {
.catch((error) => rethrowAndConvertIfExpected(error, 404));
}
- public async getMembers(vaultId: string, vaultKeys: VaultKeys): Promise<(UserDto | GroupDto)[]> {
- let vaultAdminAuthorizationJWT = await this.buildVaultAdminAuthorizationJWT(vaultId, vaultKeys);
- return axiosAuth.get<(UserDto | GroupDto)[]>(`/vaults/${vaultId}/members`, { headers: { 'Cryptomator-Vault-Admin-Authorization': vaultAdminAuthorizationJWT } }).then(response => {
- return response.data.map(authority => {
- if (UserDto.typeOf(authority)) {
- return UserDto.copy(authority);
- } else if (GroupDto.typeOf(authority)) {
- return GroupDto.copy(authority);
- } else {
- throw new Error('Provided data is not of type UserDTO or GroupDTO');
- }
- });
+ public async getMembers(vaultId: string): Promise {
+ return axiosAuth.get(`/vaults/${vaultId}/members`).then(response => {
+ return response.data.map(member => new MemberDto(member.id, member.name, member.type, member.role, member.pictureUrl));
}).catch(err => rethrowAndConvertIfExpected(err, 403));
}
- public async addUser(vaultId: string, userId: string, vaultKeys: VaultKeys): Promise> {
- let vaultAdminAuthorizationJWT = await this.buildVaultAdminAuthorizationJWT(vaultId, vaultKeys);
- return axiosAuth.put(`/vaults/${vaultId}/users/${userId}`, null, { headers: { 'Cryptomator-Vault-Admin-Authorization': vaultAdminAuthorizationJWT } })
+ public async addUser(vaultId: string, userId: string, role?: VaultRole): Promise> {
+ return axiosAuth.put(`/vaults/${vaultId}/users/${userId}` + (role ? `?role=${role}` : ''))
.catch((error) => rethrowAndConvertIfExpected(error, 402, 404, 409));
}
- public async addGroup(vaultId: string, groupId: string, vaultKeys: VaultKeys): Promise> {
- let vaultAdminAuthorizationJWT = await this.buildVaultAdminAuthorizationJWT(vaultId, vaultKeys);
- return axiosAuth.put(`/vaults/${vaultId}/groups/${groupId}`, null, { headers: { 'Cryptomator-Vault-Admin-Authorization': vaultAdminAuthorizationJWT } })
+ public async addGroup(vaultId: string, groupId: string, role?: VaultRole): Promise> {
+ return axiosAuth.put(`/vaults/${vaultId}/groups/${groupId}` + (role ? `?role=${role}` : ''))
.catch((error) => rethrowAndConvertIfExpected(error, 402, 404, 409));
}
- public async getDevicesRequiringAccessGrant(vaultId: string, vaultKeys: VaultKeys): Promise {
- let vaultAdminAuthorizationJWT = await this.buildVaultAdminAuthorizationJWT(vaultId, vaultKeys);
- return axiosAuth.get(`/vaults/${vaultId}/devices-requiring-access-grant`, { headers: { 'Cryptomator-Vault-Admin-Authorization': vaultAdminAuthorizationJWT } })
- .then(response => response.data).catch(err => rethrowAndConvertIfExpected(err, 403));
+ public async getUsersRequiringAccessGrant(vaultId: string): Promise {
+ return axiosAuth.get(`/vaults/${vaultId}/users-requiring-access-grant`)
+ .then(response => {
+ return response.data.map(dto => UserDto.copy(dto));
+ })
+ .catch(err => rethrowAndConvertIfExpected(err, 403));
}
- public async createOrUpdateVault(vaultId: string, name: string, description: string, archived: boolean, masterkey: string, iterations: number, salt: string, signPubKey: string, signPrvKey: string): Promise {
- const body: VaultDto = { id: vaultId, name: name, description: description, archived: archived, creationTime: new Date(), masterkey: masterkey, iterations: iterations, salt: salt, authPublicKey: signPubKey, authPrivateKey: signPrvKey };
+ public async createOrUpdateVault(vaultId: string, name: string, archived: boolean, description?: string): Promise {
+ const body: VaultDto = { id: vaultId, name: name, description: description, archived: archived, creationTime: new Date() };
return axiosAuth.put(`/vaults/${vaultId}`, body)
.then(response => response.data)
.catch((error) => rethrowAndConvertIfExpected(error, 402, 404));
}
- public async grantAccess(vaultId: string, deviceId: string, jwe: string, vaultKeys: VaultKeys) {
- let vaultAdminAuthorizationJWT = await this.buildVaultAdminAuthorizationJWT(vaultId, vaultKeys);
- await axiosAuth.put(`/vaults/${vaultId}/keys/${deviceId}`, jwe, { headers: { 'Content-Type': 'text/plain', 'Cryptomator-Vault-Admin-Authorization': vaultAdminAuthorizationJWT } })
- .catch((error) => rethrowAndConvertIfExpected(error, 404, 409));
+ public async claimOwnership(vaultId: string, proof: string): Promise {
+ const params = new URLSearchParams({ proof: proof });
+ return axiosAuth.post(`/vaults/${vaultId}/claim-ownership`, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
+ .then(response => response.data)
+ .catch((error) => rethrowAndConvertIfExpected(error, 400, 404, 409));
}
- public async revokeUserAccess(vaultId: string, userId: string, vaultKeys: VaultKeys) {
- let vaultAdminAuthorizationJWT = await this.buildVaultAdminAuthorizationJWT(vaultId, vaultKeys);
- await axiosAuth.delete(`/vaults/${vaultId}/users/${userId}`, { headers: { 'Cryptomator-Vault-Admin-Authorization': vaultAdminAuthorizationJWT } })
- .catch((error) => rethrowAndConvertIfExpected(error, 404));
+ public async accessToken(vaultId: string, evenIfArchived = false): Promise {
+ return axiosAuth.get(`/vaults/${vaultId}/access-token?evenIfArchived=${evenIfArchived}`, { headers: { 'Content-Type': 'text/plain' } })
+ .then(response => response.data)
+ .catch((error) => rethrowAndConvertIfExpected(error, 403));
}
- private async buildVaultAdminAuthorizationJWT(vaultId: string, vaultKeys: VaultKeys): Promise {
- let vaultIdHeader: VaultIdHeader = { alg: 'ES384', b64: true, typ: 'JWT', vaultId: vaultId };
- let jwtPayload = { iat: this.secondsSinceEpoch() + config.serverTimeDiff };
- return vaultKeys.signVaultEditRequest(vaultIdHeader, jwtPayload);
+ public async grantAccess(vaultId: string, ...grants: AccessGrant[]) {
+ var body = grants.reduce>((accumulator, curr) => {
+ accumulator[curr.userId] = curr.token;
+ return accumulator;
+ }, {});
+ await axiosAuth.post(`/vaults/${vaultId}/access-tokens`, body)
+ .catch((error) => rethrowAndConvertIfExpected(error, 404, 409));
}
- private secondsSinceEpoch(): number {
- return Math.floor(Date.now() / 1000);
+ public async removeAuthority(vaultId: string, authorityId: string) {
+ await axiosAuth.delete(`/vaults/${vaultId}/authority/${authorityId}`)
+ .catch((error) => rethrowAndConvertIfExpected(error, 404));
}
}
class DeviceService {
@@ -267,15 +294,23 @@ class DeviceService {
return axiosAuth.delete(`/devices/${deviceId}`)
.catch((error) => rethrowAndConvertIfExpected(error, 404));
}
+
+ public async putDevice(device: DeviceDto): Promise> {
+ return axiosAuth.put(`/devices/${device.id}`, device);
+ }
}
class UserService {
- public async syncMe(): Promise {
- return axiosAuth.put('/users/me');
+ public async putMe(dto?: UserDto): Promise {
+ return axiosAuth.put('/users/me', dto);
+ }
+
+ public async me(withDevices: boolean = false): Promise {
+ return axiosAuth.get(`/users/me?withDevices=${withDevices}`).then(response => UserDto.copy(response.data));
}
- public async me(withDevices: boolean = false, withAccessibleVaults: boolean = false): Promise {
- return axiosAuth.get(`/users/me?withDevices=${withDevices}&withAccessibleVaults=${withAccessibleVaults}`).then(response => UserDto.copy(response.data));
+ public async resetMe(): Promise {
+ return axiosAuth.post('/users/me/reset');
}
public async listAll(): Promise {
diff --git a/frontend/src/common/crypto.ts b/frontend/src/common/crypto.ts
index 482e8cde7..9895b0b08 100644
--- a/frontend/src/common/crypto.ts
+++ b/frontend/src/common/crypto.ts
@@ -1,13 +1,7 @@
import * as miscreant from 'miscreant';
-import { base32, base64, base64url } from 'rfc4648';
-import { JWE } from './jwe';
-import { JWT, JWTHeader } from './jwt';
-import { CRC32, wordEncoder } from './util';
-
-export class WrappedVaultKeys {
- constructor(readonly masterkey: string, readonly signaturePrivateKey: string, readonly signaturePublicKey: string, readonly salt: string, readonly iterations: number) { }
-}
-
+import { base16, base32, base64, base64url } from 'rfc4648';
+import { JWEBuilder, JWEParser } from './jwe';
+import { CRC32, DB, wordEncoder } from './util';
export class UnwrapKeyError extends Error {
readonly actualError: any;
@@ -28,21 +22,20 @@ export interface VaultConfigHeaderHub {
clientId: string
authEndpoint: string
tokenEndpoint: string
- devicesResourceUrl: string,
authSuccessUrl: string
authErrorUrl: string
+ apiBaseUrl: string
+ // deprecated:
+ devicesResourceUrl: string
}
interface JWEPayload {
key: string
}
-export class VaultKeys {
- private static readonly SIGNATURE_KEY_DESIGNATION: EcKeyImportParams | EcKeyGenParams = {
- name: 'ECDSA',
- namedCurve: 'P-384'
- };
+const GCM_NONCE_LEN = 12;
+export class VaultKeys {
// in this browser application, this 512 bit key is used
// as a hmac key to sign the vault config.
// however when used by cryptomator, it gets split into
@@ -53,19 +46,10 @@ export class VaultKeys {
length: 512
};
- private static readonly KEK_KEY_DESIGNATION: AesDerivedKeyParams = {
- name: 'AES-GCM',
- length: 256
- };
-
- private static readonly GCM_NONCE_LEN = 12;
- private static readonly PBKDF2_ITERATION_COUNT = 1000000;
readonly masterKey: CryptoKey;
- readonly signatureKeyPair: CryptoKeyPair;
- protected constructor(masterkey: CryptoKey, signatureKeyPair: CryptoKeyPair) {
+ protected constructor(masterkey: CryptoKey) {
this.masterKey = masterkey;
- this.signatureKeyPair = signatureKeyPair;
}
/**
@@ -78,113 +62,86 @@ export class VaultKeys {
true,
['sign']
);
- const keyPair = crypto.subtle.generateKey(
- VaultKeys.SIGNATURE_KEY_DESIGNATION,
- true,
- ['sign', 'verify']
- );
- return new VaultKeys(await key, await keyPair);
- }
-
- private static async pbkdf2(password: string, salt: Uint8Array, iterations: number): Promise {
- const encodedPw = new TextEncoder().encode(password);
- const pwKey = await crypto.subtle.importKey(
- 'raw',
- encodedPw,
- 'PBKDF2',
- false,
- ['deriveKey']
- );
- return await crypto.subtle.deriveKey(
- {
- name: 'PBKDF2',
- hash: 'SHA-256',
- salt: salt,
- iterations: iterations
- },
- pwKey,
- VaultKeys.KEK_KEY_DESIGNATION,
- false,
- ['wrapKey', 'unwrapKey']
- );
+ return new VaultKeys(await key);
}
/**
- * Protects the key material. Must only be called for a newly created masterkey, otherwise it will fail.
- * @param password Password used for wrapping
- * @returns The wrapped key material
+ * Decrypts the vault's masterkey using the user's private key
+ * @param jwe JWE containing the vault key
+ * @param userPrivateKey The user's private key
+ * @returns The masterkey
*/
- public async wrap(password: string): Promise {
- // salt:
- const salt = new Uint8Array(16);
- crypto.getRandomValues(salt);
- const encodedSalt = base64.stringify(salt);
- // kek:
- const kek = VaultKeys.pbkdf2(password, salt, VaultKeys.PBKDF2_ITERATION_COUNT);
- // masterkey:
- const masterKeyIv = crypto.getRandomValues(new Uint8Array(VaultKeys.GCM_NONCE_LEN));
- const wrappedMasterKey = new Uint8Array(await crypto.subtle.wrapKey(
- 'raw',
- this.masterKey,
- await kek,
- { name: 'AES-GCM', iv: masterKeyIv }
- ));
- const encodedMasterKey = base64.stringify(new Uint8Array([...masterKeyIv, ...wrappedMasterKey]));
- // secretkey:
- const secretKeyIv = crypto.getRandomValues(new Uint8Array(VaultKeys.GCM_NONCE_LEN));
- const wrappedSecretKey = new Uint8Array(await crypto.subtle.wrapKey(
- 'pkcs8',
- this.signatureKeyPair.privateKey,
- await kek,
- { name: 'AES-GCM', iv: secretKeyIv }
- ));
- const encodedSecretKey = base64.stringify(new Uint8Array([...secretKeyIv, ...wrappedSecretKey]));
- // publickey:
- const publicKey = new Uint8Array(await crypto.subtle.exportKey('spki', this.signatureKeyPair.publicKey));
- const encodedPublicKey = base64.stringify(publicKey);
- // result:
- return new WrappedVaultKeys(encodedMasterKey, encodedSecretKey, encodedPublicKey, encodedSalt, VaultKeys.PBKDF2_ITERATION_COUNT);
+ public static async decryptWithUserKey(jwe: string, userPrivateKey: CryptoKey): Promise {
+ let rawKey = new Uint8Array();
+ try {
+ const payload: JWEPayload = await JWEParser.parse(jwe).decryptEcdhEs(userPrivateKey);
+ rawKey = base64.parse(payload.key);
+ const masterkey = crypto.subtle.importKey('raw', rawKey, VaultKeys.MASTERKEY_KEY_DESIGNATION, true, ['sign']);
+ return new VaultKeys(await masterkey);
+ } finally {
+ rawKey.fill(0x00);
+ }
}
/**
- * Unwraps the key material.
- * @param password Password used for wrapping
- * @param wrapped The wrapped key material
+ * Unwraps keys protected by the legacy "Vault Admin Password".
+ * @param vaultAdminPassword Vault Admin Password
+ * @param wrappedMasterkey The wrapped masterkey
+ * @param wrappedOwnerPrivateKey The wrapped owner private key
+ * @param ownerPublicKey The owner public key
+ * @param salt PBKDF2 Salt
+ * @param iterations PBKDF2 Iterations
* @returns The unwrapped key material.
* @throws WrongPasswordError, if the wrong password is used
+ * @deprecated Only used during "claim vault ownership" workflow for legacy vaults
*/
- public static async unwrap(password: string, wrapped: WrappedVaultKeys): Promise {
- const kek = VaultKeys.pbkdf2(password, base64.parse(wrapped.salt, { loose: true }), wrapped.iterations);
- const decodedMasterKey = base64.parse(wrapped.masterkey, { loose: true });
- const decodedPrivateKey = base64.parse(wrapped.signaturePrivateKey, { loose: true });
- const decodedPublicKey = base64.parse(wrapped.signaturePublicKey, { loose: true });
+ public static async decryptWithAdminPassword(vaultAdminPassword: string, wrappedMasterkey: string, wrappedOwnerPrivateKey: string, ownerPublicKey: string, salt: string, iterations: number): Promise<[VaultKeys, CryptoKeyPair]> {
+ // pbkdf2:
+ const encodedPw = new TextEncoder().encode(vaultAdminPassword);
+ const pwKey = crypto.subtle.importKey('raw', encodedPw, 'PBKDF2', false, ['deriveKey']);
+ const kek = crypto.subtle.deriveKey(
+ {
+ name: 'PBKDF2',
+ hash: 'SHA-256',
+ salt: base64.parse(salt, { loose: true }),
+ iterations: iterations
+ },
+ await pwKey,
+ { name: 'AES-GCM', length: 256 },
+ false,
+ ['unwrapKey']
+ );
+ // unwrapping
+ const decodedMasterKey = base64.parse(wrappedMasterkey, { loose: true });
+ const decodedPrivateKey = base64.parse(wrappedOwnerPrivateKey, { loose: true });
+ const decodedPublicKey = base64.parse(ownerPublicKey, { loose: true });
try {
const masterkey = crypto.subtle.unwrapKey(
'raw',
- decodedMasterKey.slice(VaultKeys.GCM_NONCE_LEN),
+ decodedMasterKey.slice(GCM_NONCE_LEN),
await kek,
- { name: 'AES-GCM', iv: decodedMasterKey.slice(0, VaultKeys.GCM_NONCE_LEN) },
+ { name: 'AES-GCM', iv: decodedMasterKey.slice(0, GCM_NONCE_LEN) },
VaultKeys.MASTERKEY_KEY_DESIGNATION,
true,
['sign']
);
- const signPrivKey = crypto.subtle.unwrapKey(
+ const privKey = crypto.subtle.unwrapKey(
'pkcs8',
- decodedPrivateKey.slice(VaultKeys.GCM_NONCE_LEN),
+ decodedPrivateKey.slice(GCM_NONCE_LEN),
await kek,
- { name: 'AES-GCM', iv: decodedPrivateKey.slice(0, VaultKeys.GCM_NONCE_LEN) },
- VaultKeys.SIGNATURE_KEY_DESIGNATION,
+ { name: 'AES-GCM', iv: decodedPrivateKey.slice(0, GCM_NONCE_LEN) },
+ { name: 'ECDSA', namedCurve: 'P-384' },
false,
['sign']
);
- const signPubKey = crypto.subtle.importKey(
+ const pubKey = crypto.subtle.importKey(
'spki',
decodedPublicKey,
- VaultKeys.SIGNATURE_KEY_DESIGNATION,
+ { name: 'ECDSA', namedCurve: 'P-384' },
true,
['verify']
);
- return new VaultKeys(await masterkey, { privateKey: await signPrivKey, publicKey: await signPubKey });
+ return [new VaultKeys(await masterkey), { privateKey: await privKey, publicKey: await pubKey }];
} catch (error) {
throw new UnwrapKeyError(error);
}
@@ -217,12 +174,7 @@ export class VaultKeys {
true,
['sign']
);
- const keyPair = crypto.subtle.generateKey(
- VaultKeys.SIGNATURE_KEY_DESIGNATION,
- true,
- ['sign', 'verify']
- );
- return new VaultKeys(await key, await keyPair);
+ return new VaultKeys(await key);
}
public async createVaultConfig(kid: string, hubConfig: VaultConfigHeaderHub, payload: VaultConfigPayload): Promise {
@@ -264,38 +216,22 @@ export class VaultKeys {
/**
* Encrypts this masterkey using the given public key
- * @param devicePublicKey The recipient's public key (DER-encoded)
+ * @param userPublicKey The recipient's public key (DER-encoded)
* @returns a JWE containing this Masterkey
*/
- public async encryptForDevice(devicePublicKey: Uint8Array): Promise {
- const publicKey = await crypto.subtle.importKey(
- 'spki',
- devicePublicKey,
- {
- name: 'ECDH',
- namedCurve: 'P-384'
- },
- false,
- []
- );
-
+ public async encryptForUser(userPublicKey: Uint8Array): Promise {
+ const publicKey = await crypto.subtle.importKey('spki', userPublicKey, UserKeys.KEY_DESIGNATION, false, []);
const rawkey = new Uint8Array(await crypto.subtle.exportKey('raw', this.masterKey));
try {
const payload: JWEPayload = {
key: base64.stringify(rawkey)
};
- const payloadJson = new TextEncoder().encode(JSON.stringify(payload));
-
- return JWE.build(payloadJson, publicKey);
+ return JWEBuilder.ecdhEs(publicKey).encrypt(payload);
} finally {
rawkey.fill(0x00);
}
}
- public async signVaultEditRequest(jwtHeader: JWTHeader, jwtPayload: any): Promise {
- return JWT.build(jwtHeader, jwtPayload, this.signatureKeyPair.privateKey);
- }
-
/**
* Encode masterkey for offline backup purposes, allowing re-importing the key for recovery purposes
*/
@@ -313,3 +249,203 @@ export class VaultKeys {
return wordEncoder.encodePadded(combined);
}
}
+
+export class UserKeys {
+ public static readonly KEY_USAGES: KeyUsage[] = ['deriveBits'];
+
+ public static readonly KEY_DESIGNATION: EcKeyImportParams | EcKeyGenParams = {
+ name: 'ECDH',
+ namedCurve: 'P-384'
+ };
+
+ readonly keyPair: CryptoKeyPair;
+
+ protected constructor(keyPair: CryptoKeyPair) {
+ this.keyPair = keyPair;
+ }
+
+ /**
+ * Creates a new user key pair
+ * @returns A new user key pair
+ */
+ public static async create(): Promise {
+ const keyPair = crypto.subtle.generateKey(UserKeys.KEY_DESIGNATION, true, UserKeys.KEY_USAGES);
+ return new UserKeys(await keyPair);
+ }
+
+ /**
+ * Recovers the user key pair using a recovery code. All other information can be retrieved from the backend.
+ * @param encodedPublicKey The public key (base64-encoded SPKI)
+ * @param encryptedPrivateKey The JWE holding the encrypted private key
+ * @param setupCode The password used to protect the private key
+ * @returns Decrypted UserKeys
+ * @throws {UnwrapKeyError} when attempting to decrypt the private key using an incorrect setupCode
+ */
+ public static async recover(encodedPublicKey: string, encryptedPrivateKey: string, setupCode: string): Promise {
+ const jwe: JWEPayload = await JWEParser.parse(encryptedPrivateKey).decryptPbes2(setupCode);
+ const decodedPublicKey = base64.parse(encodedPublicKey, { loose: true });
+ const decodedPrivateKey = base64.parse(jwe.key, { loose: true });
+ const privateKey = crypto.subtle.importKey('pkcs8', decodedPrivateKey, UserKeys.KEY_DESIGNATION, true, UserKeys.KEY_USAGES);
+ const publicKey = crypto.subtle.importKey('spki', decodedPublicKey, UserKeys.KEY_DESIGNATION, true, []);
+ return new UserKeys({ privateKey: await privateKey, publicKey: await publicKey });
+ }
+
+ /**
+ * Gets the base64-encoded public key in SPKI format.
+ * @returns base64-encoded public key
+ */
+ public async encodedPublicKey(): Promise {
+ const publicKey = new Uint8Array(await crypto.subtle.exportKey('spki', this.keyPair.publicKey));
+ return base64.stringify(publicKey);
+ }
+
+ /**
+ * Encrypts the user's private key using a key derived from the given setupCode
+ * @param setupCode The password to protect the private key.
+ * @returns A JWE holding the encrypted private key
+ * @see JWEBuilder.pbes2
+ */
+ public async encryptedPrivateKey(setupCode: string): Promise {
+ const rawkey = new Uint8Array(await crypto.subtle.exportKey('pkcs8', this.keyPair.privateKey));
+ try {
+ const payload: JWEPayload = {
+ key: base64.stringify(rawkey)
+ };
+ return await JWEBuilder.pbes2(setupCode).encrypt(payload);
+ } finally {
+ rawkey.fill(0x00);
+ }
+ }
+
+ /**
+ * Encrypts the user's private key using the given public key
+ * @param devicePublicKey The device's public key (DER-encoded)
+ * @returns a JWE containing the PKCS#8-encoded private key
+ * @see JWEBuilder.ecdhEs
+ */
+ public async encryptForDevice(devicePublicKey: CryptoKey | Uint8Array): Promise {
+ const publicKey = await UserKeys.publicKey(devicePublicKey);
+ const rawkey = new Uint8Array(await crypto.subtle.exportKey('pkcs8', this.keyPair.privateKey));
+ try {
+ const payload: JWEPayload = {
+ key: base64.stringify(rawkey)
+ };
+ return JWEBuilder.ecdhEs(publicKey).encrypt(payload);
+ } finally {
+ rawkey.fill(0x00);
+ }
+ }
+
+ /**
+ * Decrypts the user's private key using the browser's private key
+ * @param jwe JWE containing the PKCS#8-encoded private key
+ * @param browserPrivateKey The browser's private key
+ * @param userPublicKey User public key
+ * @returns The user's key pair
+ */
+ public static async decryptOnBrowser(jwe: string, browserPrivateKey: CryptoKey, userPublicKey: CryptoKey | BufferSource): Promise {
+ const publicKey = await UserKeys.publicKey(userPublicKey);
+ let rawKey = new Uint8Array();
+ try {
+ const payload: JWEPayload = await JWEParser.parse(jwe).decryptEcdhEs(browserPrivateKey);
+ rawKey = base64.parse(payload.key);
+ const privateKey = await crypto.subtle.importKey('pkcs8', rawKey, UserKeys.KEY_DESIGNATION, true, UserKeys.KEY_USAGES);
+ return new UserKeys({ publicKey: publicKey, privateKey: privateKey });
+ } finally {
+ rawKey.fill(0x00);
+ }
+ }
+
+ private static async publicKey(publicKey: CryptoKey | BufferSource): Promise {
+ if (publicKey instanceof CryptoKey) {
+ return publicKey;
+ } else {
+ return await crypto.subtle.importKey('spki', publicKey, UserKeys.KEY_DESIGNATION, true, []);
+ }
+ }
+}
+
+export class BrowserKeys {
+ public static readonly KEY_USAGES: KeyUsage[] = ['deriveBits'];
+
+ private static readonly KEY_DESIGNATION: EcKeyImportParams | EcKeyGenParams = {
+ name: 'ECDH',
+ namedCurve: 'P-384'
+ };
+
+ readonly keyPair: CryptoKeyPair;
+
+ protected constructor(keyPair: CryptoKeyPair) {
+ this.keyPair = keyPair;
+ }
+
+ /**
+ * Creates a new device key pair for this browser
+ * @returns A new device key pair
+ */
+ public static async create(): Promise {
+ const keyPair = crypto.subtle.generateKey(BrowserKeys.KEY_DESIGNATION, false, BrowserKeys.KEY_USAGES);
+ return new BrowserKeys(await keyPair);
+ }
+
+ /**
+ * Attempts to load previously stored key pair from the browser's IndexedDB.
+ * @returns a promise resolving to the loaded browser key pair
+ */
+ public static async load(userId: string): Promise {
+ const keyPair: CryptoKeyPair = await DB.transaction('keys', 'readonly', tx => {
+ const keyStore = tx.objectStore('keys');
+ return keyStore.get(userId);
+ });
+ if (keyPair) {
+ return new BrowserKeys(keyPair);
+ } else {
+ return undefined;
+ }
+ }
+
+ /**
+ * Deletes the key pair for the given user.
+ * @returns a promise resolving on success
+ */
+ public static async delete(userId: string): Promise {
+ await DB.transaction('keys', 'readwrite', tx => {
+ const keyStore = tx.objectStore('keys');
+ return keyStore.delete(userId);
+ });
+ }
+
+ /**
+ * Stores the key pair in the browser's IndexedDB. See https://www.w3.org/TR/WebCryptoAPI/#concepts-key-storage
+ * @returns a promise that will resolve if the key pair has been saved
+ */
+ public async store(userId: string): Promise {
+ await DB.transaction('keys', 'readwrite', tx => {
+ const keyStore = tx.objectStore('keys');
+ return keyStore.put(this.keyPair, userId);
+ });
+ }
+
+ public async id(): Promise {
+ const publicKey = new Uint8Array(await crypto.subtle.exportKey('spki', this.keyPair.publicKey));
+ const hash = new Uint8Array(await crypto.subtle.digest({ name: 'SHA-256' }, publicKey));
+ return base16.stringify(hash).toUpperCase();
+ }
+
+ public async encodedPublicKey() {
+ const publicKey = new Uint8Array(await crypto.subtle.exportKey('spki', this.keyPair.publicKey));
+ return base64.stringify(publicKey);
+ }
+}
+
+export async function getFingerprint(key: string | undefined) {
+ if (key) {
+ const encodedKey = new TextEncoder().encode(key);
+ const hashBuffer = await crypto.subtle.digest("SHA-256", encodedKey);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const hashHex = hashArray
+ .map((b) => b.toString(16).padStart(2, "0").toUpperCase())
+ .join("");
+ return hashHex;
+ }
+}
diff --git a/frontend/src/common/jwe.ts b/frontend/src/common/jwe.ts
index 66c363647..6ea42ade3 100644
--- a/frontend/src/common/jwe.ts
+++ b/frontend/src/common/jwe.ts
@@ -1,25 +1,17 @@
import { base64url } from 'rfc4648';
+import { UnwrapKeyError } from './crypto';
+// visible for testing
export class ConcatKDF {
/**
* KDF as defined in NIST SP 800-56A Rev. 2 Section 5.8.1 using SHA-256
*
* @param z A shared secret
* @param keyDataLen Desired key length (in bytes)
- * @param algorithmId Purpose of the derived key material
- * @param partyUInfo Public information about party U
- * @param partyVInfo Public information about party V
- * @param suppPubInfo Mutually known public information (optional)
- * @param suppPrivInfo Mutually known private information (optional)
+ * @param otherInfo Optional context info binding the derived key to a key agreement (see e.g. RFC 7518, Section 4.6.2)
* @returns key data
*/
- public static async kdf(z: Uint8Array, keyDataLen: number, algorithmId: Uint8Array, partyUInfo: Uint8Array, partyVInfo: Uint8Array, suppPubInfo: Uint8Array = new Uint8Array(), suppPrivInfo: Uint8Array = new Uint8Array()): Promise {
- // AlgorithmID || PartyUInfo || PartyVInfo {|| SuppPubInfo }{|| SuppPrivInfo }
- const otherInfo = new Uint8Array([...algorithmId, ...partyUInfo, ...partyVInfo, ...suppPubInfo, ...suppPrivInfo]);
- return this.kdfInternal(z, keyDataLen, new Uint8Array(otherInfo));
- }
-
- private static async kdfInternal(z: Uint8Array, keyDataLen: number, otherInfo: Uint8Array): Promise {
+ public static async kdf(z: Uint8Array, keyDataLen: number, otherInfo: Uint8Array): Promise {
const hashLen = 32; // output length of SHA-256
const reps = Math.ceil(keyDataLen / hashLen);
if (reps >= 0xFFFFFFFF) {
@@ -43,90 +35,259 @@ export class ConcatKDF {
}
}
-export class JWEHeader {
- constructor(readonly alg: string, readonly enc: string, readonly epk: JsonWebKey | null, readonly apu?: string, readonly apv?: string) { }
+export type JWEHeader = {
+ readonly alg: 'ECDH-ES' | 'PBES2-HS512+A256KW',
+ readonly enc: 'A256GCM' | 'A128GCM',
+ readonly apu?: string,
+ readonly apv?: string,
+ readonly epk?: JsonWebKey,
+ readonly p2c?: number,
+ readonly p2s?: string
}
-export class JWE {
+export const ECDH_P384: EcKeyImportParams | EcKeyGenParams = {
+ name: 'ECDH',
+ namedCurve: 'P-384'
+};
+
+export class JWEParser {
+ readonly header: JWEHeader;
+ readonly encryptedKey: Uint8Array;
+ readonly iv: Uint8Array;
+ readonly ciphertext: Uint8Array;
+ readonly tag: Uint8Array;
+
+ private constructor(readonly encodedHeader: string, readonly encodedEncryptedKey: string, readonly encodedIv: string, readonly encodedCiphertext: string, readonly encodedTag: string) {
+ const utf8dec = new TextDecoder();
+ this.header = JSON.parse(utf8dec.decode(base64url.parse(encodedHeader, { loose: true })));
+ this.encryptedKey = base64url.parse(encodedEncryptedKey, { loose: true });
+ this.iv = base64url.parse(encodedIv, { loose: true });
+ this.ciphertext = base64url.parse(encodedCiphertext, { loose: true });
+ this.tag = base64url.parse(encodedTag, { loose: true });
+ }
+
/**
- * Creates a JWE using ECDH-ES using the P-384 curve and AES-256-GCM for payload encryption.
- *
- * See RFC 7516 + RFC 7518, Section 4.6
- *
- * @param payload The secret payload
- * @param devicePublicKey The recipient's public key
- * @param apu Optional public information about the producer (PartyUInfo)
- * @param apv Optional public information about the recipient (PartyVInfo)
+ * Decodes the JWE.
+ * @param jwe The JWE string
+ * @returns Decoded JWE, ready to decrypt.
*/
- public static async build(payload: Uint8Array, recipientPublicKey: CryptoKey, apu: Uint8Array = new Uint8Array(), apv: Uint8Array = new Uint8Array()): Promise {
- /* key agreement and header params described in RFC 7518, Section 4.6: */
- const ephemeralKey = await crypto.subtle.generateKey(
+ public static parse(jwe: string): JWEParser {
+ const [encodedHeader, encodedEncryptedKey, encodedIv, encodedCiphertext, encodedTag] = jwe.split('.', 5);
+ return new JWEParser(encodedHeader, encodedEncryptedKey, encodedIv, encodedCiphertext, encodedTag);
+ }
+
+ /**
+ * Decrypts the JWE, assuming alg == ECDH-ES, enc == A256GCM and keys on the P-384 curve.
+ * @param recipientPrivateKey The recipient's private key
+ * @returns Decrypted payload
+ */
+ public async decryptEcdhEs(recipientPrivateKey: CryptoKey): Promise {
+ if (this.header.alg != 'ECDH-ES' || this.header.enc != 'A256GCM' || !this.header.epk) {
+ throw new Error('unsupported alg or enc');
+ }
+ const ephemeralKey = await crypto.subtle.importKey('jwk', this.header.epk, ECDH_P384, false, []);
+ const cek = await ECDH_ES.deriveContentKey(ephemeralKey, recipientPrivateKey, 384, 32, this.header);
+ return this.decrypt(cek);
+ }
+
+ /**
+ * Decrypts the JWE, assuming alg == PBES2-HS512+A256KW and enc == A256GCM.
+ * @param password The password to feed into the KDF
+ * @returns Decrypted payload
+ * @throws {UnwrapKeyError} if decryption failed (wrong password?)
+ */
+ public async decryptPbes2(password: string): Promise {
+ if (this.header.alg != 'PBES2-HS512+A256KW' || /* this.header.enc != 'A256GCM' || */ !this.header.p2s || !this.header.p2c) {
+ throw new Error('unsupported alg or enc');
+ }
+ const saltInput = base64url.parse(this.header.p2s, { loose: true });
+ const wrappingKey = await PBES2.deriveWrappingKey(password, this.header.alg, saltInput, this.header.p2c);
+ try {
+ const cek = crypto.subtle.unwrapKey('raw', this.encryptedKey, wrappingKey, 'AES-KW', { name: 'AES-GCM', length: 256 }, false, ['decrypt']);
+ return this.decrypt(await cek);
+ } catch (error) {
+ throw new UnwrapKeyError(error);
+ }
+ }
+
+ private async decrypt(cek: CryptoKey): Promise {
+ const utf8enc = new TextEncoder();
+ const m = new Uint8Array(this.ciphertext.length + this.tag.length);
+ m.set(this.ciphertext, 0);
+ m.set(this.tag, this.ciphertext.length);
+ const payloadJson = new Uint8Array(await crypto.subtle.decrypt(
{
- name: 'ECDH',
- namedCurve: 'P-384'
+ name: 'AES-GCM',
+ iv: this.iv,
+ additionalData: utf8enc.encode(this.encodedHeader),
+ tagLength: 128
},
- false,
- ['deriveBits']
- );
- const alg = 'ECDH-ES';
- const enc = 'A256GCM';
- const epk = await crypto.subtle.exportKey('jwk', ephemeralKey.publicKey);
- const header = new JWEHeader(alg, enc, epk, base64url.stringify(apu, { pad: false }), base64url.stringify(apv, { pad: false }));
+ cek,
+ m
+ ));
+ return JSON.parse(new TextDecoder().decode(payloadJson));
+ }
+}
+
+export class JWEBuilder {
+ private constructor(readonly header: Promise, readonly encryptedKey: Promise, readonly cek: Promise) { }
+
+ /**
+ * Prepares a new JWE using alg: ECDH-ES and enc: A256GCM.
+ *
+ * @param recipientPublicKey Static public key of the JWE's recipient
+ * @param apu Optional information about the creator
+ * @param apv Optional information about the recipient
+ * @returns A new JWEBuilder ready to encrypt the payload
+ */
+ public static ecdhEs(recipientPublicKey: CryptoKey, apu: Uint8Array = new Uint8Array(), apv: Uint8Array = new Uint8Array()): JWEBuilder {
+ /* key agreement and header params described in RFC 7518, Section 4.6: */
+ const ephemeralKey = crypto.subtle.generateKey(ECDH_P384, false, ['deriveBits']);
+ const header = (async () => {
+ alg: 'ECDH-ES',
+ enc: 'A256GCM',
+ epk: await crypto.subtle.exportKey('jwk', (await ephemeralKey).publicKey),
+ apu: base64url.stringify(apu, { pad: false }),
+ apv: base64url.stringify(apv, { pad: false })
+ })();
+ const encryptedKey = (async () => Uint8Array.of())(); // empty for Direct Key Agreement as per spec
+ const cek = (async () => ECDH_ES.deriveContentKey(recipientPublicKey, (await ephemeralKey).privateKey, 384, 32, await header))();
+ return new JWEBuilder(header, encryptedKey, cek);
+ }
+
+ /**
+ * Prepares a new JWE using alg: PBES2-HS512+A256KW and enc: A256GCM.
+ *
+ * @param password The password to feed into the KDF
+ * @param iterations The PBKDF2 iteration count (defaults to {@link PBES2.DEFAULT_ITERATION_COUNT} )
+ * @param apu Optional information about the creator
+ * @param apv Optional information about the recipient
+ * @returns A new JWEBuilder ready to encrypt the payload
+ */
+ public static pbes2(password: string, iterations: number = PBES2.DEFAULT_ITERATION_COUNT, apu: Uint8Array = new Uint8Array(), apv: Uint8Array = new Uint8Array()): JWEBuilder {
+ const saltInput = crypto.getRandomValues(new Uint8Array(16));
+ const header = (async () => {
+ alg: 'PBES2-HS512+A256KW',
+ enc: 'A256GCM',
+ p2s: base64url.stringify(saltInput, { pad: false }),
+ p2c: iterations,
+ apu: base64url.stringify(apu, { pad: false }),
+ apv: base64url.stringify(apv, { pad: false })
+ })();
+ const wrappingKey = PBES2.deriveWrappingKey(password, 'PBES2-HS512+A256KW', saltInput, iterations);
+ const cek = crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt']);
+ const encryptedKey = (async () => new Uint8Array(await crypto.subtle.wrapKey('raw', await cek, await wrappingKey, 'AES-KW')))();
+ return new JWEBuilder(header, encryptedKey, cek);
+ }
+
+ /**
+ * Builds the JWE.
+ * @param payload Payload to be encrypted
+ * @returns The JWE
+ */
+ public async encrypt(payload: object) {
+ const utf8enc = new TextEncoder();
/* JWE assembly and content encryption described in RFC 7516: */
- const encodedHeader = base64url.stringify(new TextEncoder().encode(JSON.stringify(header)), { pad: false });
+ const encodedHeader = base64url.stringify(utf8enc.encode(JSON.stringify(await this.header)), { pad: false });
const iv = crypto.getRandomValues(new Uint8Array(12));
const encodedIv = base64url.stringify(iv, { pad: false });
- const encodedEncryptedKey = ''; // empty for Direct Key Agreement as per spec
- const cek = await this.deriveKey(recipientPublicKey, ephemeralKey.privateKey, 384, 32, header);
+ const encodedEncryptedKey = base64url.stringify(await this.encryptedKey, { pad: false });
const m = new Uint8Array(await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv,
- additionalData: new TextEncoder().encode(encodedHeader),
+ additionalData: utf8enc.encode(encodedHeader),
tagLength: 128
},
- cek,
- payload
+ await this.cek,
+ utf8enc.encode(JSON.stringify(payload))
));
console.assert(m.byteLength > 16, 'result of GCM encryption expected to contain 128bit tag');
const ciphertext = m.slice(0, m.byteLength - 16);
const tag = m.slice(m.byteLength - 16);
const encodedCiphertext = base64url.stringify(ciphertext, { pad: false });
const encodedTag = base64url.stringify(tag, { pad: false });
- return encodedHeader + '.' + encodedEncryptedKey + '.' + encodedIv + '.' + encodedCiphertext + '.' + encodedTag;
+ return `${encodedHeader}.${encodedEncryptedKey}.${encodedIv}.${encodedCiphertext}.${encodedTag}`;
}
+}
- // visible for testing
- public static async deriveKey(recipientPublicKey: CryptoKey, ephemeralSecretKey: CryptoKey, ecdhKeyBits: number, desiredKeyBytes: number, header: JWEHeader, exportable: boolean = false): Promise {
+// visible for testing
+export class ECDH_ES {
+ public static async deriveContentKey(publicKey: CryptoKey, privateKey: CryptoKey, ecdhKeyBits: number, desiredKeyBytes: number, header: JWEHeader, exportable: boolean = false): Promise {
let agreedKey = new Uint8Array();
let derivedKey = new Uint8Array();
try {
- const algorithmId = this.lengthPrefixed(new TextEncoder().encode(header.enc));
- const partyUInfo = this.lengthPrefixed(base64url.parse(header.apu || '', { loose: true }));
- const partyVInfo = this.lengthPrefixed(base64url.parse(header.apv || '', { loose: true }));
+ const algorithmId = ECDH_ES.lengthPrefixed(new TextEncoder().encode(header.enc));
+ const partyUInfo = ECDH_ES.lengthPrefixed(base64url.parse(header.apu || '', { loose: true }));
+ const partyVInfo = ECDH_ES.lengthPrefixed(base64url.parse(header.apv || '', { loose: true }));
const suppPubInfo = new ArrayBuffer(4);
new DataView(suppPubInfo).setUint32(0, desiredKeyBytes * 8, false);
agreedKey = new Uint8Array(await crypto.subtle.deriveBits(
{
name: 'ECDH',
- public: recipientPublicKey
+ public: publicKey
},
- ephemeralSecretKey,
+ privateKey,
ecdhKeyBits
));
- derivedKey = await ConcatKDF.kdf(new Uint8Array(agreedKey), desiredKeyBytes, algorithmId, partyUInfo, partyVInfo, new Uint8Array(suppPubInfo));
- return crypto.subtle.importKey('raw', derivedKey, { name: 'AES-GCM', length: desiredKeyBytes * 8 }, exportable, ['encrypt']);
+ const otherInfo = new Uint8Array([...algorithmId, ...partyUInfo, ...partyVInfo, ...new Uint8Array(suppPubInfo)]);
+ derivedKey = await ConcatKDF.kdf(new Uint8Array(agreedKey), desiredKeyBytes, otherInfo);
+ return crypto.subtle.importKey('raw', derivedKey, { name: 'AES-GCM', length: desiredKeyBytes * 8 }, exportable, ['encrypt', 'decrypt']);
} finally {
derivedKey.fill(0x00);
agreedKey.fill(0x00);
}
}
- private static lengthPrefixed(data: Uint8Array): Uint8Array {
- const result = new ArrayBuffer(4 + data.byteLength);
- new DataView(result, 0, 4).setUint32(0, data.byteLength, false);
- new Uint8Array(result).set(data, 4);
- return new Uint8Array(result);
+ public static lengthPrefixed(data: Uint8Array): Uint8Array {
+ const result = new Uint8Array(4 + data.byteLength);
+ new DataView(result.buffer, 0, 4).setUint32(0, data.byteLength, false);
+ result.set(data, 4);
+ return result;
+ }
+}
+
+// visible for testing
+export class PBES2 {
+ public static readonly DEFAULT_ITERATION_COUNT = 1000000;
+ private static readonly NULL_BYTE = Uint8Array.of(0x00);
+
+ // TODO: can we dedup this with crypto.ts's PBKDF2? Or is the latter unused anyway, once we migrate all ciphertext to JWE containers
+ public static async deriveWrappingKey(password: string, alg: 'PBES2-HS512+A256KW' | 'PBES2-HS256+A128KW', salt: Uint8Array, iterations: number, extractable: boolean = false): Promise {
+ let hash, keyLen;
+ if (alg == 'PBES2-HS512+A256KW') {
+ hash = 'SHA-512';
+ keyLen = 256;
+ } else if (alg == 'PBES2-HS256+A128KW') {
+ hash = 'SHA-256';
+ keyLen = 128;
+ } else {
+ throw new Error('only PBES2-HS512+A256KW and PBES2-HS256+A128KW supported');
+ }
+ const utf8enc = new TextEncoder();
+ const encodedPw = utf8enc.encode(password);
+ const pwKey = crypto.subtle.importKey(
+ 'raw',
+ encodedPw,
+ 'PBKDF2',
+ false,
+ ['deriveKey']
+ );
+ return crypto.subtle.deriveKey(
+ {
+ name: 'PBKDF2',
+ hash: hash,
+ salt: new Uint8Array([...utf8enc.encode(alg), ...PBES2.NULL_BYTE, ...salt]), // see https://www.rfc-editor.org/rfc/rfc7518#section-4.8.1.1
+ iterations: iterations
+ },
+ await pwKey,
+ {
+ name: 'AES-KW',
+ length: keyLen
+ },
+ extractable,
+ ['wrapKey', 'unwrapKey']
+ );
}
}
diff --git a/frontend/src/common/jwt.ts b/frontend/src/common/jwt.ts
index 2a30851bc..16979d945 100644
--- a/frontend/src/common/jwt.ts
+++ b/frontend/src/common/jwt.ts
@@ -1,7 +1,6 @@
import { base64url } from 'rfc4648';
-export interface JWTHeader {
-
+export type JWTHeader = {
alg: 'ES384';
typ: 'JWT';
b64: true;
diff --git a/frontend/src/common/util.ts b/frontend/src/common/util.ts
index 13692402a..b127c6889 100644
--- a/frontend/src/common/util.ts
+++ b/frontend/src/common/util.ts
@@ -1,5 +1,26 @@
import { dictionary } from './4096words_en';
+export class DB {
+ private static readonly NAME = 'hub';
+
+ public static async transaction(objectStore: string, mode: IDBTransactionMode, query: (transaction: IDBTransaction) => IDBRequest): Promise {
+ const db = await new Promise((resolve, reject) => {
+ const req = indexedDB.open(DB.NAME);
+ req.onsuccess = () => resolve(req.result);
+ req.onerror = () => reject(req.error);
+ req.onupgradeneeded = () => req.result.createObjectStore(objectStore);
+ });
+ const transaction = db.transaction(objectStore, mode);
+ return new Promise((resolve, reject) => {
+ const req = query(transaction);
+ req.onsuccess = () => resolve(req.result);
+ req.onerror = () => reject(req.error);
+ }).finally(() => {
+ db.close();
+ });
+ }
+}
+
export class Deferred {
public promise: Promise;
public reject: (reason?: any) => void;
diff --git a/frontend/src/common/vaultconfig.ts b/frontend/src/common/vaultconfig.ts
index 312a57159..ba6b380f5 100644
--- a/frontend/src/common/vaultconfig.ts
+++ b/frontend/src/common/vaultconfig.ts
@@ -20,9 +20,10 @@ export class VaultConfig {
clientId: cfg.keycloakClientIdCryptomator,
authEndpoint: cfg.keycloakAuthEndpoint,
tokenEndpoint: cfg.keycloakTokenEndpoint,
- devicesResourceUrl: `${absBackendBaseURL}devices/`,
authSuccessUrl: `${absFrontendBaseURL}unlock-success?vault=${vaultId}`,
- authErrorUrl: `${absFrontendBaseURL}unlock-error?vault=${vaultId}`
+ authErrorUrl: `${absFrontendBaseURL}unlock-error?vault=${vaultId}`,
+ apiBaseUrl: absBackendBaseURL,
+ devicesResourceUrl: `${absBackendBaseURL}devices/`,
};
const jwtPayload: VaultConfigPayload = {
diff --git a/frontend/src/components/AdminSettings.vue b/frontend/src/components/AdminSettings.vue
index 86a1a2f75..d9554d4a4 100644
--- a/frontend/src/components/AdminSettings.vue
+++ b/frontend/src/components/AdminSettings.vue
@@ -193,9 +193,10 @@ import { useI18n } from 'vue-i18n';
import backend, { BillingDto, VersionDto } from '../common/backend';
import config, { absFrontendBaseURL } from '../common/config';
import { FetchUpdateError, LatestVersionDto, updateChecker } from '../common/updatecheck';
+import { Locale } from '../i18n/index';
import FetchError from './FetchError.vue';
-const { t, d, locale } = useI18n({ useScope: 'global' });
+const { t, d, locale, fallbackLocale } = useI18n({ useScope: 'global' });
const props = defineProps<{
token?: string
@@ -265,7 +266,9 @@ async function fetchData() {
function manageSubscription() {
const returnUrl = `${absFrontendBaseURL}admin`;
- const languagePathComponent = locale.value == 'en' ? '' : `${locale.value}/`;
+ const supportedLanguages = [Locale.EN, Locale.DE];
+ const supportedLanguagePathComponents = Object.fromEntries(supportedLanguages.map(lang => [lang, lang == Locale.EN ? '' : `${lang}/`]));
+ const languagePathComponent = supportedLanguagePathComponents[(locale.value as string).split('-')[0]] ?? supportedLanguagePathComponents[fallbackLocale.value as string] ?? '';
window.open(`https://cryptomator.org/${languagePathComponent}hub/billing/?hub_id=${admin.value?.hubId}&return_url=${encodeURIComponent(returnUrl)}`, '_self');
}
diff --git a/frontend/src/components/ArchiveVaultDialog.vue b/frontend/src/components/ArchiveVaultDialog.vue
index 7d657d17f..1e1a846e0 100644
--- a/frontend/src/components/ArchiveVaultDialog.vue
+++ b/frontend/src/components/ArchiveVaultDialog.vue
@@ -81,7 +81,7 @@ async function archiveVault() {
onArchiveVaultError.value = null;
const v = props.vault;
try {
- const vaultDto = await backend.vaults.createOrUpdateVault(v.id, v.name, v.description, true, v.masterkey, v.iterations, v.salt, v.authPublicKey, v.authPrivateKey );
+ const vaultDto = await backend.vaults.createOrUpdateVault(v.id, v.name, true, v.description);
emit('archived', vaultDto);
open.value = false;
} catch (error) {
diff --git a/frontend/src/components/AuditLog.vue b/frontend/src/components/AuditLog.vue
index 1b6855172..ccf5558d0 100644
--- a/frontend/src/components/AuditLog.vue
+++ b/frontend/src/components/AuditLog.vue
@@ -109,6 +109,8 @@
+
+
@@ -162,7 +164,7 @@ import { ChevronDownIcon } from '@heroicons/vue/20/solid';
import { CheckIcon, ChevronUpDownIcon, WrenchIcon } from '@heroicons/vue/24/solid';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
-import auditlog, { AuditEventDeviceRegisterDto, AuditEventDeviceRemoveDto, AuditEventDto, AuditEventVaultAccessGrantDto, AuditEventVaultCreateDto, AuditEventVaultKeyRetrieveDto, AuditEventVaultMemberAddDto, AuditEventVaultMemberRemoveDto, AuditEventVaultUpdateDto } from '../common/auditlog';
+import auditlog, { AuditEventDeviceRegisterDto, AuditEventDeviceRemoveDto, AuditEventDto, AuditEventVaultAccessGrantDto, AuditEventVaultCreateDto, AuditEventVaultKeyRetrieveDto, AuditEventVaultMemberAddDto, AuditEventVaultMemberRemoveDto, AuditEventVaultMemberUpdateDto, AuditEventVaultOwnershipClaimDto, AuditEventVaultUpdateDto } from '../common/auditlog';
import { PaymentRequiredError } from '../common/backend';
import AuditLogDetailsDeviceRegister from './AuditLogDetailsDeviceRegister.vue';
import AuditLogDetailsDeviceRemove from './AuditLogDetailsDeviceRemove.vue';
@@ -171,6 +173,8 @@ import AuditLogDetailsVaultCreate from './AuditLogDetailsVaultCreate.vue';
import AuditLogDetailsVaultKeyRetrieve from './AuditLogDetailsVaultKeyRetrieve.vue';
import AuditLogDetailsVaultMemberAdd from './AuditLogDetailsVaultMemberAdd.vue';
import AuditLogDetailsVaultMemberRemove from './AuditLogDetailsVaultMemberRemove.vue';
+import AuditLogDetailsVaultMemberUpdate from './AuditLogDetailsVaultMemberUpdate.vue';
+import AuditLogDetailsVaultOwnershipClaim from './AuditLogDetailsVaultOwnershipClaim.vue';
import AuditLogDetailsVaultUpdate from './AuditLogDetailsVaultUpdate.vue';
import FetchError from './FetchError.vue';
diff --git a/frontend/src/components/AuditLogDetailsVaultMemberAdd.vue b/frontend/src/components/AuditLogDetailsVaultMemberAdd.vue
index 9a42c94e7..51322d610 100644
--- a/frontend/src/components/AuditLogDetailsVaultMemberAdd.vue
+++ b/frontend/src/components/AuditLogDetailsVaultMemberAdd.vue
@@ -31,6 +31,16 @@
{{ event.authorityId }}
+
+
+ role
+
+
+ Member
+ Owner
+ {{ event.role }}
+
+
diff --git a/frontend/src/components/AuditLogDetailsVaultMemberUpdate.vue b/frontend/src/components/AuditLogDetailsVaultMemberUpdate.vue
new file mode 100644
index 000000000..017f96386
--- /dev/null
+++ b/frontend/src/components/AuditLogDetailsVaultMemberUpdate.vue
@@ -0,0 +1,69 @@
+
+
+ {{ t('auditLog.details.vaultMember.update') }}
+ |
+
+
+
+ -
+
updated by
+
+ -
+ {{ resolvedUpdatedBy.name }}
+
{{ event.updatedBy }}
+
+
+
+ -
+
vault
+
+ -
+ {{ resolvedVault.name }}
+
{{ event.vaultId }}
+
+
+
+ -
+
authority
+
+ -
+ {{ resolvedAuthority.name }}
+
{{ event.authorityId }}
+
+
+
+ -
+
role
+
+ -
+ Member
+ Owner
+ {{ event.role }}
+
+
+
+ |
+
+
+
diff --git a/frontend/src/components/AuditLogDetailsVaultOwnershipClaim.vue b/frontend/src/components/AuditLogDetailsVaultOwnershipClaim.vue
new file mode 100644
index 000000000..035a73d0b
--- /dev/null
+++ b/frontend/src/components/AuditLogDetailsVaultOwnershipClaim.vue
@@ -0,0 +1,48 @@
+
+
+ {{ t('auditLog.details.vaultOwnership.claim') }}
+ |
+
+
+
+ -
+
claimed by
+
+ -
+ {{ resolvedClaimedBy.name }}
+
{{ event.claimedBy }}
+
+
+
+ -
+
vault
+
+ -
+ {{ resolvedVault.name }}
+
{{ event.vaultId }}
+
+
+
+ |
+
+
+
diff --git a/frontend/src/components/AuthenticatedMain.vue b/frontend/src/components/AuthenticatedMain.vue
index 18d0063f9..bb9e6610c 100644
--- a/frontend/src/components/AuthenticatedMain.vue
+++ b/frontend/src/components/AuthenticatedMain.vue
@@ -35,7 +35,7 @@ onMounted(fetchData);
async function fetchData() {
onFetchError.value = null;
try {
- me.value = await backend.users.me(true, true);
+ me.value = await backend.users.me();
} catch (error) {
console.error('Retrieving logged in user failed.', error);
onFetchError.value = error instanceof Error ? error : new Error('Unknown Error');
diff --git a/frontend/src/components/AuthenticateVaultAdminDialog.vue b/frontend/src/components/ClaimVaultOwnershipDialog.vue
similarity index 82%
rename from frontend/src/components/AuthenticateVaultAdminDialog.vue
rename to frontend/src/components/ClaimVaultOwnershipDialog.vue
index 8c6588ecc..da081453b 100644
--- a/frontend/src/components/AuthenticateVaultAdminDialog.vue
+++ b/frontend/src/components/ClaimVaultOwnershipDialog.vue
@@ -17,30 +17,30 @@
- {{ t('authenticateVaultAdminDialog.title') }}
+ {{ t('claimVaultOwnershipDialog.title') }}
- {{ t('authenticateVaultAdminDialog.error.formValidationFailed') }}
+ {{ t('claimVaultOwnershipDialog.error.formValidationFailed') }}
- {{ t('authenticateVaultAdminDialog.error.wrongPassword') }}
+ {{ t('claimVaultOwnershipDialog.error.wrongPassword') }}
{{ t('common.unexpectedError', [onAuthenticationError.message]) }}
@@ -62,7 +62,7 @@ import { KeyIcon } from '@heroicons/vue/24/outline';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { VaultDto } from '../common/backend';
-import { UnwrapKeyError, VaultKeys, WrappedVaultKeys } from '../common/crypto';
+import { UnwrapKeyError, VaultKeys } from '../common/crypto';
class FormValidationFailedError extends Error {
constructor() {
@@ -85,7 +85,7 @@ const props = defineProps<{
const emit = defineEmits<{
close: []
- action: [vaultKeys: VaultKeys]
+ action: [vaultKeys: VaultKeys, ownerSigningKey: CryptoKeyPair]
}>();
defineExpose({
@@ -102,10 +102,13 @@ async function authenticateVaultAdmin() {
if (!form.value?.checkValidity()) {
throw new FormValidationFailedError();
}
- const wrappedKey = new WrappedVaultKeys(props.vault.masterkey, props.vault.authPrivateKey, props.vault.authPublicKey, props.vault.salt, props.vault.iterations);
- const vaultKeys = await VaultKeys.unwrap(password.value, wrappedKey);
- emit('action', vaultKeys);
- open.value = false;
+ if (props.vault.masterkey && props.vault.authPrivateKey && props.vault.authPublicKey && props.vault.salt && props.vault.iterations) {
+ const [vaultKeys, ownerKeyPair] = await VaultKeys.decryptWithAdminPassword(password.value, props.vault.masterkey, props.vault.authPrivateKey, props.vault.authPublicKey, props.vault.salt, props.vault.iterations);
+ emit('action', vaultKeys, ownerKeyPair);
+ open.value = false;
+ } else {
+ throw new Error('Vault is missing legacy "vault admin password" related properties.');
+ }
} catch (error) {
console.error('Authentication of vault admin failed.', error);
onAuthenticationError.value = error instanceof Error ? error : new Error('Unknown Error');
diff --git a/frontend/src/components/CreateVault.vue b/frontend/src/components/CreateVault.vue
index c480e5ed6..02cca908b 100644
--- a/frontend/src/components/CreateVault.vue
+++ b/frontend/src/components/CreateVault.vue
@@ -6,7 +6,7 @@
@@ -88,7 +72,6 @@
{{ t('createVault.error.formValidationFailed') }}
-
{{ t('createVault.enterVaultDetails.passwordConfirmation.passwordsDoNotMatch') }}
{{ t('common.unexpectedError', [onCreateError.message]) }}