diff --git a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/AuditRecordReadMarshallable.java b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/AuditRecordReadMarshallable.java index f31dbdf9..df60203f 100644 --- a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/AuditRecordReadMarshallable.java +++ b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/AuditRecordReadMarshallable.java @@ -45,8 +45,9 @@ public void readMarshallable(@NotNull WireIn wire) throws IORuntimeException case WireTags.VALUE_VERSION_0: auditRecord = readV0(wire); break; + case WireTags.VALUE_VERSION_1: // NOPMD case WireTags.VALUE_VERSION_CURRENT: - auditRecord = readV1(wire); + auditRecord = readBitmappedRecord(wire); break; default: throw new IORuntimeException("Unsupported record version: " + version); @@ -75,7 +76,7 @@ private StoredAuditRecord readV0(WireIn wire) .build(); } - private StoredAuditRecord readV1(WireIn wire) + private StoredAuditRecord readBitmappedRecord(WireIn wire) { checkV1Type(wire); int bitmap = wire.read(WireTags.KEY_FIELDS).int32(); @@ -93,6 +94,7 @@ private StoredAuditRecord readV1(WireIn wire) fields.ifSelectedRun(Field.STATUS, () -> recordBuilder.withStatus(readStatus(wire))); fields.ifSelectedRun(Field.OPERATION, () -> recordBuilder.withOperation(wire.read(WireTags.KEY_OPERATION).text())); fields.ifSelectedRun(Field.OPERATION_NAKED, () -> recordBuilder.withNakedOperation(wire.read(WireTags.KEY_NAKED_OPERATION).text())); + fields.ifSelectedRun(Field.SUBJECT, () -> recordBuilder.withSubject(wire.read(WireTags.KEY_SUBJECT).text())); return recordBuilder.build(); } diff --git a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/AuditRecordWriteMarshallable.java b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/AuditRecordWriteMarshallable.java index 49b9a8a2..74b6dead 100644 --- a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/AuditRecordWriteMarshallable.java +++ b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/AuditRecordWriteMarshallable.java @@ -49,5 +49,6 @@ public void writeMarshallable(@NotNull WireOut wire) actualFields.ifSelectedRun(Field.STATUS, () -> wire.write(WireTags.KEY_STATUS).text(auditRecord.getStatus().name())); actualFields.ifSelectedRun(Field.OPERATION, () -> wire.write(WireTags.KEY_OPERATION).text(auditRecord.getOperation().getOperationString())); actualFields.ifSelectedRun(Field.OPERATION_NAKED, () -> wire.write(WireTags.KEY_NAKED_OPERATION).text(auditRecord.getOperation().getNakedOperationString())); + actualFields.ifSelectedRun(Field.SUBJECT, () -> wire.write(WireTags.KEY_SUBJECT).text(auditRecord.getSubject().get())); } } diff --git a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/FieldFilterFlavorAdapter.java b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/FieldFilterFlavorAdapter.java index 3590f0f4..59a19898 100644 --- a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/FieldFilterFlavorAdapter.java +++ b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/FieldFilterFlavorAdapter.java @@ -32,12 +32,23 @@ private FieldFilterFlavorAdapter() static FieldSelector getFieldsAvailableInRecord(AuditRecord auditRecord, FieldSelector configuredFields) { - FieldSelector fields = auditRecord.getBatchId().isPresent() - ? configuredFields - : configuredFields.withoutField(FieldSelector.Field.BATCH_ID); + FieldSelector fields = configuredFields; - return auditRecord.getClientAddress() == null - ? fields.withoutField(FieldSelector.Field.CLIENT_IP).withoutField(FieldSelector.Field.CLIENT_PORT) - : fields; + if (!auditRecord.getBatchId().isPresent()) + { + fields = fields.withoutField(FieldSelector.Field.BATCH_ID); + } + + if (!auditRecord.getSubject().isPresent()) + { + fields = fields.withoutField(FieldSelector.Field.SUBJECT); + } + + if (auditRecord.getClientAddress() == null) + { + fields = fields.withoutField(FieldSelector.Field.CLIENT_IP).withoutField(FieldSelector.Field.CLIENT_PORT); + } + + return fields; } } diff --git a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/FieldSelector.java b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/FieldSelector.java index 673a1094..28b35a68 100644 --- a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/FieldSelector.java +++ b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/FieldSelector.java @@ -17,6 +17,8 @@ import java.util.List; +import com.google.common.annotations.VisibleForTesting; + public final class FieldSelector { /** @@ -36,7 +38,8 @@ public enum Field STATUS(1 << 5), OPERATION(1 << 6), OPERATION_NAKED(1 << 7), - TIMESTAMP(1 << 8); + TIMESTAMP(1 << 8), + SUBJECT(1 << 9); private final int bit; @@ -89,6 +92,17 @@ public boolean isSelected(Field field) return (bitmap & field.getBit()) > 0; } + /** + * Toggle a field as selected + * @param field the field to select + * @return a new field selector with the field selected + */ + @VisibleForTesting + FieldSelector withField(Field field) + { + return new FieldSelector(bitmap | field.getBit()); + } + /** * Copies this field selector, but without the provided field selected. * diff --git a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/WireTags.java b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/WireTags.java index 4a218670..22d39597 100644 --- a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/WireTags.java +++ b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/WireTags.java @@ -29,10 +29,12 @@ class WireTags static final String KEY_STATUS = "status"; static final String KEY_OPERATION = "operation"; static final String KEY_NAKED_OPERATION = "naked_operation"; + static final String KEY_SUBJECT = "subject"; static final short VALUE_VERSION_0 = 0; static final short VALUE_VERSION_1 = 1; - static final short VALUE_VERSION_CURRENT = VALUE_VERSION_1; + static final short VALUE_VERSION_2 = 2; + static final short VALUE_VERSION_CURRENT = VALUE_VERSION_2; static final String VALUE_TYPE_BATCH_ENTRY = "ecaudit-batch"; static final String VALUE_TYPE_SINGLE_ENTRY = "ecaudit-single"; static final String VALUE_TYPE_AUDIT = "ecaudit"; diff --git a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/record/AuditRecord.java b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/record/AuditRecord.java index 7af310d9..36e43bad 100644 --- a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/record/AuditRecord.java +++ b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/record/AuditRecord.java @@ -35,4 +35,6 @@ public interface AuditRecord Status getStatus(); AuditOperation getOperation(); + + Optional getSubject(); } diff --git a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/record/StoredAuditRecord.java b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/record/StoredAuditRecord.java index aa099246..86373ac2 100644 --- a/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/record/StoredAuditRecord.java +++ b/common/src/main/java/com/ericsson/bss/cassandra/ecaudit/common/record/StoredAuditRecord.java @@ -34,6 +34,7 @@ public class StoredAuditRecord private final String operation; private final String nakedOperation; private final Long timestamp; + private final String subject; private StoredAuditRecord(Builder builder) { @@ -46,6 +47,7 @@ private StoredAuditRecord(Builder builder) this.operation = builder.operation; this.nakedOperation = builder.nakedOperation; this.timestamp = builder.timestamp; + this.subject = builder.subject; } public Optional getTimestamp() @@ -93,6 +95,11 @@ public Optional getNakedOperation() return Optional.ofNullable(nakedOperation); } + public Optional getSubject() + { + return Optional.ofNullable(subject); + } + public static Builder builder() { return new Builder(); @@ -109,6 +116,7 @@ public static class Builder private String operation; private String nakedOperation; private Long timestamp; + private String subject; public Builder withClientAddress(InetAddress clientAddress) { @@ -164,6 +172,12 @@ public Builder withTimestamp(long timestamp) return this; } + public Builder withSubject(String subject) + { + this.subject = subject; + return this; + } + public StoredAuditRecord build() { return new StoredAuditRecord(this); diff --git a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestAuditRecordReadMarshallable.java b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestAuditRecordReadMarshallable.java index 31afe73e..ab98bdec 100644 --- a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestAuditRecordReadMarshallable.java +++ b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestAuditRecordReadMarshallable.java @@ -84,6 +84,20 @@ public void testReadBatchBatch() throws Exception assertThatRecordIsSame(actualAuditRecord, expectedValues); } + @Test + public void testReadSubjectMatch() throws Exception + { + RecordValues expectedValues = defaultValues.butWithSubject("bob-subject"); + + givenNextRecordIs(expectedValues); + + readMarshallable.readMarshallable(wireInMock); + + StoredAuditRecord actualAuditRecord = readMarshallable.getAuditRecord(); + + assertThatRecordIsSame(actualAuditRecord, expectedValues); + } + @Test public void testReuseMarshallable() { @@ -166,9 +180,19 @@ private void givenNextRecordIs(RecordValues values) when(typeValueMock.text()).thenReturn(values.getType()); when(wireInMock.read(eq("type"))).thenReturn(typeValueMock); - int fieldsBitmap = values.getBatchId() != null ? FieldSelector.DEFAULT_FIELDS.getBitmap() : FieldSelector.DEFAULT_FIELDS.withoutField(Field.BATCH_ID).getBitmap(); + FieldSelector selectedFields = FieldSelector.DEFAULT_FIELDS; + if (values.getBatchId() == null) + { + selectedFields = selectedFields.withoutField(Field.BATCH_ID); + } + + if (values.getSubject() != null) + { + selectedFields = selectedFields.withField(Field.SUBJECT); + } + ValueIn fieldsValueMock = mock(ValueIn.class); - when(fieldsValueMock.int32()).thenReturn(fieldsBitmap); + when(fieldsValueMock.int32()).thenReturn(selectedFields.getBitmap()); when(wireInMock.read(eq("fields"))).thenReturn(fieldsValueMock); ValueIn timestampValueMock = mock(ValueIn.class); @@ -205,6 +229,13 @@ private void givenNextRecordIs(RecordValues values) ValueIn operationValueMock = mock(ValueIn.class); when(operationValueMock.text()).thenReturn(values.getOperation()); when(wireInMock.read(eq("operation"))).thenReturn(operationValueMock); + + if (values.getSubject() != null) + { + ValueIn subjectValueMock = mock(ValueIn.class); + when(subjectValueMock.text()).thenReturn(values.getSubject()); + when(wireInMock.read(eq("subject"))).thenReturn(subjectValueMock); + } } private void assertThatRecordIsSame(StoredAuditRecord actualAuditRecord, RecordValues expectedValues) throws UnknownHostException @@ -218,5 +249,6 @@ private void assertThatRecordIsSame(StoredAuditRecord actualAuditRecord, RecordV assertThat(actualAuditRecord.getNakedOperation()).isEmpty(); assertThat(actualAuditRecord.getUser()).contains(expectedValues.gethUser()); assertThat(actualAuditRecord.getTimestamp()).contains(expectedValues.getTimestamp()); + assertThat(actualAuditRecord.getSubject()).isEqualTo(Optional.ofNullable(expectedValues.getSubject())); } } diff --git a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestFieldFilterFlavorAdapter.java b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestFieldFilterFlavorAdapter.java index f524cbd0..7266d882 100644 --- a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestFieldFilterFlavorAdapter.java +++ b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestFieldFilterFlavorAdapter.java @@ -27,12 +27,13 @@ public class TestFieldFilterFlavorAdapter @Test public void testGetFieldsAvailableInRecord() { - AuditRecord recordWithoutClientIPAndBatchId = SimpleAuditRecord.builder().build(); + AuditRecord recordWithoutOptionals = SimpleAuditRecord.builder().build(); - FieldSelector fields = FieldFilterFlavorAdapter.getFieldsAvailableInRecord(recordWithoutClientIPAndBatchId, FieldSelector.ALL_FIELDS); + FieldSelector fields = FieldFilterFlavorAdapter.getFieldsAvailableInRecord(recordWithoutOptionals, FieldSelector.ALL_FIELDS); assertThat(fields.isSelected(FieldSelector.Field.CLIENT_IP)).isFalse(); assertThat(fields.isSelected(FieldSelector.Field.CLIENT_PORT)).isFalse(); assertThat(fields.isSelected(FieldSelector.Field.BATCH_ID)).isFalse(); + assertThat(fields.isSelected(FieldSelector.Field.SUBJECT)).isFalse(); } } diff --git a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestFieldSelector.java b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestFieldSelector.java index 68c7b961..2d52d58e 100644 --- a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestFieldSelector.java +++ b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestFieldSelector.java @@ -51,6 +51,7 @@ public void testFieldBitOrder() assertThat(Field.OPERATION.getBit()).isEqualTo(64); assertThat(Field.OPERATION_NAKED.getBit()).isEqualTo(128); assertThat(Field.TIMESTAMP.getBit()).isEqualTo(256); + assertThat(Field.SUBJECT.getBit()).isEqualTo(512); } @Test @@ -84,7 +85,7 @@ public void testAllFieldsSelected() assertAllFieldsAreSelected(fields); - assertThat(fields.getBitmap()).isEqualTo(511) + assertThat(fields.getBitmap()).isEqualTo(1023) .isEqualTo(FieldSelector.ALL_FIELDS.getBitmap()); } @@ -147,7 +148,7 @@ public void testInvalidBitmapLowerRange() public void testInvalidBitmapUpperRange() { assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> FieldSelector.fromBitmap(512)) // Only 9 fields available == Max 9 bits => max bitmap value 2^9 - 1 + .isThrownBy(() -> FieldSelector.fromBitmap(1024)) // Only 10 fields available == Max 10 bits => max bitmap value 2^10 - 1 .withMessageContaining("Bitmap value is out of bounds"); } diff --git a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestReadVersion2.java b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestReadVersion2.java new file mode 100644 index 00000000..395bc698 --- /dev/null +++ b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestReadVersion2.java @@ -0,0 +1,163 @@ +/* + * Copyright 2020 Telefonaktiebolaget LM Ericsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ericsson.bss.cassandra.ecaudit.common.chronicle; + +import java.io.File; +import java.net.InetAddress; +import java.util.UUID; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.ericsson.bss.cassandra.ecaudit.common.chronicle.AuditRecordReadMarshallable; +import com.ericsson.bss.cassandra.ecaudit.common.chronicle.WriteTestDataUtil; +import com.ericsson.bss.cassandra.ecaudit.common.record.Status; +import com.ericsson.bss.cassandra.ecaudit.common.record.StoredAuditRecord; +import net.openhft.chronicle.queue.ChronicleQueue; +import net.openhft.chronicle.queue.ChronicleQueueBuilder; +import net.openhft.chronicle.queue.ExcerptTailer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test reading records stored in the binary log file produced by {@link WriteTestDataUtil} using entry version 2 with the following data: + *
    + *
  • CLIENT_IP = 0.1.2.3 + *
  • CLIENT_PORT = 777 + *
  • COORDINATOR_IP = 4.5.6.7 + *
  • USER = "bob" + *
  • BATCH_ID = bd92aeb1-3373-4d6a-b65a-0d60295f66c9 + *
  • STATUS = SUCCEEDED + *
  • OPERATION = "SELECT SOMETHING" + *
  • OPERATION_NAKED = "SELECT SOMETHING NAKED" + *
  • TIMESTAMP = 1554188832013L + *
  • SUBJECT = "bob-the-subject + *
+ *

+ * Is written in 4 records with different fields selected: + *

    + *
  • record1 - Default fields selected (TIMESTAMP, CLIENT_IP, CLIENT_PORT, COORDINATOR_IP, USER, BATCH_ID, STATUS, OPERATION) + *
  • record2 - No fields selected + *
  • record3 - All fields selected (TIMESTAMP, CLIENT_IP, CLIENT_PORT, COORDINATOR_IP, USER, BATCH_ID, STATUS, OPERATION, OPERATION_NAKED, SUBJECT) + *
  • record4 - Custom fields selected (USER, STATUS, OPERATION_NAKED, SUBJECT) + *
+ */ +public class TestReadVersion2 +{ + private static ChronicleQueue chronicleQueue; + private static ExcerptTailer tailer; + + @BeforeClass + public static void beforeClass() + { + File queueDirVersion1 = new File("src/test/resources/q2"); + chronicleQueue = ChronicleQueueBuilder + .single(queueDirVersion1) + .blockSize(1024) + .readOnly(true) + .build(); + tailer = chronicleQueue.createTailer(); + } + + @AfterClass + public static void afterClass() + { + chronicleQueue.close(); + } + + @Test + public void test() throws Exception + { + readDefault(); + readEmpty(); + readFull(); + readCustom(); + } + + private void readDefault() throws Exception + { + StoredAuditRecord actualAuditRecord = readAuditRecordFromChronicle(); + + assertThat(actualAuditRecord.getClientAddress()).contains(InetAddress.getByName("0.1.2.3")); + assertThat(actualAuditRecord.getClientPort()).contains(777); + assertThat(actualAuditRecord.getCoordinatorAddress()).contains(InetAddress.getByName("4.5.6.7")); + assertThat(actualAuditRecord.getUser()).contains("bob"); + assertThat(actualAuditRecord.getBatchId()).contains(UUID.fromString("bd92aeb1-3373-4d6a-b65a-0d60295f66c9")); + assertThat(actualAuditRecord.getStatus()).contains(Status.SUCCEEDED); + assertThat(actualAuditRecord.getOperation()).contains("SELECT SOMETHING"); + assertThat(actualAuditRecord.getNakedOperation()).isEmpty(); + assertThat(actualAuditRecord.getTimestamp()).contains(1554188832013L); + assertThat(actualAuditRecord.getSubject()).isEmpty(); + } + + private void readEmpty() + { + StoredAuditRecord actualAuditRecord = readAuditRecordFromChronicle(); + + assertThat(actualAuditRecord.getClientAddress()).isEmpty(); + assertThat(actualAuditRecord.getClientPort()).isEmpty(); + assertThat(actualAuditRecord.getCoordinatorAddress()).isEmpty(); + assertThat(actualAuditRecord.getUser()).isEmpty(); + assertThat(actualAuditRecord.getBatchId()).isEmpty(); + assertThat(actualAuditRecord.getStatus()).isEmpty(); + assertThat(actualAuditRecord.getOperation()).isEmpty(); + assertThat(actualAuditRecord.getNakedOperation()).isEmpty(); + assertThat(actualAuditRecord.getTimestamp()).isEmpty(); + assertThat(actualAuditRecord.getSubject()).isEmpty(); + } + + private void readFull() throws Exception + { + StoredAuditRecord actualAuditRecord = readAuditRecordFromChronicle(); + + assertThat(actualAuditRecord.getClientAddress()).contains(InetAddress.getByName("0.1.2.3")); + assertThat(actualAuditRecord.getClientPort()).contains(777); + assertThat(actualAuditRecord.getCoordinatorAddress()).contains(InetAddress.getByName("4.5.6.7")); + assertThat(actualAuditRecord.getUser()).contains("bob"); + assertThat(actualAuditRecord.getBatchId()).contains(UUID.fromString("bd92aeb1-3373-4d6a-b65a-0d60295f66c9")); + assertThat(actualAuditRecord.getStatus()).contains(Status.SUCCEEDED); + assertThat(actualAuditRecord.getOperation()).contains("SELECT SOMETHING"); + assertThat(actualAuditRecord.getNakedOperation()).contains("SELECT SOMETHING NAKED"); + assertThat(actualAuditRecord.getTimestamp()).contains(1554188832013L); + assertThat(actualAuditRecord.getSubject()).contains("bob-the-subject"); + } + + private void readCustom() + { + StoredAuditRecord actualAuditRecord = readAuditRecordFromChronicle(); + + assertThat(actualAuditRecord.getClientAddress()).isEmpty(); + assertThat(actualAuditRecord.getClientPort()).isEmpty(); + assertThat(actualAuditRecord.getCoordinatorAddress()).isEmpty(); + assertThat(actualAuditRecord.getUser()).contains("bob"); + assertThat(actualAuditRecord.getBatchId()).isEmpty(); + assertThat(actualAuditRecord.getStatus()).contains(Status.SUCCEEDED); + assertThat(actualAuditRecord.getOperation()).isEmpty(); + assertThat(actualAuditRecord.getNakedOperation()).contains("SELECT SOMETHING NAKED"); + assertThat(actualAuditRecord.getTimestamp()).isEmpty(); + assertThat(actualAuditRecord.getSubject()).contains("bob-the-subject"); + } + + private StoredAuditRecord readAuditRecordFromChronicle() + { + AuditRecordReadMarshallable readMarshallable = new AuditRecordReadMarshallable(); + + tailer.readDocument(readMarshallable); + + return readMarshallable.getAuditRecord(); + } +} diff --git a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestWriteReadVersionCurrent.java b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestWriteReadVersionCurrent.java index 51677fc9..1c26364e 100644 --- a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestWriteReadVersionCurrent.java +++ b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/TestWriteReadVersionCurrent.java @@ -60,6 +60,20 @@ public void after() chronicleQueue.close(); } + @Test + public void writeReadSubject() throws Exception + { + AuditRecord expectedAuditRecord = likeGenericRecord().withSubject("bob-the-subject").build(); + + FieldSelector fieldsWithSubject = FieldSelector.DEFAULT_FIELDS.withField(FieldSelector.Field.SUBJECT); + + writeAuditRecordToChronicle(expectedAuditRecord, fieldsWithSubject); + + StoredAuditRecord actualAuditRecord = readAuditRecordFromChronicle(); + + assertThatRecordsMatch(actualAuditRecord, expectedAuditRecord); + } + @Test public void writeReadBatch() throws Exception { @@ -116,7 +130,12 @@ private SimpleAuditRecord.Builder likeGenericRecord() throws UnknownHostExceptio private void writeAuditRecordToChronicle(AuditRecord auditRecord) { - WriteMarshallable writeMarshallable = new AuditRecordWriteMarshallable(auditRecord, FieldSelector.DEFAULT_FIELDS); + writeAuditRecordToChronicle(auditRecord, FieldSelector.DEFAULT_FIELDS); + } + + private void writeAuditRecordToChronicle(AuditRecord auditRecord, FieldSelector fields) + { + WriteMarshallable writeMarshallable = new AuditRecordWriteMarshallable(auditRecord, fields); ExcerptAppender appender = chronicleQueue.acquireAppender(); appender.writeDocument(writeMarshallable); @@ -143,5 +162,6 @@ private void assertThatRecordsMatch(StoredAuditRecord actualAuditRecord, AuditRe assertThat(actualAuditRecord.getNakedOperation()).isEmpty(); assertThat(actualAuditRecord.getUser()).contains(expectedAuditRecord.getUser()); assertThat(actualAuditRecord.getTimestamp()).contains(expectedAuditRecord.getTimestamp()); + assertThat(actualAuditRecord.getSubject()).isEqualTo(expectedAuditRecord.getSubject()); } } diff --git a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/WriteTestDataUtil.java b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/WriteTestDataUtil.java index 9b4725f7..f6c5588b 100644 --- a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/WriteTestDataUtil.java +++ b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/chronicle/WriteTestDataUtil.java @@ -45,7 +45,7 @@ public class WriteTestDataUtil { public static void main(String[] args) throws Exception { - String version = "X"; // Set the version here! + String version = "2"; // Set the version here! // Data AuditRecord record = SimpleAuditRecord.builder() @@ -56,6 +56,7 @@ public static void main(String[] args) throws Exception .withStatus(Status.SUCCEEDED) .withOperation(mockOperation("SELECT SOMETHING", "SELECT SOMETHING NAKED")) .withTimestamp(1554188832013L) + .withSubject("bob-the-subject") .build(); // Write Data to Queue @@ -68,7 +69,7 @@ public static void main(String[] args) throws Exception appender.writeDocument(new AuditRecordWriteMarshallable(record, FieldSelector.DEFAULT_FIELDS)); appender.writeDocument(new AuditRecordWriteMarshallable(record, FieldSelector.NO_FIELDS)); appender.writeDocument(new AuditRecordWriteMarshallable(record, FieldSelector.ALL_FIELDS)); - appender.writeDocument(new AuditRecordWriteMarshallable(record, FieldSelector.fromFields(asList("USER", "OPERATION_NAKED", "STATUS")))); // Custom fields + appender.writeDocument(new AuditRecordWriteMarshallable(record, FieldSelector.fromFields(asList("USER", "OPERATION_NAKED", "STATUS", "SUBJECT")))); // Custom fields chronicleQueue.close(); } diff --git a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/record/SimpleAuditRecord.java b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/record/SimpleAuditRecord.java index edf5ad25..3c1f71a9 100644 --- a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/record/SimpleAuditRecord.java +++ b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/record/SimpleAuditRecord.java @@ -29,6 +29,7 @@ public class SimpleAuditRecord implements AuditRecord private final Status status; private final AuditOperation operation; private final long timestamp; + private final String subject; private SimpleAuditRecord(Builder builder) { @@ -39,6 +40,7 @@ private SimpleAuditRecord(Builder builder) this.status = builder.status; this.operation = builder.operation; this.timestamp = builder.timestamp; + this.subject = builder.subject; } @Override @@ -83,6 +85,12 @@ public AuditOperation getOperation() return operation; } + @Override + public Optional getSubject() + { + return Optional.ofNullable(subject); + } + public static Builder builder() { return new Builder(); @@ -97,6 +105,7 @@ public static class Builder private Status status; private AuditOperation operation; private long timestamp; + private String subject; public Builder withClientAddress(InetSocketAddress clientAddress) { @@ -140,6 +149,12 @@ public Builder withTimestamp(long timestamp) return this; } + public Builder withSubject(String subject) + { + this.subject = subject; + return this; + } + public AuditRecord build() { return new SimpleAuditRecord(this); diff --git a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/record/TestStoredAuditRecord.java b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/record/TestStoredAuditRecord.java index 6a2fc79d..47d005e0 100644 --- a/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/record/TestStoredAuditRecord.java +++ b/common/src/test/java/com/ericsson/bss/cassandra/ecaudit/common/record/TestStoredAuditRecord.java @@ -42,6 +42,7 @@ public void testEmptyRecord() assertThat(record.getOperation()).isEmpty(); assertThat(record.getNakedOperation()).isEmpty(); assertThat(record.getTimestamp()).isEmpty(); + assertThat(record.getSubject()).isEmpty(); } @Test @@ -57,6 +58,7 @@ public void testFullRecord() throws UnknownHostException .withOperation("insert into user (name) values (?)['Bob']") .withNakedOperation("insert into user (name) values (?)") .withTimestamp(123456789L) + .withSubject("subject") .build(); assertThat(record.getClientAddress()).contains(InetAddress.getByName("1.2.3.4")); @@ -68,5 +70,6 @@ public void testFullRecord() throws UnknownHostException assertThat(record.getOperation()).contains("insert into user (name) values (?)['Bob']"); assertThat(record.getNakedOperation()).contains("insert into user (name) values (?)"); assertThat(record.getTimestamp()).contains(123456789L); + assertThat(record.getSubject()).contains("subject"); } } diff --git a/common/src/test/resources/q2/20200318.cq4 b/common/src/test/resources/q2/20200318.cq4 new file mode 100644 index 00000000..e85abfa8 Binary files /dev/null and b/common/src/test/resources/q2/20200318.cq4 differ diff --git a/common/src/test/resources/q2/directory-listing.cq4t b/common/src/test/resources/q2/directory-listing.cq4t new file mode 100644 index 00000000..5bb89a97 Binary files /dev/null and b/common/src/test/resources/q2/directory-listing.cq4t differ diff --git a/conf/audit.yaml b/conf/audit.yaml index 0caed781..edd17c87 100644 --- a/conf/audit.yaml +++ b/conf/audit.yaml @@ -18,6 +18,15 @@ # This configuration file will be automatically picked up by ecAudit if it is placed in the Cassandra configuration # directory. +# The authenticator backend where WrappingAuditAuthenticator delegate authentication requests. +# +# The value must represent a class name implementing the IAuditAuthenticator interface. This can be either: +# - com.ericsson.bss.cassandra.ecaudit.auth.AuditPasswordAuthenticator, provided by ecAudit +# - a custom implementation of IAuditAuthenticator +# +# By default ecAudit delegates to the AuditPasswordAuthenticator. +wrapped_authenticator: com.ericsson.bss.cassandra.ecaudit.auth.AuditPasswordAuthenticator + # The authorizer backend where the AuditAuthorizer delegate authorization requests. # diff --git a/doc/setup.md b/doc/setup.md index 3e389500..284e6907 100644 --- a/doc/setup.md +++ b/doc/setup.md @@ -28,6 +28,12 @@ Read on below to learn how to tune the logger backend and manage audit whitelist In the [audit.yaml reference](audit_yaml_reference.md) you'll find more details about different options. +### Wrapped Authenticator Backend + +The ecAudit plug-in supports wrapping ```authenticators``` implementing the ```IAuditAuthenticator``` interface, using +the ```wrapped_authenticator``` setting in the ```audit.yaml``` file together with setting the authenticator as ```WrappingAuditAuthenticator``` in the ```cassandra.yaml```. +This is useful if requiring a non-default (i.e. not ```PasswordAuthenticator```) implementation of an authenticator but still want authentication logs. +If omitting the ```wrapped_autenticator``` setting when using ```WrappingAuditAuthenticator```, then the plug-in will fall back to using the default ```AuditPasswordAuthenticator```. ### Wrapped Authorizer Backend diff --git a/ecaudit/src/main/java/com/ericsson/bss/cassandra/ecaudit/AuditAdapter.java b/ecaudit/src/main/java/com/ericsson/bss/cassandra/ecaudit/AuditAdapter.java index c2274944..d2a78ed0 100644 --- a/ecaudit/src/main/java/com/ericsson/bss/cassandra/ecaudit/AuditAdapter.java +++ b/ecaudit/src/main/java/com/ericsson/bss/cassandra/ecaudit/AuditAdapter.java @@ -183,12 +183,27 @@ public void auditBatch(BatchStatement statement, UUID uuid, ClientState state, B * @throws AuthenticationException if the audit operation could not be performed */ public void auditAuth(String username, Status status, long timestamp) throws AuthenticationException + { + auditAuth(username, null, status, timestamp); + } + + /** + * Audit an authentication attempt with a subject. + * + * @param userName the user to authenticate + * @param subject the subject to authenticate + * @param status the status of the operation + * @param timestamp the system timestamp for the request + * @throws AuthenticationException if the audit operation could not be performed + */ + public void auditAuth(String userName, String subject, Status status, long timestamp) { if (auditor.shouldLogForStatus(status)) { AuditEntry logEntry = entryBuilderFactory.createAuthenticationEntryBuilder() .coordinator(FBUtilities.getBroadcastAddress()) - .user(username) + .user(userName) + .subject(subject) .status(status) .operation(statusToAuthenticationOperation(status)) .timestamp(timestamp) diff --git a/ecaudit/src/main/java/com/ericsson/bss/cassandra/ecaudit/auth/AuditPasswordAuthenticator.java b/ecaudit/src/main/java/com/ericsson/bss/cassandra/ecaudit/auth/AuditPasswordAuthenticator.java index a58962ee..27676094 100644 --- a/ecaudit/src/main/java/com/ericsson/bss/cassandra/ecaudit/auth/AuditPasswordAuthenticator.java +++ b/ecaudit/src/main/java/com/ericsson/bss/cassandra/ecaudit/auth/AuditPasswordAuthenticator.java @@ -21,6 +21,7 @@ import java.util.Set; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,14 +30,16 @@ import org.apache.cassandra.auth.AuthenticatedUser; import org.apache.cassandra.auth.IAuthenticator; import org.apache.cassandra.auth.IResource; +import org.apache.cassandra.auth.IRoleManager; import org.apache.cassandra.auth.PasswordAuthenticator; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.exceptions.AuthenticationException; import org.apache.cassandra.exceptions.ConfigurationException; /** * A decorator of {@link PasswordAuthenticator} with added audit logging. */ -public class AuditPasswordAuthenticator implements IAuthenticator +public class AuditPasswordAuthenticator implements IAuditAuthenticator { private static final Logger LOG = LoggerFactory.getLogger(AuditPasswordAuthenticator.class); @@ -89,8 +92,32 @@ public void setup() @Override public SaslNegotiator newSaslNegotiator() { + return newAuditSaslNegotiator(); + } + + @Override + public AuditSaslNegotiator newAuditSaslNegotiator() + { + // For BWC, check if being wrapped. If not, log authentication attempts as normal. + // If being wrapped, i.e. not used standalone, let the wrapper do the authentication + // logging for us (disable logging) + IAuthenticator authenticator = DatabaseDescriptor.getAuthenticator(); + boolean disableLogging = authenticator instanceof WrappingAuditAuthenticator; + LOG.debug("Setting up SASL negotiation with client peer"); - return new AuditPlainTextSaslAuthenticator(wrappedAuthenticator.newSaslNegotiator()); + return new AuditPlainTextSaslAuthenticator(wrappedAuthenticator.newSaslNegotiator(), disableLogging); + } + + @Override + public Set supportedOptions() + { + return ImmutableSet.of(IRoleManager.Option.LOGIN, IRoleManager.Option.SUPERUSER, IRoleManager.Option.PASSWORD, IRoleManager.Option.OPTIONS); + } + + @Override + public Set alterableOptions() + { + return ImmutableSet.of(IRoleManager.Option.PASSWORD, IRoleManager.Option.OPTIONS); } @Override @@ -99,15 +126,17 @@ public AuthenticatedUser legacyAuthenticate(Map credentials) thr return wrappedAuthenticator.legacyAuthenticate(credentials); } - private class AuditPlainTextSaslAuthenticator implements SaslNegotiator + private class AuditPlainTextSaslAuthenticator implements AuditSaslNegotiator { private final SaslNegotiator saslNegotiator; private String decodedUsername; + private final boolean disableLogging; - AuditPlainTextSaslAuthenticator(SaslNegotiator saslNegotiator) + AuditPlainTextSaslAuthenticator(SaslNegotiator saslNegotiator, boolean disableLogging) { this.saslNegotiator = saslNegotiator; + this.disableLogging = disableLogging; } @Override @@ -126,6 +155,13 @@ public boolean isComplete() @Override public AuthenticatedUser getAuthenticatedUser() throws AuthenticationException { + // If wrapped by the WrappingAuditAuthenticator just try to authenticate without logging + // since the wrapper will log for us + if (disableLogging) + { + return saslNegotiator.getAuthenticatedUser(); + } + long timestamp = System.currentTimeMillis(); auditAdapter.auditAuth(decodedUsername, Status.ATTEMPT, timestamp); try @@ -141,6 +177,12 @@ public AuthenticatedUser getAuthenticatedUser() throws AuthenticationException } } + @Override + public String getUser() + { + return decodedUsername; + } + /** * Decoded the credentials so that we know what username was used in the authentication attempt. * diff --git a/ecaudit/src/main/java/com/ericsson/bss/cassandra/ecaudit/auth/AuditRoleManager.java b/ecaudit/src/main/java/com/ericsson/bss/cassandra/ecaudit/auth/AuditRoleManager.java index 0fae9708..926f3298 100644 --- a/ecaudit/src/main/java/com/ericsson/bss/cassandra/ecaudit/auth/AuditRoleManager.java +++ b/ecaudit/src/main/java/com/ericsson/bss/cassandra/ecaudit/auth/AuditRoleManager.java @@ -28,8 +28,10 @@ import org.apache.cassandra.auth.AuthenticatedUser; import org.apache.cassandra.auth.CassandraRoleManager; import org.apache.cassandra.auth.DataResource; +import org.apache.cassandra.auth.IAuthenticator; import org.apache.cassandra.auth.IResource; import org.apache.cassandra.auth.IRoleManager; +import org.apache.cassandra.auth.PasswordAuthenticator; import org.apache.cassandra.auth.RoleOptions; import org.apache.cassandra.auth.RoleResource; import org.apache.cassandra.config.DatabaseDescriptor; @@ -68,11 +70,11 @@ public AuditRoleManager() { this(new CassandraRoleManager(), new AuditWhitelistManager(), - DatabaseDescriptor.getAuthenticator() instanceof AuditPasswordAuthenticator); + DatabaseDescriptor.getAuthenticator()); } @VisibleForTesting - AuditRoleManager(IRoleManager wrappedRoleManager, AuditWhitelistManager whitelistManager, boolean hasAuditPasswordAuthenticator) + AuditRoleManager(IRoleManager wrappedRoleManager, AuditWhitelistManager whitelistManager, IAuthenticator authenticator) { LOG.info("Auditing enabled on role manager"); @@ -80,12 +82,22 @@ public AuditRoleManager() this.whitelistManager = whitelistManager; permissionChecker = new PermissionChecker(); - supportedOptions = hasAuditPasswordAuthenticator - ? ImmutableSet.of(Option.LOGIN, Option.SUPERUSER, Option.PASSWORD, Option.OPTIONS) - : ImmutableSet.of(Option.LOGIN, Option.SUPERUSER); - alterableOptions = hasAuditPasswordAuthenticator - ? ImmutableSet.of(Option.PASSWORD, Option.OPTIONS) - : ImmutableSet.of(); + if (authenticator instanceof IOptionsProvider) + { + IOptionsProvider optionsProvider = (IOptionsProvider) authenticator; + supportedOptions = optionsProvider.supportedOptions(); + alterableOptions = optionsProvider.alterableOptions(); + } + else + { + // To be compatible with CassandraRoleManager options + supportedOptions = authenticator.getClass() == PasswordAuthenticator.class + ? ImmutableSet.of(Option.LOGIN, Option.SUPERUSER, Option.PASSWORD) + : ImmutableSet.of(Option.LOGIN, Option.SUPERUSER); + alterableOptions = authenticator.getClass().equals(PasswordAuthenticator.class) + ? ImmutableSet.of(Option.PASSWORD) + : ImmutableSet.