Skip to content

Commit

Permalink
Merge branch 'release/1.3.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
SailReal committed Feb 5, 2024
2 parents 03cd1af + 46dded1 commit 84549cd
Show file tree
Hide file tree
Showing 18 changed files with 1,407 additions and 720 deletions.
18 changes: 15 additions & 3 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>hub-backend</artifactId>
<version>1.3.1</version>
<version>1.3.2</version>

<properties>
<compiler-plugin.version>3.11.0 </compiler-plugin.version>
<compiler-plugin.version>3.11.0</compiler-plugin.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.jdk.version>17</project.jdk.version>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.container-image.group>cryptomator</quarkus.container-image.group>
<quarkus.container-image.name>hub</quarkus.container-image.name>
<quarkus.platform.version>3.4.3</quarkus.platform.version>
<quarkus.platform.version>3.2.10.Final</quarkus.platform.version>
<quarkus.jib.base-jvm-image>eclipse-temurin:17-jre</quarkus.jib.base-jvm-image> <!-- irrelevant for -Pnative -->
<jwt.version>4.4.0</jwt.version>
<surefire-plugin.version>3.1.2</surefire-plugin.version>
Expand All @@ -28,6 +28,18 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<!-- temporarily pin Flyway version until Quarkus LTS contains Flyway >= 9.21.0; see https://github.com/cryptomator/hub/issues/256 -->
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>9.22.3</version>
</dependency>
<dependency>
<!-- temporarily pin Flyway version until Quarkus LTS contains Flyway >= 9.21.0; see https://github.com/cryptomator/hub/issues/256 -->
<groupId>io.quarkus</groupId>
<artifactId>quarkus-flyway</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
import jakarta.ws.rs.core.Response;
import org.cryptomator.hub.entities.AuditEventDeviceRegister;
import org.cryptomator.hub.entities.AuditEventDeviceRemove;
import org.cryptomator.hub.entities.AuditEventVaultAccessGrant;
import org.cryptomator.hub.entities.Device;
import org.cryptomator.hub.entities.LegacyAccessToken;
import org.cryptomator.hub.entities.LegacyDevice;
import org.cryptomator.hub.entities.User;
import org.cryptomator.hub.validation.NoHtmlOrScriptChars;
Expand All @@ -41,6 +41,9 @@
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

@Path("/devices")
public class DeviceResource {
Expand Down Expand Up @@ -117,6 +120,20 @@ public DeviceDto get(@PathParam("deviceId") @ValidId String deviceId) {
}
}

@Deprecated
@GET
@Path("/{deviceId}/legacy-access-tokens")
@RolesAllowed("user")
@Produces(MediaType.APPLICATION_JSON)
@NoCache
@Transactional
@Operation(summary = "list legacy access tokens", description = "get all legacy access tokens for this device ({vault1: token1, vault1: token2, ...}). The device must be owned by the currently logged-in user")
@APIResponse(responseCode = "200")
public Map<UUID, String> getLegacyAccessTokens(@PathParam("deviceId") @ValidId String deviceId) {
return LegacyAccessToken.getByDeviceAndOwner(deviceId, jwt.getSubject())
.collect(Collectors.toMap(token -> token.id.vaultId , token -> token.jwe));
}

@DELETE
@Path("/{deviceId}")
@RolesAllowed("user")
Expand Down Expand Up @@ -154,4 +171,6 @@ public static DeviceDto fromEntity(Device entity) {
}

}

public record LegacyAccessTokenDto(@JsonProperty("vaultId") UUID vaultId, @JsonProperty("token") String token) {}
}
33 changes: 33 additions & 0 deletions backend/src/main/java/org/cryptomator/hub/api/UsersResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
Expand All @@ -15,8 +17,10 @@
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.cryptomator.hub.entities.AccessToken;
import org.cryptomator.hub.entities.AuditEventVaultAccessGrant;
import org.cryptomator.hub.entities.Device;
import org.cryptomator.hub.entities.User;
import org.cryptomator.hub.entities.Vault;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
Expand All @@ -25,7 +29,9 @@
import java.net.URI;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -62,6 +68,33 @@ public Response putMe(@Nullable @Valid UserDto dto) {
return Response.created(URI.create(".")).build();
}

@POST
@Path("/me/access-tokens")
@RolesAllowed("user")
@Transactional
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "adds/updates user-specific vault keys", description = "Stores one or more vaultid-vaultkey-tuples for the currently logged-in user, as defined in the request body ({vault1: token1, vault2: token2, ...}).")
@APIResponse(responseCode = "200", description = "all keys stored")
public Response updateMyAccessTokens(@NotNull Map<UUID, String> tokens) {
var user = User.<User>findById(jwt.getSubject());
for (var entry : tokens.entrySet()) {
var vault = Vault.<Vault>findById(entry.getKey());
if (vault == null) {
continue; // skip
}
var token = AccessToken.<AccessToken>findById(new AccessToken.AccessId(user.id, vault.id));
if (token == null) {
token = new AccessToken();
token.vault = vault;
token.user = user;
}
token.vaultKey = entry.getValue();
token.persist();
AuditEventVaultAccessGrant.log(user.id, vault.id, user.id);
}
return Response.ok().build();
}

