-
Notifications
You must be signed in to change notification settings - Fork 9.7k
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
protect watch with auth #7809
protect watch with auth #7809
Conversation
@@ -180,6 +200,19 @@ func (sws *serverWatchStream) recvLoop() error { | |||
// support >= key queries | |||
creq.RangeEnd = []byte{} | |||
} | |||
|
|||
if !sws.isWatchPermitted(creq) { | |||
wr := &pb.WatchResponse{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should fill out all of the fields like the regular error path below. Older clients will be confused by this behavior since they'll get id=0 and not know about permission_denied and think the watch was successfully created.
clientv3/watch.go
Outdated
@@ -462,6 +471,9 @@ func (w *watchGrpcStream) run() { | |||
close(ws.recvc) | |||
closing[ws] = struct{}{} | |||
} | |||
case pbresp.PermissionDenied: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be handled through the pbresp.Created
path if it's only going to happen on create events; it should work as-is if returning a createresponse with canceled=true and id=-1.
etcdserver/etcdserverpb/rpc.proto
Outdated
@@ -653,6 +653,9 @@ message WatchResponse { | |||
// The client should treat the watcher as canceled and should not try to create any | |||
// watcher with the same start_revision again. | |||
int64 compact_revision = 5; | |||
// permissionDenied is set to true if the requesting user doesn't have a | |||
// read permission of watched keys. | |||
bool permission_denied = 6; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm hesitant to include boolean flags to indicate errors since errors will be mutually exclusive. Maybe a string that can be translated back to a grpc error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your review and sorry for my late reply. I added the field (and other changes you pointed out problems) for expressing the error of permission denied. But as you say, if it can be expressed in the error code, it would be more straightfoward. I'll change the error propagation style in the next update.
clientv3/client.go
Outdated
@@ -93,7 +93,10 @@ func NewFromURL(url string) (*Client, error) { | |||
// Close shuts down the client's etcd connections. | |||
func (c *Client) Close() error { | |||
c.cancel() | |||
c.Watcher.Close() | |||
err := c.Watcher.Close() | |||
if err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Close
should probably keep going regardless of whether if there's an error, otherwise it'll leak lease resources if there's any watch error
clientv3/watch.go
Outdated
@@ -324,6 +329,10 @@ func (w *watchGrpcStream) Close() (err error) { | |||
case err = <-w.errc: | |||
default: | |||
} | |||
|
|||
if w.closeErr != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this needed now?
e2e/ctl_v3_auth_test.go
Outdated
cx.t.Fatalf("watchTest #%d-%d: ctlV3Put error (%v)", i, j, err) | ||
} | ||
} | ||
close(donec) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
defer close(donec)
at top of goroutine; otherwise the fatal will cause the test to wait until go test
timeout
@@ -116,3 +116,30 @@ func ctlV3Watch(cx ctlCtx, args []string, kvs ...kv) error { | |||
} | |||
return proc.Stop() | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
share setup code from cmdArgs :=
to if cx.interactive { ... }
instead of duplicating: ctlV3WatchProc(cx ctlCtx, args []string) (*expect.ExpectProcess, error)
@mitake kindly ping. Can we get this in soon? it blocks our 3.2 release. The approach looks good. |
@xiang90 sorry for that, I'll update this PR today! |
@heyitsanthony @xiang90 In this version, I tried to propagate the permission denied error via I think I'm still thinking about this change breaks semantics of clientv3. I'd like to hear your comments. |
mitake@e1103cc |
@heyitsanthony @xiang90 Anyway, I updated this PR based on your feedback. If the way of error propagation isn't suitable, I'll revisit it again. |
clientv3/client.go
Outdated
@@ -93,8 +93,11 @@ func NewFromURL(url string) (*Client, error) { | |||
// Close shuts down the client's etcd connections. | |||
func (c *Client) Close() error { | |||
c.cancel() | |||
c.Watcher.Close() | |||
closeErr := c.Watcher.Close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is breaking TestGRPCResolver
and TestGRPCResolverMulti
because it changes behaviour of integration.ClusterV3.Terminate()
...
Anyway, current Client.Close()
ignores an error from c.Watcher.Close()
, do you have good idea for receiving the watcher error? @heyitsanthony @xiang90
@mitake an auth error probably shouldn't take down the entire watch stream (e.g., by returning an error on the stream); there would have to be an extra error path outside of watch channels just for handling auth errors. Sending a response with |
@heyitsanthony I see, then as you mentioned in the comment (#7809 (comment)), a new member for indicating the reason should be in |
@heyitsanthony I updated with the enum approach, how about this? |
@heyitsanthony sorry it seems that I misunderstood your intention. An auth error shouldn't do closing an entire watch stream like this: https://github.com/coreos/etcd/pull/7809/files#diff-bdef56e462f289cae8e13ebbe16ba455R476 ? The error should be handled in the receiver side, is this ok? |
@heyitsanthony @xiang90 I let clientv3 return canceled events to its client. I think the latest PR can handle the permission denied error in a suitable manner and preserve the semantics of existing watch APIs. Could you take a look? |
bb0876e
to
a82370c
Compare
@mitake using |
clientv3/watch.go
Outdated
// signal to stream goroutine to update closingc | ||
close(ws.recvc) | ||
closing[ws] = struct{}{} | ||
if pbresp.CancelReason == pb.WatchResponse_PERMISSION_DENIED { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this really need special handling? why isn't the regular cancel=true path enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought the special handling is required because the error of permission denied will be replied from etcd to the client before registering a substream so the new dispatchCanceledEvent()
is needed. Otherwise, the client cannot know the reason of the cancel. In a case of etcdctl, the command cannot print a detailed reason for a user.
@heyitsanthony ok then I'll change the type of the filed for cancel reason from enum to string in the next update. |
@heyitsanthony updated for changing the reason field from enum to string. Could you take a look? The way of propagating the error isn't changed now. If the change is required, I'll do it in next update. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in general OK; I believe the patch can be simplified by setting Created: true
in the error response like the other Canceled: true
path (tried it out, seems to work OK)
clientv3/watch.go
Outdated
@@ -65,6 +66,9 @@ type WatchResponse struct { | |||
Created bool | |||
|
|||
closeErr error | |||
|
|||
// CancelReason is a reason of canceling watch | |||
CancelReason string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cancelReason
? data can be accessed through Err()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, it should be private. I'll fix it in the next update. Thanks.
clientv3/watch.go
Outdated
@@ -85,6 +89,9 @@ func (wr *WatchResponse) Err() error { | |||
case wr.CompactRevision != 0: | |||
return v3rpc.ErrCompacted | |||
case wr.Canceled: | |||
if len(wr.CancelReason) != 0 { | |||
return errors.New(wr.CancelReason) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be able to reuse rpctypes errors here:
return v3rpc.Error(grpc.Errorf(codes.FailedPrecondition, "%s", wr.cancelReason))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I'll ueuse the code.
etcdserver/etcdserverpb/rpc.proto
Outdated
@@ -654,6 +654,9 @@ message WatchResponse { | |||
// watcher with the same start_revision again. | |||
int64 compact_revision = 5; | |||
|
|||
// cancel_reason indicates a reason of cancel. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/a reason of cancel/the reason for canceling the watcher./
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll fix it in the next update.
@@ -129,6 +129,9 @@ func getWatchChan(c *clientv3.Client, args []string) (clientv3.WatchChan, error) | |||
|
|||
func printWatchCh(ch clientv3.WatchChan) { | |||
for resp := range ch { | |||
if resp.Canceled { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"watch was canceled (%v)\n"
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
etcdserver/api/v3rpc/watch.go
Outdated
Header: sws.newResponseHeader(sws.watchStream.Rev()), | ||
WatchId: -1, | ||
Canceled: true, | ||
CancelReason: rpctypes.ErrGRPCPermissionDenied.Error(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add Created: true
since it's a response to a watch create
clientv3/watch.go
Outdated
// signal to stream goroutine to update closingc | ||
close(ws.recvc) | ||
closing[ws] = struct{}{} | ||
if len(pbresp.CancelReason) != 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
client probably doesn't need this new path if the server sets Created
to true
when returning the error response
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried the idea of setting Created
as true
, but from the cancel reason couldn't propagated to a client. This is because pbresp.WatchId
is -1 so the existing path of pbresp.Created
cannot reply the watch response to the client. It is because handling the canceled response with the path of created will close watchGrpcStream.revc
in watchGrpcStream.addSubstream()
.
This is why I added the new path and new function dispatchCanceledEvent()
: https://github.com/coreos/etcd/pull/7809/files#diff-bdef56e462f289cae8e13ebbe16ba455R571
Of course the watching can be canceled but the client cannot know the reason. How do you think?
clientv3/watch.go
Outdated
@@ -605,7 +640,7 @@ func (w *watchGrpcStream) serveSubstream(ws *watcherStream, resumec chan struct{ | |||
return | |||
} | |||
|
|||
if wr.Created { | |||
if wr.Created || wr.Canceled { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|| wr.Canceled
not needed if Created
is true
in the server response?
@heyitsanthony anyway, I updated minor coding style problems. The error propagation way are currently kept as before. If the cancel reason can be propagated with a more simpler way, of course I'll change it. |
@mitake Seriously, it needs With created=true: [anthony@etcd]$ ./bin/etcd-create-true &
[anthony@etcd]$ ETCDCTL_API=3 ./bin/etcdctl user add root:root
User root created
[anthony@etcd]$ ETCDCTL_API=3 ./bin/etcdctl auth enable
Authentication Enabled
[anthony@etcd]$ ETCDCTL_API=3 ./bin/etcdctl-create-true watch abc
Error: watch is canceled by the server
[anthony@etcd]$ ETCDCTL_API=3 ./bin/etcdctl-3.1.8 watch abc
Error: watch is canceled by the server Without created=true: [anthony@etcd]$ ./bin/etcd-no-create-true &
[anthony@etcd]$ ETCDCTL_API=3 ./bin/etcdctl-3.1.8 watch abc
[hangs]
[anthony@etcd]$ ETCDCTL_API=3 ./bin/etcdctl-no-create-true -w simple watch abc
watch was canceled (rpc error: code = FailedPrecondition desc = rpc error: code = PermissionDenied desc = etcdserver: permission denied)
Error: watch is canceled by the server I'm more concerned about clients hanging than trying to propagate the specific error through the client at this point. |
@heyitsanthony I see, the problem of hanging seems to be important. I picked and squashed your commit and added a todo of e2e (https://github.com/coreos/etcd/pull/7809/files#diff-12807139fa89b9ce1993cb698466b7ecR141). Could you take a look? |
lgtm once CI greenlights. thanks! |
@heyitsanthony thanks for your detailed review! CI passed so I'm merging it. |
Fix #7789
@heyitsanthony how do you think about this change? I couldn't separate auth and watch in v3rpc in a clean manner (like maintenance RPCs) because its stream RPC structure was hard to decompose. I'd like to hear your opinion.