Notify Tentacles when pending Remote approval is revoked
This commit is contained in:
1 parent
205cb27483
commit
9bc74dbb13
8 files changed
+61
-12
No files matched your search
@@ -154,9 +154,11 @@ Its sequence starts at one and increases exactly once per event. Supported kinds
|
|||||||
- `remote.action`: sequence, kind, action token, host-issued epoch, role and approved
|
- `remote.action`: sequence, kind, action token, host-issued epoch, role and approved
|
||||||
grant subset. Created only by the internal core authorizer. Approval expires
|
grant subset. Created only by the internal core authorizer. Approval expires
|
||||||
after 30 seconds if not consumed. No network secret is included.
|
after 30 seconds if not consumed. No network secret is included.
|
||||||
- `remote.revoked`: sequence, kind, lease, epoch and reason (`closed`, `expired`,
|
- `remote.revoked`: sequence, kind, epoch, exactly one of action or lease, and reason (`closed`, `expired`,
|
||||||
`local`, `replaced`). Failure to enqueue revocation retires the session; it
|
`local`, `replaced`). Failure to enqueue revocation retires the session; it
|
||||||
cannot block immediate cancellation. An unconsumed approval expires silently.
|
cannot block immediate cancellation. Pending approval expiry/revocation carries
|
||||||
|
its action token; an opened lease carries its lease token. Both retire external
|
||||||
|
setup workers through the same epoch-bound notification.
|
||||||
|
|
||||||
Child calls:
|
Child calls:
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ func (runtime *tentacleRuntime) interact(ctx context.Context, command tentacleCo
|
|||||||
defer stopCommand()
|
defer stopCommand()
|
||||||
stop := context.AfterFunc(command.session.OutputContext(), cancel)
|
stop := context.AfterFunc(command.session.OutputContext(), cancel)
|
||||||
defer stop()
|
defer stop()
|
||||||
// Policy cancellation can interrupt setup before a lease exists, when no
|
// Policy cancellation can interrupt setup before approval exists.
|
||||||
// remote.revoked event is emitted. Tell the original continuation to stop its
|
// Tell the original continuation to stop its
|
||||||
// inspection/setup workers before releasing the interaction slot. Never pass
|
// inspection/setup workers before releasing the interaction slot. Never pass
|
||||||
// cancelled form values or reuse the cancelled interaction context.
|
// cancelled form values or reuse the cancelled interaction context.
|
||||||
cleanupNext := ""
|
cleanupNext := ""
|
||||||
|
|||||||
@@ -182,8 +182,8 @@ func (session *Session) revoke_remote_locked(reason string, notify bool) {
|
|||||||
access.timer.Stop()
|
access.timer.Stop()
|
||||||
}
|
}
|
||||||
session.release_remote_owner()
|
session.release_remote_owner()
|
||||||
if notify && access.lease != "" {
|
if notify {
|
||||||
event := tentaclewire.SessionEvent{Kind: "remote.revoked", Lease: access.lease, Epoch: access.epoch, Reason: reason}
|
event := tentaclewire.SessionEvent{Kind: "remote.revoked", Action: access.action, Lease: access.lease, Epoch: access.epoch, Reason: reason}
|
||||||
if !session.remote_event_locked(&event) {
|
if !session.remote_event_locked(&event) {
|
||||||
session.fail_locked(ErrLimit)
|
session.fail_locked(ErrLimit)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ func TestRemotePolicyRevokesPendingAndLiveSDKAccess(t *testing.T) {
|
|||||||
if _, code := remoteCall(t, session, "remote.heartbeat", tentaclewire.RemoteOperation{Lease: lease.Lease, Epoch: lease.Epoch, Sequence: 1}); code != -32001 {
|
if _, code := remoteCall(t, session, "remote.heartbeat", tentaclewire.RemoteOperation{Lease: lease.Lease, Epoch: lease.Epoch, Sequence: 1}); code != -32001 {
|
||||||
t.Fatal("heartbeat revived lease", code)
|
t.Fatal("heartbeat revived lease", code)
|
||||||
}
|
}
|
||||||
var event tentaclewire.SessionEvent
|
}
|
||||||
if json.Unmarshal(invokeSessionSDK(t, session, "event", nil), &event) != nil || event.Kind != "remote.revoked" || event.Lease != lease.Lease {
|
var event tentaclewire.SessionEvent
|
||||||
t.Fatal("revocation event", event)
|
if json.Unmarshal(invokeSessionSDK(t, session, "event", nil), &event) != nil || event.Kind != "remote.revoked" || event.Epoch != action.Epoch || event.Lease != lease.Lease || (!opened && event.Action != action.Action) {
|
||||||
}
|
t.Fatal("revocation event", event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A fresh approval can replace the cancelled epoch even if its
|
// A fresh approval can replace the cancelled epoch even if its
|
||||||
|
|||||||
@@ -470,3 +470,33 @@ func TestSessionAggregateQueueBound(t *testing.T) {
|
|||||||
t.Fatal("aggregate byte ceiling exceeded")
|
t.Fatal("aggregate byte ceiling exceeded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPendingRemoteRevocationReachesBothSDKs(t *testing.T) {
|
||||||
|
for _, language := range []string{"go", "python"} {
|
||||||
|
for _, reason := range []string{"local", "expired", "replaced"} {
|
||||||
|
t.Run(language+"/"+reason, func(t *testing.T) {
|
||||||
|
session := startSessionSDK(t, test_manager(t, DefaultLimits(), nil), language, "pending-revoke")
|
||||||
|
action, err := session.AuthorizeRemote("controller", []string{"remote-present"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
invokeSessionSDK(t, session, "event", nil)
|
||||||
|
session.mutex.Lock()
|
||||||
|
if reason == "expired" {
|
||||||
|
session.refresh_remote_locked(time.Millisecond)
|
||||||
|
} else {
|
||||||
|
session.revoke_remote_locked(reason, true)
|
||||||
|
}
|
||||||
|
session.mutex.Unlock()
|
||||||
|
var event tentaclewire.SessionEvent
|
||||||
|
raw := invokeSessionSDK(t, session, "event", nil)
|
||||||
|
if json.Unmarshal(raw, &event) != nil || event.Kind != "remote.revoked" || event.Action != action.Action || event.Lease != "" || event.Epoch != action.Epoch || event.Reason != reason {
|
||||||
|
t.Fatal("pending action revocation was not delivered", string(raw))
|
||||||
|
}
|
||||||
|
if _, code := remoteCall(t, session, "remote.open", tentaclewire.RemoteOpen{Action: action.Action, Epoch: action.Epoch, Role: action.Role, Grants: action.Grants}); code != -32001 {
|
||||||
|
t.Fatal("retired action opened a lease", code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,7 +99,7 @@ func ValidSessionEvent(event SessionEvent) bool {
|
|||||||
case "remote.action":
|
case "remote.action":
|
||||||
return event.Speech == nil && len(event.Action) == 32 && event.Lease == "" && event.Reason == "" && ValidRoleGrants(event.Role, event.Grants)
|
return event.Speech == nil && len(event.Action) == 32 && event.Lease == "" && event.Reason == "" && ValidRoleGrants(event.Role, event.Grants)
|
||||||
case "remote.revoked":
|
case "remote.revoked":
|
||||||
return event.Speech == nil && event.Action == "" && len(event.Lease) == 32 && event.Role == "" && len(event.Grants) == 0 && slices.Contains([]string{"closed", "expired", "local", "replaced"}, event.Reason)
|
return event.Speech == nil && ((event.Action == "" && len(event.Lease) == 32) || (len(event.Action) == 32 && event.Lease == "")) && event.Role == "" && len(event.Grants) == 0 && slices.Contains([]string{"closed", "expired", "local", "replaced"}, event.Reason)
|
||||||
case "remote.output":
|
case "remote.output":
|
||||||
return event.Action == "" && len(event.Lease) == 32 && event.Role == "" && len(event.Grants) == 0 && event.Reason == "" && event.Speech != nil && ValidRemoteSpeech(*event.Speech)
|
return event.Action == "" && len(event.Lease) == 32 && event.Role == "" && len(event.Grants) == 0 && event.Reason == "" && event.Speech != nil && ValidRemoteSpeech(*event.Speech)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,3 +33,18 @@ func TestSessionNegotiationRequiresIndependentGrants(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRemoteRevocationIdentifiesExactlyOneActionOrLease(t *testing.T) {
|
||||||
|
token := "0123456789abcdef0123456789abcdef"
|
||||||
|
for _, item := range []struct {
|
||||||
|
action, lease string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{token, "", true}, {"", token, true}, {token, token, false}, {"", "", false}, {"short", "", false},
|
||||||
|
} {
|
||||||
|
event := SessionEvent{Sequence: 1, Epoch: 1, Kind: "remote.revoked", Reason: "expired", Action: item.action, Lease: item.lease}
|
||||||
|
if ValidSessionEvent(event) != item.valid {
|
||||||
|
t.Fatalf("unexpected validation for %+v", item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -305,7 +305,9 @@ class SessionClient:
|
|||||||
or role == "controlled" and set(grants) & {"remote-present", "keyboard-capture"}):
|
or role == "controlled" and set(grants) & {"remote-present", "keyboard-capture"}):
|
||||||
raise ProtocolError("Invalid action event")
|
raise ProtocolError("Invalid action event")
|
||||||
elif event["kind"] == "remote.revoked":
|
elif event["kind"] == "remote.revoked":
|
||||||
if ("speech" in event or not _token(event.get("lease")) or event.get("action", "") != "" or event.get("role", "") != ""
|
identity = ((_token(event.get("lease")) and event.get("action", "") == "")
|
||||||
|
or (_token(event.get("action")) and event.get("lease", "") == ""))
|
||||||
|
if ("speech" in event or not identity or event.get("role", "") != ""
|
||||||
or event.get("grants", []) != [] or event.get("reason") not in ("closed", "expired", "local", "replaced")):
|
or event.get("grants", []) != [] or event.get("reason") not in ("closed", "expired", "local", "replaced")):
|
||||||
raise ProtocolError("Invalid revoke event")
|
raise ProtocolError("Invalid revoke event")
|
||||||
elif event["kind"] == "remote.output":
|
elif event["kind"] == "remote.output":
|
||||||
|
|||||||
Reference in new issue
Block a user