Help us shape the future of Go! Take the 2026 Go Developer Survey

grpcapi

module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: Apache-2.0

README

grpcapi

English | 简体中文

grpcapi turns the "declare your API in proto, serve it over gRPC and HTTP" pattern into a reusable library: policy collection with fail-closed startup assertions, authentication and authorization, a standard interceptor chain, a gateway assembly kit, and guard test helpers. It belongs to the lynx ecosystem but does not depend on it — the only direct dependencies are the gRPC ecosystem itself (grpc, grpc-gateway, protobuf, jwt, protovalidate).

Why grpcapi?

  • Proto is the single source of truth. HTTP routes come from google.api.http, access policies from grpcapi.v1 options on the same proto file. No second route table to maintain in Go.
  • Fail-closed startup. Missing policies, unregistered operations, protected routes without an authenticator, missing CSRF, route conflicts — all fail at startup, never at request time.
  • Secure by default. Session cookies default to Secure; CSRF with no configured origins falls back to same-origin; the HTTP body limit follows the gRPC receive limit.
  • One-step assembly. server.New wires the in-process gateway, the default interceptor chain, health checks, the route table, startup assertions, and graceful shutdown.
  • Guard tests. Align the proto surface, generated OpenAPI documents, and the policy set; snapshot the authorization matrix so every change to it is visible in code review.

Core concepts

Concept Meaning
Service A gRPC service plus the HTTP routes generated from its proto annotations
Route An HTTP endpoint; usually declared in proto, with a few handwritten health/webhook extras
Policy Who may call: public / authenticated / role / permission / scope / custom
Principal Who is calling: the identity and claims produced by an authenticator

Installation

go get github.com/lynx-go/grpcapi

Go 1.26 or newer is required. The proto annotations ship as a separate Buf module — see Proto distribution.

Quick start

1. Declare routes and policies in proto
import "google/api/annotations.proto";
import "grpcapi/v1/policy.proto";

option (grpcapi.v1.api) = {
  api: "public"
  schema_version: 1
  default_exposure: EXPOSURE_PUBLIC
};

service NoteService {
  option (grpcapi.v1.service) = {
    default_policy: { authenticated: true }
    default_exposure: EXPOSURE_PUBLIC
  };

  rpc ListNotes(ListNotesRequest) returns (ListNotesResponse) {
    option (google.api.http) = { get: "/v1/notes" };
  }

  rpc DeleteNote(DeleteNoteRequest) returns (DeleteNoteResponse) {
    option (google.api.http) = { delete: "/v1/notes/{id}" };
    option (grpcapi.v1.method) = {
      policy: { permissions: ["notes.read", "notes.delete"] }
      label: "permission"
    };
  }

  rpc PurgeNotes(PurgeNotesRequest) returns (PurgeNotesResponse) {
    option (google.api.http) = { post: "/v1/notes:purge" body: "*" };
    option (grpcapi.v1.method) = {
      policy: {
        any_of: [
          { roles: ["admin"] },
          { scopes: ["notes:admin"] }
        ]
      }
      label: "server"
    };
  }
}

The proto Service plus google.api.http is the single source of truth for routing; do not maintain a second route table in Go.

2. Build the policy set
import (
    "github.com/lynx-go/grpcapi/policy"
    "github.com/lynx-go/grpcapi/policy/protopolicy"
)

set, err := policy.Build(ctx,
    policy.FromRegistry("notes.api.v1"), // or policy.FromFiles / FromFileDescriptorSet
    policy.Options{
        Provider: protopolicy.New(protopolicy.Options{}),
        Registry: policy.DefaultRegistry(),
        // Vocabulary: policy.NewVocabulary(map[string][]string{"scope": {"notes"}}),
    })
srv, err := server.New(ctx, server.Config{
    Policy: set,
    Services: []server.Registration{{
        API:        "public",
        Descriptor: notesv1.File_notes_proto.Services().ByName("NoteService"),
        RegisterGRPC: func(s *grpc.Server) {
            notesv1.RegisterNoteServiceServer(s, impl)
        },
        RegisterHTTP: func(ctx context.Context, mux *runtime.ServeMux, conn grpc.ClientConnInterface) error {
            return notesv1.RegisterNoteServiceHandlerClient(ctx, mux, notesv1.NewNoteServiceClient(conn))
        },
    }},
    GRPCAddr: ":9060",
    HTTPAddr: ":8080",
    // This example configures no Authenticators, yet the proto in step 1
    // declares protected policies; fail-closed semantics require you to
    // acknowledge that explicitly, or server.New fails at startup.
    // Remove this switch once JWT/session/API-key auth is wired in.
    AllowNoAuthenticator: true,
})
if err != nil {
    log.Fatal(err)
}
log.Fatal(srv.Run(ctx))

