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

Feature: User Audit Log #310

Merged
merged 14 commits into from
Feb 6, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
import org.cryptomator.hub.entities.events.DeviceRemovedEvent;
import org.cryptomator.hub.entities.events.SettingWotUpdateEvent;
import org.cryptomator.hub.entities.events.SignedWotIdEvent;
import org.cryptomator.hub.entities.events.UserAccountResetEvent;
import org.cryptomator.hub.entities.events.UserKeysChangeEvent;
import org.cryptomator.hub.entities.events.UserSetupCodeChangeEvent;
import org.cryptomator.hub.entities.events.VaultAccessGrantedEvent;
import org.cryptomator.hub.entities.events.VaultCreatedEvent;
import org.cryptomator.hub.entities.events.VaultKeyRetrievedEvent;
Expand Down Expand Up @@ -82,8 +85,11 @@ public List<AuditEventDto> getAllEvents(@QueryParam("startDate") Instant startDa
@JsonSubTypes({ //
@JsonSubTypes.Type(value = DeviceRegisteredEventDto.class, name = DeviceRegisteredEvent.TYPE), //
@JsonSubTypes.Type(value = DeviceRemovedEventDto.class, name = DeviceRemovedEvent.TYPE), //
@JsonSubTypes.Type(value = SettingWotUpdateEvent.class, name = SettingWotUpdateEvent.TYPE), //
@JsonSubTypes.Type(value = SignedWotIdEvent.class, name = SignedWotIdEvent.TYPE), //
@JsonSubTypes.Type(value = SettingWotUpdateEventDto.class, name = SettingWotUpdateEvent.TYPE), //
@JsonSubTypes.Type(value = SignedWotIdEventDto.class, name = SignedWotIdEvent.TYPE), //
@JsonSubTypes.Type(value = UserAccountResetEventDto.class, name = UserAccountResetEvent.TYPE), //
@JsonSubTypes.Type(value = UserKeysChangeEventDto.class, name = UserKeysChangeEvent.TYPE), //
@JsonSubTypes.Type(value = UserSetupCodeChangeEventDto.class, name = UserSetupCodeChangeEvent.TYPE), //
@JsonSubTypes.Type(value = VaultCreatedEventDto.class, name = VaultCreatedEvent.TYPE), //
@JsonSubTypes.Type(value = VaultUpdatedEventDto.class, name = VaultUpdatedEvent.TYPE), //
@JsonSubTypes.Type(value = VaultAccessGrantedEventDto.class, name = VaultAccessGrantedEvent.TYPE), //
Expand All @@ -106,7 +112,10 @@ static AuditEventDto fromEntity(AuditEvent entity) {
case DeviceRegisteredEvent evt -> new DeviceRegisteredEventDto(evt.getId(), evt.getTimestamp(), DeviceRegisteredEvent.TYPE, evt.getRegisteredBy(), evt.getDeviceId(), evt.getDeviceName(), evt.getDeviceType());
case DeviceRemovedEvent evt -> new DeviceRemovedEventDto(evt.getId(), evt.getTimestamp(), DeviceRemovedEvent.TYPE, evt.getRemovedBy(), evt.getDeviceId());
case SignedWotIdEvent evt -> new SignedWotIdEventDto(evt.getId(), evt.getTimestamp(), SignedWotIdEvent.TYPE, evt.getUserId(), evt.getSignerId(), evt.getSignerKey(), evt.getSignature());
case SettingWotUpdateEvent evt -> new SettingWotUpdateDto(evt.getId(), evt.getTimestamp(), SettingWotUpdateEvent.TYPE, evt.getUpdatedBy(), evt.getWotMaxDepth(), evt.getWotIdVerifyLen());
case SettingWotUpdateEvent evt -> new SettingWotUpdateEventDto(evt.getId(), evt.getTimestamp(), SettingWotUpdateEvent.TYPE, evt.getUpdatedBy(), evt.getWotMaxDepth(), evt.getWotIdVerifyLen());
case UserAccountResetEvent evt -> new UserAccountResetEventDto(evt.getId(), evt.getTimestamp(), UserAccountResetEvent.TYPE, evt.getResetBy());
case UserKeysChangeEvent evt -> new UserKeysChangeEventDto(evt.getId(), evt.getTimestamp(), UserKeysChangeEvent.TYPE, evt.getChangedBy(), evt.getUserName());
case UserSetupCodeChangeEvent evt -> new UserSetupCodeChangeEventDto(evt.getId(), evt.getTimestamp(), UserSetupCodeChangeEvent.TYPE, evt.getChangedBy());
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());
Expand All @@ -127,10 +136,21 @@ record DeviceRegisteredEventDto(long id, Instant timestamp, String type, @JsonPr
record DeviceRemovedEventDto(long id, Instant timestamp, String type, @JsonProperty("removedBy") String removedBy, @JsonProperty("deviceId") String deviceId) implements AuditEventDto {
}

record SignedWotIdEventDto(long id, Instant timestamp, String type, @JsonProperty("userId") String userId, @JsonProperty("signerId") String signerId, @JsonProperty("signerKey") String signerKey, @JsonProperty("signature") String signature) implements AuditEventDto {
record SignedWotIdEventDto(long id, Instant timestamp, String type, @JsonProperty("userId") String userId, @JsonProperty("signerId") String signerId, @JsonProperty("signerKey") String signerKey,
@JsonProperty("signature") String signature) implements AuditEventDto {
}

record SettingWotUpdateDto(long id, Instant timestamp, String type, @JsonProperty("updatedBy") String updatedBy, @JsonProperty("wotMaxDepth") int wotMaxDepth, @JsonProperty("wotIdVerifyLen") int wotIdVerifyLen) implements AuditEventDto {
record SettingWotUpdateEventDto(long id, Instant timestamp, String type, @JsonProperty("updatedBy") String updatedBy, @JsonProperty("wotMaxDepth") int wotMaxDepth,
@JsonProperty("wotIdVerifyLen") int wotIdVerifyLen) implements AuditEventDto {
}

record UserAccountResetEventDto(long id, Instant timestamp, String type, @JsonProperty("resetBy") String resetBy) implements AuditEventDto {
}

record UserKeysChangeEventDto(long id, Instant timestamp, String type, @JsonProperty("changedBy") String changedBy, @JsonProperty("userName") String userName) implements AuditEventDto {
}

record UserSetupCodeChangeEventDto(long id, Instant timestamp, String type, @JsonProperty("changedBy") String changedBy) implements AuditEventDto {
}

record VaultCreatedEventDto(long id, Instant timestamp, String type, @JsonProperty("createdBy") String createdBy, @JsonProperty("vaultId") UUID vaultId, @JsonProperty("vaultName") String vaultName,
Expand Down
17 changes: 13 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 @@ -34,6 +34,7 @@
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
Expand Down Expand Up @@ -79,10 +80,16 @@ public Response putMe(@Nullable @Valid UserDto dto) {
user.setPictureUrl(jwt.getClaim("picture"));
user.setEmail(jwt.getClaim("email"));
if (dto != null) {
user.setEcdhPublicKey(dto.ecdhPublicKey);
user.setEcdsaPublicKey(dto.ecdsaPublicKey);
user.setPrivateKeys(dto.privateKeys);
user.setSetupCode(dto.setupCode);
if (!Objects.equals(user.getSetupCode(), dto.setupCode)) {
user.setSetupCode(dto.setupCode);
eventLogger.logUserSetupCodeChanged(jwt.getSubject());
}
if (!Objects.equals(user.getEcdhPublicKey(), dto.ecdhPublicKey) || !Objects.equals(user.getEcdsaPublicKey(), dto.ecdsaPublicKey) || !Objects.equals(user.getPrivateKeys(), dto.privateKeys)) {
user.setEcdhPublicKey(dto.ecdhPublicKey);
user.setEcdsaPublicKey(dto.ecdsaPublicKey);
user.setPrivateKeys(dto.privateKeys);
eventLogger.logUserKeysChanged(jwt.getSubject(), jwt.getName());
}
updateDevices(user, dto);
}
userRepo.persist(user);
Expand Down Expand Up @@ -163,11 +170,13 @@ public UserDto getMe(@QueryParam("withDevices") boolean withDevices) {
public Response resetMe() {
User user = userRepo.findById(jwt.getSubject());
user.setEcdhPublicKey(null);
user.setEcdsaPublicKey(null);
user.setPrivateKeys(null);
user.setSetupCode(null);
userRepo.persist(user);
deviceRepo.deleteByOwner(user.getId());
accessTokenRepo.deleteByUser(user.getId());
eventLogger.logUserAccountReset(jwt.getSubject());
return Response.noContent().build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ public void logDeviceRemoved(String removedBy, String deviceId) {
auditEventRepository.persist(event);
}

public void logUserAccountReset(String resetBy) {
var event = new UserAccountResetEvent();
event.setTimestamp(Instant.now());
event.setResetBy(resetBy);
auditEventRepository.persist(event);
}

public void logUserKeysChanged(String changedBy, String userName) {
var event = new UserKeysChangeEvent();
event.setTimestamp(Instant.now());
event.setChangedBy(changedBy);
event.setUserName(userName);
auditEventRepository.persist(event);
}

public void logUserSetupCodeChanged(String changedBy) {
var event = new UserSetupCodeChangeEvent();
event.setTimestamp(Instant.now());
event.setChangedBy(changedBy);
auditEventRepository.persist(event);
}

public void logVaultAccessGranted(String grantedBy, UUID vaultId, String authorityId) {
var event = new VaultAccessGrantedEvent();
event.setTimestamp(Instant.now());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.cryptomator.hub.entities.events;

import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

import java.util.Objects;

@Entity
@Table(name = "audit_event_user_account_reset")
@DiscriminatorValue(UserAccountResetEvent.TYPE)
public class UserAccountResetEvent extends AuditEvent {

public static final String TYPE = "USER_ACCOUNT_RESET";

@Column(name = "reset_by")
private String resetBy;

public String getResetBy() {
return resetBy;
}

public void setResetBy(String resetBy) {
this.resetBy = resetBy;
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
UserAccountResetEvent that = (UserAccountResetEvent) o;
return Objects.equals(resetBy, that.resetBy);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), resetBy);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.cryptomator.hub.entities.events;

import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

import java.util.Objects;

@Entity
@Table(name = "audit_event_user_keys_change")
@DiscriminatorValue(UserKeysChangeEvent.TYPE)
public class UserKeysChangeEvent extends AuditEvent {

public static final String TYPE = "USER_KEYS_CHANGE";

@Column(name = "changed_by")
private String changedBy;

@Column(name = "user_name")
private String userName;

public String getChangedBy() {
return changedBy;
}

public void setChangedBy(String changedBy) {
this.changedBy = changedBy;
}

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
UserKeysChangeEvent that = (UserKeysChangeEvent) o;
return Objects.equals(changedBy, that.changedBy) && Objects.equals(userName, that.userName);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), changedBy, userName);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.cryptomator.hub.entities.events;

import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

import java.util.Objects;

@Entity
@Table(name = "audit_event_user_setupcode_change")
@DiscriminatorValue(UserSetupCodeChangeEvent.TYPE)
public class UserSetupCodeChangeEvent extends AuditEvent {

public static final String TYPE = "USER_SETUP_CODE_CHANGE";

@Column(name = "changed_by")
private String changedBy;

public String getChangedBy() {
return changedBy;
}

public void setChangedBy(String changedBy) {
this.changedBy = changedBy;
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
UserSetupCodeChangeEvent that = (UserSetupCodeChangeEvent) o;
return Objects.equals(changedBy, that.changedBy);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), changedBy);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE TABLE "audit_event_user_keys_change"
(
"id" BIGINT NOT NULL,
"changed_by" VARCHAR(255) COLLATE "C" NOT NULL,
"user_name" VARCHAR NOT NULL,
CONSTRAINT "AUDIT_EVENT_USER_ACCOUNT_SETUP_COMPLETE_PK" PRIMARY KEY ("id"),
CONSTRAINT "AUDIT_EVENT_USER_ACCOUNT_SETUP_COMPLETE_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
);

CREATE TABLE "audit_event_user_account_reset"
(
"id" BIGINT NOT NULL,
"reset_by" VARCHAR(255) COLLATE "C" NOT NULL,
CONSTRAINT "AUDIT_EVENT_USER_ACCOUNT_RESET_PK" PRIMARY KEY ("id"),
CONSTRAINT "AUDIT_EVENT_USER_ACCOUNT_RESET_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
);

CREATE TABLE "audit_event_user_setupcode_change"
(
"id" BIGINT NOT NULL,
"changed_by" VARCHAR(255) COLLATE "C" NOT NULL,
CONSTRAINT "AUDIT_EVENT_USER_SETUPCODE_CHANGE_PK" PRIMARY KEY ("id"),
CONSTRAINT "AUDIT_EVENT_USER_ACCOUNT_SETUPCODE_CHANGE_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
);
18 changes: 17 additions & 1 deletion frontend/src/common/auditlog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ export type AuditEventSignedWotIdDto = AuditEventDtoBase & {
signature: string;
}

export type AuditEventUserAccountResetDto = AuditEventDtoBase & {
type: 'USER_ACCOUNT_RESET',
resetBy: string;
}

export type AuditEventUserKeysChangeDto = AuditEventDtoBase & {
type: 'USER_KEYS_CHANGE',
changedBy: string,
userName: string;
}

export type AuditEventUserSetupCodeChangeDto = AuditEventDtoBase & {
type: 'USER_SETUP_CODE_CHANGE',
changedBy: string;
}

export type AuditEventVaultCreateDto = AuditEventDtoBase & {
type: 'VAULT_CREATE',
createdBy: string;
Expand Down Expand Up @@ -96,7 +112,7 @@ export type AuditEventVaultOwnershipClaimDto = AuditEventDtoBase & {
vaultId: string;
}

export type AuditEventDto = AuditEventDeviceRegisterDto | AuditEventDeviceRemoveDto | AuditEventSettingWotUpdateDto | AuditEventSignedWotIdDto | AuditEventVaultCreateDto | AuditEventVaultUpdateDto | AuditEventVaultAccessGrantDto | AuditEventVaultKeyRetrieveDto | AuditEventVaultMemberAddDto | AuditEventVaultMemberRemoveDto | AuditEventVaultMemberUpdateDto | AuditEventVaultOwnershipClaimDto;
export type AuditEventDto = AuditEventDeviceRegisterDto | AuditEventDeviceRemoveDto | AuditEventSettingWotUpdateDto | AuditEventSignedWotIdDto | AuditEventUserAccountResetDto | AuditEventUserKeysChangeDto | AuditEventUserSetupCodeChangeDto | AuditEventVaultCreateDto | AuditEventVaultUpdateDto | AuditEventVaultAccessGrantDto | AuditEventVaultKeyRetrieveDto | AuditEventVaultMemberAddDto | AuditEventVaultMemberRemoveDto | AuditEventVaultMemberUpdateDto | AuditEventVaultOwnershipClaimDto;

/* Entity Cache */

Expand Down
6 changes: 6 additions & 0 deletions frontend/src/components/AuditLog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@
<AuditLogDetailsDeviceRemove v-else-if="auditEvent.type == 'DEVICE_REMOVE'" :event="auditEvent" />
<AuditLogDetailsSettingWotUpdate v-else-if="auditEvent.type == 'SETTING_WOT_UPDATE'" :event="auditEvent" />
<AuditLogDetailsSignedWotId v-else-if="auditEvent.type == 'SIGN_WOT_ID'" :event="auditEvent" />
<AuditLogDetailsUserAccountReset v-else-if="auditEvent.type == 'USER_ACCOUNT_RESET'" :event="auditEvent" />
<AuditLogUserKeysChange v-else-if="auditEvent.type == 'USER_KEYS_CHANGE'" :event="auditEvent" />
<AuditLogUserSetupCodeChanged v-else-if="auditEvent.type == 'USER_SETUP_CODE_CHANGE'" :event="auditEvent" />
<AuditLogDetailsVaultCreate v-else-if="auditEvent.type == 'VAULT_CREATE'" :event="auditEvent" />
<AuditLogDetailsVaultUpdate v-else-if="auditEvent.type == 'VAULT_UPDATE'" :event="auditEvent" />
<AuditLogDetailsVaultAccessGrant v-else-if="auditEvent.type == 'VAULT_ACCESS_GRANT'" :event="auditEvent" />
Expand Down Expand Up @@ -172,6 +175,7 @@ import AuditLogDetailsDeviceRegister from './AuditLogDetailsDeviceRegister.vue';
import AuditLogDetailsDeviceRemove from './AuditLogDetailsDeviceRemove.vue';
import AuditLogDetailsSettingWotUpdate from './AuditLogDetailsSettingWotUpdate.vue';
import AuditLogDetailsSignedWotId from './AuditLogDetailsSignedWotId.vue';
import AuditLogDetailsUserAccountReset from './AuditLogDetailsUserAccountReset.vue';
import AuditLogDetailsVaultAccessGrant from './AuditLogDetailsVaultAccessGrant.vue';
import AuditLogDetailsVaultCreate from './AuditLogDetailsVaultCreate.vue';
import AuditLogDetailsVaultKeyRetrieve from './AuditLogDetailsVaultKeyRetrieve.vue';
Expand All @@ -180,6 +184,8 @@ import AuditLogDetailsVaultMemberRemove from './AuditLogDetailsVaultMemberRemove
import AuditLogDetailsVaultMemberUpdate from './AuditLogDetailsVaultMemberUpdate.vue';
import AuditLogDetailsVaultOwnershipClaim from './AuditLogDetailsVaultOwnershipClaim.vue';
import AuditLogDetailsVaultUpdate from './AuditLogDetailsVaultUpdate.vue';
import AuditLogUserKeysChange from './AuditLogUserKeysChange.vue';
import AuditLogUserSetupCodeChanged from './AuditLogUserSetupCodeChanged.vue';
import FetchError from './FetchError.vue';

enum State {
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/components/AuditLogDetailsUserAccountReset.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<template>
<td class="whitespace-nowrap px-3 py-4 text-sm font-medium text-gray-900">
{{ t('auditLog.details.user.account.reset') }}
</td>
<td class="whitespace-nowrap py-4 pl-3 pr-4 sm:pr-6">
<dl class="flex flex-col gap-2">
<div class="flex items-baseline gap-2">
<dt class="text-xs text-gray-500">
<code>reset by</code>
</dt>
<dd class="flex items-baseline gap-2 text-sm text-gray-900">
<span v-if="resolvedResetBy != null">{{ resolvedResetBy.name }}</span>
<code class="text-xs" :class="{'text-gray-600': resolvedResetBy != null}">{{ event.resetBy }}</code>
</dd>
</div>
</dl>
</td>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import auditlog, { AuditEventUserAccountResetDto } from '../common/auditlog';
import { AuthorityDto } from '../common/backend';

const { t } = useI18n({ useScope: 'global' });

const props = defineProps<{
event: AuditEventUserAccountResetDto
}>();

const resolvedResetBy = ref<AuthorityDto>();

onMounted(async () => {
resolvedResetBy.value = await auditlog.entityCache.getAuthority(props.event.resetBy);
});
</script>
Loading