-
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) and case classes (which will be converted to json-like string by inspecting all field names at compile time), and provides 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 })
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 })
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 })
Everything that is stringified by safe-string-interpolation is through Safe
instance. For example, Secret
has a Safe
instance that is always hidden, and if it isn't a secret, the macros
machinery written in Safe
companion object will do the job for you. This means, safe instance for case classes will be created automatically by macros regardless of its deep nested structure. The string will be json like ! The macros in Safe
indirectly ask you to not use any type class generation / generation programming / or other libraries that does that. This is to make sure compiler is breathing properly.
Hopefully this solves all your logging
, and toString
and unintended secret exposure problems !!..