Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 7 commits into from
May 28, 2024
Merged
30 changes: 30 additions & 0 deletions examples/data/rbac/policy.json
Original file line number Diff line number Diff line change
@@ -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": []
}
57 changes: 46 additions & 11 deletions examples/features/authz/README.md
easwars marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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:

Expand All @@ -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 legit username (`super-user`) is associated. This is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: UnaryEcho although a legitimate username (super-user) is associated with the RPC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

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.
35 changes: 28 additions & 7 deletions examples/features/authz/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"log"
"net"
"strings"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/authz"
Expand Down Expand Up @@ -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)")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: file watcher -> filewatcher

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.


errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata")
)
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer the full name Interceptor rather than shortening to Int, please change throughout

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this example server is specifically for authz, let's put this check earlier (after parsing flags), then error log and exit the process if an invalid authz option is specified.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

}
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.
Expand Down