Skip to content

Commit 69c1390

Browse files
SailRealiammajid
andcommitted
Add ipAddress and deviceId to vault key retrieve event and prepare to
show the last device access in user profile Co-authored-by: iammajid <[email protected]>
1 parent afc7a17 commit 69c1390

File tree

10 files changed

+102
-25
lines changed

10 files changed

+102
-25
lines changed

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,22 @@ public List<AuditEventDto> getAllEvents(@QueryParam("startDate") Instant startDa
8181
return auditEventRepo.findAllInPeriod(startDate, endDate, paginationId, order.equals("asc"), pageSize).map(AuditEventDto::fromEntity).toList();
8282
}
8383

84+
@GET
85+
@Path("/last-vault-key-retrieve")
86+
@RolesAllowed("admin")
87+
@Produces(MediaType.APPLICATION_JSON)
88+
@Operation(summary = "list last vault key retrieve auditlog entry by device id", description = "list last vault key retrieve auditlog entry by device id")
89+
@Parameter(name = "deviceIds", description = "list of deviceIds", in = ParameterIn.QUERY)
90+
@APIResponse(responseCode = "200", description = "Body contains list of events")
91+
@APIResponse(responseCode = "402", description = "Community license used or license expired")
92+
@APIResponse(responseCode = "403", description = "requesting user does not have admin role")
93+
public List<AuditEventDto> lastVaultKeyRetrieve(@QueryParam("deviceIds") List<String> deviceIds) {
94+
if (!license.isSet() || license.isExpired()) {
95+
throw new PaymentRequiredException("Community license used or license expired");
96+
}
97+
return auditEventRepo.findLastVaultKeyRetrieve(deviceIds).map(AuditEventDto::fromEntity).toList();
98+
}
99+
84100
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
85101
@JsonSubTypes({ //
86102
@JsonSubTypes.Type(value = DeviceRegisteredEventDto.class, name = DeviceRegisteredEvent.TYPE), //
@@ -119,7 +135,7 @@ static AuditEventDto fromEntity(AuditEvent entity) {
119135
case VaultCreatedEvent evt -> new VaultCreatedEventDto(evt.getId(), evt.getTimestamp(), VaultCreatedEvent.TYPE, evt.getCreatedBy(), evt.getVaultId(), evt.getVaultName(), evt.getVaultDescription());
120136
case VaultUpdatedEvent evt -> new VaultUpdatedEventDto(evt.getId(), evt.getTimestamp(), VaultUpdatedEvent.TYPE, evt.getUpdatedBy(), evt.getVaultId(), evt.getVaultName(), evt.getVaultDescription(), evt.isVaultArchived());
121137
case VaultAccessGrantedEvent evt -> new VaultAccessGrantedEventDto(evt.getId(), evt.getTimestamp(), VaultAccessGrantedEvent.TYPE, evt.getGrantedBy(), evt.getVaultId(), evt.getAuthorityId());
122-
case VaultKeyRetrievedEvent evt -> new VaultKeyRetrievedEventDto(evt.getId(), evt.getTimestamp(), VaultKeyRetrievedEvent.TYPE, evt.getRetrievedBy(), evt.getVaultId(), evt.getResult());
138+
case VaultKeyRetrievedEvent evt -> new VaultKeyRetrievedEventDto(evt.getId(), evt.getTimestamp(), VaultKeyRetrievedEvent.TYPE, evt.getRetrievedBy(), evt.getVaultId(), evt.getResult(), evt.getIpAddress(), evt.getDeviceId());
123139
case VaultMemberAddedEvent evt -> new VaultMemberAddedEventDto(evt.getId(), evt.getTimestamp(), VaultMemberAddedEvent.TYPE, evt.getAddedBy(), evt.getVaultId(), evt.getAuthorityId(), evt.getRole());
124140
case VaultMemberRemovedEvent evt -> new VaultMemberRemovedEventDto(evt.getId(), evt.getTimestamp(), VaultMemberRemovedEvent.TYPE, evt.getRemovedBy(), evt.getVaultId(), evt.getAuthorityId());
125141
case VaultMemberUpdatedEvent evt -> new VaultMemberUpdatedEventDto(evt.getId(), evt.getTimestamp(), VaultMemberUpdatedEvent.TYPE, evt.getUpdatedBy(), evt.getVaultId(), evt.getAuthorityId(), evt.getRole());
@@ -166,7 +182,7 @@ record VaultAccessGrantedEventDto(long id, Instant timestamp, String type, @Json
166182
}
167183

168184
record VaultKeyRetrievedEventDto(long id, Instant timestamp, String type, @JsonProperty("retrievedBy") String retrievedBy, @JsonProperty("vaultId") UUID vaultId,
169-
@JsonProperty("result") VaultKeyRetrievedEvent.Result result) implements AuditEventDto {
185+
@JsonProperty("result") VaultKeyRetrievedEvent.Result result, @JsonProperty("ipAddress") String ipAddress, @JsonProperty("deviceId") String deviceId) implements AuditEventDto {
170186
}
171187

172188
record VaultMemberAddedEventDto(long id, Instant timestamp, String type, @JsonProperty("addedBy") String addedBy, @JsonProperty("vaultId") UUID vaultId, @JsonProperty("authorityId") String authorityId,

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.auth0.jwt.exceptions.JWTVerificationException;
66
import com.fasterxml.jackson.annotation.JsonProperty;
77
import io.quarkus.security.identity.SecurityIdentity;
8+
import io.vertx.core.http.HttpServerRequest;
89
import jakarta.annotation.Nullable;
910
import jakarta.annotation.security.RolesAllowed;
1011
import jakarta.inject.Inject;
@@ -29,6 +30,7 @@
2930
import jakarta.ws.rs.PathParam;
3031
import jakarta.ws.rs.Produces;
3132
import jakarta.ws.rs.QueryParam;
33+
import jakarta.ws.rs.core.Context;
3234
import jakarta.ws.rs.core.MediaType;
3335
import jakarta.ws.rs.core.Response;
3436
import org.cryptomator.hub.entities.AccessToken;
@@ -96,6 +98,9 @@ public class VaultResource {
9698
@Inject
9799
LicenseHolder license;
98100

101+
@Context
102+
HttpServerRequest request;
103+
99104
@GET
100105
@Path("/accessible")
101106
@RolesAllowed("user")
@@ -276,15 +281,15 @@ public Response legacyUnlock(@PathParam("vaultId") UUID vaultId, @PathParam("dev
276281
if (accessTokenSeats > license.getSeats()) {
277282
throw new PaymentRequiredException("Number of effective vault users exceeds available license seats");
278283
}
279-
284+
var ipAddress = request.remoteAddress().toString();
280285
try {
281286
var access = legacyAccessTokenRepo.unlock(vaultId, deviceId, jwt.getSubject());
282-
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.SUCCESS);
287+
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.SUCCESS, ipAddress, deviceId);
283288
var subscriptionStateHeaderName = "Hub-Subscription-State";
284289
var subscriptionStateHeaderValue = license.isSet() ? "ACTIVE" : "INACTIVE"; // license expiration is not checked here, because it is checked in the ActiveLicense filter
285290
return Response.ok(access.getJwe()).header(subscriptionStateHeaderName, subscriptionStateHeaderValue).build();
286-
} catch (NoResultException e){
287-
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.UNAUTHORIZED);
291+
} catch (NoResultException e) {
292+
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.UNAUTHORIZED, ipAddress, deviceId);
288293
throw new ForbiddenException("Access to this device not granted.");
289294
}
290295
}
@@ -317,17 +322,18 @@ public Response unlock(@PathParam("vaultId") UUID vaultId, @QueryParam("evenIfAr
317322
if (user.getEcdhPublicKey() == null) {
318323
throw new ActionRequiredException("User account not initialized.");
319324
}
320-
325+
var ipAddress = request.remoteAddress().host();
326+
var deviceId = request.getHeader("deviceId");
321327
var access = accessTokenRepo.unlock(vaultId, jwt.getSubject());
322328
if (access != null) {
323-
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.SUCCESS);
329+
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.SUCCESS, ipAddress, deviceId);
324330
var subscriptionStateHeaderName = "Hub-Subscription-State";
325331
var subscriptionStateHeaderValue = license.isSet() ? "ACTIVE" : "INACTIVE"; // license expiration is not checked here, because it is checked in the ActiveLicense filter
326332
return Response.ok(access.getVaultKey(), MediaType.TEXT_PLAIN_TYPE).header(subscriptionStateHeaderName, subscriptionStateHeaderValue).build();
327333
} else if (vaultRepo.findById(vaultId) == null) {
328334
throw new NotFoundException("No such vault.");
329335
} else {
330-
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.UNAUTHORIZED);
336+
eventLogger.logVaultKeyRetrieved(jwt.getSubject(), vaultId, VaultKeyRetrievedEvent.Result.UNAUTHORIZED, ipAddress, deviceId);
331337
throw new ForbiddenException("Access to this vault not granted.");
332338
}
333339
}

