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,8 @@
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.UserAccountSetupCompleteEvent;
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 @@ -84,6 +86,8 @@ public List<AuditEventDto> getAllEvents(@QueryParam("startDate") Instant startDa
@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 = UserAccountSetupCompleteEvent.class, name = UserAccountSetupCompleteEvent.TYPE), //
@JsonSubTypes.Type(value = UserAccountResetEvent.class, name = UserAccountResetEvent.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 +110,9 @@ 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 UserAccountSetupCompleteEvent evt -> new UserAccountSetupCompleteEventDto(evt.getId(), evt.getTimestamp(), UserAccountSetupCompleteEvent.TYPE, evt.getCompletedBy());
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 +133,18 @@ 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 UserAccountSetupCompleteEventDto(long id, Instant timestamp, String type, @JsonProperty("completedBy") String completedBy) implements AuditEventDto {
}

record VaultCreatedEventDto(long id, Instant timestamp, String type, @JsonProperty("createdBy") String createdBy, @JsonProperty("vaultId") UUID vaultId, @JsonProperty("vaultName") String vaultName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public Response putMe(@Nullable @Valid UserDto dto) {
user.setEcdsaPublicKey(dto.ecdsaPublicKey);
user.setPrivateKeys(dto.privateKeys);
user.setSetupCode(dto.setupCode);
eventLogger.logUserAccountSetupComplete(jwt.getSubject());
updateDevices(user, dto);
}
userRepo.persist(user);
Expand Down Expand Up @@ -168,6 +169,7 @@ public Response resetMe() {
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,20 @@ 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 logUserAccountSetupComplete(String completedBy) {
var event = new UserAccountSetupCompleteEvent();
event.setTimestamp(Instant.now());
event.setCompletedBy(completedBy);
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);
}
SailReal marked this conversation as resolved.
Show resolved Hide resolved
}
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_setup_complete")
@DiscriminatorValue(UserAccountSetupCompleteEvent.TYPE)
public class UserAccountSetupCompleteEvent extends AuditEvent {

public static final String TYPE = "USER_ACCOUNT_SETUP_COMPLETE";

@Column(name = "completed_by")
private String completedBy;

public String getCompletedBy() {
return completedBy;
}

public void setCompletedBy(String completedBy) {
this.completedBy = completedBy;
}
SailReal marked this conversation as resolved.
Show resolved Hide resolved

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

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), completedBy);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE "audit_event_user_account_setup_complete"
(
"id" BIGINT NOT NULL,
"completed_by" VARCHAR(255) COLLATE "C" 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
);
12 changes: 11 additions & 1 deletion frontend/src/common/auditlog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ export type AuditEventSignedWotIdDto = AuditEventDtoBase & {
signature: string;
}

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

export type AuditEventUserAccountSetupCompleteDto = AuditEventDtoBase & {
type: 'USER_ACCOUNT_SETUP_COMPLETE',
completedBy: string;
}

export type AuditEventVaultCreateDto = AuditEventDtoBase & {
type: 'VAULT_CREATE',
createdBy: string;
Expand Down Expand Up @@ -96,7 +106,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 | AuditEventUserAccountSetupCompleteDto | AuditEventVaultCreateDto | AuditEventVaultUpdateDto | AuditEventVaultAccessGrantDto | AuditEventVaultKeyRetrieveDto | AuditEventVaultMemberAddDto | AuditEventVaultMemberRemoveDto | AuditEventVaultMemberUpdateDto | AuditEventVaultOwnershipClaimDto;

/* Entity Cache */

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/AuditLog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
<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" />
<AuditLogDetailsUserAccountSetupComplete v-else-if="auditEvent.type == 'USER_ACCOUNT_SETUP_COMPLETE'" :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 +174,8 @@ 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 AuditLogDetailsUserAccountSetupComplete from './AuditLogDetailsUserAccountSetupComplete.vue';
import AuditLogDetailsVaultAccessGrant from './AuditLogDetailsVaultAccessGrant.vue';
import AuditLogDetailsVaultCreate from './AuditLogDetailsVaultCreate.vue';
import AuditLogDetailsVaultKeyRetrieve from './AuditLogDetailsVaultKeyRetrieve.vue';
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);
});
SailReal marked this conversation as resolved.
Show resolved Hide resolved
</script>
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.setup.complete') }}
</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>completed by</code>
</dt>
<dd class="flex items-baseline gap-2 text-sm text-gray-900">
<span v-if="resolvedCompletedBy != null">{{ resolvedCompletedBy.name }}</span>
<code class="text-xs" :class="{'text-gray-600': resolvedCompletedBy != null}">{{ event.completedBy }}</code>
</dd>
</div>
</dl>
</td>
</template>

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

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

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

const resolvedCompletedBy = ref<AuthorityDto>();

onMounted(async () => {
resolvedCompletedBy.value = await auditlog.entityCache.getAuthority(props.event.completedBy);
});
</script>
4 changes: 3 additions & 1 deletion frontend/src/i18n/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@
"auditLog.details.device.register": "Gerät registrieren",
"auditLog.details.device.remove": "Gerät entfernen",
"auditLog.details.setting.wot.update": "WoT-Einstellung aktualisieren",
"auditLog.details.wot.signedIdentity": "Identität beglaubigt",
"auditLog.details.user.account.reset": "Account zurückgesetzt",
"auditLog.details.user.account.setup.complete": "Account eingerichtet",
iammajid marked this conversation as resolved.
Show resolved Hide resolved
"auditLog.details.vault.create": "Tresor erstellen",
"auditLog.details.vault.update": "Tresor aktualisieren",
"auditLog.details.vaultAccess.grant": "Tresor-Zugriff gewähren",
Expand All @@ -90,6 +91,7 @@
"auditLog.details.vaultMember.remove": "Tresor-Mitglied entfernen",
"auditLog.details.vaultMember.update": "Tresor-Mitglied aktualisieren",
"auditLog.details.vaultOwnership.claim": "Tresor-Eigentümer werden",
"auditLog.details.wot.signedIdentity": "Identität beglaubigt",
SailReal marked this conversation as resolved.
Show resolved Hide resolved
"auditLog.pagination.showing": "Zeige Einträge {0} bis {1}",
"auditLog.paymentRequired.message": "Lizenz erforderlich",
"auditLog.paymentRequired.description": "Audit-Logs sind nur mit einer kostenpflichtigen Lizenz verfügbar. Du kannst eine im Admin-Bereich erhalten.",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@
"auditLog.details.device.register": "Register Device",
"auditLog.details.device.remove": "Remove Device",
"auditLog.details.setting.wot.update": "Update Wot Settings",
"auditLog.details.wot.signedIdentity": "Signed Identity",
"auditLog.details.user.account.reset": "Reset User Account",
"auditLog.details.user.account.setup.complete": "Complete Account Setup",
"auditLog.details.vault.create": "Create Vault",
"auditLog.details.vault.update": "Update Vault",
"auditLog.details.vaultAccess.grant": "Grant Vault Access",
Expand All @@ -90,6 +91,7 @@
"auditLog.details.vaultMember.remove": "Remove Vault Member",
"auditLog.details.vaultMember.update": "Update Vault Member",
"auditLog.details.vaultOwnership.claim": "Claim Vault Ownership",
"auditLog.details.wot.signedIdentity": "Signed Identity",
"auditLog.pagination.showing": "Showing entries {0} to {1}",
"auditLog.paymentRequired.message": "License Required",
"auditLog.paymentRequired.description": "Audit Logs are only available with a paid license. You can get one in the admin section.",
Expand Down