Skip to content

Commit

Permalink
Merge pull request #252 from cryptomator/feature/legacy-device-migrat…
Browse files Browse the repository at this point in the history
…ion-api

Add new API for Legacy Device Migration
  • Loading branch information
overheadhunter authored Jan 26, 2024
2 parents 94ebb6b + 58ff0d6 commit 3dc07d3
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 4 deletions.
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 @@ -12,6 +12,7 @@
import java.io.Serializable;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Stream;

@Entity
@Table(name = "access_token_legacy")
Expand All @@ -22,6 +23,13 @@
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
""")
@NamedNativeQuery(name = "LegacyAccessToken.getByDevice", 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 d.id = :deviceId AND d.owner_id = :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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,19 @@
import io.quarkus.test.security.oidc.Claim;
import io.quarkus.test.security.oidc.OidcSecurity;
import io.restassured.RestAssured;
import org.hamcrest.CoreMatchers;
import org.hamcrest.text.IsEqualIgnoringCase;
import io.restassured.http.ContentType;
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 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;
import static org.hamcrest.text.IsEqualIgnoringCase.equalToIgnoringCase;

@QuarkusTest
@DisplayName("Resource /users")
Expand Down Expand Up @@ -70,6 +69,36 @@ public void testGetAll() {
.body("id", hasItems("user1", "user2"));
}

@Test
@DisplayName("POST /users/me/access-tokens returns 200")
public void testPostAccessTokens1() {
var body = """
{
"7E57C0DE-0000-4000-8000-000100001111": "jwe.jwe.jwe.vault1.user1",
"7E57C0DE-0000-4000-8000-BADBADBADBAD": "noSuchVault"
},
""";
given().contentType(ContentType.JSON).body(body)
.when().post("/users/me/access-tokens")
.then().statusCode(200);
}

@Test
@DisplayName("POST /users/me/access-tokens returns 200 for empty list")
public void testPostAccessTokens2() {
given().contentType(ContentType.JSON).body("{}")
.when().post("/users/me/access-tokens")
.then().statusCode(200);
}

@Test
@DisplayName("POST /users/me/access-tokens returns 400 for malformed body")
public void testPostAccessTokens3() {
given().contentType(ContentType.JSON).body("")
.when().post("/users/me/access-tokens")
.then().statusCode(400);
}

}

@Nested
Expand Down

0 comments on commit 3dc07d3

Please sign in to comment.