backend/src/main/java/org/cryptomator/hub/entities/events/AuditEvent.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import jakarta.persistence.Table;
1818

1919
import java.time.Instant;
20+
import java.util.List;
2021
import java.util.Objects;
2122
import java.util.stream.Stream;
2223

@@ -42,6 +43,17 @@
4243
AND ae.id > :paginationId
4344
ORDER BY ae.id ASC
4445
""")
46+
@NamedQuery(name = "AuditEvent.lastVaultKeyRetrieve",
47+
query = """
48+
SELECT v1
49+
FROM VaultKeyRetrievedEvent v1
50+
WHERE v1.deviceId IN (:deviceIds)
51+
AND v1.timestamp = (
52+
SELECT MAX(v2.timestamp)
53+
FROM VaultKeyRetrievedEvent v2
54+
WHERE v2.deviceId = v1.deviceId
55+
)
56+
""")
4557
@SequenceGenerator(name = "audit_event_id_seq", sequenceName = "audit_event_id_seq", allocationSize = 1)
4658
public class AuditEvent {
4759

@@ -118,5 +130,9 @@ public Stream<AuditEvent> findAllInPeriod(Instant startDate, Instant endDate, lo
118130
query.page(0, pageSize);
119131
return query.stream();
120132
}
133+
134+
public Stream<AuditEvent> findLastVaultKeyRetrieve(List<String> deviceIds) {
135+
return find("#AuditEvent.lastVaultKeyRetrieve", Parameters.with("deviceIds", deviceIds)).stream();
136+
}
121137
}
122138
}

backend/src/main/java/org/cryptomator/hub/entities/events/EventLogger.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,14 @@ public void logVaultAccessGranted(String grantedBy, UUID vaultId, String authori
8484
auditEventRepository.persist(event);
8585
}
8686

87-
public void logVaultKeyRetrieved(String retrievedBy, UUID vaultId, VaultKeyRetrievedEvent.Result result) {
87+
public void logVaultKeyRetrieved(String retrievedBy, UUID vaultId, VaultKeyRetrievedEvent.Result result, String ipAddress, String deviceId) {
8888
var event = new VaultKeyRetrievedEvent();
8989
event.setTimestamp(Instant.now());
9090
event.setRetrievedBy(retrievedBy);
9191
event.setVaultId(vaultId);
9292
event.setResult(result);
93+
event.setIpAddress(ipAddress);
94+
event.setDeviceId(deviceId);
9395
auditEventRepository.persist(event);
9496
}
9597

backend/src/main/java/org/cryptomator/hub/entities/events/VaultKeyRetrievedEvent.java

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ public class VaultKeyRetrievedEvent extends AuditEvent {
2727
@Enumerated(EnumType.STRING)
2828
private Result result;
2929

30+
@Column(name = "ip_address")
31+
private String ipAddress;
32+
33+
@Column(name = "device_id")
34+
private String deviceId;
35+
3036
public String getRetrievedBy() {
3137
return retrievedBy;
3238
}
@@ -51,25 +57,37 @@ public void setResult(Result result) {
5157
this.result = result;
5258
}
5359

54-
@Override
55-
public boolean equals(Object o) {
56-
if (this == o) return true;
57-
if (o == null || getClass() != o.getClass()) return false;
58-
VaultKeyRetrievedEvent that = (VaultKeyRetrievedEvent) o;
59-
return super.equals(that) //
60-
&& Objects.equals(retrievedBy, that.retrievedBy) //
61-
&& Objects.equals(vaultId, that.vaultId) //
62-
&& Objects.equals(result, that.result);
60+
public String getIpAddress() {
61+
return ipAddress;
6362
}
6463

65-
@Override
66-
public int hashCode() {
67-
return Objects.hash(super.getId(), retrievedBy, vaultId, result);
64+
public void setIpAddress(String ipAddress) {
65+
this.ipAddress = ipAddress;
66+
}
67+
68+
public String getDeviceId() {
69+
return deviceId;
70+
}
71+
72+
public void setDeviceId(String device) {
73+
this.deviceId = device;
6874
}
6975

7076
public enum Result {
7177
SUCCESS,
7278
UNAUTHORIZED
7379
}
7480

81+
@Override
82+
public boolean equals(Object o) {
83+
if (o == null || getClass() != o.getClass()) return false;
84+
if (!super.equals(o)) return false;
85+
VaultKeyRetrievedEvent that = (VaultKeyRetrievedEvent) o;
86+
return Objects.equals(retrievedBy, that.retrievedBy) && Objects.equals(vaultId, that.vaultId) && result == that.result && Objects.equals(ipAddress, that.ipAddress) && Objects.equals(deviceId, that.deviceId);
87+
}
88+
89+
@Override
90+
public int hashCode() {
91+
return Objects.hash(super.hashCode(), retrievedBy, vaultId, result, ipAddress, deviceId);
92+
}
7593
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "audit_event_vault_key_retrieve" ADD "ip_address" VARCHAR(46), ADD "device_id" VARCHAR(255) COLLATE "C";

frontend/src/common/auditlog.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export type AuditEventVaultKeyRetrieveDto = AuditEventDtoBase & {
8181
retrievedBy: string;
8282
vaultId: string;
8383
result: 'SUCCESS' | 'UNAUTHORIZED';
84+
ipAddress?: string;
85+
deviceId?: string;
8486
}
8587

8688
export type AuditEventVaultMemberAddDto = AuditEventDtoBase & {
@@ -186,6 +188,16 @@ class AuditLogService {
186188
}))
187189
.catch((error) => rethrowAndConvertIfExpected(error, 402));
188190
}
191+
192+
public async lastVaultKeyRetrieveEvents(deviceIds: string[]): Promise<AuditEventDto[]> {
193+
const query = `deviceIds=${deviceIds.join('&deviceIds=')}`;
194+
return axiosAuth.get<AuditEventDto[]>(`/auditlog/last-vault-key-retrieve?${query}`)
195+
.then(response => response.data.map(dto => {
196+
dto.timestamp = new Date(dto.timestamp);
197+
return dto;
198+
}))
199+
.catch((error) => rethrowAndConvertIfExpected(error, 402));
200+
}
189201
}
190202

191203
/* Export */

frontend/src/common/backend.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,9 @@ class VaultService {
202202
.catch((error) => rethrowAndConvertIfExpected(error, 400, 404, 409));
203203
}
204204

205-
public async accessToken(vaultId: string, evenIfArchived = false): Promise<string> {
206-
return axiosAuth.get(`/vaults/${vaultId}/access-token?evenIfArchived=${evenIfArchived}`, { headers: { 'Content-Type': 'text/plain' } })
205+
public async accessToken(vaultId: string, deviceId?: string, evenIfArchived = false): Promise<string> {
206+
const deviceIdQueryParam = deviceId !== undefined ? `&deviceId=${deviceId}` : '';
207+
return axiosAuth.get(`/vaults/${vaultId}/access-token?evenIfArchived=${evenIfArchived}${deviceIdQueryParam}`, { headers: { 'Content-Type': 'text/plain', 'deviceId': deviceId } })
207208
.then(response => response.data)
208209
.catch((error) => rethrowAndConvertIfExpected(error, 402, 403));
209210
}

frontend/src/components/UserProfile.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headless
6565
import { ArrowTopRightOnSquareIcon, CheckIcon, ChevronUpDownIcon, LanguageIcon } from '@heroicons/vue/24/solid';
6666
import { onMounted, ref } from 'vue';
6767
import { useI18n } from 'vue-i18n';
68+
import auditlog from '../common/auditlog';
6869
import backend, { UserDto, VersionDto } from '../common/backend';
6970
7071
import config from '../common/config';
@@ -93,6 +94,9 @@ async function fetchData() {
9394
try {
9495
me.value = await userdata.me;
9596
version.value = await backend.version.get();
97+
const deviceIds = me.value.devices.map(d => d.id);
98+
const lastVaultKeyRetrieveEvents = await auditlog.service.lastVaultKeyRetrieveEvents(deviceIds);
99+
console.log(lastVaultKeyRetrieveEvents);
96100
} catch (error) {
97101
console.error('Retrieving user information failed.', error);
98102
onFetchError.value = error instanceof Error ? error : new Error('Unknown Error');

frontend/src/components/VaultDetails.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ async function fetchOwnerData() {
302302
await refreshTrusts();
303303
usersRequiringAccessGrant.value = await backend.vaults.getUsersRequiringAccessGrant(props.vaultId);
304304
vaultRecoveryRequired.value = false;
305-
const vaultKeyJwe = await backend.vaults.accessToken(props.vaultId, true);
305+
const deviceId = await (await userdata.browserKeys)?.id();
306+
const vaultKeyJwe = await backend.vaults.accessToken(props.vaultId, deviceId, true);
306307
vaultKeys.value = await loadVaultKeys(vaultKeyJwe);
307308
} catch (error) {
308309
if (error instanceof ForbiddenError) {

0 commit comments

Comments
 (0)