Skip to content

Commit 3b29aa5

Browse files
adrwsvc-squareup-copybara
authored andcommitted
Add @AuditRequestResponse which will send request, response,
and metadata to the bound `AuditClient`. This involved a refactor of the existing RequestLoggingInterceptor to provide for a more easily extensible hook to support logging, sending to audit client, or other future use cases. GitOrigin-RevId: 2bce3195b1e703101597dca876ba269eb3cb1991
1 parent 60aa451 commit 3b29aa5

25 files changed

+1439
-537
lines changed

misk-audit-client/api/misk-audit-client.api

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,25 @@ public final class misk/audit/AuditClientConfig {
1717
public fun toString ()Ljava/lang/String;
1818
}
1919

20+
public abstract interface annotation class misk/audit/AuditRequestResponse : java/lang/annotation/Annotation {
21+
public abstract fun applicationName ()Ljava/lang/String;
22+
public abstract fun automatedChange ()Z
23+
public abstract fun description ()Ljava/lang/String;
24+
public abstract fun detailURL ()Ljava/lang/String;
25+
public abstract fun includeRequest ()Z
26+
public abstract fun includeRequestHeaders ()Z
27+
public abstract fun includeReseponseHeaders ()Z
28+
public abstract fun includeResponse ()Z
29+
public abstract fun richDescription ()Ljava/lang/String;
30+
public abstract fun target ()Ljava/lang/String;
31+
}
32+
2033
public final class misk/audit/FakeAuditClient : misk/testing/FakeFixture, misk/audit/AuditClient {
2134
public static final field Companion Lmisk/audit/FakeAuditClient$Companion;
22-
public fun <init> (Ljava/lang/String;Lmisk/scope/ActionScoped;Ljava/time/Clock;Lwisp/deployment/Deployment;)V
35+
public static final field DEFAULT_USER Ljava/lang/String;
36+
public fun <init> (Lmisk/audit/FakeAuditClient$OptionalBinder;)V
2337
public final fun getEnableLogging ()Z
24-
public final fun getSentEvents ()Ljava/util/List;
38+
public final fun getSentEvents ()Ljava/util/concurrent/LinkedBlockingDeque;
2539
public fun logEvent (Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
2640
public final fun setEnableLogging (Z)V
2741
}
@@ -62,6 +76,18 @@ public final class misk/audit/FakeAuditClient$FakeAuditEvent {
6276
public fun toString ()Ljava/lang/String;
6377
}
6478

79+
public final class misk/audit/FakeAuditClient$OptionalBinder {
80+
public fun <init> ()V
81+
public final fun getAppName ()Ljava/lang/String;
82+
public final fun getCallerProvider ()Lmisk/scope/ActionScoped;
83+
public final fun getClock ()Ljava/time/Clock;
84+
public final fun getDeployment ()Lwisp/deployment/Deployment;
85+
public final fun setAppName (Ljava/lang/String;)V
86+
public final fun setCallerProvider (Lmisk/scope/ActionScoped;)V
87+
public final fun setClock (Ljava/time/Clock;)V
88+
public final fun setDeployment (Lwisp/deployment/Deployment;)V
89+
}
90+
6591
public final class misk/audit/FakeAuditClientModule : misk/inject/KAbstractModule {
6692
public fun <init> ()V
6793
}

misk-audit-client/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dependencies {
2424
testFixturesImplementation(project(":misk-inject"))
2525
testFixturesImplementation(project(":misk-testing-api"))
2626
testFixturesImplementation(project(":wisp:wisp-logging"))
27+
testFixturesImplementation(project(":wisp:wisp-time-testing"))
2728
}
2829

2930
mavenPublishing {

misk-audit-client/src/main/kotlin/misk/audit/AuditClient.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,37 @@ interface AuditClient {
2323
applicationName: String? = null,
2424
)
2525
}
26+
27+
/**
28+
* Annotation indicating that request and response information should be sent to the audit client.
29+
*
30+
* If you would like to turn off logging for all non-successful requests, use the parameter [successOnly].
31+
* otherwise, all requests will be sent to the audit client including failures.
32+
*
33+
* If arguments and responses may include sensitive information, it is expected that the toString()
34+
* methods of these objects will redact it.
35+
*/
36+
@Retention(AnnotationRetention.RUNTIME)
37+
@Target(AnnotationTarget.FUNCTION)
38+
annotation class AuditRequestResponse(
39+
/** The specific resource the event relates to. */
40+
val target: String = "",
41+
/** Human-readable description of the event, limited to 4000 characters. Optional, but suggested even if richDescription is provided. */
42+
val description: String = "",
43+
/** Whether this change is being done directly by a human (false) or by an automated system (true) */
44+
val automatedChange: Boolean = false,
45+
/** Rich text description of the event, limited to 4000 characters. Optional if description is provided. */
46+
val richDescription: String = "",
47+
/** URL to a page with more details about the event. */
48+
val detailURL: String = "",
49+
/** Name of the application (slug) if different from the eventSource, otherwise null. */
50+
val applicationName: String = "",
51+
/** If false, request body will not be included. */
52+
val includeRequest: Boolean = false,
53+
/** If false, response body will not be included. */
54+
val includeResponse: Boolean = false,
55+
/** If false, request headers will not be included. */
56+
val includeRequestHeaders: Boolean = false,
57+
/** If false, response headers will not be included. */
58+
val includeReseponseHeaders: Boolean = false,
59+
)

misk-audit-client/src/test/kotlin/misk/audit/FakeAuditClientTest.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ package misk.audit
22

33
import jakarta.inject.Inject
44
import misk.MiskTestingServiceModule
5-
import misk.config.AppNameModule
65
import misk.environment.DeploymentModule
76
import misk.inject.KAbstractModule
87
import misk.logging.LogCollectorModule
9-
import misk.security.authz.AccessControlModule
108
import misk.testing.MiskTest
119
import misk.testing.MiskTestModule
1210
import misk.web.MiskWebModule
@@ -33,7 +31,7 @@ class FakeAuditClientTest {
3331
)
3432
assertThat(logCollector.takeMessages(FakeAuditClient::class).size).isEqualTo(0)
3533
assertThat(client.sentEvents).hasSize(1)
36-
assertThat(client.sentEvents[0]).isEqualTo(
34+
assertThat(client.sentEvents.take()).isEqualTo(
3735
FakeAuditClient.FakeAuditEvent(
3836
eventSource = "test-app",
3937
eventTarget = "the-database-001",
@@ -65,9 +63,7 @@ class FakeAuditClientTest {
6563

6664
class TestModule : KAbstractModule() {
6765
override fun configure() {
68-
install(AppNameModule("test-app"))
6966
install(MiskWebModule(WebConfig(0)))
70-
install(AccessControlModule())
7167
install(MiskTestingServiceModule())
7268
install(DeploymentModule(TESTING))
7369
install(LogCollectorModule())

misk-audit-client/src/testFixtures/kotlin/misk/audit/FakeAuditClient.kt

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,21 @@ import misk.config.AppName
77
import misk.scope.ActionScoped
88
import misk.testing.FakeFixture
99
import wisp.deployment.Deployment
10+
import wisp.deployment.TESTING
1011
import wisp.logging.getLogger
12+
import wisp.time.FakeClock
1113
import java.time.Clock
14+
import java.util.concurrent.LinkedBlockingDeque
1215
import kotlin.time.Duration.Companion.nanoseconds
1316
import kotlin.time.DurationUnit
1417

1518
@Singleton
1619
class FakeAuditClient @Inject constructor(
17-
@AppName private val appName: String,
18-
private val callerProvider: ActionScoped<MiskCaller?>,
19-
private val clock: Clock,
20-
private val deployment: Deployment,
21-
): AuditClient, FakeFixture() {
22-
private val region: String = "us-west-2"
23-
val sentEvents: MutableList<FakeAuditEvent> by resettable { mutableListOf() }
20+
private val optionalBinder: OptionalBinder,
21+
) : AuditClient, FakeFixture() {
22+
private val region = "us-west-2"
23+
24+
val sentEvents: LinkedBlockingDeque<FakeAuditEvent> by resettable { LinkedBlockingDeque<FakeAuditEvent>() }
2425
var enableLogging: Boolean by resettable { false }
2526

2627
data class FakeAuditEvent(
@@ -50,18 +51,24 @@ class FakeAuditClient @Inject constructor(
5051
) {
5152
val event =
5253
FakeAuditEvent(
53-
eventSource = appName,
54+
eventSource = optionalBinder.appName,
5455
eventTarget = target,
55-
timestampSent = clock.instant().toEpochMilli().nanoseconds.toInt(DurationUnit.NANOSECONDS),
56-
applicationName = applicationName ?: appName,
56+
timestampSent = optionalBinder.clock.instant()
57+
.toEpochMilli().nanoseconds.toInt(DurationUnit.NANOSECONDS),
58+
applicationName = applicationName ?: optionalBinder.appName,
5759
approverLDAP = approverLDAP,
5860
automatedChange = automatedChange,
5961
description = description,
6062
richDescription = richDescription,
61-
environment = deployment.mapToEnvironmentName(),
63+
environment = optionalBinder.deployment.mapToEnvironmentName(),
6264
detailURL = detailURL,
6365
region = region,
64-
requestorLDAP = requestorLDAP ?: if (automatedChange) null else callerProvider.get()?.principal,
66+
requestorLDAP = requestorLDAP
67+
?: if (automatedChange) {
68+
null
69+
} else {
70+
optionalBinder.callerProvider.getIfInScope()?.principal ?: DEFAULT_USER
71+
},
6572
)
6673

6774
sentEvents.add(event)
@@ -73,6 +80,25 @@ class FakeAuditClient @Inject constructor(
7380

7481
companion object {
7582
private val logger = getLogger<FakeAuditClient>()
83+
const val DEFAULT_USER = "default-user"
7684
}
7785

86+
/**
87+
* https://github.com/google/guice/wiki/FrequentlyAskedQuestions#how-can-i-inject-optional-parameters-into-a-constructor
88+
*/
89+
@Singleton
90+
class OptionalBinder @Inject constructor() {
91+
@com.google.inject.Inject(optional = true)
92+
@AppName var appName: String = "test-app"
93+
94+
@com.google.inject.Inject(optional = true)
95+
var callerProvider: ActionScoped<MiskCaller?> =
96+
ActionScoped.of(MiskCaller(user = DEFAULT_USER))
97+
98+
@com.google.inject.Inject(optional = true)
99+
var clock: Clock = FakeClock()
100+
101+
@com.google.inject.Inject(optional = true)
102+
var deployment: Deployment = TESTING
103+
}
78104
}

misk-audit-client/src/testFixtures/kotlin/misk/audit/FakeAuditClientModule.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ package misk.audit
22

33
import misk.inject.KAbstractModule
44
import misk.inject.asSingleton
5+
import misk.testing.TestFixture
6+
import misk.web.interceptors.hooks.AuditClientHook
7+
import misk.web.interceptors.hooks.RequestResponseHook
58

6-
class FakeAuditClientModule: KAbstractModule() {
9+
class FakeAuditClientModule : KAbstractModule() {
710
override fun configure() {
811
bind<AuditClient>().to<FakeAuditClient>().asSingleton()
12+
multibind<TestFixture>().to<FakeAuditClient>()
13+
multibind<RequestResponseHook.Factory>().to<AuditClientHook.Factory>()
914
}
1015
}

misk-hibernate/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dependencies {
4444
implementation(project(":misk-api"))
4545
implementation(project(":misk-actions"))
4646
implementation(project(":misk-admin"))
47+
implementation(project(":misk-audit-client"))
4748
implementation(project(":misk-backoff"))
4849
implementation(project(":misk-core"))
4950
implementation(project(":misk-crypto"))
@@ -60,6 +61,7 @@ dependencies {
6061
testImplementation(project(":wisp:wisp-logging-testing"))
6162
testImplementation(project(":wisp:wisp-time-testing"))
6263
testImplementation(project(":misk-hibernate-testing"))
64+
testImplementation(testFixtures(project(":misk-audit-client")))
6365
testImplementation(testFixtures(project(":misk-jdbc")))
6466
testImplementation(project(":misk-testing"))
6567
testImplementation(testFixtures(project(":misk-crypto")))

misk-hibernate/src/main/kotlin/misk/hibernate/actions/HibernateDatabaseQueryDynamicAction.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import misk.web.metadata.database.DatabaseQueryMetadata
2424
import wisp.logging.getLogger
2525
import jakarta.inject.Inject
2626
import jakarta.inject.Singleton
27+
import misk.audit.AuditRequestResponse
2728
import misk.web.dashboard.AdminDashboardAccess
2829
import kotlin.reflect.KClass
2930

@@ -40,6 +41,7 @@ internal class HibernateDatabaseQueryDynamicAction @Inject constructor(
4041
@RequestContentType(MediaTypes.APPLICATION_JSON)
4142
@ResponseContentType(MediaTypes.APPLICATION_JSON)
4243
@AdminDashboardAccess
44+
@AuditRequestResponse
4345
fun query(@RequestBody request: Request): Response {
4446
val caller = callerProvider.get()!!
4547
val queryClass = request.queryClass

misk-hibernate/src/main/kotlin/misk/hibernate/actions/HibernateDatabaseQueryStaticAction.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import wisp.logging.getLogger
2828
import java.lang.reflect.ParameterizedType
2929
import jakarta.inject.Inject
3030
import jakarta.inject.Singleton
31+
import misk.audit.AuditRequestResponse
3132
import misk.web.dashboard.AdminDashboardAccess
3233
import kotlin.reflect.KClass
3334

@@ -45,6 +46,7 @@ internal class HibernateDatabaseQueryStaticAction @Inject constructor(
4546
@RequestContentType(MediaTypes.APPLICATION_JSON)
4647
@ResponseContentType(MediaTypes.APPLICATION_JSON)
4748
@AdminDashboardAccess
49+
@AuditRequestResponse
4850
fun query(@RequestBody request: Request): Response {
4951
val caller = callerProvider.get()!!
5052
val queryClass = request.queryClass

misk-hibernate/src/test/kotlin/misk/hibernate/actions/HibernateDatabaseQueryDynamicActionTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.junit.jupiter.api.BeforeEach
1616
import org.junit.jupiter.api.Test
1717
import java.time.LocalDate
1818
import jakarta.inject.Inject
19+
import misk.audit.FakeAuditClient
1920
import javax.persistence.Transient
2021
import kotlin.reflect.full.declaredMemberProperties
2122
import kotlin.reflect.jvm.javaField
@@ -34,6 +35,7 @@ class HibernateDatabaseQueryDynamicActionTest {
3435
HibernateDatabaseQueryDynamicAction.Response
3536
>
3637
@Inject @Movies lateinit var transacter: Transacter
38+
@Inject lateinit var auditClient: FakeAuditClient
3739

3840
@BeforeEach
3941
fun before() {
@@ -225,5 +227,20 @@ class HibernateDatabaseQueryDynamicActionTest {
225227
),
226228
results.results
227229
)
230+
231+
assertEquals(FakeAuditClient.FakeAuditEvent(
232+
eventSource = "test-app",
233+
eventTarget = "HibernateDatabaseQueryDynamicAction",
234+
timestampSent = 2147483647,
235+
applicationName = "test-app",
236+
approverLDAP = null,
237+
automatedChange = false,
238+
description = "HibernateDatabaseQueryDynamicAction principal=joey",
239+
richDescription = "HibernateDatabaseQueryDynamicAction principal=joey time=0.000 ns code=200",
240+
environment = "testing",
241+
detailURL = null,
242+
region = "us-west-2",
243+
requestorLDAP = "joey"
244+
), auditClient.sentEvents.take())
228245
}
229246
}

0 commit comments

Comments
 (0)