Prevents leaking sensitive fields defined inside
case class
.
In Scala, case class
(es) are omnipresent: they are the building blocks for complex business domain models, due to how
easily they can be defined and instantiated; on top of that, the Scala compiler provides a convenient toString
method
for us that will pretty print in console/log their content, for example:
case class UserPreferences(useDarkTheme: Boolean, maxHistoryItems: Int)
val id = 123
val up = store.getUserPreferencesByID(123)
log.info(s"user preferences for user $id are $up")
will print
user preferences for user 123 are UserPreferences(true, 5)
However, this becomes a double-edge sword when handling sensitive data: assume you're writing an HTTP server, and you have a case class to pass its headers around, i.e.
case class HttpHeaders(userId: String, apiKey: String, languages: Seq[Locale], correlationId: String)
or a case class representing a user in a DB
case class User(id: UUID, nickname: String, email: String)
you probably wouldn't want to leak by mistake an apiKey
(for security reasons) or an email
(for PII/GDPR reasons).
Sure, you can get creative and define middleware layers/utility methods and so on to circumvent the issue, but wouldn't it be better if you were simply to say "when I dump the whole object, I don't want this field to be printed out" ?
@redacted
to the rescue!
This Readme is being intentionally kept short; if you'd like to learn more about advanced usecases of the annotation, or how the compiler plugin itself is structured and works, feel free to head over the project's website https://polentino.github.io/redacted/ :)
In your project/plugins.sbt
add the following line
addSbtPlugin("io.github.polentino" % "sbt-redacted" % "1.1.0")
and then enable it in your specific (sub)project in build.sbt
like so
lazy val root = (project in file("."))
.enablePlugins(RedactedPlugin)
.setting(
redactedVersion := "0.7.1"
)
Once configured your project with either option 1
or 2
, all you have to do is the following
import io.github.polentino.redacted.redacted
case class HttpHeaders(userId: UUID, @redacted apiKey: String, languages: Seq[Locale], correlationId: String)
case class User(id: UUID, nickname: String, @redacted email: String)
That's all!
From now on, every time you'll try to dump the whole object,or invoke toString
method
val headers: HttpHeaders = HttpHeaders(
userId = UUID.randomUUID(),
apiKey = "abcdefghijklmnopqrstuvwxyz",
languages = Seq("it_IT", "en_US"),
correlationId = "corr-id-123"
)
val user: User = User(
id = UUID.randomUUID(),
nickname = "polentino911",
email = "[email protected]"
)
println(headers)
println(user)
this will actually be printed
$ HttpHeaders(d58b6a78-5411-4bd4-a0d3-e1ed38b579c4, ***, Seq(it_IT, en_US), corr-id-123)
$ User(8b2d4570-d043-473b-a56d-fe98105ccc2b, polentino911, ***)
But, of course, accessing the field itself will return its content, i.e.
println(headers.apiKey)
println(user.email)
will still print the real values:
$ abcdefghijklmnopqrstuvwxyz
$ [email protected]
redacted
supports all Scala versions listed in the table below. However, it is advised to use the ones with a green
checkmark ( ✅ ) since those are the Long Term Support
ones specified in the Scala website.
Scala Version | LTS ? |
---|---|
3.6.4 | - |
3.5.2 | - |
3.4.3 | - |
3.3.5 | ✅ |
3.3.4 | ✅ |
3.3.3 | ✅ |
3.3.1 | ✅ |
3.3.0 | ✅ |
3.2.2 | - |
3.1.3 | - |
2.13.16 | - |
2.12.20 | - |
Given a case class with at least one field annotated with @redacted
, i.e.
final case class User(id: UUID, @redacted name: String)
the compiler plugin will replace the default implementation of its toString
method with this
final case class User(id: UUID, @redacted name: String) {
def toString(): String = "User(" + this.id + ",***" + ")"
}
The way it's done is the following:
The compiler plugin will inspect each type definition and check whether the class being analysed is a case class
, and
if it has at least one of its fields annotated with @redacted
; if that's the case, it will then proceed to rewrite
the default toString
implementation by selectively returning either the ***
string, or the value of the field,
depending on the presence (or not) of @redacted
, resulting in an implementation that looks like so:
def toString(): String =
"<class name>(" + this.< field not redacted > + "," + "***" +
...+")"
- create Sbt plugin (https://github.com/polentino/sbt-redacted)
- add some benchmarks with jmh
- Awesome pointers and ideas by Kit Langton (although it's about macros and not compiler plugins)
- Compiler Plugin Development in Scala 3 | Let's talk about Scala 3
- act