]> go.fuhry.dev Git - runtime.git/commitdiff
[grpc] support loading ACLs from ephs
authorDan Fuhry <dan@fuhry.com>
Fri, 14 Nov 2025 16:52:07 +0000 (11:52 -0500)
committerDan Fuhry <dan@fuhry.com>
Fri, 14 Nov 2025 17:05:25 +0000 (12:05 -0500)
Supporting changes:

- Rearranged grpc into internal/{client,server,common} packages to deal with circular dep between ephs and grpc
- Rename `mtls.parseName` -> `mtls.ParseRemoteIdentity` and make the returned struct and its properties public
- In ephs server ACLs, try to parse principal; if successful, set `name`, `class` and `domain` substitutions in the key
- Fixed severely incorrect invocation of acl.Check in ephs server (not a security issue, legitimate requests were blocked but no illegitimate requests were allowed)
- Add `utils/context` package - wrapper for `context` with shared `Interruptible` context that cancels on SIGTERM and SIGINT

23 files changed:
ephs/BUILD.bazel
ephs/client.go
ephs/servicer/acl.go
ephs/servicer/acl_test.go
ephs/servicer/servicer.go
grpc/BUILD.bazel
grpc/imports.go [new file with mode: 0644]
grpc/internal/acl/BUILD.bazel [moved from grpc/acl/BUILD.bazel with 67% similarity]
grpc/internal/acl/acl_yaml.go [moved from grpc/acl/acl_yaml.go with 60% similarity]
grpc/internal/client/BUILD.bazel [new file with mode: 0644]
grpc/internal/client/client.go [moved from grpc/client.go with 91% similarity]
grpc/internal/common/BUILD.bazel [new file with mode: 0644]
grpc/internal/common/conn_base.go [moved from grpc/conn_base.go with 98% similarity]
grpc/internal/common/conn_quic.go [moved from grpc/conn_quic.go with 99% similarity]
grpc/internal/common/conn_tcp.go [moved from grpc/conn_tcp.go with 98% similarity]
grpc/internal/server/BUILD.bazel [new file with mode: 0644]
grpc/internal/server/context.go [moved from grpc/context.go with 98% similarity]
grpc/internal/server/healthcheck.go [moved from grpc/healthcheck.go with 99% similarity]
grpc/internal/server/server.go [moved from grpc/server.go with 96% similarity]
mtls/verify_names.go
utils/context/BUILD.bazel [new file with mode: 0644]
utils/context/imports.go [new file with mode: 0644]
utils/context/interruptible.go [new file with mode: 0644]

index d5ead53a7d68186f066999d1f6ff939318139790..242cfba06030dfbd6ecf5a326ab3636d990b01c6 100644 (file)
@@ -6,7 +6,8 @@ go_library(
     importpath = "go.fuhry.dev/runtime/ephs",
     visibility = ["//visibility:public"],
     deps = [
-        "//grpc",
+        "//grpc/internal/client",
+        "//grpc/internal/common",
         "//mtls",
         "//proto/service/ephs",
         "//utils/log",
index baba8bfa9c7348a542ea68bddbd76e23191e9524..0ca364b23704188a0b9a10d2bce4edaddca487d1 100644 (file)
@@ -12,7 +12,8 @@ import (
        "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"
@@ -415,7 +416,7 @@ func (c *clientImpl) grpcClient() (ephs_pb.EphsClient, error) {
 
        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 {
index e9cf0557e2eec2009967264ccf5f7ad6c1802158..5591e7c94ccf3ed7e9a5883fc347920095f128ff 100644 (file)
@@ -8,6 +8,7 @@ import (
 
        "gopkg.in/yaml.v3"
 
+       "go.fuhry.dev/runtime/mtls"
        "go.fuhry.dev/runtime/utils/stringmatch"
 )
 
@@ -66,7 +67,15 @@ func (r *AclRule) Match(principal, key string) bool {
                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)
 }
index d744c7a23c9884a2e4be106466ddbaf23ac24c67..6dba593e5cd8318399cba83b6fde48eacccc1aec 100644 (file)
@@ -9,95 +9,116 @@ import (
 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)
        }
 }
index a95d72a8cf21d108e1ad4a5e9f9b5d946c2de992..d1f47c7ec6b226384a86e6c4bda29d6e696eba33 100644 (file)
@@ -170,22 +170,23 @@ func (s *ephsServicer) checkPath(ctx context.Context, path string) (string, erro
                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
 }
 
index 9d137236dc701c93b9ff83983dd8a2c3eeafba1c..736056ec60718bffcdb0654e32aaa708a09e51b6 100644 (file)
@@ -2,32 +2,12 @@ load("@rules_go//go:def.bzl", "go_library")
 
 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",
     ],
 )
