snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 20:40:15 +05:00
commit e241b33f26
1365 changed files with 339570 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
# RBAC authorization
This example uses the `StaticInterceptor` and `FileWatcherInterceptor` from the
`google.golang.org/grpc/authz` package. It uses a header based RBAC policy to
match each gRPC method to a required role. For simplicity, the context is
injected with mock metadata which includes the required roles, but this should
be fetched from an appropriate service based on the authenticated context.
## Try it
Server is expected to require the following roles on an authenticated user to
authorize usage of these methods:
- `UnaryEcho` requires the role `UNARY_ECHO:W`
- `BidirectionalStreamingEcho` requires the role `STREAM_ECHO:RW`
Upon receiving a request, the server first checks that a token was supplied,
decodes it and checks that a secret is correctly set (hardcoded to
`super-secret` for simplicity, this should use a proper ID provider in
production).
If the above is successful, it uses the username in the token to set appropriate
roles (hardcoded to the 2 required roles above if the username matches
`super-user` for simplicity, these roles should be supplied externally as well).
### Authorization with static policy
Start the server with:
```
go run server/main.go
```
The client implementation shows how using a valid token (setting username and
secret) with each of the endpoints will return successfully. It also exemplifies
how using a bad token will result in `codes.PermissionDenied` being returned
from the service.
Start the client with:
```
go run client/main.go
```
### Authorization by watching a policy file
The server accepts an optional `--authz-option filewatcher` flag to set up
authorization policy by reading a [policy
file](/examples/data/rbac/policy.json), and to look for update on the policy
file every 100 millisecond. Having `GRPC_GO_LOG_SEVERITY_LEVEL` environment
variable set to `info` will log out the reload activity of the policy every time
a file update is detected.
Start the server with:
```
GRPC_GO_LOG_SEVERITY_LEVEL=info go run server/main.go --authz-option filewatcher
```
Start the client with:
```
go run client/main.go
```
The client will first hit `codes.PermissionDenied` error when invoking
`UnaryEcho` although a legitimate username (`super-user`) is associated with the
RPC. This is because the policy file has an intentional glitch (falsely asks for
role `UNARY_ECHO:RW`).
While the server is still running, edit and save the policy file to replace
`UNARY_ECHO:RW` with the correct role `UNARY_ECHO:W` (policy reload activity
should now be found in server logs). This time when the client is started again
with the command above, it will be able to get responses just as in the
static-policy example.

View File