server.New takes care of the in-process gateway connection, mux/dial/register/mount, the default interceptor chain, health checks, the route table, startup assertions, and graceful shutdown.

The registration closures can be generated instead of handwritten:

go build -o bin/protoc-gen-grpcapi ./cmd/protoc-gen-grpcapi
# buf.gen.yaml
plugins:
  - local: bin/protoc-gen-grpcapi
    out: .
    opt: module=github.com/lynx-go/grpcapi

This generates NewNoteServiceRegistration(impl) server.Registration. Tier 1 users can still assemble gateway.NewMux / gateway.Dial / gateway.Register piece by piece.

4. Manual assembly
decider, err := authz.NewRequirementDecider(set)
authorizer, err := authz.NewInterceptor(set, decider, authz.InterceptorOptions{})

chain, err := interceptor.Assemble(
    interceptor.RecoveryStage(interceptor.RecoveryOptions{}),
    interceptor.RequestIDStage(interceptor.RequestIDOptions{}),
    interceptor.ClientInfoStage(interceptor.NewClientInfo(interceptor.ClientInfoConfig{})),
    interceptor.AuthnStage(authnUnary, authnStream),
    interceptor.AuthzStage(authorizer.Unary(), authorizer.Stream()),
    interceptor.ValidateStage(interceptor.NewValidate()),
)
srv := grpc.NewServer(chain.ServerOptions()...)

Reference authenticator implementations live in authn/jwt, authn/session, authn/apikey, and authn/admin; the CSRF middleware is in csrf.

When assembling by hand, also assert at startup that every registered method has a policy (server.New has this built in):

if err := interceptor.AssertAllRegisteredHavePolicy(srv, set); err != nil {
    log.Fatal(err)
}

Testing guards

surf, err := surface.Build([]protoreflect.FileDescriptor{notesv1.File_notes_proto}, set)
if err != nil {
    t.Fatal(err)
}
docs, err := guard.OpenAPIv2(os.DirFS("gen"), guard.OpenAPIv2Options{})
if err != nil {
    t.Fatal(err)
}

guard.Run(t, guard.SuiteConfig{
    Surface: surf,
    Docs:    docs,
    Policy: guard.MatchOptions{
        MetadataKey:   "x-grpcapi-policy",
        MinOperations: 3,
    },
    Naming:   guard.SnakeCase,
    Manifest: &guard.ManifestConfig{Path: "docs/policy.manifest.json"},
    Matrix: &guard.MatrixConfig{
        Path: "docs/authz-matrix.md",
        Options: guard.MatrixOptions{
            Header: []string{"Method", "Label"},
            Row: func(op policy.Operation) []string {
                return []string{op.ID, op.EffectiveLabel()}
            },
        },
    },
})

// Guard the assembled server's route table against the policy set.
guard.AssertRouteTable(t, srv.Routes(), guard.RouteTableOptions{Policies: set})

The legacy SwaggerAccessMatches / SwaggerSnakeCaseProperties / SwaggerNoDefinition assertions remain available; new projects should combine guard.Run(SuiteConfig{...}) with the Document assertions.

Policy quick reference

