Google's New Golang SDK for Protobufs
- 5 minutes read - 980 wordsGoogle has released a new Golang SDK for protobuf. In the [announcement], a useful tool to redact properties is described. If like me, this is somewhat novel to you, here’s a mashup using Google’s Protocol Buffer Basics w/ redaction.
To be very clear, as it’s an important distinction:
| Version | Repo | Docs | 
|---|---|---|
| v2 | google.golang.org/protobuf | Docs | 
| v1 | github.com/golang/protobuf | Docs | 
Project
Here’s my project structure:
.
├── protoc-3.11.4-linux-x86_64
│   ├── bin
│   │   └── protoc
│   ├── include
│   │   └── google
│   └── readme.txt
└── src
    ├── go.mod
    ├── go.sum
    ├── main.go
    ├── protos
    │   ├── addressbook.pb.go
    │   └── addressbook.proto
    └── README.md
You may structure as you wish.
I’m using protoc 3.11.4 (link) and:
PATH=${PATH}:${PWD}/protoc-3.11.4-linux-x86_64/bin
protoc --version
libprotoc 3.11.4
I’m using Go 1.14 and Modules and so, I used the following to ‘install’ the Golang plugin:
go get google.golang.org/protobuf/cmd/protoc-gen-go
In my experience, ensuring protocol buffer package names result in appropriately generated (Golang) imports, can be challenging. This challenge is exacerbated if you wish to generate bindings in multiple languages. This approach works for me. protos as a subdirectory alongside my Golang (src) sources containing the protocol buffer files and the generated Golang.
Definition
Here’s the tweaked protobuf file that I’m using:
syntax = "proto3";
package example;
import "google/protobuf/descriptor.proto";
import "google/protobuf/timestamp.proto";
option go_package = ".;protos";
extend google.protobuf.FieldOptions {
    bool non_sensitive = 50000;
}
message Person {
    string name = 1 [(non_sensitive) = true];
    int32 id =2;
    string email = 3 [(non_sensitive) = true];
    enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
    }
    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
    }
    repeated PhoneNumber phones = 4;
    google.protobuf.Timestamp last_updated = 5;
}
message AddressBook {
    repeated Person people = 1 [(non_sensitive) = true];
}
This differs from the Google tutorial file in the following ways:
- My 
packagename isexamplethis should match yourgo mod init examplevalue - There’s an additional import 
google/protobuf/descriptor.proto(from the protoc include directory) to bringgoogle.protobuf.FieldOptionsinto scope - There’s an 
extend google.protobuf.FieldOptionsthat definesnon_sensitiveas a boolean - This property (
non_sensitive) is used to annotate thePersonmessagenameandemailproperties and theAddressBookmessagepeopleproperty 
NB Custom Options was new to me.
Compilation
From within src:
protoc \
  --proto_path=./protos \
  --go_out=./protos \
  addressbook.proto
You’re expecting this to (re)generate ./src/protos/addressbook.pb.go and this should have package protos thanks to the protobuf’s:
option go_package = ".;protos";
Golang
The tutorial walks through the following steps:
- Create 
Personprotobuf - Create 
AddressBookprotobuf adding thePersonto it - Marshaling the 
AddressBookprotobuf to []byte - Writing the []byte to a file
 - Reading the []byte from the file
 - Unmarshaling the []byte back to an 
AddressBookprotobuf. 
I’ve added a couple of intermediate steps to pretty-print the protobufs by marshaling to JSON.
More importantly, I’ve included the redact function from the Announcement and applied this to Person and to the AddressBook before these are marshaled, i.e.:
- Create 
Personprotobuf - redact 
Person - Create 
AddressBookprotobuf adding thePersonto it - redact 
AddressBook - marshall 
AddressBookto JSON and pretty-print - Marshaling the 
AddressBookprotobuf to []byte - Writing the []byte to a file
 - Reading the []byte from the file
 - Unmarshaling the []byte back to an 
