Skip to content

Commit 5b1786c

Browse files
authored
[RORDEV-1573] Enable/Disable audit per block (#1175)
1 parent 29e1b68 commit 5b1786c

File tree

11 files changed

+94
-14
lines changed

11 files changed

+94
-14
lines changed

core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/Block.scala

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import tech.beshu.ror.implicits.*
4040
class Block(val name: Name,
4141
val policy: Policy,
4242
val verbosity: Verbosity,
43+
val audit: Audit,
4344
val rules: NonEmptyList[Rule])
4445
(implicit val loggingContext: LoggingContext)
4546
extends Logging {
@@ -101,12 +102,13 @@ object Block {
101102
def createFrom(name: Name,
102103
policy: Option[Policy],
103104
verbosity: Option[Verbosity],
105+
audit: Option[Audit],
104106
rules: NonEmptyList[RuleDefinition[Rule]])
105107
(implicit loggingContext: LoggingContext): Either[BlocksLevelCreationError, Block] = {
106108
val sortedRules = rules.sorted
107109
BlockValidator.validate(name, sortedRules) match {
108110
case Validated.Valid(_) =>
109-
Right(createBlockInstance(name, policy, verbosity, sortedRules))
111+
Right(createBlockInstance(name, policy, verbosity, audit, sortedRules))
110112
case Validated.Invalid(errors) =>
111113
implicit val validationErrorShow: Show[BlockValidationError] = blockValidationErrorShow(name)
112114
Left(BlocksLevelCreationError(Message(errors.toList.map(_.show).mkString("\n"))))
@@ -116,12 +118,14 @@ object Block {
116118
private def createBlockInstance(name: Name,
117119
policy: Option[Policy],
118120
verbosity: Option[Verbosity],
121+
audit: Option[Audit],
119122
rules: NonEmptyList[RuleDefinition[Rule]])
120123
(implicit loggingContext: LoggingContext) =
121124
new Block(
122125
name,
123126
policy.getOrElse(Block.Policy.Allow),
124127
verbosity.getOrElse(Block.Verbosity.Info),
128+
audit.getOrElse(Block.Audit.Enabled),
125129
rules.map(_.rule)
126130
)
127131

@@ -181,6 +185,16 @@ object Block {
181185
implicit val eq: Eq[Verbosity] = Eq.fromUniversalEquals
182186
}
183187

188+
sealed trait Audit
189+
190+
object Audit {
191+
case object Enabled extends Audit
192+
193+
case object Disabled extends Audit
194+
195+
implicit val eq: Eq[Audit] = Eq.fromUniversalEquals
196+
}
197+
184198
private class Lifter[B <: BlockContext] {
185199
def apply[A](task: Task[A]): WriterT[Task, Vector[HistoryItem[B]], A] =
186200
WriterT.liftF[Task, Vector[HistoryItem[B]], A](task)

core/src/main/scala/tech/beshu/ror/accesscontrol/factory/RawRorConfigBasedCoreFactory.scala

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,24 +220,34 @@ class RawRorConfigBasedCoreFactory(esVersion: EsVersion)
220220
case "info" => Right(Verbosity.Info)
221221
case "error" => Right(Verbosity.Error)
222222
case unknown => Left(BlocksLevelCreationError(Message(s"Unknown verbosity value: ${unknown.show}. Supported types: 'info'(default), 'error'.")))
223+
}.decoder
224+
implicit val blockAuditDecoder: Decoder[Block.Audit] =
225+
Decoder.instance { c =>
226+
for {
227+
enabled <- c.downField("enabled").as[Boolean]
228+
} yield {
229+
if (enabled) Block.Audit.Enabled else Block.Audit.Disabled
230+
}
223231
}
224-
.decoder
225232
Decoder
226233
.instance { c =>
227234
val result = for {
228235
name <- c.downField(Attributes.Block.name).as[Block.Name]
229236
policy <- c.downField(Attributes.Block.policy).as[Option[Block.Policy]]
230237
verbosity <- c.downField(Attributes.Block.verbosity).as[Option[Block.Verbosity]]
238+
audit <- c.downField(Attributes.Block.audit).as[Option[Block.Audit]]
231239
rules <- rulesNelDecoder(definitions, globalSettings, mocksProvider)
232240
.toSyncDecoder
233241
.decoder
234242
.tryDecode(c.withFocus(
235243
_.mapObject(_
236244
.remove(Attributes.Block.name)
237245
.remove(Attributes.Block.policy)
238-
.remove(Attributes.Block.verbosity))
246+
.remove(Attributes.Block.verbosity)
247+
.remove(Attributes.Block.audit)
248+
)
239249
))
240-
block <- Block.createFrom(name, policy, verbosity, rules).left.map(DecodingFailureOps.fromError(_))
250+
block <- Block.createFrom(name, policy, verbosity, audit, rules).left.map(DecodingFailureOps.fromError(_))
241251
} yield BlockDecodingResult(
242252
block = block,
243253
localUsers = rules.map(localUsersForRule).combineAll,
@@ -541,6 +551,7 @@ object RawRorConfigBasedCoreFactory {
541551
val name = "name"
542552
val policy = "type"
543553
val verbosity = "verbosity"
554+
val audit = "audit"
544555
}
545556

546557
}

core/src/main/scala/tech/beshu/ror/accesscontrol/logging/AccessControlListLoggingDecorator.scala

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,33 @@ class AccessControlListLoggingDecorator(val underlying: AccessControlList,
105105
import tech.beshu.ror.accesscontrol.logging.AccessControlListLoggingDecorator.responseContextShow
106106
logger.info(responseContextShow[B].show(responseContext))
107107
}
108-
auditingTool.foreach {
109-
_
110-
.audit(responseContext, auditEnvironmentContext)
111-
.runAsync {
112-
case Right(_) =>
113-
case Left(ex) =>
114-
logger.warn(s"[${responseContext.requestContext.id.show}] Auditing issue", ex)
108+
blockAuditSettings(responseContext) match {
109+
case Some(Block.Audit.Disabled) =>
110+
()
111+
case None | Some(Block.Audit.Enabled) =>
112+
auditingTool.foreach {
113+
_
114+
.audit(responseContext, auditEnvironmentContext)
115+
.runAsync {
116+
case Right(_) =>
117+
case Left(ex) =>
118+
logger.warn(s"[${responseContext.requestContext.id.show}] Auditing issue", ex)
119+
}
115120
}
116121
}
117122
}
118123

124+
private def blockAuditSettings[B <: BlockContext](responseContext: ResponseContext[B]): Option[Block.Audit] = {
125+
responseContext match {
126+
case AllowedBy(_, block, _, _) => Some(block.audit)
127+
case Allow(_, _, block, _) => Some(block.audit)
128+
case ForbiddenBy(_, block, _, _) => Some(block.audit)
129+
case Forbidden(_, _) => None
130+
case RequestedIndexNotExist(_, _) => None
131+
case Errored(_, _) => None
132+
}
133+
}
134+
119135
private def isLoggableEntry(context: ResponseContext[_]): Boolean = {
120136
def shouldBeLogged(block: Block) = {
121137
block.verbosity match {

core/src/test/scala/tech/beshu/ror/unit/acl/EnabledAccessControlListTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ class EnabledAccessControlListTests extends AnyWordSpec with MockFactory with In
176176
Block.Name(name),
177177
policy,
178178
Block.Verbosity.Info,
179+
Block.Audit.Enabled,
179180
NonEmptyList.of(
180181
new RegularRule {
181182
override val name: Rule.Name = Rule.Name("auth")

core/src/test/scala/tech/beshu/ror/unit/acl/blocks/BlockTests.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class BlockTests extends AnyWordSpec with BlockContextAssertion with Inside with
5555
name = blockName,
5656
policy = Block.Policy.Allow,
5757
verbosity = Block.Verbosity.Info,
58+
audit = Block.Audit.Enabled,
5859
rules = NonEmptyList.fromListUnsafe(
5960
passingRule("r1") ::
6061
passingRule("r2", withLoggedUser) ::
@@ -83,6 +84,7 @@ class BlockTests extends AnyWordSpec with BlockContextAssertion with Inside with
8384
name = blockName,
8485
policy = Block.Policy.Allow,
8586
verbosity = Block.Verbosity.Info,
87+
audit = Block.Audit.Enabled,
8688
rules = NonEmptyList.fromListUnsafe(
8789
passingRule("r1") :: passingRule("r2") :: throwingRule("r3") :: notPassingRule("r4") :: passingRule("r5") :: Nil
8890
)
@@ -109,6 +111,7 @@ class BlockTests extends AnyWordSpec with BlockContextAssertion with Inside with
109111
name = blockName,
110112
policy = Block.Policy.Allow,
111113
verbosity = Block.Verbosity.Info,
114+
audit = Block.Audit.Enabled,
112115
rules = NonEmptyList.fromListUnsafe(
113116
passingRule("r1") :: passingRule("r2") :: passingRule("r3") :: Nil
114117
)
@@ -140,6 +143,7 @@ class BlockTests extends AnyWordSpec with BlockContextAssertion with Inside with
140143
name = blockName,
141144
policy = Block.Policy.Allow,
142145
verbosity = Block.Verbosity.Info,
146+
audit = Block.Audit.Enabled,
143147
rules = NonEmptyList.fromListUnsafe(
144148
passingRule("r1", withLoggedUser) ::
145149
passingRule("r2") ::
@@ -178,6 +182,7 @@ class BlockTests extends AnyWordSpec with BlockContextAssertion with Inside with
178182
name = blockName,
179183
policy = Block.Policy.Allow,
180184
verbosity = Block.Verbosity.Info,
185+
audit = Block.Audit.Enabled,
181186
rules = NonEmptyList.fromListUnsafe(
182187
passingRule("r1", _.withUserMetadata(_.withLoggedUser(DirectlyLoggedUser(User.Id("user1"))))) ::
183188
passingRule("r2", _.withUserMetadata(_.withLoggedUser(DirectlyLoggedUser(User.Id("user2"))))) ::

core/src/test/scala/tech/beshu/ror/unit/acl/logging/AuditingToolTests.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter
128128
Block.Name("mock-block"),
129129
Block.Policy.Forbid(),
130130
Block.Verbosity.Info,
131+
Block.Audit.Enabled,
131132
NonEmptyList.one(new MethodsRule(MethodsRule.Settings(NonEmptySet.one(Method.GET))))
132133
),
133134
GeneralIndexRequestBlockContext(requestContext, UserMetadata.empty, Set.empty, List.empty, Set.empty, Set.empty),
@@ -248,6 +249,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter
248249
Block.Name("mock-block"),
249250
policy,
250251
verbosity,
252+
Block.Audit.Enabled,
251253
NonEmptyList.one(new MethodsRule(MethodsRule.Settings(NonEmptySet.one(Method.GET))))
252254
),
253255
GeneralIndexRequestBlockContext(requestContext, UserMetadata.empty, Set.empty, List.empty, Set.empty, Set.empty),

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
publishedPluginVersion=1.67.1
2-
pluginVersion=1.68.0-pre5
2+
pluginVersion=1.68.0-pre6
33
pluginName=readonlyrest
44

55
org.gradle.jvmargs=-Xmx6144m

integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ readonlyrest:
1414
another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}"
1515
tid: "{TASK_ID}"
1616
bytes: "{CONTENT_LENGTH_IN_BYTES}"
17+
block: "{REASON}"
1718
- type: data_stream
1819
data_stream: "audit_data_stream"
1920
serializer:
@@ -25,6 +26,7 @@ readonlyrest:
2526
another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}"
2627
tid: "{TASK_ID}"
2728
bytes: "{CONTENT_LENGTH_IN_BYTES}"
29+
block: "{REASON}"
2830

2931
access_control_rules:
3032

@@ -37,6 +39,8 @@ readonlyrest:
3739
methods: GET
3840
auth_key: username:dev
3941
indices: ["twitter"]
42+
audit:
43+
enabled: true ## twitter audit toggle
4044

4145
- name: "Rule 2"
4246
verbosity: error

integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ readonlyrest:
1414
another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}"
1515
tid: "{TASK_ID}"
1616
bytes: "{CONTENT_LENGTH_IN_BYTES}"
17+
block: "{REASON}"
1718

1819
access_control_rules:
1920

@@ -26,6 +27,8 @@ readonlyrest:
2627
methods: GET
2728
auth_key: username:dev
2829
indices: ["twitter"]
30+
audit:
31+
enabled: true ## twitter audit toggle
2932

3033
- name: "Rule 2"
3134
verbosity: error

integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,25 +161,49 @@ class LocalClusterAuditingToolsSuite
161161
"using ConfigurableQueryAuditLogSerializer" in {
162162
val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed)
163163

164+
// Change config to use configurable serializer and perform the request
164165
updateRorConfig(
165166
originalString = """type: "static"""",
166167
newString = """type: "configurable"""",
167168
)
168169
performAndAssertExampleSearchRequest(indexManager)
169170

171+
// Assert, that there is a single audit entry
170172
forEachAuditManager { adminAuditManager =>
171173
eventually {
172174
val auditEntries = adminAuditManager.getEntries.force().jsons
173175
auditEntries.size shouldBe 1
174176

175177
auditEntries.exists(entry =>
176-
entry("node_name_with_static_suffix").str == "ROR_SINGLE_1 with suffix" &&
178+
entry("block").str.contains("name: 'Rule 1'") &&
179+
entry("node_name_with_static_suffix").str == "ROR_SINGLE_1 with suffix" &&
177180
entry("another_field").str == "ROR_SINGLE GET" &&
178181
entry("tid").numOpt.isDefined &&
179182
entry("bytes").num == 0
180183
) shouldBe true
181184
}
182185
}
186+
187+
// Disable audit for Rule 1, clean managers, perform second request
188+
updateRorConfig(
189+
"enabled: true ## twitter audit toggle",
190+
"enabled: false ## twitter audit toggle",
191+
)
192+
adminAuditManagers.values.foreach(_.truncate())
193+
performAndAssertExampleSearchRequest(indexManager)
194+
195+
// Wait for 2s and assert, that there is no serialized event
196+
Thread.sleep(2000)
197+
forEachAuditManager { adminAuditManager =>
198+
val auditEntries = adminAuditManager.getEntries.force().jsons
199+
auditEntries.size shouldBe 0
200+
}
201+
202+
// Restore the default config
203+
updateRorConfig(
204+
"enabled: false ## twitter audit toggle",
205+
"enabled: true ## twitter audit toggle",
206+
)
183207
updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1")
184208
}
185209
}

0 commit comments

Comments
 (0)