Skip to content
Afsal Thaj edited this page Dec 25, 2018 · 34 revisions

safe-string-interpolation

  • 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 do s"my domain object is ${domainObject.show}", but the creation of show 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 !

Solution

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. !

Secrets

Easy. Just wrap your any secret field anywhere with Secret.apply. More examples to follow

Simple Example (just to give an intro )

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)

Case class Example

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

Can't Cheat!

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.

How about secrets ?

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.

Your own secret ?

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 !