From 5b8fa91dbb1bd99b89622ce67b24115f9f4c69cf Mon Sep 17 00:00:00 2001 From: Kailun Li Date: Fri, 10 May 2024 22:23:17 +0800 Subject: [PATCH 1/5] authz: add example to illustrate the use of file watcher interceptor --- examples/data/rbac/policy.json | 30 ++++++++++++++++++++++ examples/features/authz/README.md | 26 +++++++++++++++++-- examples/features/authz/server/main.go | 35 ++++++++++++++++++++------ 3 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 examples/data/rbac/policy.json 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..bbcfdac6276b 100644 --- a/examples/features/authz/README.md +++ b/examples/features/authz/README.md @@ -1,6 +1,6 @@ # RBAC authorization -This example uses the `StaticInterceptor` from the `google.golang.org/grpc/authz` +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 @@ -8,7 +8,7 @@ service based on the authenticated context. ## 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 authorize usage of these methods: - `UnaryEcho` requires the role `UNARY_ECHO:W` @@ -22,6 +22,8 @@ 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: ``` @@ -38,3 +40,23 @@ Start the client with: ``` go run client/main.go ``` + +### Authorization by watching a policy file + +The server can accept 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 legit username (`super-user`) is associated. 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..3699ec954422 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 file watcher)") errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata") ) @@ -197,13 +201,30 @@ 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 unaryAuthzInt grpc.UnaryServerInterceptor + var streamAuthzInt grpc.StreamServerInterceptor + switch *authzOpt { + case authzOptStatic: + // Create an authorization interceptor using a static policy. + staticInt, err := authz.NewStatic(authzPolicy) + if err != nil { + log.Fatalf("Creating a static authz interceptor: %v", err) + } + unaryAuthzInt, streamAuthzInt = staticInt.UnaryInterceptor, staticInt.StreamInterceptor + case authzOptFileWatcher: + // Create an authorization interceptor by watching a policy file. + fileWatcherInt, err := authz.NewFileWatcher(data.Path("rbac/policy.json"), 100*time.Millisecond) + if err != nil { + log.Fatalf("Creating a file watcher authz interceptor: %v", err) + } + unaryAuthzInt, streamAuthzInt = fileWatcherInt.UnaryInterceptor, fileWatcherInt.StreamInterceptor + default: + log.Fatalf("Invalid authz option: %s", *authzOpt) } - unaryInts := grpc.ChainUnaryInterceptor(authUnaryInterceptor, staticInteceptor.UnaryInterceptor) - streamInts := grpc.ChainStreamInterceptor(authStreamInterceptor, staticInteceptor.StreamInterceptor) + + unaryInts := grpc.ChainUnaryInterceptor(authUnaryInterceptor, unaryAuthzInt) + streamInts := grpc.ChainStreamInterceptor(authStreamInterceptor, streamAuthzInt) s := grpc.NewServer(grpc.Creds(creds), unaryInts, streamInts) // Register EchoServer on the server. From 5c4f090cb25dab0d6fe388517ae14cb5d2fb4576 Mon Sep 17 00:00:00 2001 From: Kailun Li Date: Tue, 14 May 2024 13:00:47 +0800 Subject: [PATCH 2/5] Adjust README (grammar & rewrap) --- examples/features/authz/README.md | 41 ++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/examples/features/authz/README.md b/examples/features/authz/README.md index bbcfdac6276b..79498f49685d 100644 --- a/examples/features/authz/README.md +++ b/examples/features/authz/README.md @@ -1,26 +1,27 @@ # 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. +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: +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 @@ -43,7 +44,12 @@ go run client/main.go ### Authorization by watching a policy file -The server can accept 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. +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: @@ -57,6 +63,13 @@ Start the client with: go run client/main.go ``` -The client will first hit `codes.PermissionDenied` error when invoking `UnaryEcho` although a legit username (`super-user`) is associated. This is because the policy file has an intentional glitch (falsely asks for role `UNARY_ECHO:RW`). +The client will first hit `codes.PermissionDenied` error when invoking +`UnaryEcho` although a legit username (`super-user`) is associated. 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. +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. From 5471be9ed68dc82d2cce0b593fd586dbc9f0cb80 Mon Sep 17 00:00:00 2001 From: Kailun Li Date: Thu, 23 May 2024 13:37:34 +0800 Subject: [PATCH 3/5] Adjust README and server according to comments --- examples/features/authz/README.md | 6 +++--- examples/features/authz/server/main.go | 25 +++++++++++++------------ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/examples/features/authz/README.md b/examples/features/authz/README.md index 79498f49685d..b896474aa86d 100644 --- a/examples/features/authz/README.md +++ b/examples/features/authz/README.md @@ -64,9 +64,9 @@ go run client/main.go ``` The client will first hit `codes.PermissionDenied` error when invoking -`UnaryEcho` although a legit username (`super-user`) is associated. This is -because the policy file has an intentional glitch (falsely asks for role -`UNARY_ECHO:RW`). +`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 diff --git a/examples/features/authz/server/main.go b/examples/features/authz/server/main.go index 3699ec954422..cb426a76fd3c 100644 --- a/examples/features/authz/server/main.go +++ b/examples/features/authz/server/main.go @@ -83,7 +83,7 @@ const ( var ( port = flag.Int("port", 50051, "the port to serve on") - authzOpt = flag.String("authz-option", authzOptStatic, "the authz option (static or file watcher)") + authzOpt = flag.String("authz-option", authzOptStatic, "the authz option (static or filewatcher)") errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata") ) @@ -190,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) @@ -202,29 +206,26 @@ func main() { } // Create authorization interceptors according to the authz-option command-line flag. - var unaryAuthzInt grpc.UnaryServerInterceptor - var streamAuthzInt grpc.StreamServerInterceptor - switch *authzOpt { - case authzOptStatic: + var unaryAuthzInterceptor grpc.UnaryServerInterceptor + var streamAuthzInterceptor grpc.StreamServerInterceptor + if *authzOpt == authzOptStatic { // Create an authorization interceptor using a static policy. staticInt, err := authz.NewStatic(authzPolicy) if err != nil { log.Fatalf("Creating a static authz interceptor: %v", err) } - unaryAuthzInt, streamAuthzInt = staticInt.UnaryInterceptor, staticInt.StreamInterceptor - case authzOptFileWatcher: + unaryAuthzInterceptor, streamAuthzInterceptor = staticInt.UnaryInterceptor, staticInt.StreamInterceptor + } else if *authzOpt == authzOptFileWatcher { // Create an authorization interceptor by watching a policy file. fileWatcherInt, err := authz.NewFileWatcher(data.Path("rbac/policy.json"), 100*time.Millisecond) if err != nil { log.Fatalf("Creating a file watcher authz interceptor: %v", err) } - unaryAuthzInt, streamAuthzInt = fileWatcherInt.UnaryInterceptor, fileWatcherInt.StreamInterceptor - default: - log.Fatalf("Invalid authz option: %s", *authzOpt) + unaryAuthzInterceptor, streamAuthzInterceptor = fileWatcherInt.UnaryInterceptor, fileWatcherInt.StreamInterceptor } - unaryInts := grpc.ChainUnaryInterceptor(authUnaryInterceptor, unaryAuthzInt) - streamInts := grpc.ChainStreamInterceptor(authStreamInterceptor, streamAuthzInt) + unaryInts := grpc.ChainUnaryInterceptor(authUnaryInterceptor, unaryAuthzInterceptor) + streamInts := grpc.ChainStreamInterceptor(authStreamInterceptor, streamAuthzInterceptor) s := grpc.NewServer(grpc.Creds(creds), unaryInts, streamInts) // Register EchoServer on the server. From 25ccde723f6d15038d930c79f487458ba010bec5 Mon Sep 17 00:00:00 2001 From: Kailun Li Date: Thu, 23 May 2024 13:55:44 +0800 Subject: [PATCH 4/5] More naming adjustments --- examples/features/authz/server/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/features/authz/server/main.go b/examples/features/authz/server/main.go index cb426a76fd3c..11e5babedf0f 100644 --- a/examples/features/authz/server/main.go +++ b/examples/features/authz/server/main.go @@ -224,9 +224,9 @@ func main() { unaryAuthzInterceptor, streamAuthzInterceptor = fileWatcherInt.UnaryInterceptor, fileWatcherInt.StreamInterceptor } - unaryInts := grpc.ChainUnaryInterceptor(authUnaryInterceptor, unaryAuthzInterceptor) - streamInts := grpc.ChainStreamInterceptor(authStreamInterceptor, streamAuthzInterceptor) - 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{}) From 2c2c84aa869b303f703bb27e36fc2a343c003b34 Mon Sep 17 00:00:00 2001 From: Kailun Li Date: Thu, 23 May 2024 13:59:39 +0800 Subject: [PATCH 5/5] More naming adjustments --- examples/features/authz/server/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/features/authz/server/main.go b/examples/features/authz/server/main.go index 11e5babedf0f..40bad5b3ebef 100644 --- a/examples/features/authz/server/main.go +++ b/examples/features/authz/server/main.go @@ -210,18 +210,18 @@ func main() { var streamAuthzInterceptor grpc.StreamServerInterceptor if *authzOpt == authzOptStatic { // Create an authorization interceptor using a static policy. - staticInt, err := authz.NewStatic(authzPolicy) + staticInterceptor, err := authz.NewStatic(authzPolicy) if err != nil { log.Fatalf("Creating a static authz interceptor: %v", err) } - unaryAuthzInterceptor, streamAuthzInterceptor = staticInt.UnaryInterceptor, staticInt.StreamInterceptor + unaryAuthzInterceptor, streamAuthzInterceptor = staticInterceptor.UnaryInterceptor, staticInterceptor.StreamInterceptor } else if *authzOpt == authzOptFileWatcher { // Create an authorization interceptor by watching a policy file. - fileWatcherInt, err := authz.NewFileWatcher(data.Path("rbac/policy.json"), 100*time.Millisecond) + 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 = fileWatcherInt.UnaryInterceptor, fileWatcherInt.StreamInterceptor + unaryAuthzInterceptor, streamAuthzInterceptor = fileWatcherInterceptor.UnaryInterceptor, fileWatcherInterceptor.StreamInterceptor } unaryInterceptors := grpc.ChainUnaryInterceptor(authUnaryInterceptor, unaryAuthzInterceptor)