diff --git a/grpc/imports.go b/grpc/imports.go
new file mode 100644 (file)
index 0000000..6c2de64
--- /dev/null
@@ -0,0 +1,40 @@
+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
similarity index 67%
rename from grpc/acl/BUILD.bazel
rename to grpc/internal/acl/BUILD.bazel
index d3e089647ec701eaf3f85f2f052ee23ec4a2bf7d..f190af5effb6a62cf8787b00fec5b86dc694a13c 100644 (file)
@@ -3,11 +3,14 @@ load("@rules_go//go:def.bzl", "go_library")
 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",
     ],
similarity index 60%
rename from grpc/acl/acl_yaml.go
rename to grpc/internal/acl/acl_yaml.go
index 648ef44deaccf73bbdcf4b5a820d489bf401d75a..9fc0591dc8000d290acfd3f055b013d6855c9e71 100644 (file)
@@ -2,13 +2,17 @@ package acl
 
 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"
 )
@@ -39,6 +43,8 @@ type aclEntry struct {
 // "principal" - the thing to allow
 type aclYaml struct {
        Services map[string][]*aclEntry `yaml:",inline"`
+
+       logger log.Logger
 }
 
 var aclSearchPaths = []string{
@@ -48,15 +54,30 @@ 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": {
@@ -65,24 +86,83 @@ func TryLoadAcl(serverId mtls.Identity) ACLChecker {
                                },
                        },
                },
+
+               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")
 
