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 anErrUnauthenticated-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'sreturn 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
role != "": a variable compared with a value.
A check's own error handed back: return nil, err.
pb.RegisterNoteServiceServer(s, impl) / apiconnect.NewNoteServiceHandler(impl).
Handing the request on: return handler(ctx, req), return next(ctx, req).
Installing interceptors on a server or handler.
func (in *AuthInterceptor) WrapUnary(: the receiver's type.
_, ok := List[p]: the presence variable and the list.
Earlier passes: strings.HasPrefix(m, "/pkg.S/"), m == "/pkg.S/M", case "...":.
A refusal: a returned error built with an auth code, or a sentinel.
cfg.RequireAuth, !s.opts.Public: a setting, not a request value.
A function that is itself an interceptor, and of which kind.