-
Notifications
You must be signed in to change notification settings - Fork 5
Home
-
Being able to pass anything on to scala string interpolations might have messed up your logs, exposed your secrets, and what not! I know you hate it.
-
We may also forget stringifying domain objects when using scala string interpolations, but stringifying it manually is a tedious job. Instead we just do
toString
which sometimes can spew out useless string representations.
Bad logs:
INFO: The student logged in: @4f9a2c08 // object.toString
INFO: The student logged in: Details(NameParts("john", "stephen"), "efg", "whoknowswhatiswhat"...)
INFO: The student logged in: scala.Map(...)
INFO: The student logged in: Details("name", "libraryPassword!!")
-
Sometimes we rely on
scalaz.Show/cats.Show
instances on companion objects of case classes and then dos"my domain object is ${domainObject.show}"
, but the creation ofshow
instances has never been proved practical in larger applications. -
One simplification we did so far is to have automatic show instances (may be using shapeless), and guessing password-like fields and replacing it with "*****".
Hmmm... Not anymore !
safeStr""
is just like s""
, but it is type safe and allows only
- strings and doesn't allow you to do toString accidentally,
- case classes which will be converted to json-like string by inspecting all fields, be it deeply nested or not, at compile time,
- and provides consistent way to hide secrets.
import SafeString._
val stringg: SafeString =
safeStr"This is safer, guranteed and its all compile time, but pass $onlyString, and $onlyCaseClass and nothing else"
safeStr
returns a SafeString
which your logger interfaces (an example below) can then accept !
trait Loggers[F[_], E] {
def info: SafeString => F[Unit]
def error: SafeString => F[Unit]
def debug: SafeString => F[Unit]
Everything here is compile time. !
Easy. Just wrap your any secret field anywhere with Secret.apply
. More examples to follow
scala> val a: String = "ghi"
a: String = ghi
scala> val b: String = "xyz"
b: String = xyz
scala> val c: Int = 1
c: Int = 1
scala> // safeStr interpolation
scala> safeStr"The scala string interpol can be a bit dangerous with your secrets. ${a}, ${b}, ${c}"
<console>:24: error: The provided type isn't a string nor it's a case class, or you might have tried a `toString` on non-strings!
safeStr"The scala string interpol can be a bit dangerous with your secrets. ${a}, ${b}, ${c}"
^
scala> safeStr"The scala string interpol can be a bit dangerous with your secrets. ${a}, ${b}"
res2: com.thaj.safe.string.interpolator.SafeString = SafeString(The scala string interpol can be a bit dangerous with your secrets. ghi, xyz)
scala> case class Dummy(name: String, age: Int)
defined class Dummy
scala> val dummy = Dummy("Afsal", 1)
dummy: Dummy = Dummy(Afsal,1)
scala> val a: String = "realstring"
a: String = realstring
scala> safeStr"This is safer ! ${a} : ${dummy}"
res3: com.thaj.safe.string.interpolator.SafeString = SafeString(This is safer ! realstring : { age: 1, name: Afsal })
This works for any level of deep nested structure of case class. This is done with the support of macro materializer in Safe.scala
scala> safeStr"I am going to call a toString on a case class to satisfy compiler ! ${a} : ${dummy.toString}"
<console>:23: error: The provided type isn't a string nor it's a case class, or you might have tried a `toString` on non-strings!
safeStr"I am going to call a toString on a case class to satisfy compiler ! ${a} : ${dummy.toString}"
^
safe-string-interpolator hates it when you do toString
on non-string types. Instead, you can use yourType.asStr
and safe-string-interpolator will ensure it is safe to convert it to String.
i.e,
val a: String = "afsal"
val b: String = "john"
val c: Int = 1
scala> safeStr"The scala string interpol can be a bit dangerous with your secrets. ${a}, ${b}, ${c.toString}"
<console>:24: error: The provided type isn't a string nor it's a case class, or you might have tried a `toString` on non-strings!
scala> safeStr"The scala string interpol can be a bit dangerous with your secrets. ${a}, ${b}, ${c.asStr}"
// Compiles sucess
PS:
An only issue with this tight approach to being safe is that sometimes you may need to end up doing thisIsADynamicString.asStr
, and that's more of a failed fight with scala type inference.
As mentioned before, just wrap the secret with Secret.apply.
scala> import com.thaj.safe.string.interpolator.SafeString._
import com.thaj.safe.string.interpolator.SafeString._
scala> import com.thaj.safe.string.interpolator.Secret
import com.thaj.safe.string.interpolator.Secret
scala> val conn = DbConnection("posgr", Secret("this will be hidden"))
conn: DbConnection = DbConnection(posgr,Secret(this will be hidden))
scala> safeStr"the db conn is $conn"
res0: com.thaj.safe.string.interpolator.SafeString = SafeString(the db conn is { password: *******************, name: posgr })
Secrets will be hidden wherever it exists in your nested case class.
If you don't want to use interpolation.Secret
data type and need to use your own, then define Safe
instance for it.
case class MySecret(value: String) extends AnyVal
implicit val safeMySec: Safe[MySecret] = _ => "****"
val conn = DbConnection("posgr", MySecret("this will be hidden"))
scala> safeStr"the db is $conn"
res1: com.thaj.safe.string.interpolator.SafeString = SafeString(the db is { password: ****, name: posgr })
Our application isn’t resilient if it lacks human-readable logs and doesn’t manage secret variables consistently. Moreover, it said to be maintainable only when it is type driven and possess more compile time behaviour, in this context, be able to fail a build/compile when someone does a toString
in places where you shouldn’t. Hope it helps !