module

GoRpcProcedureGate

Part of GoAuthTagger: a gRPC / Connect interceptor that lets a request through by the procedure it calls.

var PublicMethods = map[string]struct{}{"/app.v1.AuthService/SignIn": {}} func IsPublicMethod(p string) bool { _, ok := PublicMethods[p]; return ok } func (a *Authorizer) CheckAccess(ctx context.Context, p string, r *Result) error { if r != nil { return nil } if !IsPublicMethod(p) { return ErrUnauthenticated } ... } func (in *AuthInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { ... in.authorizer.CheckAccess(ctx, req.Spec().Procedure, result) ... connect.WithInterceptors(NewAuthInterceptor(authorizer))

A package-level list of procedure names (/pkg.Service/Method) is read through its membership tests: an index (List[p], truthy: a zero value such as "" is not a member), a v, ok := List[p] presence test, or a func X(p string) bool whose return is one of those (return ok, return !ok, return List[p], slices.Contains(List, p)); a helper whose return reads otherwise is not followed. A test gates when

  • negated (!IsPublic(p), !(Public[p] || authed), !ok) and its block returns a refusal — an error built with an auth code (status.Error( codes.Unauthenticated, ...), connect.NewError(connect.CodePermission Denied, ...)) or an ErrUnauthenticated-style sentinel, never a comparison or a counter: every procedure the list does not name is refused (an allow-list);
  • positive and its block refuses or returns a check's error (if AdminOnly[m] { if err := requireAdmin(ctx); err != nil { return nil, err } }): the listed procedures are refused (a deny-list);
  • positive, its block hands the request on (return handler(ctx, req), next(...), a check's return nil), no refusal comes before it, and one comes after: an allow-list.

Procedures an earlier pass in the same function lets through (strings.HasPrefix(m, "/pkg.Auth/"), case "/pkg.S/M":, m == "...") stay open. Another condition joined with && that reads a setting (cfg.RequireAuth && !Public[m]) is named in the guard: it can open the procedure. A listed procedure an allow-list's function can still refuse later (memos' private access mode) gets an anonymous note saying so.

The function holding the test has to be an interceptor (WrapUnary, a grpc.UnaryServerInfo parameter, a grpc-gateway middleware) or be called from one, and that interceptor has to be installed (grpc.UnaryInterceptor (X), ChainUnaryInterceptor, connect.WithInterceptors(...), runtime.WithMiddlewares(...)). A unary interceptor does not cover a streaming rpc (one with stream in the .proto); a stream one does. A service registered on a server built without options (grpc.NewServer(), NewXServiceHandler(impl)) is reachable unguarded, so it is not gated. A gate covers the proto packages its list names. Connect endpoints carry the procedure as their URL; a gRPC-gateway route from a .proto file is mapped back through its package, service and rpc lines.

Constants

BOOL_FUNC = /\Afunc\s+(?:\([^)]*\)\s*)?(\w+)\s*\([^)]*\)\s*bool\s*\{/
CASE_PASS = /\Acase\s+(.+):\z/
COMPARISON = /\A(\w+)\s*(!=|==)\s*(\S+)\z/

role != "": a variable compared with a value.

CONNECT_BARE = /\bNew(\w+Service)Handler\s*\(\s*[^,()]+\s*\)/
EQUAL_PASS = /(?:==)\s*"(\/[^"]+)"/
ERROR_RETURN = /\Areturn\s+(?:nil\s*,\s*)?\w*[Ee]rr\w*\s*\z/

A check's own error handed back: return nil, err.

FUNC_HEAD = /\Afunc\s+(?:\([^)]*\)\s*)?(\w+)\s*\(/
GRPC_REGISTER = /\bRegister(\w+Service)Server\s*\(\s*(\w+)\s*,/

pb.RegisterNoteServiceServer(s, impl) / apiconnect.NewNoteServiceHandler(impl).

HAND_ON = /\Areturn\s+(?:handler|next|h|invoker)\s*\(/

Handing the request on: return handler(ctx, req), return next(ctx, req).

INSTALL = /\b(?:grpc\s*\.\s*(?:Chain)?(?:Unary|Stream)Interceptor|connect\s*\.\s*WithInterceptors|runtime\s*\.\s*WithMiddlewares)\s*\(/

Installing interceptors on a server or handler.

LIST_ENTRY = /\A"([^"]*)"\s*(?::\s*(.*?))?\s*,?\s*(?:\/\/.*)?\z/
MAX_HELPERS = 64
METHOD_RECEIVER_TYPE = /\Afunc\s*\(\s*\w*\s*\*?\s*(\w+)\s*\)/

func (in *AuthInterceptor) WrapUnary(: the receiver's type.

OK_LOOKUP = /\b\w+\s*,\s*(\w+)\s*:?=\s*(\w+)\s*\[/

_, ok := List[p]: the presence variable and the list.

PREFIX_PASS = /strings\s*\.\s*HasPrefix\s*\(\s*[\w.()]+\s*,\s*"([^"]+)"\s*\)/

Earlier passes: strings.HasPrefix(m, "/pkg.S/"), m == "/pkg.S/M", case "...":.

PROCEDURE_KEY = /"(\/(?:[A-Za-z_]\w*\.)*[A-Za-z_]\w*\/[A-Za-z_]\w*)"/
PROCEDURE_LIST = /\A(?:var\s+)?([A-Za-z_]\w*)\s*=\s*(?:map\s*\[\s*string\s*\].*|\[\]\s*string\s*)\{\s*\z/
PROTO_PACKAGE = /^\s*package\s+([\w.]+)\s*;/
PROTO_RPC = /^\s*rpc\s+(\w+)\s*\(/
PROTO_SERVICE = /^\s*service\s+(\w+)\s*\{/
PROTO_STREAM_RPC = /^\s*rpc\s+(\w+)\s*\(\s*stream\b|^\s*rpc\s+(\w+)\s*\([^)]*\)\s*returns\s*\(\s*stream\b/
RETURN_NIL = /\Areturn\s+nil\s*\z/
RPC_PROCEDURE = /\A\/((?:[A-Za-z_]\w*\.)*)([A-Za-z_]\w*)\/[A-Za-z_]\w*\z/
RPC_REFUSAL = /\breturn\b.*?(?:\bstatus\s*\.\s*(?:Error|Errorf|New|Newf)\s*\(\s*codes\s*\.\s*(Unauthenticated|PermissionDenied)\b|\bconnect\s*\.\s*NewError\s*\(\s*connect\s*\.\s*Code(Unauthenticated|PermissionDenied)\b|\bErr(Unauthenticated|Unauthorized|PermissionDenied|Forbidden)\w*\b)/

A refusal: a returned error built with an auth code, or a sentinel.

RPC_TECHS = ["go_connect_rpc", "grpc"] of ::String
SETTING_PART = /\A!?\s*[a-z]\w*(?:\.[A-Za-z]\w*)+\z/

cfg.RequireAuth, !s.opts.Public: a setting, not a request value.

STREAM_MARK = /\bWrapStreamingHandler\b|\bStreamServerInfo\b/
UNARY_MARK = /\bWrapUnary\b|\bUnaryServerInfo\b|\bruntime\s*\.\s*HandlerFunc\b/

A function that is itself an interceptor, and of which kind.

ZERO_VALUE = Set {"\"\"", "0", "false", "nil"}

Nested types