Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ipAddress and deviceId to vault key retrieve event and show last device access in user profile #320

Merged
merged 15 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- WoT: Admins can adjust WoT parameters (#297)
- Permission to create new vaults can now be controlled via the `create-vaults` role in Keycloak (#206)
- Preserver user locale setting (#313)
- New log event entries: UserAccountReset, UserKeysChange and UserSetupCodeChange (#310)
- Audit log filter by event type (#312)
- Show last IP address and last vault access timestamp of devices in user profile (#320)
- Italian, Korean, Dutch and Portuguese translation
- Audit log filter by event type

### Changed

Expand All @@ -32,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Switched to JWK thumbprint format in user profile
- Switched to Repository Pattern (#273)
- Redesigned Admin Panel (#308)
- Enhanced audit log VaultKeyRetrievedEvent, contains now IP address and device ID (#320)

### Fixed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ static AuditEventDto fromEntity(AuditEvent entity) {
case VaultCreatedEvent evt -> new VaultCreatedEventDto(evt.getId(), evt.getTimestamp(), VaultCreatedEvent.TYPE, evt.getCreatedBy(), evt.getVaultId(), evt.getVaultName(), evt.getVaultDescription());
case VaultUpdatedEvent evt -> new VaultUpdatedEventDto(evt.getId(), evt.getTimestamp(), VaultUpdatedEvent.TYPE, evt.getUpdatedBy(), evt.getVaultId(), evt.getVaultName(), evt.getVaultDescription(), evt.isVaultArchived());
case VaultAccessGrantedEvent evt -> new VaultAccessGrantedEventDto(evt.getId(), evt.getTimestamp(), VaultAccessGrantedEvent.TYPE, evt.getGrantedBy(), evt.getVaultId(), evt.getAuthorityId());
case VaultKeyRetrievedEvent evt -> new VaultKeyRetrievedEventDto(evt.getId(), evt.getTimestamp(), VaultKeyRetrievedEvent.TYPE, evt.getRetrievedBy(), evt.getVaultId(), evt.getResult());
case VaultKeyRetrievedEvent evt -> new VaultKeyRetrievedEventDto(evt.getId(), evt.getTimestamp(), VaultKeyRetrievedEvent.TYPE, evt.getRetrievedBy(), evt.getVaultId(), evt.getResult(), evt.getIpAddress(), evt.getDeviceId());
case VaultMemberAddedEvent evt -> new VaultMemberAddedEventDto(evt.getId(), evt.getTimestamp(), VaultMemberAddedEvent.TYPE, evt.getAddedBy(), evt.getVaultId(), evt.getAuthorityId(), evt.getRole());
case VaultMemberRemovedEvent evt -> new VaultMemberRemovedEventDto(evt.getId(), evt.getTimestamp(), VaultMemberRemovedEvent.TYPE, evt.getRemovedBy(), evt.getVaultId(), evt.getAuthorityId());
case VaultMemberUpdatedEvent evt -> new VaultMemberUpdatedEventDto(evt.getId(), evt.getTimestamp(), VaultMemberUpdatedEvent.TYPE, evt.getUpdatedBy(), evt.getVaultId(), evt.getAuthorityId(), evt.getRole());
Expand Down Expand Up @@ -177,7 +177,7 @@ record VaultAccessGrantedEventDto(long id, Instant timestamp, String type, @Json
}

record VaultKeyRetrievedEventDto(long id, Instant timestamp, String type, @JsonProperty("retrievedBy") String retrievedBy, @JsonProperty("vaultId") UUID vaultId,
@JsonProperty("result") VaultKeyRetrievedEvent.Result result) implements AuditEventDto {
@JsonProperty("result") VaultKeyRetrievedEvent.Result result, @JsonProperty("ipAddress") String ipAddress, @JsonProperty("deviceId") String deviceId) implements AuditEventDto {
}

record VaultMemberAddedEventDto(long id, Instant timestamp, String type, @JsonProperty("addedBy") String addedBy, @JsonProperty("vaultId") UUID vaultId, @JsonProperty("authorityId") String authorityId,
Expand Down
14 changes: 12 additions & 2 deletions backend/src/main/java/org/cryptomator/hub/api/DeviceResource.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.cryptomator.hub.api;

import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.annotation.Nullable;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.persistence.NoResultException;
Expand All @@ -25,6 +26,7 @@
import org.cryptomator.hub.entities.LegacyDevice;
import org.cryptomator.hub.entities.User;
import org.cryptomator.hub.entities.events.EventLogger;
import org.cryptomator.hub.entities.events.VaultKeyRetrievedEvent;
import org.cryptomator.hub.validation.NoHtmlOrScriptChars;
import org.cryptomator.hub.validation.OnlyBase64Chars;
import org.cryptomator.hub.validation.ValidId;
Expand Down Expand Up @@ -174,10 +176,18 @@ public record DeviceDto(@JsonProperty("id") @ValidId String id,
@JsonProperty("publicKey") @NotNull @OnlyBase64Chars String publicKey,
@JsonProperty("userPrivateKey") @NotNull @ValidJWE String userPrivateKeys, // singular name for history reasons (don't break client compatibility)
@JsonProperty("owner") @ValidId String ownerId,
@JsonProperty("creationTime") Instant creationTime) {
@JsonProperty("creationTime") Instant creationTime,
@JsonProperty("lastIpAddress") String lastIpAddress,
@JsonProperty("lastAccessTime") Instant lastAccessTime) {

public static DeviceDto fromEntity(Device entity) {
return new DeviceDto(entity.getId(), entity.getName(), entity.getType(), entity.getPublickey(), entity.getUserPrivateKeys(), entity.getOwner().getId(), entity.getCreationTime().truncatedTo(ChronoUnit.MILLIS));
return new DeviceDto(entity.getId(), entity.getName(), entity.getType(), entity.getPublickey(), entity.getUserPrivateKeys(), entity.getOwner().getId(), entity.getCreationTime().truncatedTo(ChronoUnit.MILLIS), null, null);
}

public static DeviceDto fromEntity(Device d, @Nullable VaultKeyRetrievedEvent event) {
var lastIpAddress = (event != null) ? event.getIpAddress() : null;
var lastAccessTime = (event != null) ? event.getTimestamp() : null;
return new DeviceResource.DeviceDto(d.getId(), d.getName(), d.getType(), d.getPublickey(), d.getUserPrivateKeys(), d.getOwner().getId(), d.getCreationTime().truncatedTo(ChronoUnit.MILLIS), lastIpAddress, lastAccessTime);
}

}
Expand Down
21 changes: 17 additions & 4 deletions backend/src/main/java/org/cryptomator/hub/api/UsersResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
import org.cryptomator.hub.entities.User;
import org.cryptomator.hub.entities.Vault;
import org.cryptomator.hub.entities.WotEntry;
import org.cryptomator.hub.entities.events.AuditEvent;
import org.cryptomator.hub.entities.events.EventLogger;
import org.cryptomator.hub.entities.events.VaultKeyRetrievedEvent;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
Expand Down Expand Up @@ -58,6 +60,8 @@ public class UsersResource {
WotEntry.Repository wotRepo;
@Inject
EffectiveWot.Repository effectiveWotRepo;
@Inject
AuditEvent.Repository auditEventRepo;

@Inject
JsonWebToken jwt;
Expand Down Expand Up @@ -156,11 +160,20 @@ public Response updateMyAccessTokens(@NotNull Map<UUID, String> tokens) {
@Operation(summary = "get the logged-in user")
@APIResponse(responseCode = "200", description = "returns the current user")
@APIResponse(responseCode = "404", description = "no user matching the subject of the JWT passed as Bearer Token")
public UserDto getMe(@QueryParam("withDevices") boolean withDevices) {
public UserDto getMe(@QueryParam("withDevices") boolean withDevices, @QueryParam("withLastAccess") boolean withLastAccess) {
User user = userRepo.findById(jwt.getSubject());
Function<Device, DeviceResource.DeviceDto> mapDevices = d -> new DeviceResource.DeviceDto(d.getId(), d.getName(), d.getType(), d.getPublickey(), d.getUserPrivateKeys(), d.getOwner().getId(), d.getCreationTime().truncatedTo(ChronoUnit.MILLIS));
var devices = withDevices ? user.devices.stream().map(mapDevices).collect(Collectors.toSet()) : Set.<DeviceResource.DeviceDto>of();
return new UserDto(user.getId(), user.getName(), user.getPictureUrl(), user.getEmail(), user.getLanguage(), devices, user.getEcdhPublicKey(), user.getEcdsaPublicKey(), user.getPrivateKeys(), user.getSetupCode());
Set<DeviceResource.DeviceDto> deviceDtos;
if (withLastAccess) {
var devices = user.devices.stream().collect(Collectors.toMap(Device::getId, Function.identity()));
var events = auditEventRepo.findLastVaultKeyRetrieve(devices.keySet()).collect(Collectors.toMap(VaultKeyRetrievedEvent::getDeviceId, Function.identity()));
deviceDtos = devices.values().stream().map(d -> {
var event = events.get(d.getId());
return DeviceResource.DeviceDto.fromEntity(d, event);
}).collect(Collectors.toSet());
} else {
deviceDtos = withDevices ? user.devices.stream().map(DeviceResource.DeviceDto::fromEntity).collect(Collectors.toSet()) : Set.of();
}
return new UserDto(user.getId(), user.getName(), user.getPictureUrl(), user.getEmail(), user.getLanguage(), deviceDtos, user.getEcdhPublicKey(), user.getEcdsaPublicKey(), user.getPrivateKeys(), user.getSetupCode());
}

@POST
Expand Down
20 changes: 13 additions & 7 deletions backend/src/main/java/org/cryptomator/hub/api/VaultResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.quarkus.security.identity.SecurityIdentity;
import io.vertx.core.http.HttpServerRequest;
import jakarta.annotation.Nullable;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
Expand All @@ -29,6 +30,7 @@
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.cryptomator.hub.entities.AccessToken;
Expand Down Expand Up @@ -96,6 +98,9 @@ public class VaultResource {
@Inject
LicenseHolder license;

@Context
HttpServerRequest request;

@GET
@Path("/accessible")
@RolesAllowed("user")
Expand Down Expand Up @@ -276,15 +281,15 @@ public Response legacyUnlock(@PathParam("vaultId") UUID vaultId, @PathParam("dev
if (accessTokenSeats > license.getSeats()) {
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
}

var ipAddress = request.remoteAddress().hostAddress();
try {
var access = legacyAccessTokenRepo.unlock(vaultId, deviceId, jwt.getSubject());
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.SUCCESS);
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.SUCCESS, ipAddress, deviceId);
var subscriptionStateHeaderName = "Hub-Subscription-State";
var subscriptionStateHeaderValue = license.isSet() ? "ACTIVE" : "INACTIVE"; // license expiration is not checked here, because it is checked in the ActiveLicense filter
return Response.ok(access.getJwe()).header(subscriptionStateHeaderName, subscriptionStateHeaderValue).build();
} catch (NoResultException e){
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.UNAUTHORIZED);
} catch (NoResultException e) {
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.UNAUTHORIZED, ipAddress, deviceId);
throw new ForbiddenException("Access to this device not granted.");
}
}
Expand Down Expand Up @@ -317,17 +322,18 @@ public Response unlock(@PathParam("vaultId") UUID vaultId, @QueryParam("evenIfAr
if (user.getEcdhPublicKey() == null) {
throw new ActionRequiredException("User account not initialized.");
}

var ipAddress = request.remoteAddress().hostAddress();
var deviceId = request.getHeader("Hub-Device-ID");
var access = accessTokenRepo.unlock(vaultId, jwt.getSubject());
if (access != null) {
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.SUCCESS);
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.SUCCESS, ipAddress, deviceId);
var subscriptionStateHeaderName = "Hub-Subscription-State";
var subscriptionStateHeaderValue = license.isSet() ? "ACTIVE" : "INACTIVE"; // license expiration is not checked here, because it is checked in the ActiveLicense filter
return Response.ok(access.getVaultKey(), MediaType.TEXT_PLAIN_TYPE).header(subscriptionStateHeaderName, subscriptionStateHeaderValue).build();
} else if (vaultRepo.findById(vaultId) == null) {
throw new NotFoundException("No such vault.");
} else {
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.UNAUTHORIZED);
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.UNAUTHORIZED, ipAddress, deviceId);
throw new ForbiddenException("Access to this vault not granted.");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;

@Entity
Expand All @@ -45,6 +46,17 @@
AND (:allTypes = true OR ae.type IN :types)
ORDER BY ae.id ASC
""")
@NamedQuery(name = "AuditEvent.lastVaultKeyRetrieve",
query = """
SELECT e1
FROM VaultKeyRetrievedEvent e1
WHERE e1.deviceId IN (:deviceIds)
AND e1.timestamp = (
SELECT MAX(e2.timestamp)
FROM VaultKeyRetrievedEvent e2
WHERE e2.deviceId = e1.deviceId
)
""")
@SequenceGenerator(name = "audit_event_id_seq", sequenceName = "audit_event_id_seq", allocationSize = 1)
public class AuditEvent {

Expand Down Expand Up @@ -127,5 +139,9 @@ public Stream<AuditEvent> findAllInPeriod(Instant startDate, Instant endDate, Li
query.page(0, pageSize);
return query.stream();
}

public Stream<VaultKeyRetrievedEvent> findLastVaultKeyRetrieve(Set<String> deviceIds) {
return find("#AuditEvent.lastVaultKeyRetrieve", Parameters.with("deviceIds", deviceIds)).stream();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,14 @@ public void logVaultAccessGranted(String grantedBy, UUID vaultId, String authori
auditEventRepository.persist(event);
}

public void logVaultKeyRetrieved(String retrievedBy, UUID vaultId, VaultKeyRetrievedEvent.Result result) {
public void logVaultKeyRetrieved(String retrievedBy, UUID vaultId, VaultKeyRetrievedEvent.Result result, String ipAddress, String deviceId) {
var event = new VaultKeyRetrievedEvent();
event.setTimestamp(Instant.now());
event.setRetrievedBy(retrievedBy);
event.setVaultId(vaultId);
event.setResult(result);
event.setIpAddress(ipAddress);
event.setDeviceId(deviceId);
auditEventRepository.persist(event);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ public class VaultKeyRetrievedEvent extends AuditEvent {
@Enumerated(EnumType.STRING)
private Result result;

@Column(name = "ip_address")
private String ipAddress;

@Column(name = "device_id")
private String deviceId;

public String getRetrievedBy() {
return retrievedBy;
}
Expand All @@ -51,25 +57,37 @@ public void setResult(Result result) {
this.result = result;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
VaultKeyRetrievedEvent that = (VaultKeyRetrievedEvent) o;
return super.equals(that) //
&& Objects.equals(retrievedBy, that.retrievedBy) //
&& Objects.equals(vaultId, that.vaultId) //
&& Objects.equals(result, that.result);
public String getIpAddress() {
return ipAddress;
}

@Override
public int hashCode() {
return Objects.hash(super.getId(), retrievedBy, vaultId, result);
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}

public String getDeviceId() {
return deviceId;
}

public void setDeviceId(String device) {
this.deviceId = device;
}

public enum Result {
SUCCESS,
UNAUTHORIZED
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
VaultKeyRetrievedEvent that = (VaultKeyRetrievedEvent) o;
return Objects.equals(retrievedBy, that.retrievedBy) && Objects.equals(vaultId, that.vaultId) && result == that.result && Objects.equals(ipAddress, that.ipAddress) && Objects.equals(deviceId, that.deviceId);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), retrievedBy, vaultId, result, ipAddress, deviceId);
}
}
2 changes: 2 additions & 0 deletions backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ 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

%test.quarkus.http.proxy.proxy-address-forwarding=true

# Cache
# /app, /index.html and / for 1min in case hub gets updated
# /api never because the backend content can change at any time
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "audit_event_vault_key_retrieve" ADD "ip_address" VARCHAR(46), ADD "device_id" VARCHAR(255) COLLATE "C";
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public void testCreateNoDeviceDto() {
@Order(1)
@DisplayName("PUT /devices/ with DTO returns 400")
public void testCreateNoDeviceId() {
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"));
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"), null, null);
given().contentType(ContentType.JSON).body(deviceDto)
.when().put("/devices/{deviceId}", " ") //a whitespace
.then().statusCode(400);
Expand Down Expand Up @@ -142,7 +142,7 @@ public void testCreate999() throws SQLException {
""");
}

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"));
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"), null, null);

given().contentType(ContentType.JSON).body(deviceDto)
.when().put("/devices/{deviceId}", "device999")
Expand All @@ -160,7 +160,7 @@ public void testCreate999() throws SQLException {
@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", "jwe.jwe.jwe.user1.deviceX", "user1", 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"), null, null);

given().contentType(ContentType.JSON).body(deviceDto)
.when().put("/devices/{deviceId}", "deviceX")
Expand All @@ -171,7 +171,7 @@ public void testCreateX() {
@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"));
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"), null, null);

given().contentType(ContentType.JSON).body(deviceDto)
.when().put("/devices/{deviceId}", "deviceY")
Expand All @@ -192,7 +192,7 @@ public void testGet999AfterCreate() {
@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"));
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"), null, null);

given().contentType(ContentType.JSON).body(deviceDto)
.when().put("/devices/{deviceId}", "device999")
Expand Down Expand Up @@ -251,7 +251,7 @@ public class AsAnonymous {
@Test
@DisplayName("PUT /devices/device1 returns 401")
public void testCreate1() {
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"));
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"), null, null);

given().contentType(ContentType.JSON).body(deviceDto)
.when().put("/devices/{deviceId}", "device1")
Expand Down
Loading