Skip to content

Commit

Permalink
Client side OPA policy implementation (#98)
Browse files Browse the repository at this point in the history
* WIP

* Add support for client side OPA checks.

In practice this mostly just means "did I connect to a peer holding a cert I expect?" but in theory anything could be plumbed in.

A local client can't really stop the user so this is more about trust about the remote server being as expected.

* Add logging so it's possible to debug opa
  • Loading branch information
sfc-gh-jchacon authored Apr 11, 2022
1 parent dfe9e9b commit 4a7e8ca
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 44 deletions.
64 changes: 61 additions & 3 deletions auth/opa/rpcauth/rpcauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,17 @@ type RPCAuthzHook interface {

// New creates a new Authorizer from an opa.AuthzPolicy. Any supplied authorization
// hooks will be executed, in the order provided, on each policy evauluation.
// NOTE: The policy is used for both client and server hooks below. If you need
// distinct policy for client vs server, create 2 Authorizer's.
func New(policy *opa.AuthzPolicy, authzHooks ...RPCAuthzHook) *Authorizer {
return &Authorizer{policy: policy, hooks: authzHooks}
}

// NewWithPolicy creates a new Authorizer from a policy string. Any supplied
// authorization hooks will be executed, in the order provided, on each policy
// evaluation.
// NOTE: The policy is used for both client and server hooks below. If you need
// distinct policy for client vs server, create 2 Authorizer's.
func NewWithPolicy(ctx context.Context, policy string, authzHooks ...RPCAuthzHook) (*Authorizer, error) {
p, err := opa.NewAuthzPolicy(ctx, policy)
if err != nil {
Expand Down Expand Up @@ -120,7 +124,7 @@ func (g *Authorizer) Eval(ctx context.Context, input *RPCAuthInput) error {
func (g *Authorizer) Authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
msg, ok := req.(proto.Message)
if !ok {
return nil, status.Errorf(codes.Internal, "unable to authorize request of type %T, which is not proto.Message", req)
return nil, status.Errorf(codes.Internal, "unable to authorize request of type %T which is not proto.Message", req)
}
authInput, err := NewRPCAuthInput(ctx, info.FullMethod, msg)
if err != nil {
Expand All @@ -132,7 +136,61 @@ func (g *Authorizer) Authorize(ctx context.Context, req interface{}, info *grpc.
return handler(ctx, req)
}

// AuthorizeStream implements grpc.StreamServerInterceptor
// AuthorizeClient implements grpc.UnaryClientInterceptor
func (g *Authorizer) AuthorizeClient(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
msg, ok := req.(proto.Message)
if !ok {
return status.Errorf(codes.Internal, "unable to authorize request of type %T which is not proto.Message", req)
}
authInput, err := NewRPCAuthInput(ctx, method, msg)
if err != nil {
return status.Errorf(codes.Internal, "unable to create auth input: %v", err)
}
if err := g.Eval(ctx, authInput); err != nil {
return err
}
return invoker(ctx, method, req, reply, cc, opts...)
}

// AuthorizeClientStream implements grpc.StreamClientInterceptor and applies policy checks on any SendMsg calls.
func (g *Authorizer) AuthorizeClientStream(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
clientStream, err := streamer(ctx, desc, cc, method, opts...)
if err != nil {
return nil, status.Errorf(codes.Internal, "can't create clientStream: %v", err)
}
wrapped := &wrappedClientStream{
ClientStream: clientStream,
method: method,
authz: g,
}
return wrapped, nil
}

// wrappedClientStream wraps an existing grpc.ClientStream with authorization checking.
type wrappedClientStream struct {
grpc.ClientStream
method string
authz *Authorizer
}

// see: grpc.ClientStream.SendMsg
func (e *wrappedClientStream) SendMsg(req interface{}) error {
ctx := e.Context()
msg, ok := req.(proto.Message)
if !ok {
return status.Errorf(codes.Internal, "unable to authorize request of type %T which is not proto.Message", req)
}
authInput, err := NewRPCAuthInput(ctx, e.method, msg)
if err != nil {
return err
}
if err := e.authz.Eval(ctx, authInput); err != nil {
return err
}
return e.ClientStream.SendMsg(req)
}

// AuthorizeStream implements grpc.StreamServerInterceptor and applies policy checks on any RecvMsg calls.
func (g *Authorizer) AuthorizeStream(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
wrapped := &wrappedStream{
ServerStream: ss,
Expand Down Expand Up @@ -161,7 +219,7 @@ func (e *wrappedStream) RecvMsg(req interface{}) error {
}
msg, ok := req.(proto.Message)
if !ok {
return status.Errorf(codes.Internal, "unable to authorize request of type %T, which is not proto.Message", req)
return status.Errorf(codes.Internal, "unable to authorize request of type %T which is not proto.Message", req)
}
authInput, err := NewRPCAuthInput(ctx, e.info.FullMethod, msg)
if err != nil {
Expand Down
137 changes: 117 additions & 20 deletions auth/opa/rpcauth/rpcauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,9 +363,10 @@ func TestAuthzHook(t *testing.T) {
}

func TestNewWithPolicy(t *testing.T) {
_, err := NewWithPolicy(context.Background(), policyString)
ctx := context.Background()
_, err := NewWithPolicy(ctx, policyString)
testutil.FatalOnErr("NewWithPolicy valid", err, t)
if _, err := NewWithPolicy(context.Background(), ""); err == nil {
if _, err := NewWithPolicy(ctx, ""); err == nil {
t.Error("didn't get error for empty policy")
}
}
Expand All @@ -386,6 +387,8 @@ func TestRpcAuthInput(t *testing.T) {
tcp, err := net.ResolveTCPAddr("tcp4", "127.0.0.1:1")
testutil.FatalOnErr("ResolveIPAddr", err, t)

ctx := context.Background()

for _, tc := range []struct {
name string
ctx context.Context
Expand All @@ -395,7 +398,7 @@ func TestRpcAuthInput(t *testing.T) {
}{
{
name: "method only",
ctx: context.Background(),
ctx: ctx,
method: "/AMethod",
compare: &RPCAuthInput{
Method: "/AMethod",
Expand All @@ -404,7 +407,7 @@ func TestRpcAuthInput(t *testing.T) {
},
{
name: "method and metadata",
ctx: metadata.NewIncomingContext(context.Background(), md),
ctx: metadata.NewIncomingContext(ctx, md),
method: "/AMethod",
compare: &RPCAuthInput{
Method: "/AMethod",
Expand All @@ -414,7 +417,7 @@ func TestRpcAuthInput(t *testing.T) {
},
{
name: "method and request",
ctx: context.Background(),
ctx: ctx,
method: "/AMethod",
req: &emptypb.Empty{},
compare: &RPCAuthInput{
Expand All @@ -426,7 +429,7 @@ func TestRpcAuthInput(t *testing.T) {
},
{
name: "method and a peer context but no addr",
ctx: peer.NewContext(context.Background(), &peer.Peer{}),
ctx: peer.NewContext(ctx, &peer.Peer{}),
method: "/AMethod",
compare: &RPCAuthInput{
Method: "/AMethod",
Expand All @@ -435,7 +438,7 @@ func TestRpcAuthInput(t *testing.T) {
},
{
name: "method and a peer context",
ctx: peer.NewContext(context.Background(), &peer.Peer{
ctx: peer.NewContext(ctx, &peer.Peer{
Addr: tcp,
}),
method: "/AMethod",
Expand All @@ -452,7 +455,7 @@ func TestRpcAuthInput(t *testing.T) {
},
{
name: "method and a peer context with non tls auth",
ctx: peer.NewContext(context.Background(), &peer.Peer{
ctx: peer.NewContext(ctx, &peer.Peer{
Addr: tcp,
AuthInfo: testAuthInfo{},
}),
Expand All @@ -471,7 +474,7 @@ func TestRpcAuthInput(t *testing.T) {
},
{
name: "method and a peer context with tls auth",
ctx: peer.NewContext(context.Background(), &peer.Peer{
ctx: peer.NewContext(ctx, &peer.Peer{
Addr: tcp,
AuthInfo: credentials.TLSInfo{
SPIFFEID: &url.URL{
Expand Down Expand Up @@ -514,36 +517,73 @@ func TestAuthorize(t *testing.T) {
FullMethod: "/Foo/Bar",
}
gotCalled := false
handler := func(ctx context.Context, req interface{}) (resp interface{}, err error) {
handler := func(context.Context, interface{}) (interface{}, error) {
gotCalled = true
return
return nil, nil
}
ctx := context.Background()

authorizer, err := NewWithPolicy(context.Background(), policyString)
authorizer, err := NewWithPolicy(ctx, policyString)
testutil.FatalOnErr("NewWithPolicy", err, t)

// Should fail on no proto.Message
_, err = authorizer.Authorize(context.Background(), nil, info, handler)
_, err = authorizer.Authorize(ctx, nil, info, handler)
testutil.FatalOnNoErr("not a proto message", err, t)

// Should function normally
_, err = authorizer.Authorize(context.Background(), req, info, handler)
_, err = authorizer.Authorize(ctx, req, info, handler)
testutil.FatalOnErr("Authorize", err, t)
if !gotCalled {
t.Fatal("never called handler")
}

// Create one with a hook so we can fail.
authorizer, err = NewWithPolicy(context.Background(), policyString, RPCAuthzHookFunc(func(ctx context.Context, input *RPCAuthInput) error {
authorizer, err = NewWithPolicy(ctx, policyString, RPCAuthzHookFunc(func(context.Context, *RPCAuthInput) error {
return status.Error(codes.FailedPrecondition, "hook failed")
}))
testutil.FatalOnErr("NewWithPolicy with hooks", err, t)

// This time it should fail due to the hook.
_, err = authorizer.Authorize(context.Background(), req, info, handler)
_, err = authorizer.Authorize(ctx, req, info, handler)
testutil.FatalOnNoErr("Authorize with failing hook", err, t)
}

func TestAuthorizeClient(t *testing.T) {
req := &emptypb.Empty{}
method := "/Foo/Bar"

gotCalled := false
invoker := func(context.Context, string, interface{}, interface{}, *grpc.ClientConn, ...grpc.CallOption) error {
gotCalled = true
return nil
}
ctx := context.Background()

authorizer, err := NewWithPolicy(ctx, policyString)
testutil.FatalOnErr("NewWithPolicy", err, t)

// Should fail on no proto.Message
err = authorizer.AuthorizeClient(ctx, method, nil, nil, nil, invoker)
testutil.FatalOnNoErr("not a proto message", err, t)

// Should function normally
err = authorizer.AuthorizeClient(ctx, method, req, nil, nil, invoker)
testutil.FatalOnErr("AuthorizeClient", err, t)
if !gotCalled {
t.Fatal("never called invoker")
}

// Create one with a hook so we can fail.
authorizer, err = NewWithPolicy(ctx, policyString, RPCAuthzHookFunc(func(context.Context, *RPCAuthInput) error {
return status.Error(codes.FailedPrecondition, "hook failed")
}))
testutil.FatalOnErr("NewWithPolicy with hooks", err, t)

// This time it should fail due to the hook.
err = authorizer.AuthorizeClient(ctx, method, req, nil, nil, invoker)
testutil.FatalOnNoErr("AuthorizeClient with failing hook", err, t)
}

type fakeServerStream struct {
testutil.FakeServerStream
Ctx context.Context
Expand All @@ -557,34 +597,43 @@ func (f *fakeServerStream) Context() context.Context {
return f.Ctx
}

type fakeClientStream struct {
testutil.FakeClientStream
}

func (*fakeClientStream) SendMsg(req interface{}) error {
return nil
}

func TestAuthorizeStream(t *testing.T) {
req := &emptypb.Empty{}
info := &grpc.StreamServerInfo{
FullMethod: "/Foo/Bar",
}
ctx := context.Background()

handler := func(srv interface{}, stream grpc.ServerStream) error {
return stream.RecvMsg(srv)
}

fake := &fakeServerStream{Ctx: context.Background()}
fake := &fakeServerStream{Ctx: ctx}

authorizer, err := NewWithPolicy(context.Background(), policyString)
authorizer, err := NewWithPolicy(ctx, policyString)
testutil.FatalOnErr("NewWithPolicy", err, t)

err = authorizer.AuthorizeStream(req, fake, info, handler)
testutil.FatalOnErr("AuthorizeStream", err, t)

// Should fail with a normal fake server
err = authorizer.AuthorizeStream(req, &testutil.FakeServerStream{Ctx: context.Background()}, info, handler)
err = authorizer.AuthorizeStream(req, &testutil.FakeServerStream{Ctx: ctx}, info, handler)
testutil.FatalOnNoErr("AuthorizeStream failing RecvMsg", err, t)

// Should fail on a non proto.Message request
err = authorizer.AuthorizeStream(nil, fake, info, handler)
testutil.FatalOnNoErr("AuthorizeStream non proto", err, t)

// Create one with a hook so we can fail.
authorizer, err = NewWithPolicy(context.Background(), policyString, RPCAuthzHookFunc(func(ctx context.Context, input *RPCAuthInput) error {
authorizer, err = NewWithPolicy(ctx, policyString, RPCAuthzHookFunc(func(context.Context, *RPCAuthInput) error {
return status.Error(codes.FailedPrecondition, "hook failed")
}))
testutil.FatalOnErr("NewWithPolicy with hooks", err, t)
Expand All @@ -593,3 +642,51 @@ func TestAuthorizeStream(t *testing.T) {
err = authorizer.AuthorizeStream(req, fake, info, handler)
testutil.FatalOnNoErr("AuthorizeStream with failing hook", err, t)
}

func TestAuthorizeClientStream(t *testing.T) {
req := &emptypb.Empty{}
method := "/Foo/Bar"
desc := &grpc.StreamDesc{
StreamName: "/Bar",
}

ctx := context.Background()

streamer := func(context.Context, *grpc.StreamDesc, *grpc.ClientConn, string, ...grpc.CallOption) (grpc.ClientStream, error) {
return &fakeClientStream{}, nil
}

badStreamer := func(context.Context, *grpc.StreamDesc, *grpc.ClientConn, string, ...grpc.CallOption) (grpc.ClientStream, error) {
return nil, errors.New("error")
}

authorizer, err := NewWithPolicy(ctx, policyString)
testutil.FatalOnErr("NewWithPolicy", err, t)

// Test bad streamer returns error
_, err = authorizer.AuthorizeClientStream(ctx, desc, nil, method, badStreamer)
testutil.FatalOnNoErr("AuthorizeClientStream with bad streamer", err, t)

stream, err := authorizer.AuthorizeClientStream(ctx, desc, nil, method, streamer)
testutil.FatalOnErr("AuthorizeClientStream", err, t)

// Should fail on a non proto.Message request
err = stream.SendMsg(nil)
testutil.FatalOnNoErr("AuthorizeClientStream.SendMsg non proto", err, t)

// Should work with a basic msg
err = stream.SendMsg(req)
testutil.FatalOnErr("AuthorizeClientStream real request", err, t)

// Create one with a hook so we can fail.
authorizer, err = NewWithPolicy(ctx, policyString, RPCAuthzHookFunc(func(context.Context, *RPCAuthInput) error {
return status.Error(codes.FailedPrecondition, "hook failed")
}))
testutil.FatalOnErr("NewWithPolicy with hooks", err, t)

// This time it should fail due to the hook.
stream, err = authorizer.AuthorizeClientStream(ctx, desc, nil, method, streamer)
testutil.FatalOnErr("AuthorizeClientStream with failing hook setup", err, t)
err = stream.SendMsg(req)
testutil.FatalOnNoErr("AuthorizeClientStream with failing hook", err, t)
}
Loading

0 comments on commit 4a7e8ca

Please sign in to comment.