AddressBookprotobuf. - marshall 
AddressBookto JSON and pretty-print 
As:
package main
import (
	"encoding/json"
	"flag"
	"io/ioutil"
	"log"
	pb "example/protos"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/types/descriptorpb"
)
var (
	filename = flag.String("filename", "", "Filename")
)
func redact(pm proto.Message) {
	m := pm.ProtoReflect()
	m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
		opts := fd.Options().(*descriptorpb.FieldOptions)
		if proto.GetExtension(opts, pb.E_NonSensitive).(bool) {
			return true
		}
		m.Clear(fd)
		return true
	})
}
func main() {
	flag.Parse()
	if *filename == "" {
		log.Fatal("Must specify filename")
	}
	{
		p = &pb.Person{
			Name:  "Frederik Jack",
			Id:    1,
			Email: "freddie@email.com",
			Phones: []*pb.Person_PhoneNumber{
				{
					Number: "425-555-1234",
					Type:   pb.Person_HOME,
				},
			},
		}
		// Redact
		redact(p)
		b := &pb.AddressBook{
			People: []*pb.Person{
				p,
			},
		}
		// Redact
		redact(b)
		// Pretty print
		j, err := json.MarshalIndent(b, "", "   ")
		if err != nil {
			log.Fatal(err)
		}
		log.Printf("%s\n", j)
		// Marshall
		out, err := proto.Marshal(b)
		if err != nil {
			log.Fatal(err)
		}
		// Write to filename
		if err := ioutil.WriteFile(*filename, out, 0644); err != nil {
			log.Fatal(err)
		}
	}
	{
		// Read from filename
		in, err := ioutil.ReadFile(*filename)
		if err != nil {
			log.Fatal(err)
		}
		// Unmarshall
		b := &pb.AddressBook{}
		if err := proto.Unmarshal(in, b); err != nil {
			log.Fatal(err)
		}
		// Pretty print
		j, err := json.MarshalIndent(b, "", "   ")
		if err != nil {
			log.Fatal(err)
		}
		log.Printf("%s\n", j)
	}
}
NB ‘conventionally’ I alias the protoc-generated packages as
pbbut this conflicts with theredactdefinition as-was.
I renamed the function parameter topmto avoid this.
NB The Custom (Field)Option
non_sensitiveis compiled into a Golang variable inaddressbook.pb.go:// Extension fields to descriptor.FieldOptions. var ( // optional bool non_sensitive = 50000; E_NonSensitive = &file_addressbook_proto_extTypes[0] )and this is referenced in
redactusing the import alias aspb.E_NonSensitive
Results
Without any redaction, nothing will be filtered and the code would return:
2020/03/10 14:17:52 {
   "people": [
      {
         "name": "John Doe",
         "id": 1234,
         "email": "jdoe@email.com",
         "phones": [
            {
               "number": "555-1234",
               "type": 1
            }
         ]
      }
   ]
}
2020/03/10 14:17:52 {
   "people": [
      {
         "name": "John Doe",
         "id": 1234,
         "email": "jdoe@email.com",
         "phones": [
            {
               "number": "555-1234",
               "type": 1
            }
         ]
      }
   ]
}
With redaction applied initially to Person (p) and then to AddressBook (b), we get:
2020/03/10 14:20:23 {
   "people": [
      {
         "name": "John Doe",
         "email": "jdoe@email.com"
      }
   ]
}
2020/03/10 14:20:23 {
   "people": [
      {
         "name": "John Doe",
         "email": "jdoe@email.com"
      }
   ]
}
If the protobuf annotation were to be removed from AddressBook message people property, and redaction were still applied to AddressBook (b), we’d get:
2020/03/10 14:26:13 {}
2020/03/10 14:26:13 {}
This is because the people property is not marked non_senstive and is thus redacted.
As the Announcement post suggests, an improvement would be to recurse into the structure (e.g. AddressBook) and apply the redaction at each struct within.
HTH!