Skip to content

Commit

Permalink
feat: add protoc-gen-go-iam
Browse files Browse the repository at this point in the history
Protobuf plugin that generates IAM-specific service and method
descriptors, and middleware that automatically checks policies using
CEL Go.
  • Loading branch information
odsod committed May 14, 2021
1 parent 86d7bc8 commit ce9d4e3
Show file tree
Hide file tree
Showing 14 changed files with 997 additions and 280 deletions.
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ build/protoc-gen-go: go.mod
$(info [$@] rebuilding plugin...)
@go build -o $@ google.golang.org/protobuf/cmd/protoc-gen-go

.PHONY: build/protoc-gen-go-iam
build/protoc-gen-go-iam:
$(info [$@] rebuilding plugin...)
@go build -o $@ ./cmd/protoc-gen-go-iam

.PHONY: buf-lint
buf-lint: $(buf)
$(info [$@] linting proto files...)
Expand All @@ -30,8 +35,13 @@ buf-generate: $(buf) build/protoc-gen-go
@rm -rf proto/gen/einride/iam/v1
@$(buf) generate --path proto/src/einride/iam/v1 --template buf.gen.yaml

protoc_plugins := \
build/protoc-gen-go \
build/protoc-gen-go-iam \
$(protoc_gen_go_grpc)

.PHONY: buf-generate-example
buf-generate-example: $(buf) build/protoc-gen-go $(protoc_gen_go_grpc)
buf-generate-example: $(buf) $(protoc_plugins)
$(info [$@] generating proto stubs...)
@rm -rf proto/gen/einride/iam/example/v1
@$(buf) generate --path proto/src/einride/iam/example/v1 --template buf.gen.example.yaml
Expand Down
8 changes: 7 additions & 1 deletion buf.gen.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: v1beta1
plugins:
- name: go
out: proto/gen
opt:
opt:
- module=go.einride.tech/iam/proto/gen
path: ./build/protoc-gen-go

Expand All @@ -12,3 +12,9 @@ plugins:
opt:
- module=go.einride.tech/iam/proto/gen
- require_unimplemented_servers=false

- name: go-iam
out: proto/gen
opt:
- module=go.einride.tech/iam/proto/gen
path: ./build/protoc-gen-go-iam
192 changes: 192 additions & 0 deletions cmd/protoc-gen-go-iam/internal/geniam/descriptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package geniam

import (
"strconv"

"google.golang.org/protobuf/compiler/protogen"
)

type iamServiceDescriptorCodeGenerator struct {
gen *protogen.Plugin
file *protogen.File
service *protogen.Service
}

func (c iamServiceDescriptorCodeGenerator) GlobalFunctionGoName() string {
return c.service.GoName + "IAM"
}

func (c iamServiceDescriptorCodeGenerator) ServiceDescriptorInterfaceGoName() string {
return c.service.GoName + "IAMDescriptor"
}

func (c iamServiceDescriptorCodeGenerator) ServiceDescriptorStructGoName() string {
return private(c.service.GoName + "IAMDescriptor")
}

func (c iamServiceDescriptorCodeGenerator) ServiceDescriptorVariableGoName() string {
return fileVarName(c.file, "iamDescriptor_"+c.service.GoName)
}

func (c iamServiceDescriptorCodeGenerator) ServiceDescriptorVariableInitFunctionGoName() string {
return fileVarName(c.file, "init_iamDescriptor_"+c.service.GoName)
}

func (c iamServiceDescriptorCodeGenerator) GenerateCode(g *protogen.GeneratedFile) {
c.generateGlobalFunction(g)
c.generateServiceDescriptorInterface(g)
c.generateServiceDescriptorStruct(g)
c.generateServiceDescriptorVariable(g)
}

func (c iamServiceDescriptorCodeGenerator) GenerateInitFunctionCalls(g *protogen.GeneratedFile) {
g.P(c.ServiceDescriptorVariableInitFunctionGoName(), "()")
}

func (c iamServiceDescriptorCodeGenerator) generateGlobalFunction(g *protogen.GeneratedFile) {
g.P()
g.P(
"// ", c.GlobalFunctionGoName(),
" returns a descriptor for the ", c.service.Desc.Name(), " IAM configuration.",
)
g.P("func ", c.GlobalFunctionGoName(), "() ", c.ServiceDescriptorInterfaceGoName(), " {")
g.P("return &", c.ServiceDescriptorVariableGoName())
g.P("}")
}

func (c iamServiceDescriptorCodeGenerator) generateServiceDescriptorInterface(g *protogen.GeneratedFile) {
g.P()
g.P(
"// ", c.ServiceDescriptorInterfaceGoName(),
" describes the ", c.service.Desc.Name(), " IAM configuration.",
)
g.P("type ", c.ServiceDescriptorInterfaceGoName(), " interface {")
if getPredefinedRoles(c.service) != nil {
g.Unskip()
iamv1Roles := g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: "go.einride.tech/iam/proto/gen/einride/iam/v1",
GoName: "Roles",
})
g.P("PredefinedRoles() *", iamv1Roles)
}
for _, method := range c.service.Methods {
if getMethodAuthorizationOptions(method) == nil {
continue
}
g.Unskip()
iamv1MethodAuthorizationOptions := g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: "go.einride.tech/iam/proto/gen/einride/iam/v1",
GoName: "MethodAuthorizationOptions",
})
g.P(method.GoName, "() *", iamv1MethodAuthorizationOptions)
}
g.P("}")
}

func (c iamServiceDescriptorCodeGenerator) generateServiceDescriptorStruct(g *protogen.GeneratedFile) {
g.P()
g.P("type ", c.ServiceDescriptorStructGoName(), " struct {")
if getPredefinedRoles(c.service) != nil {
iamv1Roles := g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: "go.einride.tech/iam/proto/gen/einride/iam/v1",
GoName: "Roles",
})
g.P("predefinedRoles *", iamv1Roles)
}
for _, method := range c.service.Methods {
if getMethodAuthorizationOptions(method) == nil {
continue
}
iamv1MethodAuthorizationOptions := g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: "go.einride.tech/iam/proto/gen/einride/iam/v1",
GoName: "MethodAuthorizationOptions",
})
g.P(private(method.GoName), " *", iamv1MethodAuthorizationOptions)
}
g.P("}")
if getPredefinedRoles(c.service) != nil {
iamv1Roles := g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: "go.einride.tech/iam/proto/gen/einride/iam/v1",
GoName: "Roles",
})
g.P()
g.P("func (d *", c.ServiceDescriptorStructGoName(), ") PredefinedRoles() *", iamv1Roles, " {")
g.P("return d.predefinedRoles")
g.P("}")
}
for _, method := range c.service.Methods {
if getMethodAuthorizationOptions(method) == nil {
continue
}
methodOptions := g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: "go.einride.tech/iam/proto/gen/einride/iam/v1",
GoName: "MethodAuthorizationOptions",
})
g.P()
g.P("func (d *", c.ServiceDescriptorStructGoName(), ") ", method.GoName, "() *", methodOptions, " {")
g.P("return d.", private(method.GoName))
g.P("}")
}
}

func (c iamServiceDescriptorCodeGenerator) generateServiceDescriptorVariable(g *protogen.GeneratedFile) {
globalFiles := g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: "google.golang.org/protobuf/reflect/protoregistry",
GoName: "GlobalFiles",
})
getExtension := g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: "google.golang.org/protobuf/proto",
GoName: "GetExtension",
})
g.P()
g.P("var ", c.ServiceDescriptorVariableGoName(), " ", c.ServiceDescriptorStructGoName())
g.P()
g.P("func ", c.ServiceDescriptorVariableInitFunctionGoName(), "() {")
if getPredefinedRoles(c.service) != nil {
roles := g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: "go.einride.tech/iam/proto/gen/einride/iam/v1",
GoName: "Roles",
})
ePredefinedRoles := g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: "go.einride.tech/iam/proto/gen/einride/iam/v1",
GoName: "E_PredefinedRoles",
})
g.P("// Init predefined roles.")
g.P("serviceDesc, err := ", globalFiles, ".FindDescriptorByName(")
g.P(strconv.Quote(string(c.service.Desc.FullName())), ",")
g.P(")")
g.P("if err != nil {")
g.P("panic(\"unable to find service descriptor ", c.service.Desc.FullName(), "\")")
g.P("}")
g.P(c.ServiceDescriptorVariableGoName(), ".predefinedRoles = ", getExtension, "(")
g.P("serviceDesc.Options(),")
g.P(ePredefinedRoles, ",")
g.P(").(*", roles, ")")
}
for _, method := range c.service.Methods {
if getMethodAuthorizationOptions(method) == nil {
continue
}
methodOptions := g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: "go.einride.tech/iam/proto/gen/einride/iam/v1",
GoName: "MethodAuthorizationOptions",
})
eMethodAuthorization := g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: "go.einride.tech/iam/proto/gen/einride/iam/v1",
GoName: "E_MethodAuthorization",
})
g.P("// Init ", method.Desc.Name(), " authorization options.")
g.P("methodDesc", method.Desc.Name(), ", err := ", globalFiles, ".FindDescriptorByName(")
g.P(strconv.Quote(string(method.Desc.FullName())), ",")
g.P(")")
g.P("if err != nil {")
g.P("panic(\"unable to find method descriptor ", method.Desc.FullName(), "\")")
g.P("}")
g.P(c.ServiceDescriptorVariableGoName(), ".", private(method.GoName), " = ", getExtension, "(")
g.P("methodDesc", method.Desc.Name(), ".Options(),")
g.P(eMethodAuthorization, ",")
g.P(").(*", methodOptions, ")")
// TODO: For methods with per-resource permissions, resolve the resource descriptors here.
}
g.P("}")
}
31 changes: 31 additions & 0 deletions cmd/protoc-gen-go-iam/internal/geniam/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package geniam

import (
"google.golang.org/protobuf/compiler/protogen"
)

const Version = "development"

const generatedFilenameSuffix = "_iam.go"

func GenerateFile(gen *protogen.Plugin, f *protogen.File) {
filename := f.GeneratedFilenamePrefix + generatedFilenameSuffix
g := gen.NewGeneratedFile(filename, f.GoImportPath)
g.Skip()
g.P("package ", f.GoPackageName)
g.P()
for _, service := range f.Services {
descriptor := iamServiceDescriptorCodeGenerator{gen: gen, file: f, service: service}
descriptor.GenerateCode(g)
}
g.P()
g.P("func init() {")
g.P("// This init function runs after the init function of ", f.GeneratedFilenamePrefix, ".pb.go.")
g.P("// We depend on the Go initialization order to ensure this.")
g.P("// See: https://golang.org/ref/spec#Package_initialization")
for _, service := range f.Services {
descriptor := iamServiceDescriptorCodeGenerator{gen: gen, file: f, service: service}
descriptor.GenerateInitFunctionCalls(g)
}
g.P("}")
}
27 changes: 27 additions & 0 deletions cmd/protoc-gen-go-iam/internal/geniam/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package geniam

import (
"strings"
"unicode/utf8"

iamv1 "go.einride.tech/iam/proto/gen/einride/iam/v1"
"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/proto"
)

func getPredefinedRoles(service *protogen.Service) *iamv1.Roles {
return proto.GetExtension(service.Desc.Options(), iamv1.E_PredefinedRoles).(*iamv1.Roles)
}

func getMethodAuthorizationOptions(method *protogen.Method) *iamv1.MethodAuthorizationOptions {
return proto.GetExtension(method.Desc.Options(), iamv1.E_MethodAuthorization).(*iamv1.MethodAuthorizationOptions)
}

func private(s string) string {
_, n := utf8.DecodeRuneInString(s)
return strings.ToLower(s[:n]) + s[n:]
}

func fileVarName(f *protogen.File, suffix string) string {
return private(f.GoDescriptorIdent.GoName) + "_" + suffix
}
31 changes: 31 additions & 0 deletions cmd/protoc-gen-go-iam/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import (
"fmt"
"os"
"path/filepath"

"go.einride.tech/iam/cmd/protoc-gen-go-iam/internal/geniam"
"google.golang.org/protobuf/compiler/protogen"
)

const docURL = "https://pkg.go.dev/go.einride.tech/iam"

func main() {
if len(os.Args) == 2 && os.Args[1] == "--version" {
_, _ = fmt.Fprintf(os.Stdout, "%v %v\n", filepath.Base(os.Args[0]), geniam.Version)
os.Exit(0)
}
if len(os.Args) == 2 && os.Args[1] == "--help" {
_, _ = fmt.Fprintf(os.Stdout, "See %s for usage information.\n", docURL)
os.Exit(0)
}
protogen.Options{}.Run(func(gen *protogen.Plugin) error {
for _, f := range gen.Files {
if f.Generate {
geniam.GenerateFile(gen, f)
}
}
return nil
})
}
Loading

0 comments on commit ce9d4e3

Please sign in to comment.