diff --git a/examples/data/rbac/policy.json b/examples/data/rbac/policy.json new file mode 100644 index 000000000000..97d6ae5a7d95 --- /dev/null +++ b/examples/data/rbac/policy.json @@ -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": [] +} \ No newline at end of file diff --git a/examples/features/authz/README.md b/examples/features/authz/README.md index 498beb367f1e..b896474aa86d 100644 --- a/examples/features/authz/README.md +++ b/examples/features/authz/README.md @@ -1,26 +1,29 @@ # RBAC authorization -This example uses the `StaticInterceptor` 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. +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 requires the following roles on an authenticated user to authorize usage -of these methods: +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). +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). +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: @@ -38,3 +41,35 @@ 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. diff --git a/examples/features/authz/server/main.go b/examples/features/authz/server/main.go index e66a5406de7b..40bad5b3ebef 100644 --- a/examples/features/authz/server/main.go +++ b/examples/features/authz/server/main.go @@ -28,6 +28,7 @@ import ( "log" "net" "strings" + "time" "google.golang.org/grpc" "google.golang.org/grpc/authz" @@ -76,10 +77,13 @@ const ( "deny_rules": [] } ` + authzOptStatic = "static" + authzOptFileWatcher = "filewatcher" ) 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") ) @@ -186,6 +190,10 @@ func authStreamInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServe 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) @@ -197,14 +205,28 @@ func main() { log.Fatalf("Loading credentials: %v", err) } - // Create an authorization interceptor using a static policy. - staticInteceptor, err := authz.NewStatic(authzPolicy) - if err != nil { - log.Fatalf("Creating a static authz interceptor: %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 } - unaryInts := grpc.ChainUnaryInterceptor(authUnaryInterceptor, staticInteceptor.UnaryInterceptor) - streamInts := grpc.ChainStreamInterceptor(authStreamInterceptor, staticInteceptor.StreamInterceptor) - s := grpc.NewServer(grpc.Creds(creds), unaryInts, streamInts) + + 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{})