importpath = "go.fuhry.dev/runtime/ephs",
visibility = ["//visibility:public"],
deps = [
- "//grpc",
+ "//grpc/internal/client",
+ "//grpc/internal/common",
"//mtls",
"//proto/service/ephs",
"//utils/log",
"time"
"github.com/quic-go/quic-go"
- "go.fuhry.dev/runtime/grpc"
+ grpc "go.fuhry.dev/runtime/grpc/internal/client"
+ grpc_common "go.fuhry.dev/runtime/grpc/internal/common"
"go.fuhry.dev/runtime/mtls"
ephs_pb "go.fuhry.dev/runtime/proto/service/ephs"
"go.fuhry.dev/runtime/utils/log"
if c.client == nil {
c.client, err = grpc.NewGrpcClient(c.defaultCtx, serverId, c.id,
- grpc.WithConnectionFactory(&grpc.QUICConnectionFactory{
+ grpc.WithConnectionFactory(&grpc_common.QUICConnectionFactory{
QUICConfig: ephsQuicConfig.Clone(),
}))
if err != nil {
"gopkg.in/yaml.v3"
+ "go.fuhry.dev/runtime/mtls"
"go.fuhry.dev/runtime/utils/stringmatch"
)
return false
}
- keyMatcher = keyMatcher.Sub(map[string]string{"principal": principal})
+ vars := map[string]string{"principal": principal}
+
+ if identity, err := mtls.ParseRemoteIdentity(principal); err == nil {
+ vars["class"] = identity.Class.String()
+ vars["domain"] = identity.Domain
+ vars["name"] = identity.Principal
+ }
+
+ keyMatcher = keyMatcher.Sub(vars)
return keyMatcher.Match(key)
}
const testRules = `
rules:
- principal:
- exact: bob
+ exact: spiffe://domain.example.com/user/bob
key:
- exact: /{{principal}}/can/write/this
+ exact: "{{name}}/can/write/this"
- principal:
any: true
key:
- exact: /writable/by/self/{{principal}}
+ exact: writable/by/self/{{name}}
- principal:
or:
- - exact: bob
- - exact: suzie
+ - exact: spiffe://domain.example.com/user/bob
+ - exact: spiffe://domain.example.com/user/suzie
key:
- prefix: /or/test/{{principal}}/
+ prefix: or/test/{{name}}/
+ - principal:
+ prefix: spiffe://domain.example.com/service/
+ key:
+ exact: acl/{{name}}.yaml
`
func TestAcl(t *testing.T) {
+ assert := assert.New(t)
+
type testCase struct {
description, princ, key string
expect bool
}
acl, err := LoadAclString(testRules)
- assert.NoError(t, err)
+ assert.NoError(err)
+ if err != nil {
+ return
+ }
testCases := []testCase{
{
- "bob has access to /bob/can/write/this",
- "bob",
- "/bob/can/write/this",
+ "bob has access to bob/can/write/this",
+ "spiffe://domain.example.com/user/bob",
+ "bob/can/write/this",
true,
},
{
- "bob does not have access to /suzie/can/write/this",
- "bob",
- "/suzie/can/write/this",
+ "bob does not have access to suzie/can/write/this",
+ "spiffe://domain.example.com/user/bob",
+ "suzie/can/write/this",
false,
},
{
- "suzie does not have access to /suzie/can/write/this",
- "suzie",
- "/suzie/can/write/this",
+ "suzie does not have access to suzie/can/write/this",
+ "spiffe://domain.example.com/user/suzie",
+ "suzie/can/write/this",
false,
},
{
- "bob has access to /writable/by/self/bob",
- "bob",
- "/writable/by/self/bob",
+ "bob has access to writable/by/self/bob",
+ "spiffe://domain.example.com/user/bob",
+ "writable/by/self/bob",
true,
},
{
- "suzie has access to /writable/by/self/suzie",
- "suzie",
- "/writable/by/self/suzie",
+ "suzie has access to writable/by/self/suzie",
+ "spiffe://domain.example.com/user/suzie",
+ "writable/by/self/suzie",
true,
},
{
- "bob has access to /writable/by/self/suzie",
- "suzie",
- "/writable/by/self/suzie",
+ "bob has access to writable/by/self/suzie",
+ "spiffe://domain.example.com/user/suzie",
+ "writable/by/self/suzie",
true,
},
{
- "bob has access to /or/test/bob/foo",
- "bob",
- "/or/test/bob/foo",
+ "bob has access to or/test/bob/foo",
+ "spiffe://domain.example.com/user/bob",
+ "or/test/bob/foo",
true,
},
{
- "suzie has access to /or/test/suzie/foo",
- "suzie",
- "/or/test/suzie/foo",
+ "suzie has access to or/test/suzie/foo",
+ "spiffe://domain.example.com/user/suzie",
+ "or/test/suzie/foo",
true,
},
{
- "frank does not have access to /or/test/frank/foo",
- "frank",
- "/or/test/frank/foo",
+ "frank does not have access to or/test/frank/foo",
+ "spiffe://domain.example.com/user/frank",
+ "or/test/frank/foo",
+ false,
+ },
+ {
+ "bob does not have access no/rule/for/this",
+ "spiffe://domain.example.com/user/bob",
+ "no/rule/for/this",
false,
},
{
- "bob does not have access /no/rule/for/this",
- "bob",
- "/no/rule/for/this",
+ "bob has access to acl/bob.yaml",
+ "spiffe://domain.example.com/service/bob",
+ "acl/bob.yaml",
+ true,
+ },
+ {
+ "bob has no access to acl/frank.yaml",
+ "spiffe://domain.example.com/service/bob",
+ "acl/frank.yaml",
false,
},
}
for i, tc := range testCases {
- assert.Equal(t, tc.expect, acl.Check(tc.princ, tc.key),
+ assert.Equal(tc.expect, acl.Check(tc.princ, tc.key),
"test case %d: %s", i, tc.description)
}
}
return "", err
}
- aclPath := strings.Join(parts[2:], "/")
+ aclPath := strings.Join(parts[3:], "/")
+
+ if parts[2] == "local" {
+ parts[2] = cell
+ }
if s.acl != nil {
if !s.acl.Check(ident, aclPath) {
return "", status.Errorf(
codes.PermissionDenied,
- "access to path %q denied for %q",
- aclPath,
- ident)
+ "access to path %q denied for %q (rule path: %q)",
+ strings.Join(parts, "/"),
+ ident,
+ aclPath)
}
}
- if parts[2] == "local" {
- parts[2] = cell
- }
-
return strings.Join(parts, "/"), nil
}
go_library(
name = "grpc",
- srcs = [
- "client.go",
- "conn_base.go",
- "conn_quic.go",
- "conn_tcp.go",
- "context.go",
- "healthcheck.go",
- "server.go",
- ],
+ srcs = ["imports.go"],
importpath = "go.fuhry.dev/runtime/grpc",
visibility = ["//visibility:public"],
deps = [
- "//grpc/acl",
- "//mtls",
- "//mtls/certutil",
- "//sd",
- "//utils/hostname",
- "//utils/log",
- "@com_github_hashicorp_golang_lru_v2//:golang-lru",
- "@com_github_quic_go_quic_go//:quic-go",
- "@dev_fuhry_go_grpc_quic//:grpc-quic",
- "@org_golang_google_grpc//:grpc",
- "@org_golang_google_grpc//codes",
- "@org_golang_google_grpc//credentials",
- "@org_golang_google_grpc//health/grpc_health_v1",
- "@org_golang_google_grpc//peer",
- "@org_golang_google_grpc//status",
+ "//grpc/internal/client",
+ "//grpc/internal/common",
+ "//grpc/internal/server",
],
)
--- /dev/null
+package grpc
+
+import (
+ "go.fuhry.dev/runtime/grpc/internal/client"
+ "go.fuhry.dev/runtime/grpc/internal/common"
+ "go.fuhry.dev/runtime/grpc/internal/server"
+)
+
+// type aliases: common
+type ConnectionFactory = common.ConnectionFactory
+type ContextDialer = common.ContextDialer
+type QUICConnectionFactory = common.QUICConnectionFactory
+type TCPConnectionFactory = common.TCPConnectionFactory
+
+// type aliases: client
+type Client = client.Client
+type ClientOption = client.ClientOption
+type AddressProvider = client.AddressProvider
+type ClientConn = client.ClientConn
+
+// type aliases: server
+type Server = server.Server
+
+// function aliases: common
+var NewDefaultConnectionFactory = common.NewDefaultConnectionFactory
+var RegisterTransport = common.RegisterTransport
+
+// function aliases: client
+var WithConnectionFactory = client.WithConnectionFactory
+var WithAddressProvider = client.WithAddressProvider
+var WithStaticAddress = client.WithStaticAddress
+
+var NewGrpcClient = client.NewGrpcClient
+
+// function aliases: server
+var RandomPort = server.RandomPort
+var NewGrpcServer = server.NewGrpcServer
+var NewGrpcServerWithPort = server.NewGrpcServerWithPort
+var PeerIdentity = server.PeerIdentity
+var NewHealthCheckServicer = server.NewHealthCheckServicer
go_library(
name = "acl",
srcs = ["acl_yaml.go"],
- importpath = "go.fuhry.dev/runtime/grpc/acl",
+ importpath = "go.fuhry.dev/runtime/grpc/internal/acl",
visibility = ["//visibility:public"],
deps = [
+ "//config_watcher",
"//constants",
+ "//ephs",
"//mtls",
+ "//utils/context",
"//utils/log",
"@in_gopkg_yaml_v3//:yaml_v3",
],
import (
"fmt"
+ "io"
"net/url"
"os"
"path"
"strings"
+ "go.fuhry.dev/runtime/config_watcher"
"go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/ephs"
"go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/utils/context"
"go.fuhry.dev/runtime/utils/log"
"gopkg.in/yaml.v3"
)
// "principal" - the thing to allow
type aclYaml struct {
Services map[string][]*aclEntry `yaml:",inline"`
+
+ logger log.Logger
}
var aclSearchPaths = []string{
func TryLoadAcl(serverId mtls.Identity) ACLChecker {
logger := log.WithPrefix("ACLChecker")
+ var (
+ fsErr, ephsErr error
+ )
for _, dir := range aclSearchPaths {
path := path.Join(dir, serverId.Name()+"_acl.yaml")
- if ay, err := loadAclFromPath(path); err == nil {
- logger.V(1).Infof("loaded ACLs from %s", path)
+ if ay, err := loadAclFromPath(serverId, path); err == nil {
+ logger.V(1).Infof("loaded ACLs from filesystem @ %s", path)
return ay
+ } else {
+ fsErr = err
}
}
- logger.V(1).Infof("using default ACLs for server %s", serverId.Name())
+ if ay, err := loadAclFromEphs(serverId); err == nil {
+ logger.V(1).Infof("loaded ACLs for service %q from ephs", serverId.Name())
+ return ay
+ } else {
+ ephsErr = err
+ }
+
+ logger.V(1).Infof(
+ "using default ACLs for server %s\n filesystem err: %v (paths: %+v)\n ephs err: %v",
+ serverId.Name(), fsErr, aclSearchPaths, ephsErr)
+
return &aclYaml{
Services: map[string][]*aclEntry{
"DEFAULT": {
},
},
},
+
+ logger: log.Default().WithPrefix(fmt.Sprintf("aclYaml(%s, <default>)", serverId.Name())),
}
}
-func loadAclFromPath(path string) (*aclYaml, error) {
+func loadAclFromPath(serverId mtls.Identity, path string) (*aclYaml, error) {
contents, err := os.ReadFile(path)
if err != nil {
return nil, err
}
- ay := &aclYaml{}
+ ay := &aclYaml{
+ logger: log.Default().WithPrefix(fmt.Sprintf("aclYaml(%s, %q)", serverId.Name(), path)),
+ }
+
err = yaml.Unmarshal(contents, ay)
if err != nil {
return nil, err
}
+ ctx, _ := context.Interruptible()
+ if w, err := config_watcher.Watch(ctx, path); err == nil {
+ go ay.watch(w)
+ }
+
return ay, nil
}
+func loadAclFromEphs(serverId mtls.Identity) (*aclYaml, error) {
+ ephsClient, err := ephs.DefaultClient()
+ if err != nil {
+ return nil, err
+ }
+
+ ephsPath := fmt.Sprintf("/ephs/local/acl/%s.yaml", serverId.Name())
+ reader, err := ephsClient.Get(ephsPath)
+ if err != nil {
+ return nil, err
+ }
+
+ contents, err := io.ReadAll(reader)
+ if err != nil {
+ return nil, err
+ }
+
+ ay := &aclYaml{
+ logger: log.Default().WithPrefix(fmt.Sprintf("aclYaml(%s, %q)", serverId.Name(), ephsPath)),
+ }
+
+ err = yaml.Unmarshal(contents, ay)
+ if err != nil {
+ return nil, err
+ }
+
+ ctx, _ := context.Interruptible()
+ if w, err := config_watcher.Watch(ctx, ephsPath); err == nil {
+ go ay.watch(w)
+ }
+
+ return ay, nil
+}
+
+func (ay *aclYaml) watch(cw config_watcher.ConfigWatcher) {
+ for {
+ update, err := cw.Next()
+ if err != nil {
+ return
+ }
+
+ if err := yaml.Unmarshal(update.Contents, ay); err != nil {
+ ay.logger.Warningf("failed to parse reloaded ACLs: %v", err)
+ } else {
+ ay.logger.Notice("ACLs reloaded")
+ }
+ }
+}
+
func (ay *aclYaml) Check(method string, spiffe *url.URL) error {
logger := log.WithPrefix("ACLChecker")
--- /dev/null
+load("@rules_go//go:def.bzl", "go_library")
+
+package(
+ default_visibility = [
+ "//ephs:__subpackages__",
+ "//grpc:__subpackages__",
+ ],
+)
+
+go_library(
+ name = "client",
+ srcs = ["client.go"],
+ importpath = "go.fuhry.dev/runtime/grpc/internal/client",
+ deps = [
+ "//grpc/internal/common",
+ "//mtls",
+ "//sd",
+ "@org_golang_google_grpc//:grpc",
+ ],
+)
-package grpc
+package client
import (
"context"
"fmt"
"net"
+ "go.fuhry.dev/runtime/grpc/internal/common"
"go.fuhry.dev/runtime/mtls"
"go.fuhry.dev/runtime/sd"
"google.golang.org/grpc"
)
type ClientConn = grpc.ClientConn
+type ConnectionFactory = common.ConnectionFactory
type Client interface {
Conn() (*grpc.ClientConn, error)
serverId mtls.Identity
clientId mtls.Identity
watcher AddressProvider
- connFac ConnectionFactory
+ connFac common.ConnectionFactory
}
type staticAddressProvider struct {
return s.addresses, nil
}
-func WithConnectionFactory(fac ConnectionFactory) ClientOption {
+func WithConnectionFactory(fac common.ConnectionFactory) ClientOption {
return &clientOption{
f: func(c *client) error {
c.connFac = fac
ctx: ctx,
serverId: serverId,
clientId: clientId,
- connFac: NewDefaultConnectionFactory(),
+ connFac: common.NewDefaultConnectionFactory(),
}
for _, opt := range opts {
--- /dev/null
+load("@rules_go//go:def.bzl", "go_library")
+
+package(
+ default_visibility = [
+ "//ephs:__subpackages__",
+ "//grpc:__subpackages__",
+ ],
+)
+
+go_library(
+ name = "common",
+ srcs = [
+ "conn_base.go",
+ "conn_quic.go",
+ "conn_tcp.go",
+ ],
+ importpath = "go.fuhry.dev/runtime/grpc/internal/common",
+ deps = [
+ "//utils/log",
+ "@com_github_quic_go_quic_go//:quic-go",
+ "@dev_fuhry_go_grpc_quic//:grpc-quic",
+ "@org_golang_google_grpc//credentials",
+ ],
+)
-package grpc
+package common
import (
"context"
-package grpc
+package common
import (
"context"
-package grpc
+package common
import (
"context"
--- /dev/null
+load("@rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "server",
+ srcs = [
+ "context.go",
+ "healthcheck.go",
+ "server.go",
+ ],
+ importpath = "go.fuhry.dev/runtime/grpc/internal/server",
+ visibility = ["//grpc:__subpackages__"],
+ deps = [
+ "//grpc/internal/acl",
+ "//grpc/internal/common",
+ "//mtls",
+ "//mtls/certutil",
+ "//sd",
+ "//utils/hostname",
+ "//utils/log",
+ "@com_github_hashicorp_golang_lru_v2//:golang-lru",
+ "@dev_fuhry_go_grpc_quic//:grpc-quic",
+ "@org_golang_google_grpc//:grpc",
+ "@org_golang_google_grpc//codes",
+ "@org_golang_google_grpc//credentials",
+ "@org_golang_google_grpc//health/grpc_health_v1",
+ "@org_golang_google_grpc//peer",
+ "@org_golang_google_grpc//status",
+ ],
+)
-package grpc
+package server
import (
"context"
-package grpc
+package server
import (
"context"
-package grpc
+package server
import (
"context"
lru "github.com/hashicorp/golang-lru/v2"
grpc_quic "go.fuhry.dev/grpc-quic"
- "go.fuhry.dev/runtime/grpc/acl"
+ "go.fuhry.dev/runtime/grpc/internal/acl"
+ "go.fuhry.dev/runtime/grpc/internal/common"
"go.fuhry.dev/runtime/mtls"
"go.fuhry.dev/runtime/mtls/certutil"
"go.fuhry.dev/runtime/sd"
acl acl.ACLChecker
log log.Logger
sessions *lru.Cache[string, *session]
- connFac ConnectionFactory
+ connFac common.ConnectionFactory
hc HealthCheckServicer
}
verifier: cv,
log: log.WithPrefix(fmt.Sprintf("grpcServer:%s", id.Name())),
sessions: sessionsLru,
- connFac: NewDefaultConnectionFactory(),
+ connFac: common.NewDefaultConnectionFactory(),
hc: NewHealthCheckServicer(),
}
type IdentityClass uint
-type remoteIdentity struct {
- class IdentityClass
- domain string
- princ string
+type RemoteIdentity struct {
+ Class IdentityClass
+ Domain string
+ Principal string
}
const (
}
func (cv *mtlsPeerVerifier) checkName(name string) error {
- id, err := parseName(name)
+ id, err := ParseRemoteIdentity(name)
if err != nil {
return err
}
- cv.log.V(3).Debugf("parsed name %q to %+v (class=%s)", name, id, id.class.String())
+ cv.log.V(3).Debugf("parsed name %q to %+v (class=%s)", name, id, id.Class.String())
if _, ok := cv.allowedPrincipals[All]; ok {
return nil
}
if allowedDomains, ok := cv.allowedPrincipals[Domain]; ok {
- if allowedDomains.Contains(id.domain) {
- cv.log.V(3).Debugf("domain %q exactly matched allowlist", id.domain)
+ if allowedDomains.Contains(id.Domain) {
+ cv.log.V(3).Debugf("domain %q exactly matched allowlist", id.Domain)
return nil
}
domainOk := false
if val[0] != '.' {
continue
}
- if strings.HasSuffix(id.domain, val) {
- cv.log.V(3).Debugf("domain %q matched suffix rule %q", id.domain, val)
+ if strings.HasSuffix(id.Domain, val) {
+ cv.log.V(3).Debugf("domain %q matched suffix rule %q", id.Domain, val)
domainOk = true
break
}
}
if !domainOk {
- return fmt.Errorf("trust domain %q is not allowed to authenticate to this service", id.domain)
+ return fmt.Errorf("trust domain %q is not allowed to authenticate to this service", id.Domain)
}
}
- if allowedPrincipals, ok := cv.allowedPrincipals[id.class]; ok {
- if !allowedPrincipals.Contains(id.princ) {
- return fmt.Errorf("principal %q is not allowed to authenticate to this service", id.princ)
+ if allowedPrincipals, ok := cv.allowedPrincipals[id.Class]; ok {
+ if !allowedPrincipals.Contains(id.Principal) {
+ return fmt.Errorf("principal %q is not allowed to authenticate to this service", id.Principal)
}
- cv.log.V(3).Debugf("principal %q matched allowlist", id.princ)
+ cv.log.V(3).Debugf("principal %q matched allowlist", id.Principal)
return nil
}
return fmt.Errorf("principals of this type are not allowed")
}
-func parseName(name string) (*remoteIdentity, error) {
+func ParseRemoteIdentity(name string) (*RemoteIdentity, error) {
exps := []struct {
expr *regexp.Regexp
class IdentityClass
parts := e.expr.FindStringSubmatch(name)
iDomain, iPrinc := e.expr.SubexpIndex("domain"), e.expr.SubexpIndex("principal")
- return &remoteIdentity{
- class: e.class,
- domain: parts[iDomain],
- princ: parts[iPrinc],
+ return &RemoteIdentity{
+ Class: e.class,
+ Domain: parts[iDomain],
+ Principal: parts[iPrinc],
}, nil
}
--- /dev/null
+load("@rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "context",
+ srcs = [
+ "imports.go",
+ "interruptible.go",
+ ],
+ importpath = "go.fuhry.dev/runtime/utils/context",
+ visibility = ["//visibility:public"],
+)
--- /dev/null
+package context
+
+import (
+ "context"
+)
+
+type Context = context.Context
+type CancelFunc = context.CancelFunc
+type CancelCauseFunc = context.CancelCauseFunc
+
+var Canceled = context.Canceled
+var DeadlineExceeded = context.DeadlineExceeded
+
+var Background = context.Background
+var TODO = context.TODO
+
+var AfterFunc = context.AfterFunc
+var Cause = context.Cause
+var WithCancel = context.WithCancel
+var WithCancelCause = context.WithCancelCause
+var WithDeadline = context.WithDeadline
+var WithDeadlineCause = context.WithDeadlineCause
+var WithTimeout = context.WithTimeout
+var WithValue = context.WithValue
--- /dev/null
+package context
+
+import (
+ "os/signal"
+ "sync"
+ "syscall"
+)
+
+var interruptibleCtx Context
+var interruptibleCancel CancelFunc
+var interruptibleOnce sync.Once
+
+func Interruptible() (Context, CancelFunc) {
+ interruptibleOnce.Do(func() {
+ interruptibleCtx, interruptibleCancel = signal.NotifyContext(
+ Background(),
+ syscall.SIGINT,
+ syscall.SIGTERM,
+ )
+ })
+
+ return interruptibleCtx, interruptibleCancel
+}