@GET
@Path("/me")
@RolesAllowed("user")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@

@Entity
@NamedQuery(name = "AccessToken.deleteByUser", query = "DELETE FROM AccessToken a WHERE a.id.userId = :userId")
@NamedQuery(name = "AccessToken.get", query = """
SELECT token
FROM AccessToken token
INNER JOIN EffectiveVaultAccess perm ON token.id.vaultId = perm.id.vaultId AND token.id.userId = perm.id.authorityId
WHERE token.id.vaultId = :vaultId AND token.id.userId = :userId
""")
@Table(name = "access_token")
public class AccessToken extends PanacheEntityBase {

Expand All @@ -40,45 +46,8 @@ public class AccessToken extends PanacheEntityBase {
public String vaultKey;

public static AccessToken unlock(UUID vaultId, String userId) {
/*
* FIXME remove this native query and add the named query again as soon as Hibernate ORM ships version 6.2.8 or 6.3.0
* See https://github.com/quarkusio/quarkus/issues/35386 for further information
*/

try {
var query = getEntityManager()
.createNativeQuery("""
select
a1_0."user_id",
a1_0."vault_id",
u1_0."id",
u1_1."name",
u1_0."email",
u1_0."picture_url",
u1_0."privatekey",
u1_0."publickey",
u1_0."setupcode",
a1_0."vault_masterkey"
from
"user_details" u1_0
join
"authority" u1_1
on u1_0."id"=u1_1."id"
join
"effective_vault_access" e1_0
on u1_0."id"=e1_0."authority_id"
join
"access_token" a1_0
on u1_0."id"=a1_0."user_id"
and a1_0."vault_id"=:vaultId
and a1_0."user_id"=u1_0."id"
where
e1_0."vault_id"=:vaultId
and u1_0."id"=:userId
""", AccessToken.class)
.setParameter("vaultId", vaultId)
.setParameter("userId", userId);
return (AccessToken) query.getSingleResult();
return find("#AccessToken.get", Parameters.with("vaultId", vaultId).and("userId", userId)).firstResult();
} catch (NoResultException e) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,30 @@
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.NamedNativeQuery;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.NoResultException;
import jakarta.persistence.Table;

import java.io.Serializable;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Stream;

@Entity
@Table(name = "access_token_legacy")
@NamedNativeQuery(name = "LegacyAccessToken.get", resultClass = LegacyAccessToken.class, query = """
SELECT t.device_id, t.vault_id, t.jwe
FROM access_token_legacy t
INNER JOIN device_legacy d ON d.id = t.device_id
INNER JOIN effective_vault_access a ON a.vault_id = t.vault_id AND a.authority_id = d.owner_id
WHERE t.vault_id = :vaultId AND d.id = :deviceId AND d.owner_id = :userId
@NamedQuery(name = "LegacyAccessToken.get", query = """
SELECT token
FROM LegacyAccessToken token
INNER JOIN LegacyDevice device ON device.id = token.id.deviceId
INNER JOIN EffectiveVaultAccess perm ON token.id.vaultId = perm.id.vaultId AND device.ownerId = perm.id.authorityId
WHERE token.id.vaultId = :vaultId AND token.id.deviceId = :deviceId AND device.ownerId = :userId
""")
@NamedQuery(name = "LegacyAccessToken.getByDevice", query = """
SELECT token
FROM LegacyAccessToken token
INNER JOIN LegacyDevice device ON device.id = token.id.deviceId
INNER JOIN EffectiveVaultAccess perm ON token.id.vaultId = perm.id.vaultId AND device.ownerId = perm.id.authorityId
WHERE token.id.deviceId = :deviceId AND device.ownerId = :userId
""")
@Deprecated
public class LegacyAccessToken extends PanacheEntityBase {
Expand All @@ -43,6 +51,13 @@ public static LegacyAccessToken unlock(UUID vaultId, String deviceId, String use
}
}

public static Stream<LegacyAccessToken> getByDeviceAndOwner(String deviceId, String userId) {
return getEntityManager().createNamedQuery("LegacyAccessToken.getByDevice", LegacyAccessToken.class) //
.setParameter("deviceId", deviceId) //
.setParameter("userId", userId) //
.getResultStream();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@ public class LegacyDevice extends PanacheEntityBase {
@Column(name = "id", nullable = false)
public String id;

@Column(name = "owner_id", nullable = false)
public String ownerId;

// Further attributes omitted, as they are no longer used. The above ones are exceptions, as they are referenced via JPQL for joining.

}
24 changes: 23 additions & 1 deletion backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ hub.keycloak.oidc.cryptomator-client-id=cryptomator
%dev.quarkus.keycloak.devservices.realm-path=dev-realm.json
# TODO: realm-path needs to be in class path, i.e. under src/main/resources -> we might not want to include it in production jar though, so make use of maven profiles and specify optional resources https://github.com/quarkusio/quarkus-quickstarts/blob/f3f4939df30bcff062be126faaaeb58cb7c79fb6/security-keycloak-authorization-quickstart/pom.xml#L68-L75
%dev.quarkus.keycloak.devservices.realm-name=cryptomator
%dev.quarkus.keycloak.devservices.start-command=start-dev
%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.5
%dev.quarkus.keycloak.devservices.image-name=ghcr.io/cryptomator/keycloak:23.0.6
%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
Expand Down Expand Up @@ -89,6 +90,27 @@ quarkus.http.header."Cross-Origin-Opener-Policy".value=same-origin
quarkus.http.header."Cross-Origin-Resource-Policy".value=same-origin
quarkus.http.header."Content-Type".value=text/html

# Cache
# /app, /index.html and / for 1min in case hub gets updated
# /api never because the backend content can change at any time
# /assets "forever" (1 year) because those files are versioned
# /favicon.ico and /logo.svg for one day
quarkus.http.filter.app.header."Cache-Control"=private, max-age=60
quarkus.http.filter.app.methods=GET,HEAD
quarkus.http.filter.app.matches=/app/.*|/index.html|/

quarkus.http.filter.api.header."Cache-Control"=no-cache, no-store, must-revalidate
quarkus.http.filter.api.methods=GET,HEAD
quarkus.http.filter.api.matches=/api/.*

quarkus.http.filter.assets.header."Cache-Control"=max-age=31536000, immutable
quarkus.http.filter.assets.methods=GET,HEAD
quarkus.http.filter.assets.matches=/assets/.*

quarkus.http.filter.static.header."Cache-Control"=public, max-age=86400
quarkus.http.filter.static.methods=GET,HEAD
quarkus.http.filter.static.matches=/(favicon.ico|logo.svg)

# Container Image Adjustments
quarkus.container-image.registry=ghcr.io
quarkus.container-image.group=cryptomator
Expand Down
3 changes: 0 additions & 3 deletions backend/src/main/resources/dev-realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@
"email": "cli@localhost",
"enabled": true,
"serviceAccountClientId": "cryptomatorhub-cli",
"attributes": {
"picture": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJ5ZXMiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj4KICAgIDxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9ImJsYWNrIi8+CiAgICA8cGF0aCBzdHJva2Utd2lkdGg9IjUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0iTTMwIDM3LjUgbDE1IDEyLjUgbC0xNSAxMi41ICBtMjAgMCBoMjAiIC8+Cjwvc3ZnPgo="
},
"realmRoles": [
"user"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,42 @@ public void testGet1() {
.body("userPrivateKey", is("jwe.jwe.jwe.user1.device1"));
}

@Test
@Order(1)
@DisplayName("GET /devices/legacyDevice1/legacy-access-tokens returns 200")
public void testGetLegacyAccessTokens1() {
given().when().get("/devices/{deviceId}/legacy-access-tokens", "legacyDevice1")
.then().statusCode(200)
.body("7e57c0de-0000-4000-8000-000100001111", is("legacy.jwe.jwe.vault1.device1"));
}

@Test
@Order(1)
@DisplayName("GET /devices/legacyDevice2/legacy-access-tokens returns empty list (owned by different user)")
public void testGetLegacyAccessTokens2() {
given().when().get("/devices/{deviceId}/legacy-access-tokens", "legacyDevice2")
.then().statusCode(200)
.body(is("{}"));
}

@Test
@Order(1)
@DisplayName("GET /devices/legacyDevice3/legacy-access-tokens returns 200")
public void testGetLegacyAccessTokens3() {
given().when().get("/devices/{deviceId}/legacy-access-tokens", "legacyDevice3")
.then().statusCode(200)
.body("7e57c0de-0000-4000-8000-000100002222", is("legacy.jwe.jwe.vault2.device3"));
}

@Test
@Order(1)
@DisplayName("GET /devices/noSuchDevice/legacy-access-tokens returns empty list (no such device)")
public void testGetLegacyAccessTokens4() {
given().when().get("/devices/{deviceId}/legacy-access-tokens", "noSuchDevice")
.then().statusCode(200)
.body(is("{}"));
}

@Test
@Order(1)
@DisplayName("GET /devices/device2 returns 404 (owned by other user)")
Expand Down
Loading

0 comments on commit 84549cd

Please sign in to comment.