@@ -0,0 +1,141 @@
/*
*
* Copyright 2023 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Binary client demonstrates how to include authorization credentials in the
// form of metadata in every RPC for server side validation.
package main
import (
"context"
"flag"
"fmt"
"io"
"log"
"time"
"golang.org/x/oauth2"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/oauth"
"google.golang.org/grpc/examples/data"
"google.golang.org/grpc/examples/features/authz/token"
ecpb "google.golang.org/grpc/examples/features/proto/echo"
"google.golang.org/grpc/status"
)
var addr = flag.String("addr", "localhost:50051", "the address to connect to")
func callUnaryEcho(ctx context.Context, client ecpb.EchoClient, message string, opts ...grpc.CallOption) error {
resp, err := client.UnaryEcho(ctx, &ecpb.EchoRequest{Message: message}, opts...)
if err != nil {
return status.Errorf(status.Code(err), "UnaryEcho RPC failed: %v", err)
}
fmt.Println("UnaryEcho: ", resp.Message)
return nil
}
func callBidiStreamingEcho(ctx context.Context, client ecpb.EchoClient, opts ...grpc.CallOption) error {
c, err := client.BidirectionalStreamingEcho(ctx, opts...)
if err != nil {
return status.Errorf(status.Code(err), "BidirectionalStreamingEcho RPC failed: %v", err)
}
for i := 0; i < 5; i++ {
err := c.Send(&ecpb.EchoRequest{Message: fmt.Sprintf("Request %d", i+1)})
if err == io.EOF {
// Bidi streaming RPC errors happen and make Send return io.EOF,
// not the RPC error itself. Call Recv to determine the error.
break
}
if err != nil {
// Some local errors are reported this way, e.g. errors serializing
// the request message.
return status.Errorf(status.Code(err), "sending StreamingEcho message: %v", err)
}
}
c.CloseSend()
for {
resp, err := c.Recv()
if err == io.EOF {
break
}
if err != nil {
return status.Errorf(status.Code(err), "receiving StreamingEcho message: %v", err)
}
fmt.Println("BidiStreaming Echo: ", resp.Message)
}
return nil
}
func newCredentialsCallOption(t token.Token) grpc.CallOption {
tokenBase64, err := t.Encode()
if err != nil {
log.Fatalf("encoding token: %v", err)
}
oath2Token := oauth2.Token{AccessToken: tokenBase64}
return grpc.PerRPCCredentials(oauth.TokenSource{TokenSource: oauth2.StaticTokenSource(&oath2Token)})
}
func main() {
flag.Parse()
// Create tls based credential.
creds, err := credentials.NewClientTLSFromFile(data.Path("x509/ca_cert.pem"), "x.test.example.com")
if err != nil {
log.Fatalf("failed to load credentials: %v", err)
}
// Set up a connection to the server.
conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("grpc.NewClient(%q): %v", *addr, err)
}
defer conn.Close()
// Make an echo client and send RPCs.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client := ecpb.NewEchoClient(conn)
// Make RPCs as an authorized user and expect them to succeed.
authorisedUserTokenCallOption := newCredentialsCallOption(token.Token{Username: "super-user", Secret: "super-secret"})
if err := callUnaryEcho(ctx, client, "hello world", authorisedUserTokenCallOption); err != nil {
log.Fatalf("Unary RPC by authorized user failed: %v", err)
}
if err := callBidiStreamingEcho(ctx, client, authorisedUserTokenCallOption); err != nil {
log.Fatalf("Bidirectional RPC by authorized user failed: %v", err)
}
// Make RPCs as an unauthorized user and expect them to fail with status code PermissionDenied.
unauthorisedUserTokenCallOption := newCredentialsCallOption(token.Token{Username: "bad-actor", Secret: "super-secret"})
if err := callUnaryEcho(ctx, client, "hello world", unauthorisedUserTokenCallOption); err != nil {
switch c := status.Code(err); c {
case codes.PermissionDenied:
log.Printf("Unary RPC by unauthorized user failed as expected: %v", err)
default:
log.Fatalf("Unary RPC by unauthorized user failed unexpectedly: %v, %v", c, err)
}
}
if err := callBidiStreamingEcho(ctx, client, unauthorisedUserTokenCallOption); err != nil {
switch c := status.Code(err); c {
case codes.PermissionDenied:
log.Printf("Bidirectional RPC by unauthorized user failed as expected: %v", err)
default:
log.Fatalf("Bidirectional RPC by unauthorized user failed unexpectedly: %v", err)
}
}
}

View File

@@ -0,0 +1,238 @@
/*
*
* Copyright 2023 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Binary server demonstrates how to validate authorization credential metadata
// for incoming RPCs.
package main
import (
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"strings"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/authz"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/examples/data"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/grpc/examples/features/authz/token"
pb "google.golang.org/grpc/examples/features/proto/echo"
)
const (
unaryEchoWriterRole = "UNARY_ECHO:W"
streamEchoReadWriterRole = "STREAM_ECHO:RW"
authzPolicy = `
{
"name": "authz",
"allow_rules": [
{
"name": "allow_UnaryEcho",
"request": {
"paths": ["/grpc.examples.echo.Echo/UnaryEcho"],
"headers": [
{
"key": "UNARY_ECHO:W",
"values": ["true"]
}
]
}
},
{
"name": "allow_BidirectionalStreamingEcho",
"request": {
"paths": ["/grpc.examples.echo.Echo/BidirectionalStreamingEcho"],
"headers": [
{
"key": "STREAM_ECHO:RW",
"values": ["true"]
}
]
}
}
],
"deny_rules": []
}
`
authzOptStatic = "static"
authzOptFileWatcher = "filewatcher"
)
var (
port = flag.Int("port", 50051, "the port to serve on")
authzOpt = flag.String("authz-option", authzOptStatic, "the authz option (static or filewatcher)")
errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata")
)
func newContextWithRoles(ctx context.Context, username string) context.Context {
md := metadata.MD{}
if username == "super-user" {
md.Set(unaryEchoWriterRole, "true")
md.Set(streamEchoReadWriterRole, "true")
}
return metadata.NewIncomingContext(ctx, md)
}
type server struct {
pb.UnimplementedEchoServer
}
func (s *server) UnaryEcho(_ context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) {
fmt.Printf("unary echoing message %q\n", in.Message)
return &pb.EchoResponse{Message: in.Message}, nil
}
func (s *server) BidirectionalStreamingEcho(stream pb.Echo_BidirectionalStreamingEchoServer) error {
for {
in, err := stream.Recv()
if err != nil {
if err == io.EOF {
return nil
}
fmt.Printf("Receiving message from stream: %v\n", err)
return err
}
fmt.Printf("bidi echoing message %q\n", in.Message)
stream.Send(&pb.EchoResponse{Message: in.Message})
}
}
// isAuthenticated validates the authorization.
func isAuthenticated(authorization []string) (username string, err error) {
if len(authorization) < 1 {
return "", errors.New("received empty authorization token from client")
}
tokenBase64 := strings.TrimPrefix(authorization[0], "Bearer ")
// Perform the token validation here. For the sake of this example, the code
// here forgoes any of the usual OAuth2 token validation and instead checks
// for a token matching an arbitrary string.
var token token.Token
err = token.Decode(tokenBase64)
if err != nil {
return "", fmt.Errorf("base64 decoding of received token %q: %v", tokenBase64, err)
}
if token.Secret != "super-secret" {
return "", fmt.Errorf("received token %q does not match expected %q", token.Secret, "super-secret")
}
return token.Username, nil
}
// authUnaryInterceptor looks up the authorization header from the incoming RPC context,
// retrieves the username from it and creates a new context with the username before invoking
// the provided handler.
func authUnaryInterceptor(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errMissingMetadata
}
username, err := isAuthenticated(md["authorization"])
if err != nil {
return nil, status.Error(codes.Unauthenticated, err.Error())
}
return handler(newContextWithRoles(ctx, username), req)
}
// wrappedStream wraps a grpc.ServerStream associated with an incoming RPC, and
// a custom context containing the username derived from the authorization header
// specified in the incoming RPC metadata
type wrappedStream struct {
grpc.ServerStream
ctx context.Context
}
func (w *wrappedStream) Context() context.Context {
return w.ctx
}
func newWrappedStream(ctx context.Context, s grpc.ServerStream) grpc.ServerStream {
return &wrappedStream{s, ctx}
}
// authStreamInterceptor looks up the authorization header from the incoming RPC context,
// retrieves the username from it and creates a new context with the username before invoking
// the provided handler.
func authStreamInterceptor(srv any, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
md, ok := metadata.FromIncomingContext(ss.Context())
if !ok {
return errMissingMetadata
}
username, err := isAuthenticated(md["authorization"])
if err != nil {
return status.Error(codes.Unauthenticated, err.Error())
}
return handler(srv, newWrappedStream(newContextWithRoles(ss.Context(), username), ss))
}
func main() {
flag.Parse()
if *authzOpt != authzOptStatic && *authzOpt != authzOptFileWatcher {
log.Fatalf("Invalid authz option: %s", *authzOpt)
}
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("Listening on local port %q: %v", *port, err)
}
// Create tls based credential.
creds, err := credentials.NewServerTLSFromFile(data.Path("x509/server_cert.pem"), data.Path("x509/server_key.pem"))
if err != nil {
log.Fatalf("Loading credentials: %v", err)
}
// Create authorization interceptors according to the authz-option command-line flag.
var unaryAuthzInterceptor grpc.UnaryServerInterceptor
var streamAuthzInterceptor grpc.StreamServerInterceptor
if *authzOpt == authzOptStatic {
// Create an authorization interceptor using a static policy.
staticInterceptor, err := authz.NewStatic(authzPolicy)
if err != nil {
log.Fatalf("Creating a static authz interceptor: %v", err)
}
unaryAuthzInterceptor, streamAuthzInterceptor = staticInterceptor.UnaryInterceptor, staticInterceptor.StreamInterceptor
} else if *authzOpt == authzOptFileWatcher {
// Create an authorization interceptor by watching a policy file.
fileWatcherInterceptor, err := authz.NewFileWatcher(data.Path("rbac/policy.json"), 100*time.Millisecond)
if err != nil {
log.Fatalf("Creating a file watcher authz interceptor: %v", err)
}
unaryAuthzInterceptor, streamAuthzInterceptor = fileWatcherInterceptor.UnaryInterceptor, fileWatcherInterceptor.StreamInterceptor
}
unaryInterceptors := grpc.ChainUnaryInterceptor(authUnaryInterceptor, unaryAuthzInterceptor)
streamInterceptors := grpc.ChainStreamInterceptor(authStreamInterceptor, streamAuthzInterceptor)
s := grpc.NewServer(grpc.Creds(creds), unaryInterceptors, streamInterceptors)
// Register EchoServer on the server.
pb.RegisterEchoServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("Serving Echo service on local port: %v", err)
}
}

View File

@@ -0,0 +1,55 @@
/*
*
* Copyright 2023 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Package token implements an example of authorization token encoding/decoding
// that can be used in RPC headers.
package token
import (
"encoding/base64"
"encoding/json"
)
// Token is a mock authorization token sent by the client as part of the RPC headers,
// and used by the server for authorization against a predefined policy.
type Token struct {
// Secret is used by the server to authenticate the user
Secret string `json:"secret"`
// Username is used by the server to assign roles in the metadata for authorization
Username string `json:"username"`
}
// Encode returns a base64 encoded version of the JSON representation of token.
func (t *Token) Encode() (string, error) {
barr, err := json.Marshal(t)
if err != nil {
return "", err
}
s := base64.StdEncoding.EncodeToString(barr)
return s, nil
}
// Decode updates the internals of Token using the passed in base64
// encoded version of the JSON representation of token.
func (t *Token) Decode(s string) error {
barr, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return err
}
return json.Unmarshal(barr, t)
}