Skip to content

Commit 768523b

Browse files
authored
Merge branch 'master' into feature/467-sbom-creation-per-pr
2 parents a4f263b + 5a19b6b commit 768523b

3 files changed

Lines changed: 129 additions & 5 deletions

File tree

agent/src/main/resources/reference.conf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
# dispatcher to be used (http, console, capture)
1515
atum.dispatcher.type="http"
1616

17+
# Optional author/createdBy identity used for auditing (partitionings, checkpoints, additional data).
18+
# If unset, it falls back to System.getProperty("user.name"). Useful when the JVM user is a generic
19+
# system account (e.g. "yarn") and the application name is more meaningful.
20+
#atum.author="my-application-name"
21+
1722
# The REST API URI of the atum server
1823
#atum.dispatcher.http.url=
1924

agent/src/main/scala/za/co/absa/atum/agent/AtumAgent.scala

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,26 @@ trait AtumAgent {
3232
val dispatcher: Dispatcher
3333

3434
/**
35-
* Returns a user under whose security context the JVM is running.
36-
* Its purpose is for auditing in author/createdBy fields.
35+
* The user used for auditing in author/createdBy fields.
3736
*
38-
* Important: It's not supposed to be used for authorization as it can be spoofed!
37+
* It is resolved once per agent (on first access) and then cached for the agent's lifetime:
38+
* - if the `atum.author` configuration key is set (e.g. in `application.conf` or via the
39+
* `-Datum.author=...` JVM system property), that value is used - this is useful when the JVM
40+
* user is a generic system account (e.g. `yarn`) and the application name is more meaningful;
41+
* - otherwise it falls back to the user under whose security context the JVM is running
42+
* (`System.getProperty("user.name")`), which is platform independent.
43+
*
44+
* Overriding this method replaces the resolution strategy entirely (see `AtumAgent.fromConfig`).
3945
*
40-
* @return Current user.
46+
* Important: It's not supposed to be used for authorization as it can be spoofed!
4147
*/
42-
private[agent] def currentUser: String = System.getProperty("user.name") // platform independent
48+
private[agent] def currentUser: String = resolvedCurrentUser
49+
50+
// Cached, resolved-once default author identity. Ensures that agents which do not override
51+
// `currentUser` keep a stable audit identity for their whole lifetime, even if the Typesafe
52+
// Config caches are later invalidated or backing system properties change. The actual
53+
// configuration loading is owned by the companion object (see `AtumAgent.defaultAuthor`).
54+
private[this] lazy val resolvedCurrentUser: String = AtumAgent.defaultAuthor
4355

4456
/**
4557
* Sends `CheckpointDTO` to the AtumService API
@@ -139,6 +151,9 @@ object AtumAgent extends AtumAgent {
139151

140152
override val dispatcher: Dispatcher = dispatcherFromConfig()
141153

154+
// `currentUser` is intentionally not overridden here: the trait default already delegates to
155+
// `defaultAuthor` (below) and caches the result once, which is exactly what the singleton needs.
156+
142157
private[agent] def dispatcherFromConfig(config: Config = ConfigFactory.load()): Dispatcher = {
143158
config.getString("atum.dispatcher.type") match {
144159
case "http" => new HttpDispatcher(config)
@@ -148,7 +163,33 @@ object AtumAgent extends AtumAgent {
148163
}
149164
}
150165

166+
/**
167+
* The default author (createdBy) identity, resolved from the globally loaded configuration.
168+
* Callers cache the result (see the trait's `currentUser`), so this is evaluated once per agent.
169+
*
170+
* @return the default author identity for agents that do not override `currentUser`.
171+
*/
172+
private[agent] def defaultAuthor: String = resolveAuthor(ConfigFactory.load())
173+
174+
/**
175+
* Resolves the author (createdBy) identity used for auditing from the given configuration.
176+
*
177+
* If the optional `atum.author` key is present and non-blank, its (trimmed) value is used;
178+
* otherwise it falls back to `System.getProperty("user.name")`.
179+
*
180+
* @param config configuration to read the optional `atum.author` key from.
181+
* @return the resolved author identity.
182+
*/
183+
private[agent] def resolveAuthor(config: Config): String = {
184+
if (config.hasPath("atum.author") && config.getString("atum.author").trim.nonEmpty) {
185+
config.getString("atum.author").trim
186+
} else {
187+
System.getProperty("user.name") // platform independent
188+
}
189+
}
190+
151191
def fromConfig(config: Config): AtumAgent = new AtumAgent {
152192
override val dispatcher: Dispatcher = dispatcherFromConfig(config)
193+
override val currentUser: String = resolveAuthor(config)
153194
}
154195
}

agent/src/test/scala/za/co/absa/atum/agent/AtumAgentUnitTests.scala

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,84 @@ class AtumAgentUnitTests extends AnyFunSuiteLike {
194194
assert(agentA.dispatcher ne agentB.dispatcher)
195195
}
196196

197+
test("currentUser falls back to the JVM user when atum.author is not set") {
198+
val agent = AtumAgent.fromConfig(configOf(Map(
199+
"atum.dispatcher.type" -> "capture",
200+
"atum.dispatcher.capture.capture-limit" -> 10
201+
)))
202+
203+
assert(agent.currentUser == System.getProperty("user.name"))
204+
}
205+
206+
test("currentUser uses atum.author from config when set (trimmed)") {
207+
val agent = AtumAgent.fromConfig(configOf(Map(
208+
"atum.dispatcher.type" -> "capture",
209+
"atum.dispatcher.capture.capture-limit" -> 10,
210+
"atum.author" -> " my-application "
211+
)))
212+
213+
assert(agent.currentUser == "my-application")
214+
}
215+
216+
test("currentUser falls back to the JVM user when atum.author is blank") {
217+
val agent = AtumAgent.fromConfig(configOf(Map(
218+
"atum.dispatcher.type" -> "capture",
219+
"atum.dispatcher.capture.capture-limit" -> 10,
220+
"atum.author" -> " "
221+
)))
222+
223+
assert(agent.currentUser == System.getProperty("user.name"))
224+
}
225+
226+
test("currentUser is resolved independently per config-backed agent") {
227+
val agentA = AtumAgent.fromConfig(configOf(Map(
228+
"atum.dispatcher.type" -> "capture",
229+
"atum.dispatcher.capture.capture-limit" -> 10,
230+
"atum.author" -> "alice"
231+
)))
232+
val agentB = AtumAgent.fromConfig(configOf(Map(
233+
"atum.dispatcher.type" -> "capture",
234+
"atum.dispatcher.capture.capture-limit" -> 10
235+
)))
236+
237+
assert(agentA.currentUser == "alice")
238+
assert(agentB.currentUser == System.getProperty("user.name"))
239+
}
240+
241+
test("currentUser is resolved once and cached for agents that do not override it") {
242+
val originalAuthor = Option(System.getProperty("atum.author"))
243+
try {
244+
System.setProperty("atum.author", "first-app")
245+
ConfigFactory.invalidateCaches()
246+
247+
// a custom agent that does NOT override currentUser -> relies on the cached trait default
248+
val agent = new AtumAgent {
249+
override val dispatcher: CapturingDispatcher =
250+
AtumAgent.dispatcherFromConfig(configOf(Map(
251+
"atum.dispatcher.type" -> "capture",
252+
"atum.dispatcher.capture.capture-limit" -> 10
253+
))).asInstanceOf[CapturingDispatcher]
254+
}
255+
256+
// first access resolves and caches the identity
257+
assert(agent.currentUser == "first-app")
258+
259+
// change the backing system property and invalidate Typesafe caches:
260+
// a recomputing `def` would observe the new value here
261+
System.setProperty("atum.author", "second-app")
262+
ConfigFactory.invalidateCaches()
263+
264+
// the resolved-once value must remain stable for the agent's lifetime
265+
assert(agent.currentUser == "first-app")
266+
} finally {
267+
originalAuthor match {
268+
case Some(value) => System.setProperty("atum.author", value)
269+
case None => System.clearProperty("atum.author")
270+
}
271+
ConfigFactory.invalidateCaches()
272+
}
273+
}
274+
197275
private def configOf(configValues: Map[String, Any]): Config = {
198276
val emptyConfig = ConfigFactory.empty()
199277
configValues.foldLeft(emptyConfig) { case (acc, (configKey, value)) =>

0 commit comments

Comments
 (0)