examples: add example to illustrate the use of file watcher interceptor (#7226)

authz: add example to illustrate the use of file watcher interceptor
This commit is contained in:
Kailun Li 2024-05-28 23:20:18 +08:00 committed by GitHub
parent 03da31acc6
commit 6e59dd1d7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 106 additions and 19 deletions

View File

@ -0,0 +1,30 @@
{
"name": "authz",
"allow_rules": [
{
"name": "allow_UnaryEcho",
"request": {
"paths": ["/grpc.examples.echo.Echo/UnaryEcho"],
"headers": [
{
"key": "UNARY_ECHO:RW",
"values": ["true"]
}
]
}
},
{
"name": "allow_BidirectionalStreamingEcho",
"request": {
"paths": ["/grpc.examples.echo.Echo/BidirectionalStreamingEcho"],
"headers": [
{
"key": "STREAM_ECHO:RW",
"values": ["true"]
}
]
}
}
],
"deny_rules": []
}

View File

@ -1,26 +1,29 @@
# RBAC authorization # RBAC authorization
This example uses the `StaticInterceptor` from the `google.golang.org/grpc/authz` This example uses the `StaticInterceptor` and `FileWatcherInterceptor` from the
package. It uses a header based RBAC policy to match each gRPC method to a `google.golang.org/grpc/authz` package. It uses a header based RBAC policy to
required role. For simplicity, the context is injected with mock metadata which match each gRPC method to a required role. For simplicity, the context is
includes the required roles, but this should be fetched from an appropriate injected with mock metadata which includes the required roles, but this should
service based on the authenticated context. be fetched from an appropriate service based on the authenticated context.
## Try it ## Try it
Server requires the following roles on an authenticated user to authorize usage Server is expected to require the following roles on an authenticated user to
of these methods: authorize usage of these methods:
- `UnaryEcho` requires the role `UNARY_ECHO:W` - `UnaryEcho` requires the role `UNARY_ECHO:W`
- `BidirectionalStreamingEcho` requires the role `STREAM_ECHO:RW` - `BidirectionalStreamingEcho` requires the role `STREAM_ECHO:RW`
Upon receiving a request, the server first checks that a token was supplied, 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` decodes it and checks that a secret is correctly set (hardcoded to
for simplicity, this should use a proper ID provider in production). `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 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` roles (hardcoded to the 2 required roles above if the username matches
for simplicity, these roles should be supplied externally as well). `super-user` for simplicity, these roles should be supplied externally as well).
### Authorization with static policy
Start the server with: Start the server with:
@ -38,3 +41,35 @@ Start the client with:
``` ```
go run client/main.go 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

@ -28,6 +28,7 @@ import (
"log" "log"
"net" "net"
"strings" "strings"
"time"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/authz" "google.golang.org/grpc/authz"
@ -76,10 +77,13 @@ const (
"deny_rules": [] "deny_rules": []
} }
` `
authzOptStatic = "static"
authzOptFileWatcher = "filewatcher"
) )
var ( var (
port = flag.Int("port", 50051, "the port to serve on") 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") errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata")
) )
@ -186,6 +190,10 @@ func authStreamInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServe
func main() { func main() {
flag.Parse() flag.Parse()
if *authzOpt != authzOptStatic && *authzOpt != authzOptFileWatcher {
log.Fatalf("Invalid authz option: %s", *authzOpt)
}
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil { if err != nil {
log.Fatalf("Listening on local port %q: %v", *port, err) log.Fatalf("Listening on local port %q: %v", *port, err)
@ -197,14 +205,28 @@ func main() {
log.Fatalf("Loading credentials: %v", err) log.Fatalf("Loading credentials: %v", err)
} }
// Create an authorization interceptor using a static policy. // Create authorization interceptors according to the authz-option command-line flag.
staticInteceptor, err := authz.NewStatic(authzPolicy) var unaryAuthzInterceptor grpc.UnaryServerInterceptor
if err != nil { var streamAuthzInterceptor grpc.StreamServerInterceptor
log.Fatalf("Creating a static authz interceptor: %v", err) 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
} }
unaryInts := grpc.ChainUnaryInterceptor(authUnaryInterceptor, staticInteceptor.UnaryInterceptor)
streamInts := grpc.ChainStreamInterceptor(authStreamInterceptor, staticInteceptor.StreamInterceptor) unaryInterceptors := grpc.ChainUnaryInterceptor(authUnaryInterceptor, unaryAuthzInterceptor)
s := grpc.NewServer(grpc.Creds(creds), unaryInts, streamInts) streamInterceptors := grpc.ChainStreamInterceptor(authStreamInterceptor, streamAuthzInterceptor)
s := grpc.NewServer(grpc.Creds(creds), unaryInterceptors, streamInterceptors)
// Register EchoServer on the server. // Register EchoServer on the server.
pb.RegisterEchoServer(s, &server{}) pb.RegisterEchoServer(s, &server{})