Skip to content

Commit 84549cd

Browse files
committed
Merge branch 'release/1.3.2'
2 parents 03cd1af + 46dded1 commit 84549cd

18 files changed

+1407
-720
lines changed

backend/pom.xml

+15-3
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
<modelVersion>4.0.0</modelVersion>
55
<groupId>org.cryptomator</groupId>
66
<artifactId>hub-backend</artifactId>
7-
<version>1.3.1</version>
7+
<version>1.3.2</version>
88

99
<properties>
10-
<compiler-plugin.version>3.11.0 </compiler-plugin.version>
10+
<compiler-plugin.version>3.11.0</compiler-plugin.version>
1111
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1212
<project.jdk.version>17</project.jdk.version>
1313
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
1414
<quarkus.container-image.group>cryptomator</quarkus.container-image.group>
1515
<quarkus.container-image.name>hub</quarkus.container-image.name>
16-
<quarkus.platform.version>3.4.3</quarkus.platform.version>
16+
<quarkus.platform.version>3.2.10.Final</quarkus.platform.version>
1717
<quarkus.jib.base-jvm-image>eclipse-temurin:17-jre</quarkus.jib.base-jvm-image> <!-- irrelevant for -Pnative -->
1818
<jwt.version>4.4.0</jwt.version>
1919
<surefire-plugin.version>3.1.2</surefire-plugin.version>
@@ -28,6 +28,18 @@
2828
<type>pom</type>
2929
<scope>import</scope>
3030
</dependency>
31+
<dependency>
32+
<!-- temporarily pin Flyway version until Quarkus LTS contains Flyway >= 9.21.0; see https://github.com/cryptomator/hub/issues/256 -->
33+
<groupId>org.flywaydb</groupId>
34+
<artifactId>flyway-core</artifactId>
35+
<version>9.22.3</version>
36+
</dependency>
37+
<dependency>
38+
<!-- temporarily pin Flyway version until Quarkus LTS contains Flyway >= 9.21.0; see https://github.com/cryptomator/hub/issues/256 -->
39+
<groupId>io.quarkus</groupId>
40+
<artifactId>quarkus-flyway</artifactId>
41+
<version>3.3.0</version>
42+
</dependency>
3143
</dependencies>
3244
</dependencyManagement>
3345

backend/src/main/java/org/cryptomator/hub/api/DeviceResource.java

+20-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
import jakarta.ws.rs.core.Response;
2323
import org.cryptomator.hub.entities.AuditEventDeviceRegister;
2424
import org.cryptomator.hub.entities.AuditEventDeviceRemove;
25-
import org.cryptomator.hub.entities.AuditEventVaultAccessGrant;
2625
import org.cryptomator.hub.entities.Device;
26+
import org.cryptomator.hub.entities.LegacyAccessToken;
2727
import org.cryptomator.hub.entities.LegacyDevice;
2828
import org.cryptomator.hub.entities.User;
2929
import org.cryptomator.hub.validation.NoHtmlOrScriptChars;
@@ -41,6 +41,9 @@
4141
import java.time.Instant;
4242
import java.time.temporal.ChronoUnit;
4343
import java.util.List;
44+
import java.util.Map;
45+
import java.util.UUID;
46+
import java.util.stream.Collectors;
4447

4548
@Path("/devices")
4649
public class DeviceResource {
@@ -117,6 +120,20 @@ public DeviceDto get(@PathParam("deviceId") @ValidId String deviceId) {
117120
}
118121
}
119122

123+
@Deprecated
124+
@GET
125+
@Path("/{deviceId}/legacy-access-tokens")
126+
@RolesAllowed("user")
127+
@Produces(MediaType.APPLICATION_JSON)
128+
@NoCache
129+
@Transactional
130+
@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")
131+
@APIResponse(responseCode = "200")
132+
public Map<UUID, String> getLegacyAccessTokens(@PathParam("deviceId") @ValidId String deviceId) {
133+
return LegacyAccessToken.getByDeviceAndOwner(deviceId, jwt.getSubject())
134+
.collect(Collectors.toMap(token -> token.id.vaultId , token -> token.jwe));
135+
}
136+
120137
@DELETE
121138
@Path("/{deviceId}")
122139
@RolesAllowed("user")
@@ -154,4 +171,6 @@ public static DeviceDto fromEntity(Device entity) {
154171
}
155172

156173
}
174+
175+
public record LegacyAccessTokenDto(@JsonProperty("vaultId") UUID vaultId, @JsonProperty("token") String token) {}
157176
}

backend/src/main/java/org/cryptomator/hub/api/UsersResource.java

+33
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import jakarta.inject.Inject;
66
import jakarta.transaction.Transactional;
77
import jakarta.validation.Valid;
8+
import jakarta.validation.constraints.NotEmpty;
9+
import jakarta.validation.constraints.NotNull;
810
import jakarta.ws.rs.Consumes;
911
import jakarta.ws.rs.GET;
1012
import jakarta.ws.rs.POST;
@@ -15,8 +17,10 @@
1517
import jakarta.ws.rs.core.MediaType;
1618
import jakarta.ws.rs.core.Response;
1719
import org.cryptomator.hub.entities.AccessToken;
20+
import org.cryptomator.hub.entities.AuditEventVaultAccessGrant;
1821
import org.cryptomator.hub.entities.Device;
1922
import org.cryptomator.hub.entities.User;
23+
import org.cryptomator.hub.entities.Vault;
2024
import org.eclipse.microprofile.jwt.JsonWebToken;
2125
import org.eclipse.microprofile.openapi.annotations.Operation;
2226
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
@@ -25,7 +29,9 @@
2529
import java.net.URI;
2630
import java.time.temporal.ChronoUnit;
2731
import java.util.List;
32+
import java.util.Map;
2833
import java.util.Set;
34+
import java.util.UUID;
2935
import java.util.function.Function;
3036
import java.util.stream.Collectors;
3137

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

71+
@POST
72+
@Path("/me/access-tokens")
73+
@RolesAllowed("user")
74+
@Transactional
75+
@Consumes(MediaType.APPLICATION_JSON)
76+
@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, ...}).")
77+
@APIResponse(responseCode = "200", description = "all keys stored")
78+
public Response updateMyAccessTokens(@NotNull Map<UUID, String> tokens) {
79+
var user = User.<User>findById(jwt.getSubject());
80+
for (var entry : tokens.entrySet()) {
81+
var vault = Vault.<Vault>findById(entry.getKey());
82+
if (vault == null) {
83+
continue; // skip
84+
}
85+
var token = AccessToken.<AccessToken>findById(new AccessToken.AccessId(user.id, vault.id));
86+
if (token == null) {
87+
token = new AccessToken();
88+
token.vault = vault;
89+
token.user = user;
90+
}
91+
token.vaultKey = entry.getValue();
92+
token.persist();
93+
AuditEventVaultAccessGrant.log(user.id, vault.id, user.id);
94+
}
95+
return Response.ok().build();
96+
}
97+
6598
@GET
6699
@Path("/me")
67100
@RolesAllowed("user")

backend/src/main/java/org/cryptomator/hub/entities/AccessToken.java

+7-38
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020

2121
@Entity
2222
@NamedQuery(name = "AccessToken.deleteByUser", query = "DELETE FROM AccessToken a WHERE a.id.userId = :userId")
23+
@NamedQuery(name = "AccessToken.get", query = """
24+
SELECT token
25+
FROM AccessToken token
26+
INNER JOIN EffectiveVaultAccess perm ON token.id.vaultId = perm.id.vaultId AND token.id.userId = perm.id.authorityId
27+
WHERE token.id.vaultId = :vaultId AND token.id.userId = :userId
28+
""")
2329
@Table(name = "access_token")
2430
public class AccessToken extends PanacheEntityBase {
2531

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

4248
public static AccessToken unlock(UUID vaultId, String userId) {
43-
/*
44-
* 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
45-
* See https://github.com/quarkusio/quarkus/issues/35386 for further information
46-
*/
47-
4849
try {
49-
var query = getEntityManager()
50-
.createNativeQuery("""
51-
select
52-
a1_0."user_id",
53-
a1_0."vault_id",
54-
u1_0."id",
55-
u1_1."name",
56-
u1_0."email",
57-
u1_0."picture_url",
58-
u1_0."privatekey",
59-
u1_0."publickey",
60-
u1_0."setupcode",
61-
a1_0."vault_masterkey"
62-
from
63-
"user_details" u1_0
64-
join
65-
"authority" u1_1
66-
on u1_0."id"=u1_1."id"
67-
join
68-
"effective_vault_access" e1_0
69-
on u1_0."id"=e1_0."authority_id"
70-
join
71-
"access_token" a1_0
72-
on u1_0."id"=a1_0."user_id"
73-
and a1_0."vault_id"=:vaultId
74-
and a1_0."user_id"=u1_0."id"
75-
where
76-
e1_0."vault_id"=:vaultId
77-
and u1_0."id"=:userId
78-
""", AccessToken.class)
79-
.setParameter("vaultId", vaultId)
80-
.setParameter("userId", userId);
81-
return (AccessToken) query.getSingleResult();
50+
return find("#AccessToken.get", Parameters.with("vaultId", vaultId).and("userId", userId)).firstResult();
8251
} catch (NoResultException e) {
8352
return null;
8453
}

backend/src/main/java/org/cryptomator/hub/entities/LegacyAccessToken.java

+22-7
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,30 @@
55
import jakarta.persistence.Embeddable;
66
import jakarta.persistence.EmbeddedId;
77
import jakarta.persistence.Entity;
8-
import jakarta.persistence.NamedNativeQuery;
8+
import jakarta.persistence.NamedQuery;
99
import jakarta.persistence.NoResultException;
1010
import jakarta.persistence.Table;
1111

1212
import java.io.Serializable;
1313
import java.util.Objects;
1414
import java.util.UUID;
15+
import java.util.stream.Stream;
1516

1617
@Entity
1718
@Table(name = "access_token_legacy")
18-
@NamedNativeQuery(name = "LegacyAccessToken.get", resultClass = LegacyAccessToken.class, query = """
19-
SELECT t.device_id, t.vault_id, t.jwe
20-
FROM access_token_legacy t
21-
INNER JOIN device_legacy d ON d.id = t.device_id
22-
INNER JOIN effective_vault_access a ON a.vault_id = t.vault_id AND a.authority_id = d.owner_id
23-
WHERE t.vault_id = :vaultId AND d.id = :deviceId AND d.owner_id = :userId
19+
@NamedQuery(name = "LegacyAccessToken.get", query = """
20+
SELECT token
21+
FROM LegacyAccessToken token
22+
INNER JOIN LegacyDevice device ON device.id = token.id.deviceId
23+
INNER JOIN EffectiveVaultAccess perm ON token.id.vaultId = perm.id.vaultId AND device.ownerId = perm.id.authorityId
24+
WHERE token.id.vaultId = :vaultId AND token.id.deviceId = :deviceId AND device.ownerId = :userId
25+
""")
26+
@NamedQuery(name = "LegacyAccessToken.getByDevice", query = """
27+
SELECT token
28+
FROM LegacyAccessToken token
29+
INNER JOIN LegacyDevice device ON device.id = token.id.deviceId
30+
INNER JOIN EffectiveVaultAccess perm ON token.id.vaultId = perm.id.vaultId AND device.ownerId = perm.id.authorityId
31+
WHERE token.id.deviceId = :deviceId AND device.ownerId = :userId
2432
""")
2533
@Deprecated
2634
public class LegacyAccessToken extends PanacheEntityBase {
@@ -43,6 +51,13 @@ public static LegacyAccessToken unlock(UUID vaultId, String deviceId, String use
4351
}
4452
}
4553

54+
public static Stream<LegacyAccessToken> getByDeviceAndOwner(String deviceId, String userId) {
55+
return getEntityManager().createNamedQuery("LegacyAccessToken.getByDevice", LegacyAccessToken.class) //
56+
.setParameter("deviceId", deviceId) //
57+
.setParameter("userId", userId) //
58+
.getResultStream();
59+
}
60+
4661
@Override
4762
public boolean equals(Object o) {
4863
if (this == o) return true;

backend/src/main/java/org/cryptomator/hub/entities/LegacyDevice.java

+5
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,9 @@ public class LegacyDevice extends PanacheEntityBase {
1515
@Column(name = "id", nullable = false)
1616
public String id;
1717

18+
@Column(name = "owner_id", nullable = false)
19+
public String ownerId;
20+
21+
// Further attributes omitted, as they are no longer used. The above ones are exceptions, as they are referenced via JPQL for joining.
22+
1823
}

backend/src/main/resources/application.properties

+23-1
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ hub.keycloak.oidc.cryptomator-client-id=cryptomator
3030
%dev.quarkus.keycloak.devservices.realm-path=dev-realm.json
3131
# 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
3232
%dev.quarkus.keycloak.devservices.realm-name=cryptomator
33+
%dev.quarkus.keycloak.devservices.start-command=start-dev
3334
%dev.quarkus.keycloak.devservices.port=8180
3435
%dev.quarkus.keycloak.devservices.service-name=quarkus-cryptomator-hub
35-
%dev.quarkus.keycloak.devservices.image-name=ghcr.io/cryptomator/keycloak:22.0.5
36+
%dev.quarkus.keycloak.devservices.image-name=ghcr.io/cryptomator/keycloak:23.0.6
3637
%dev.quarkus.oidc.devui.grant.type=code
3738
# OIDC will be mocked during unit tests. Use fake auth url to prevent dev services to start:
3839
%test.quarkus.oidc.auth-server-url=http://localhost:43210/dev/null
@@ -89,6 +90,27 @@ quarkus.http.header."Cross-Origin-Opener-Policy".value=same-origin
8990
quarkus.http.header."Cross-Origin-Resource-Policy".value=same-origin
9091
quarkus.http.header."Content-Type".value=text/html
9192

93+
# Cache
94+
# /app, /index.html and / for 1min in case hub gets updated
95+
# /api never because the backend content can change at any time
96+
# /assets "forever" (1 year) because those files are versioned
97+
# /favicon.ico and /logo.svg for one day
98+
quarkus.http.filter.app.header."Cache-Control"=private, max-age=60
99+
quarkus.http.filter.app.methods=GET,HEAD
100+
quarkus.http.filter.app.matches=/app/.*|/index.html|/
101+
102+
quarkus.http.filter.api.header."Cache-Control"=no-cache, no-store, must-revalidate
103+
quarkus.http.filter.api.methods=GET,HEAD
104+
quarkus.http.filter.api.matches=/api/.*
105+
106+
quarkus.http.filter.assets.header."Cache-Control"=max-age=31536000, immutable
107+
quarkus.http.filter.assets.methods=GET,HEAD
108+
quarkus.http.filter.assets.matches=/assets/.*
109+
110+
quarkus.http.filter.static.header."Cache-Control"=public, max-age=86400
111+
quarkus.http.filter.static.methods=GET,HEAD
112+
quarkus.http.filter.static.matches=/(favicon.ico|logo.svg)
113+
92114
# Container Image Adjustments
93115
quarkus.container-image.registry=ghcr.io
94116
quarkus.container-image.group=cryptomator

backend/src/main/resources/dev-realm.json

-3
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,6 @@
8686
"email": "cli@localhost",
8787
"enabled": true,
8888
"serviceAccountClientId": "cryptomatorhub-cli",
89-
"attributes": {
90-
"picture": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJ5ZXMiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj4KICAgIDxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9ImJsYWNrIi8+CiAgICA8cGF0aCBzdHJva2Utd2lkdGg9IjUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0iTTMwIDM3LjUgbDE1IDEyLjUgbC0xNSAxMi41ICBtMjAgMCBoMjAiIC8+Cjwvc3ZnPgo="
91-
},
9289
"realmRoles": [
9390
"user"
9491
],

backend/src/test/java/org/cryptomator/hub/api/DeviceResourceTest.java

+36
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,42 @@ public void testGet1() {
7878
.body("userPrivateKey", is("jwe.jwe.jwe.user1.device1"));
7979
}
8080

81+
@Test
82+
@Order(1)
83+
@DisplayName("GET /devices/legacyDevice1/legacy-access-tokens returns 200")
84+
public void testGetLegacyAccessTokens1() {
85+
given().when().get("/devices/{deviceId}/legacy-access-tokens", "legacyDevice1")
86+
.then().statusCode(200)
87+
.body("7e57c0de-0000-4000-8000-000100001111", is("legacy.jwe.jwe.vault1.device1"));
88+
}
89+
90+
@Test
91+
@Order(1)
92+
@DisplayName("GET /devices/legacyDevice2/legacy-access-tokens returns empty list (owned by different user)")
93+
public void testGetLegacyAccessTokens2() {
94+
given().when().get("/devices/{deviceId}/legacy-access-tokens", "legacyDevice2")
95+
.then().statusCode(200)
96+
.body(is("{}"));
97+
}
98+
99+
@Test
100+
@Order(1)
101+
@DisplayName("GET /devices/legacyDevice3/legacy-access-tokens returns 200")
102+
public void testGetLegacyAccessTokens3() {
103+
given().when().get("/devices/{deviceId}/legacy-access-tokens", "legacyDevice3")
104+
.then().statusCode(200)
105+
.body("7e57c0de-0000-4000-8000-000100002222", is("legacy.jwe.jwe.vault2.device3"));
106+
}
107+
108+
@Test
109+
@Order(1)
110+
@DisplayName("GET /devices/noSuchDevice/legacy-access-tokens returns empty list (no such device)")
111+
public void testGetLegacyAccessTokens4() {
112+
given().when().get("/devices/{deviceId}/legacy-access-tokens", "noSuchDevice")
113+
.then().statusCode(200)
114+
.body(is("{}"));
115+
}
116+
81117
@Test
82118
@Order(1)
83119
@DisplayName("GET /devices/device2 returns 404 (owned by other user)")

0 commit comments

Comments
 (0)