Last active
March 13, 2019 13:46
-
-
Save haleyrc/03e22dffd9920e978666a7a9be26c71a to your computer and use it in GitHub Desktop.
An experiment in safer logging.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This gist is an experiment toward safer logs that don't leak PII or secrets. We present three different logging | |
// implementations and outline the pros and cons of both. Ultimately, however, the "safeish" version provides the | |
// best mix of developer-friendliness and safety. | |
// | |
// Note that none of the safety mechanisms here apply in cases where fields of a data type are logged directly, but | |
// if log.Printf(data.Password) passes code review, there are other problems. | |
// | |
// A runnable version of this is available at: | |
// https://play.golang.org/p/_jePSMkWM1O | |
package main | |
import ( | |
"fmt" | |
) | |
// mydata is an example of a typical data structure that contains a mix of sensitive and non-sensitive fields. | |
type mydata struct { | |
Password string | |
Name string | |
} | |
func (md mydata) Anonymize() interface{} { | |
return mydata{Password: "", Name: md.Name} | |
} | |
// Logger is our "standard" logging interface. In reality it would have all of the methods we need for | |
// structured logging in production (Debugf, Infof, Criticalf, etc.) | |
type Logger interface { | |
Printf(format string, v ...interface{}) | |
} | |
// SafeLogger is the stricter version that only takes types that satisfy Anonymize. If we use this interface, | |
// every call to Printf would need to be modified to wrap built-in types and user-defined types would have to | |
// be updated en masse to satisfy the Anonymizer interface. | |
type SafeLogger interface { | |
Printf(format string, v ...Anonymizer) | |
} | |
// unsafe implements the Logger interface and is similar to any standard logging package. | |
type unsafe struct{} | |
func (s unsafe) Printf(format string, v ...interface{}) { | |
fmt.Printf(format, v...) | |
} | |
// safe wraps a Logger and satisfies the SafeLogger interface. If any v are passed that don't implement Anonymizer, | |
// however, the program will fail to compile, leading to requirements such as SafeString below which is much more | |
// explicit, but also unfriendly. | |
type safe struct{ l Logger } | |
func (s safe) Printf(format string, v ...Anonymizer) { | |
var ifs []interface{} | |
for _, a := range v { | |
ifs = append(ifs, a.Anonymize()) | |
} | |
s.l.Printf(format, ifs...) | |
} | |
// Anonymizer SHOULD be implemented by all user defined data types even if they are just passthroughs and should | |
// mask or remove sensitive information not suitable for logging. | |
type Anonymizer interface { | |
Anonymize() interface{} | |
} | |
// safeString is required for the SafeLogger interface since built-in types obviously don't satisfy the Anonymizer | |
// interface. This (and simiilar versions for other types), would have to be used for *every* call to the Logger | |
// implementation. | |
type safeString string | |
func (s safeString) Anonymize() interface{} { | |
return s | |
} | |
func SafeString(s string) Anonymizer { | |
return safeString(s) | |
} | |
// safeish is a compromise that allows our types to satisfy the Anonymizer interface if they require it, but | |
// doesn't impose any unwieldy requirements on developers logging built-in types. | |
type safeish struct{} | |
func (s safeish) Printf(format string, v ...interface{}) { | |
av := make([]interface{}, 0, len(v)) | |
for _, iface := range v { | |
a, ok := iface.(Anonymizer) | |
if ok { | |
av = append(av, a.Anonymize()) | |
} else { | |
av = append(av, iface) | |
} | |
} | |
fmt.Printf(format, av...) | |
} | |
func main() { | |
data := mydata{Password: "oops", Name: "world"} | |
// The unsafe version successfully logs user-defined and built-in types, | |
// but we don't have any way of signaling information that shouldn't | |
// appear in logs. | |
fmt.Println("Unsafe:") | |
var ul Logger = unsafe{} | |
ul.Printf("\t%#v\n", data) | |
ul.Printf("\tHello, %s!\n", "world") | |
// The strict version makes it explicit that our user-defined types should | |
// implement the Anonymizer interface, but since built-in types don't, we | |
// are left to wrap every built-in type (or even user-defined types from | |
// external packages) that handles Anonymizing or passing through. | |
fmt.Println("Strict Safe:") | |
var sl SafeLogger = safe{ul} | |
sl.Printf("\t%#v\n", data) | |
// Won't compile because strings aren't safe | |
// sl.Printf("\tHello, %s!\n", "world") | |
sl.Printf("\tHello, %s!\n", SafeString("world")) | |
// The safe-ish version accepts the same arguments as the unsafe version, but | |
// checks each argument to see *if* it implements the Anonymizer interface, and | |
// uses that version of the data if it does. We don't impose any hard restrictions | |
// on what can be passed, but we also don't make the logger ungainly to use and | |
// types that we know are dangerous just need to implement one interface. | |
fmt.Println("Safe-ish:") | |
var sil Logger = safeish{} | |
sil.Printf("\t%#v\n", data) | |
sil.Printf("\tHello, %s!\n", "world") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment