Skip to content

Commit

Permalink
log audit event when signing user identity
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed Jun 21, 2024
1 parent b1ea583 commit f6ce0c0
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.cryptomator.hub.entities.events.AuditEvent;
import org.cryptomator.hub.entities.events.DeviceRegisteredEvent;
import org.cryptomator.hub.entities.events.DeviceRemovedEvent;
import org.cryptomator.hub.entities.events.SignedWotIdEvent;
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 @@ -80,6 +81,7 @@ 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 = SignedWotIdEvent.class, name = SignedWotIdEvent.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 @@ -101,6 +103,7 @@ static AuditEventDto fromEntity(AuditEvent entity) {
return switch (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 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 @@ -121,6 +124,9 @@ 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 VaultCreatedEventDto(long id, Instant timestamp, String type, @JsonProperty("createdBy") String createdBy, @JsonProperty("vaultId") UUID vaultId, @JsonProperty("vaultName") String vaultName,
@JsonProperty("vaultDescription") String vaultDescription) implements AuditEventDto {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ public Response resetMe() {
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "list all users")
public List<UserDto> getAll() {
return userRepo.findAll().<User>stream().map(UserDto::justPublicInfo).toList();
return userRepo.findAll().stream().map(UserDto::justPublicInfo).toList();
}

@PUT
Expand All @@ -188,16 +188,18 @@ public List<UserDto> getAll() {
@Operation(summary = "adds/updates trust", description = "Stores a signature for the given user.")
@APIResponse(responseCode = "204", description = "signature stored")
public Response putSignature(@PathParam("userId") String userId, @NotNull String signature) {
var signer = userRepo.findById(jwt.getSubject());
var id = new WotEntry.Id();
id.setUserId(userId);
id.setSignerId(jwt.getSubject());
id.setSignerId(signer.getId());
var entry = wotRepo.findById(id);
if (entry == null) {
entry = new WotEntry();
entry.setId(id);
}
entry.setSignature(signature);
wotRepo.persist(entry);
eventLogger.logWotIdSigned(userId, signer.getId(), signer.getEcdsaPublicKey(), signature);
return Response.status(Response.Status.NO_CONTENT).build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ public void logVaultMemberUpdated(String updatedBy, UUID vaultId, String authori
auditEventRepository.persist(event);
}

public void logWotIdSigned(String userId, String signerId, String signerKey, String signature) {
var event = new SignedWotIdEvent();
event.setTimestamp(Instant.now());
event.setUserId(userId);
event.setSignerId(signerId);
event.setSignerKey(signerKey);
event.setSignature(signature);
auditEventRepository.persist(event);
}

//legacy
public void logVaultOwnershipClaimed(String claimedBy, UUID vaultId) {
var event = new VaultOwnershipClaimedEvent();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.cryptomator.hub.entities.events;

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

import java.util.Objects;
import java.util.UUID;

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

public static final String TYPE = "SIGN_WOT_ID";

@Column(name = "user_id")
private String userId;

@Column(name = "signer_id")
private String signerId;

@Column(name = "signer_key")
private String signerKey;

@Column(name = "signature")
private String signature;

public String getUserId() {
return userId;
}

public void setUserId(String userId) {
this.userId = userId;
}

public String getSignerId() {
return signerId;
}

public void setSignerId(String signerId) {
this.signerId = signerId;
}

public String getSignerKey() {
return signerKey;
}

public void setSignerKey(String signerKey) {
this.signerKey = signerKey;
}

public String getSignature() {
return signature;
}

public void setSignature(String signature) {
this.signature = signature;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SignedWotIdEvent that = (SignedWotIdEvent) o;
return super.equals(that) //
&& Objects.equals(userId, that.userId) //
&& Objects.equals(signerId, that.signerId) //
&& Objects.equals(signerKey, that.signerKey) //
&& Objects.equals(signature, that.signature);
}

@Override
public int hashCode() {
return Objects.hash(super.getId(), userId, signerId, signerKey, signature);
}

}
11 changes: 11 additions & 0 deletions backend/src/main/resources/org/cryptomator/hub/flyway/V16__WoT.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ CREATE TABLE "wot" (
CONSTRAINT "fk_signer_id" FOREIGN KEY ("signer_id") REFERENCES "user_details" ("id") ON DELETE CASCADE
);

CREATE TABLE "audit_event_sign_wot_id"
(
"id" BIGINT NOT NULL,
"user_id" VARCHAR(255) COLLATE "C" NOT NULL,
"signer_id" VARCHAR(255) COLLATE "C" NOT NULL,
"signer_key" VARCHAR NOT NULL,
"signature" VARCHAR NOT NULL,
CONSTRAINT "AUDIT_EVENT_SIGN_WOT_ID_PK" PRIMARY KEY ("id"),
CONSTRAINT "AUDIT_EVENT_SIGN_WOT_ID_FK_AUDIT_EVENT" FOREIGN KEY ("id") REFERENCES "audit_event" ("id") ON DELETE CASCADE
);

-- @formatter:off
CREATE VIEW "effective_wot" ("trusting_user_id", "trusted_user_id", "signature_chain") AS
WITH RECURSIVE "r" ("trusting_user_id", "trusted_user_id", "depth", "signer_chain", "signature_chain") AS (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package org.cryptomator.hub.api;

import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.restassured.RestAssured;
import org.cryptomator.hub.license.LicenseHolder;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.when;
Expand All @@ -16,11 +20,20 @@
@DisplayName("Resource /auditlog")
public class AuditLogResourceIT {

@InjectMock
LicenseHolder licenseHolder;

@BeforeAll
public static void beforeAll() {
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}

@BeforeEach
public void beforeEach() {
Mockito.doReturn(true).when(licenseHolder).isSet();
Mockito.doReturn(false).when(licenseHolder).isExpired();
}

@Test
@TestSecurity(user = "Admin", roles = {"admin"})
@DisplayName("As admin, GET /auditlog?startDate=2020-02-20T00:00:00.000Z&endDate=2020-02-20T23:59:59.999Z&paginationId=9999 returns 200 with 20 entries")
Expand Down
38 changes: 34 additions & 4 deletions backend/src/test/java/org/cryptomator/hub/api/UsersResourceIT.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package org.cryptomator.hub.api;

import io.agroal.api.AgroalDataSource;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.oidc.Claim;
import io.quarkus.test.security.oidc.OidcSecurity;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import jakarta.inject.Inject;
import org.cryptomator.hub.license.LicenseHolder;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
Expand All @@ -19,13 +22,18 @@
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.Mockito;

import java.sql.SQLException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;

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.comparesEqualTo;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;

Expand All @@ -36,6 +44,9 @@ public class UsersResourceIT {
@Inject
AgroalDataSource dataSource;

@InjectMock
LicenseHolder licenseHolder;

@BeforeAll
public static void beforeAll() {
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
Expand Down Expand Up @@ -138,16 +149,19 @@ public void testGet(String method, String path) {
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class WebOfTrust {

private Instant testStart;

@BeforeAll
public void setup() throws SQLException {
testStart = Instant.now();
try (var c = dataSource.getConnection(); var s = c.createStatement()) {
s.execute("""
INSERT INTO "authority" ("id", "type", "name") VALUES ('user997', 'USER', 'User 997');
INSERT INTO "authority" ("id", "type", "name") VALUES ('user998', 'USER', 'User 998');
INSERT INTO "authority" ("id", "type", "name") VALUES ('user999', 'USER', 'User 999');
INSERT INTO "user_details" ("id") VALUES ('user997');
INSERT INTO "user_details" ("id") VALUES ('user998');
INSERT INTO "user_details" ("id") VALUES ('user999');
INSERT INTO "user_details" ("id", "ecdsa_publickey") VALUES ('user997', 'ecdsa_public997');
INSERT INTO "user_details" ("id", "ecdsa_publickey") VALUES ('user998', 'ecdsa_public998');
INSERT INTO "user_details" ("id", "ecdsa_publickey") VALUES ('user999', 'ecdsa_public999');
""");
}
}
Expand Down Expand Up @@ -180,7 +194,7 @@ public void test998Trusts999() {

@Test
@Order(1)
@DisplayName("PUT /users/trusted/user999 as user 998")
@DisplayName("PUT /users/trusted/user997 as user 998")
@TestSecurity(user = "User 998", roles = {"user"})
@OidcSecurity(claims = {
@Claim(key = "sub", value = "user998")
Expand Down Expand Up @@ -274,6 +288,22 @@ public void test999Gets998() {
.then().statusCode(404);
}

@Test
@Order(4)
@TestSecurity(user = "Admin", roles = {"admin"})
@DisplayName("As admin, GET /auditlog contains signature events")
public void testGetAuditLogEntries() {
Mockito.doReturn(true).when(licenseHolder).isSet();
Mockito.doReturn(false).when(licenseHolder).isExpired();

given().param("startDate", DateTimeFormatter.ISO_INSTANT.format(testStart))
.param("endDate", DateTimeFormatter.ISO_INSTANT.format(Instant.now()))
.param("paginationId", 9999L)
.when().get("/auditlog")
.then().statusCode(200)
.body("signature", contains("997 trusts 998", "998 trusts 999", "998 trusts 997"));
}


@AfterAll
public void tearDown() throws SQLException {
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/common/auditlog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ export type AuditEventDeviceRemoveDto = AuditEventDtoBase & {
deviceId: string;
}

export type AuditEventSignedWotIdDto = AuditEventDtoBase & {
type: 'SIGN_WOT_ID',
userId: string;
signerId: string;
signerKey: string;
signature: string;
}

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

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

/* Entity Cache */

Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/AuditLog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
</td>
<AuditLogDetailsDeviceRegister v-if="auditEvent.type == 'DEVICE_REGISTER'" :event="auditEvent" />
<AuditLogDetailsDeviceRemove v-else-if="auditEvent.type == 'DEVICE_REMOVE'" :event="auditEvent" />
<AuditLogDetailsSignedWotId v-else-if="auditEvent.type == 'SIGN_WOT_ID'" :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 @@ -164,10 +165,11 @@ import { ChevronDownIcon } from '@heroicons/vue/20/solid';
import { CheckIcon, ChevronUpDownIcon, WrenchIcon } from '@heroicons/vue/24/solid';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import auditlog, { AuditEventDeviceRegisterDto, AuditEventDeviceRemoveDto, AuditEventDto, AuditEventVaultAccessGrantDto, AuditEventVaultCreateDto, AuditEventVaultKeyRetrieveDto, AuditEventVaultMemberAddDto, AuditEventVaultMemberRemoveDto, AuditEventVaultMemberUpdateDto, AuditEventVaultOwnershipClaimDto, AuditEventVaultUpdateDto } from '../common/auditlog';
import auditlog, { AuditEventDto } from '../common/auditlog';
import { PaymentRequiredError } from '../common/backend';
import AuditLogDetailsDeviceRegister from './AuditLogDetailsDeviceRegister.vue';
import AuditLogDetailsDeviceRemove from './AuditLogDetailsDeviceRemove.vue';
import AuditLogDetailsSignedWotId from './AuditLogDetailsSignedWotId.vue';
import AuditLogDetailsVaultAccessGrant from './AuditLogDetailsVaultAccessGrant.vue';
import AuditLogDetailsVaultCreate from './AuditLogDetailsVaultCreate.vue';
import AuditLogDetailsVaultKeyRetrieve from './AuditLogDetailsVaultKeyRetrieve.vue';
Expand Down
Loading

0 comments on commit f6ce0c0

Please sign in to comment.