diff --git a/grpc/internal/client/BUILD.bazel b/grpc/internal/client/BUILD.bazel
new file mode 100644 (file)
index 0000000..42d11ce
--- /dev/null
@@ -0,0 +1,20 @@
+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",
+    ],
+)
similarity index 91%
rename from grpc/client.go
rename to grpc/internal/client/client.go
index 93072f5447ec7b0dae392c51b1db0798ceac6d04..bc5436960730346b2c8215f6eac90a337d610bd5 100644 (file)
@@ -1,16 +1,18 @@
-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)
@@ -37,7 +39,7 @@ type client struct {
        serverId mtls.Identity
        clientId mtls.Identity
        watcher  AddressProvider
-       connFac  ConnectionFactory
+       connFac  common.ConnectionFactory
 }
 
 type staticAddressProvider struct {
@@ -48,7 +50,7 @@ func (s *staticAddressProvider) GetAddrs(_ context.Context) ([]sd.ServiceAddress
        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
@@ -98,7 +100,7 @@ func NewGrpcClient(ctx context.Context, serverId, clientId mtls.Identity, opts .
                ctx:      ctx,
                serverId: serverId,
                clientId: clientId,
-               connFac:  NewDefaultConnectionFactory(),
+               connFac:  common.NewDefaultConnectionFactory(),
        }
 
        for _, opt := range opts {
diff --git a/grpc/internal/common/BUILD.bazel b/grpc/internal/common/BUILD.bazel
new file mode 100644 (file)
index 0000000..024c911
--- /dev/null
@@ -0,0 +1,24 @@
+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",
+    ],
+)
similarity index 98%
rename from grpc/conn_base.go
rename to grpc/internal/common/conn_base.go
index ef76db61fd33abcd5f17a59465500d5b440ef9b7..be0d682ccc46d9431a97ff6e674e4edf31373aeb 100644 (file)
@@ -1,4 +1,4 @@
-package grpc
+package common
 
 import (
        "context"
similarity index 99%
rename from grpc/conn_quic.go
rename to grpc/internal/common/conn_quic.go
index c949686808d6cabffbc45b7351a5d2612c4c3ac3..76eba448ecfcfc884b3b070dff9fc1dd326ce36b 100644 (file)
@@ -1,4 +1,4 @@
-package grpc
+package common
 
 import (
        "context"
similarity index 98%
rename from grpc/conn_tcp.go
rename to grpc/internal/common/conn_tcp.go
index 048b26aec3ca83d244ddf0fb273b7dda01f60fd7..32d24568acc89dce54b43bb683cf48fa852c431e 100644 (file)
@@ -1,4 +1,4 @@
-package grpc
+package common
 
 import (
        "context"
diff --git a/grpc/internal/server/BUILD.bazel b/grpc/internal/server/BUILD.bazel
new file mode 100644 (file)
index 0000000..b7a1bb2
--- /dev/null
@@ -0,0 +1,29 @@
+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",
+    ],
+)
similarity index 98%
rename from grpc/context.go
rename to grpc/internal/server/context.go
index 897e3455b01ba17e08a716f932f81078d46d5fee..4a92d58349d7774507822f619a78d29bddaf5103 100644 (file)
@@ -1,4 +1,4 @@
-package grpc
+package server
 
 import (
        "context"
similarity index 99%
rename from grpc/healthcheck.go
rename to grpc/internal/server/healthcheck.go
index ce8b14b23d609d9a5c5c007b9541d9f7b5151fdc..c6e3d67fa5ab6019f84eb4ffc65cfaf40321300b 100644 (file)
@@ -1,4 +1,4 @@
-package grpc
+package server
 
 import (
        "context"
similarity index 96%
rename from grpc/server.go
rename to grpc/internal/server/server.go
index 9e56c73c80fe893bfda4fd6f74704039135924d8..7ce5aef538476e1e3973fb0c3117363ce53e6d98 100644 (file)
@@ -1,4 +1,4 @@
-package grpc
+package server
 
 import (
        "context"
@@ -10,7 +10,8 @@ import (
 
        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"
@@ -33,7 +34,7 @@ type Server struct {
        acl        acl.ACLChecker
        log        log.Logger
        sessions   *lru.Cache[string, *session]
-       connFac    ConnectionFactory
+       connFac    common.ConnectionFactory
        hc         HealthCheckServicer
 }
 
@@ -86,7 +87,7 @@ func NewGrpcServerWithPort(id mtls.Identity, port uint16) (*Server, error) {
                verifier:  cv,
                log:       log.WithPrefix(fmt.Sprintf("grpcServer:%s", id.Name())),
                sessions:  sessionsLru,
-               connFac:   NewDefaultConnectionFactory(),
+               connFac:   common.NewDefaultConnectionFactory(),
                hc:        NewHealthCheckServicer(),
        }
 
index 57796ced6daffbc68e76c77f17400d9f5509c3a4..96845489dbf6026a52c8e41a6758c527f8cbcd86 100644 (file)
@@ -13,10 +13,10 @@ import (
 
 type IdentityClass uint
 
-type remoteIdentity struct {
-       class  IdentityClass
-       domain string
-       princ  string
+type RemoteIdentity struct {
+       Class     IdentityClass
+       Domain    string
+       Principal string
 }
 
 const (
@@ -198,19 +198,19 @@ func (cv *mtlsPeerVerifier) AllowFrom(class IdentityClass, principals ...string)
 }
 
 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
@@ -218,30 +218,30 @@ func (cv *mtlsPeerVerifier) checkName(name string) error {
                        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
@@ -267,10 +267,10 @@ func parseName(name string) (*remoteIdentity, error) {
 
                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
        }
 
diff --git a/utils/context/BUILD.bazel b/utils/context/BUILD.bazel
new file mode 100644 (file)
index 0000000..d91ba7c
--- /dev/null
@@ -0,0 +1,11 @@
+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"],
+)
diff --git a/utils/context/imports.go b/utils/context/imports.go
new file mode 100644 (file)
index 0000000..992768a
--- /dev/null
@@ -0,0 +1,24 @@
+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
diff --git a/utils/context/interruptible.go b/utils/context/interruptible.go
new file mode 100644 (file)
index 0000000..1b6383f
--- /dev/null
@@ -0,0 +1,23 @@
+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
+}