policy: { anonymous: true }                 // public
policy: { authenticated: true }             // any authenticated caller
policy: { roles: ["admin"] }                // any of these roles
policy: { permissions: ["notes.read"] }     // all of these permissions
policy: { scopes: ["notes:write"] }         // any of these scopes
policy: { any_of: [ { roles: ["admin"] }, { scopes: ["notes:admin"] } ] }
policy: { preset: "tw.server" preset_args: { scope: "echoes:write" } }
policy: { require: [ { kind: "tenant" values: ["acme"] } ] }
  • The default registry provides authenticated / claim / principal_kind / role / permission / scope.
  • The default scope requirement matches the claim exactly; resource:op semantics are available through the policy/scope registry entry (compatible with v2's resource.op).
  • forbid is deny-overrides.
  • With inherit_default: true, the method policy merges with the service/file defaults: require/forbid combine with AND across layers (forbid is always deny-overrides); each any_of group is folded directly into the alternatives — groups are always OR, and inheritance never turns them into a cross-layer Cartesian AND. To intersect on top of the defaults, put several conditions into the same any_of group (AND within a group).

Packages

Package Purpose
policy Collect and evaluate policies; policy/protopolicy reads the proto annotations
authz Authorization decisions; gRPC interceptors and HTTP middleware sharing one Decider
authn Authenticator interface plus reference implementations: jwt, session, apikey, admin
interceptor Standard interceptor chain: recovery, request ID, client info, authn/authz hooks, validation
gateway grpc-gateway assembly: mux, dial, register, header policy, error encoding
server Composition root: one-step assembly with built-in startup assertions
guard Test-time assertions aligning the proto surface, OpenAPI docs, and policy
surface Unified API-surface IR derived from proto + policy
csrf Double-submit cookie CSRF middleware
principal Request identity and claims, with context accessors
contextx Request ID / client info / audit helpers on context
testkit Test fixtures for server.New; test-only

Proto distribution

The proto module is published to the Buf Schema Registry: buf.build/lynx-go/grpcapi. Register it in your project's buf.yaml:

version: v2
deps:
  - buf.build/lynx-go/grpcapi

Then import "grpcapi/v1/policy.proto" in your protos. v3 removed authz.proto: the legacy annotations are vendored per MIGRATION.md (path A) and consumed through a legacy Provider; they are not for new projects.

Documentation

  • example/ — a runnable Tier-0 example: proto declares the routes and policies, server.New does the rest.
  • MIGRATION.md — upgrading from v2, including where the legacy authz annotations went.
  • GENERALIZATION.md — full design rationale and decisions. A design document; the authoritative API reference is the godoc.
  • docs/history/DESIGN.md — the archived v2 design.
  • CHANGELOG.md — release notes.

Versioning

  • 0.x: minor releases may carry breaking changes. Read CHANGELOG.md and the migration guide before upgrading.
  • API freeze scope (from v0.3.0): the public API surface consumers have already adopted is frozen until v1 — the gateway package (all public symbols), interceptor.Stage and its name/order constants, policy.Build / policy.Set, and authn.Options / authn.Chain. Breaking changes may only arrive as new types, new functions, or new fields (additive-only); no renames or removals of existing symbols. v0.2.1 gateway symbols removed during the v3 convergence are restored as deprecated shims (gateway/compat.go: IncomingMatcher, OutgoingMatcher, NewErrorHandler, BuilderEncoder), so upgrading consumers compile again; the shims are scheduled for removal in v1.0.
  • Minimum usable version: v0.2.0. v0.1.0's go.mod requires an unpublished zero pseudo-version of genproto and cannot be resolved externally; do not reference it.
  • genproto is a separate Go module, and the main repo's replace only applies within this repo. When upgrading, tag genproto first, then bump the main repo's require version — a missing tag surfaces downstream as an obscure module-resolution compile error.

License

Apache-2.0

Directories

Path Synopsis
Package audit 提供 slog 落审计参考实现,填 interceptor.AuditStage 槽位。
Package audit 提供 slog 落审计参考实现,填 interceptor.AuditStage 槽位。
Package authn 定义认证 SPI:把请求凭证变成 principal.Principal。
Package authn 定义认证 SPI:把请求凭证变成 principal.Principal。
admin
Package admin 在认证成功后从独立 admin store 补充后台角色与停用标记。
Package admin 在认证成功后从独立 admin store 补充后台角色与停用标记。
apikey
Package apikey 提供 API key 认证参考实现:key 哈希查找 + scope 注入。
Package apikey 提供 API key 认证参考实现:key 哈希查找 + scope 注入。
jwt
Package jwt 提供 JWT/OIDC 认证参考实现。
Package jwt 提供 JWT/OIDC 认证参考实现。
session
Package session 提供 cookie session 认证参考实现。
Package session 提供 cookie session 认证参考实现。
ticket
Package ticket 提供短时单用途票据认证器参考实现。
Package ticket 提供短时单用途票据认证器参考实现。
Package authz 按 policy.Set 做授权决策,并提供 gRPC 拦截器与 HTTP 中间件两个执行入口;两者共享同一 Decider,保证双协议行为一致。
Package authz 按 policy.Set 做授权决策,并提供 gRPC 拦截器与 HTTP 中间件两个执行入口;两者共享同一 Decider,保证双协议行为一致。
Package bridge 提供 proto 路由之外的流式 HTTP 协议桥参考实现:
Package bridge 提供 proto 路由之外的流式 HTTP 协议桥参考实现:
cmd
protoc-gen-grpcapi command
Command protoc-gen-grpcapi 为每个 proto Service 生成 server.Registration 适配器,消除手写 gRPC + gateway 注册样板。
Command protoc-gen-grpcapi 为每个 proto Service 生成 server.Registration 适配器,消除手写 gRPC + gateway 注册样板。
Package contextx 提供 gRPC 请求上下文的通用存取件:客户端信息 (ClientInfo)与审计可变槽(AuditTrail)。
Package contextx 提供 gRPC 请求上下文的通用存取件:客户端信息 (ClientInfo)与审计可变槽(AuditTrail)。
Package cors 提供 CORS(跨源资源共享)中间件。
Package cors 提供 CORS(跨源资源共享)中间件。
Package csrf 提供 double-submit cookie 形式的 CSRF 中间件。
Package csrf 提供 double-submit cookie 形式的 CSRF 中间件。
Command example 是 grpcapi v3 Tier 0 的活文档:proto 定义路由与策略, server.New 一步完成 gRPC + gateway + 中间件 + 启动断言。
Command example 是 grpcapi v3 Tier 0 的活文档:proto 定义路由与策略, server.New 一步完成 gRPC + gateway + 中间件 + 启动断言。
echo/v1
Package echov1 is a reverse proxy.
Package echov1 is a reverse proxy.
Package gateway 装配 grpc-gateway 反向代理:统一错误处理、protojson 序列化、 入/出站 header 策略与同地址拨号。
Package gateway 装配 grpc-gateway 反向代理:统一错误处理、protojson 序列化、 入/出站 header 策略与同地址拨号。
genproto module
Package guard 是测试期断言族:把 proto 派生的 Surface、外部生成的 OpenAPI 文档与 policy.Set 三方对齐,防止「代码、文档、策略」漂移。
Package guard 是测试期断言族:把 proto 派生的 Surface、外部生成的 OpenAPI 文档与 policy.Set 三方对齐,防止「代码、文档、策略」漂移。
Package interceptor 提供标准 gRPC 拦截器链:panic 恢复、request ID、 客户端信息、认证/授权挂点、消息校验,以及把 Stage 装配为 grpc.ServerOption 的 Assemble。
Package interceptor 提供标准 gRPC 拦截器链:panic 恢复、request ID、 客户端信息、认证/授权挂点、消息校验,以及把 Stage 装配为 grpc.ServerOption 的 Assemble。
Package openapi 把 surface IR(proto + google.api.http + policy.Set)投影为 OpenAPI 3.0 文档:路由来自最终部署路径(含 API 组前缀),安全语义来自 policy——anonymous 方法不带 security,其余标注 bearerAuth;策略谓词以 x-grpcapi-requirements 扩展原样保留。
Package openapi 把 surface IR(proto + google.api.http + policy.Set)投影为 OpenAPI 3.0 文档:路由来自最终部署路径(含 API 组前缀),安全语义来自 policy——anonymous 方法不带 security,其余标注 bearerAuth;策略谓词以 x-grpcapi-requirements 扩展原样保留。
Package policy 定义 proto 注解声明的 API 策略 IR:策略、暴露面、操作、 词表与可注册的谓词。
Package policy 定义 proto 注解声明的 API 策略 IR:策略、暴露面、操作、 词表与可注册的谓词。
protopolicy
Package protopolicy 是 policy.Provider 的默认实现:读取 grpcapi.v1 (policy.proto) 的文件/服务/方法扩展。
Package protopolicy 是 policy.Provider 的默认实现:读取 grpcapi.v1 (policy.proto) 的文件/服务/方法扩展。
scope
resource.go 提供资源实例寻址门(torchwood `databases:blog` 形态)的官方 范式:Requirement 声明"本方法寻址 <resource> 类资源",运行期从请求提取 目标实例 ID,与 principal 的 scope 集合比对。
resource.go 提供资源实例寻址门(torchwood `databases:blog` 形态)的官方 范式:Requirement 声明"本方法寻址 <resource> 类资源",运行期从请求提取 目标实例 ID,与 principal 的 scope 集合比对。
Package principal 定义 API 请求中的身份主体(Principal)及其 context 存取。
Package principal 定义 API 请求中的身份主体(Principal)及其 context 存取。
Package ratelimit 提供固定窗口内存限流参考实现,填 interceptor.RateLimitStage 槽位。
Package ratelimit 提供固定窗口内存限流参考实现,填 interceptor.RateLimitStage 槽位。
Package server 把 policy、surface、authn、authz、interceptor、gateway 组装成可运行的 gRPC + grpc-gateway 双协议服务,是本库的组合根。
Package server 把 policy、surface、authn、authz、interceptor、gateway 组装成可运行的 gRPC + grpc-gateway 双协议服务,是本库的组合根。
Package surface 把 proto Service + google.api.http + policy.Set 组合成 统一的 API 接口面 IR。
Package surface 把 proto Service + google.api.http + policy.Set 组合成 统一的 API 接口面 IR。
Package testkit 提供 server.New 的测试夹具:自动启动/关闭、HTTP/gRPC 客户端、 测试 Principal 与认证器。
Package testkit 提供 server.New 的测试夹具:自动启动/关闭、HTTP/gRPC 客户端、 测试 Principal 与认证器。
Package usage 提供内存用量计量参考实现,填 interceptor.UsageStage 槽位。
Package usage 提供内存用量计量参考实现,填 interceptor.UsageStage 槽位。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL