Project status: alpha
Not all planned features are completed. The API, spec, status and other user facing objects are subject to change. We do not support backward-compatibility for the alpha releases.
go-openfga
is a simple implementation of an easy to use (and currently partial) client for the OpenFGA gRPC API.
It also provides a simple way to run openfga in process.
import openfga "go.linka.cloud/go-openfga"
protoc-gen-go-openfga
is a protoc plugin that generates openfga schema and go code to register access checks into the interceptor.
go install go.linka.cloud/go-openfga/cmd/protoc-gen-go-openfga
Use the plugin as any other protoc plugins.
For a given base openfga
module:
module base
type system
relations
define admin: [user]
define writer: [user]
define reader: [user]
define watcher: [user]
type user
For a given resource.proto
:
syntax = "proto3";
package example;
option go_package = "./example";
import "openfga/openfga.proto";
import "patch/go.proto";
option (go.lint).all = true;
service ResourceService {
option (openfga.module) = {
name: "resource",
extends: [ {
type: "system",
relations: [
{ define: "resource_admin", as: "[user, user with non_expired_grant] or admin" },
{ define: "resource_writer", as: "[user] or resource_admin" },
{ define: "resource_reader", as: "[user] or resource_admin or reader" },
{ define: "resource_watcher", as: "[user] or resource_admin or watcher" },
{ define: "can_create_resource", as: "resource_writer" },
{ define: "can_list_resources", as: "resource_reader" },
{ define: "can_watch_resources", as: "resource_watcher" }
]
} ],
definitions: [ {
type: "resource",
relations: [
{ define: "system", as: "[system]" },
{ define: "admin", as: "[user] or resource_admin from system" },
{ define: "reader", as: "[user] or resource_reader from system" },
{ define: "can_read", as: "reader" },
{ define: "can_update", as: "admin" },
{ define: "can_delete", as: "admin" }
]
} ],
conditions: [ "non_expired_grant(current_time: timestamp, grant_time: timestamp, grant_duration: duration) { current_time < grant_time + grant_duration }" ]
};
rpc Create (CreateRequest) returns (CreateResponse) {
option (openfga.access) = { type: "system", id: "default", check: "can_create_resource" };
};
rpc Read (ReadRequest) returns (ReadResponse) {
option (openfga.access) = { type: "resource", id: "{id}", check: "can_read" };
}
rpc Update (UpdateRequest) returns (UpdateResponse) {
option (openfga.access) = { type: "resource", id: "{resource.id}", check: "can_update" };
}
rpc Delete(DeleteRequest) returns (DeleteResponse) {
option (openfga.access) = { type: "resource", id: "{id}", check: "can_delete" };
}
rpc List(ListRequest) returns (ListResponse) {
option (openfga.access) = { type: "system", id: "default", check: "can_list_resources" };
}
rpc Watch(WatchRequest) returns (stream Event) {
option (openfga.access) = { type: "system", id: "default", check: "can_watch_resources" };
}
}
// ... requests, responses and event definitions ...
The following resource.fga
openfga
module will be generated:
# Code generated by protoc-gen-go-openfga. DO NOT EDIT.
module resource
extend type system
relations
define resource_admin: [user, user with non_expired_grant] or admin
define resource_writer: [user] or resource_admin
define resource_reader: [user] or resource_admin or reader
define resource_watcher: [user] or resource_admin or watcher
define can_create_resource: resource_writer
define can_list_resources: resource_reader
define can_watch_resources: resource_watcher
type resource
relations
define system: [system]
define admin: [user] or resource_admin from system
define reader: [user] or resource_reader from system
define can_read: reader
define can_update: admin
define can_delete: admin
condition non_expired_grant(current_time: timestamp, grant_time: timestamp, grant_duration: duration) { current_time < grant_time + grant_duration }
And following code will be generated:
// Code generated by protoc-gen-go-openfga. DO NOT EDIT.
package resource
import (
"context"
_ "embed"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
fgainterceptors "go.linka.cloud/go-openfga/interceptors"
)
var (
_ = codes.OK
_ = status.New
)
//go:embed resource.fga
var FGAModel string
const (
FGASystemType = "system"
FGASystemResourceAdmin = "resource_admin"
FGASystemResourceWriter = "resource_writer"
FGASystemResourceReader = "resource_reader"
FGASystemResourceWatcher = "resource_watcher"
FGASystemCanCreateResource = "can_create_resource"
FGASystemCanListResources = "can_list_resources"
FGASystemCanWatchResources = "can_watch_resources"
FGAResourceType = "resource"
FGAResourceSystem = "system"
FGAResourceAdmin = "admin"
FGAResourceReader = "reader"
FGAResourceCanRead = "can_read"
FGAResourceCanUpdate = "can_update"
FGAResourceCanDelete = "can_delete"
)
func FGASystemObject(id string) string {
return FGASystemType + ":" + id
}
func FGAResourceObject(id string) string {
return FGAResourceType + ":" + id
}
func RegisterFGA(fga fgainterceptors.FGA) {
fga.Register(ResourceService_Create_FullMethodName, func(ctx context.Context, req any) (object string, relation string, err error) {
return FGASystemType + ":" + "default", FGASystemCanCreateResource, nil
})
fga.Register(ResourceService_Read_FullMethodName, func(ctx context.Context, req any) (object string, relation string, err error) {
r, ok := req.(*ReadRequest)
if !ok {
panic("unexpected request type: expected ReadRequest")
}
id := r.GetID()
if id == "" {
return "", "", status.Error(codes.InvalidArgument, "id is required")
}
return FGAResourceObject(id), FGAResourceCanRead, nil
})
fga.Register(ResourceService_Update_FullMethodName, func(ctx context.Context, req any) (object string, relation string, err error) {
r, ok := req.(*UpdateRequest)
if !ok {
panic("unexpected request type: expected UpdateRequest")
}
id := r.GetResource().GetID()
if id == "" {
return "", "", status.Error(codes.InvalidArgument, "resource.id is required")
}
return FGAResourceObject(id), FGAResourceCanUpdate, nil
})
fga.Register(ResourceService_Delete_FullMethodName, func(ctx context.Context, req any) (object string, relation string, err error) {
r, ok := req.(*DeleteRequest)
if !ok {
panic("unexpected request type: expected DeleteRequest")
}
id := r.GetID()
if id == "" {
return "", "", status.Error(codes.InvalidArgument, "id is required")
}
return FGAResourceObject(id), FGAResourceCanDelete, nil
})
fga.Register(ResourceService_List_FullMethodName, func(ctx context.Context, req any) (object string, relation string, err error) {
return FGASystemType + ":" + "default", FGASystemCanListResources, nil
})
fga.Register(ResourceService_Watch_FullMethodName, func(ctx context.Context, req any) (object string, relation string, err error) {
return FGASystemType + ":" + "default", FGASystemCanWatchResources, nil
})
}
See the example directory for complete example.
package main
import (
"context"
_ "embed"
"fmt"
"log"
"time"
"github.com/fullstorydev/grpchan/inprocgrpc"
"github.com/openfga/openfga/pkg/server"
"github.com/openfga/openfga/pkg/storage/memory"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"go.linka.cloud/go-openfga"
pb "go.linka.cloud/go-openfga/example/pb"
"go.linka.cloud/go-openfga/interceptors"
)
//go:embed base.fga
var modelBase string
// userKey is the key used to store the user in the context metadata
const userKey = "user"
// defaultSystem is the default system object
var defaultSystem = pb.FGASystemObject("default")
// userContext returns a new context with the user set in the metadata
func userContext(ctx context.Context, user string) context.Context {
return metadata.NewOutgoingContext(ctx, metadata.Pairs(userKey, user))
}
// contextUser returns the user from the context metadata
func contextUser(ctx context.Context) (string, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok || len(md.Get(userKey)) == 0 {
return "", status.Errorf(codes.Unauthenticated, "missing user from metadata")
}
return "user:" + md.Get(userKey)[0], nil
}
// mustContextUser returns the user from the context metadata or panics
func mustContextUser(ctx context.Context) string {
user, err := contextUser(ctx)
if err != nil {
panic(err)
}
return user
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// create the in-memory openfga server
mem := memory.New()
f, err := openfga.New(server.WithDatastore(mem))
if err != nil {
log.Fatal(err)
}
defer f.Close()
// create the store
s, err := f.CreateStore(ctx, "default")
if err != nil {
log.Fatal(err)
}
// write the model
model, err := s.WriteAuthorizationModel(ctx, modelBase, pb.FGAModel)
if err != nil {
log.Fatal(err)
}
// create the interceptors
fga, err := interceptors.New(ctx, model, interceptors.WithUserFunc(func(ctx context.Context) (string, map[string]any, error) {
user, err := contextUser(ctx)
if err != nil {
return "", nil, err
}
return user, nil, nil
}))
if err != nil {
log.Fatal(err)
}
// register some users with system roles
for _, v := range []string{
pb.FGASystemResourceReader,
pb.FGASystemResourceWriter,
pb.FGASystemResourceAdmin,
pb.FGASystemResourceWatcher,
} {
if err := model.Write(ctx, defaultSystem, v, fmt.Sprintf("user:%s", v)); err != nil {
log.Fatal(err)
}
}
// create the service
svc := NewResourceService()
// register the service permissions
pb.RegisterFGA(fga)
// create the in-process grpc channel
channel := (&inprocgrpc.Channel{}).
WithServerUnaryInterceptor(fga.UnaryServerInterceptor()).
WithServerStreamInterceptor(fga.StreamServerInterceptor())
// register the service as usual
pb.RegisterResourceServiceServer(channel, svc)
// create a client
client := pb.NewResourceServiceClient(channel)
// validate checks
if _, err := client.List(userContext(ctx, pb.FGASystemResourceReader), &pb.ListRequest{}); err != nil {
log.Fatal(err)
}
if _, err := client.Create(userContext(ctx, pb.FGASystemResourceReader), &pb.CreateRequest{Resource: &pb.Resource{ID: "0"}}); err == nil {
log.Fatal("reader should not be able to create")
}
if _, err := client.Create(userContext(ctx, pb.FGASystemResourceWriter), &pb.CreateRequest{Resource: &pb.Resource{ID: "0"}}); err != nil {
log.Fatal(err)
}
wctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
ss, err := client.Watch(userContext(wctx, pb.FGASystemResourceWriter), &pb.WatchRequest{})
if err != nil {
log.Fatal(err)
}
// try to receive an event as the interceptor is not called when creating the stream
if _, err := ss.Recv(); err == nil {
log.Fatal("writer should not be able to watch")
}
wctx, cancel = context.WithTimeout(ctx, time.Second)
defer cancel()
ss, err = client.Watch(userContext(wctx, pb.FGASystemResourceAdmin), &pb.WatchRequest{})
if err != nil {
log.Fatal(err)
}
// create a resource to trigger an event
go func() {
time.Sleep(100 * time.Millisecond)
if _, err := client.Create(userContext(ctx, pb.FGASystemResourceWriter), &pb.CreateRequest{Resource: &pb.Resource{ID: "1"}}); err != nil {
log.Fatal(err)
}
}()
if _, err := ss.Recv(); err != nil {
log.Fatal(err)
}
}