Files
cthulhu/internal/app/tentacle_remote_consent_test.go

246 lines
8.2 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"encoding/json"
"errors"
"os"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/interaction"
"git.stormux.org/storm/cthulhu/internal/tentacle"
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
)
type consentFormRequest struct {
ctx context.Context
form interaction.Form
reply chan internalUIResult
}
type consentTestForms struct{ requests chan consentFormRequest }
func (forms *consentTestForms) Open(context.Context, interaction.Form) (<-chan internalUIResult, error) {
return nil, errors.New("ordinary form used for consent")
}
func (forms *consentTestForms) Notice(context.Context, tentaclewire.Presentation) {}
func (forms *consentTestForms) OpenLocal(ctx context.Context, form interaction.Form) (<-chan internalUIResult, error) {
reply := make(chan internalUIResult, 1)
forms.requests <- consentFormRequest{ctx, form, reply}
output := make(chan internalUIResult, 1)
go func() {
select {
case value := <-reply:
output <- value
case <-ctx.Done():
output <- internalUIResult{Cancelled: true}
}
}()
return output, nil
}
func consent_runtime_fixture(t *testing.T) (*tentacleRuntime, *tentacle.Session, *consentTestForms, *focusGate) {
t.Helper()
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
manager, err := tentacle.NewManager(tentacle.DefaultLimits(), nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := manager.Shutdown(ctx); err != nil {
t.Error(err)
}
})
policy := tentacle.NewRemotePolicy()
if err := manager.UseRemotePolicy(policy); err != nil {
t.Fatal(err)
}
session, err := manager.Start(t.Context(), tentacle.Spec{ID: "consent-probe", Program: []string{executable, "-test.run=^TestRemoteManagementPeer$", "--", "remote-management-peer"}, SessionEvents: true, RemoteGrants: []string{"output-observe", "clipboard-exchange"}})
if err != nil {
t.Fatal(err)
}
select {
case <-session.Ready():
case <-time.After(5 * time.Second):
t.Fatal("peer not ready")
}
if err := session.Activate(); err != nil {
t.Fatal(err)
}
forms := &consentTestForms{requests: make(chan consentFormRequest, 1)}
runtime := &tentacleRuntime{remotePolicy: policy, forms: forms, sessions: []*tentacle.Session{session}, interactionSlots: make(chan struct{}, 1)}
return runtime, session, forms, &focusGate{remotePolicy: policy}
}
func TestRemoteConsentFormDefaultsAndRevocation(t *testing.T) {
for _, action := range []string{"approve", "cancel", "empty", "lock", "handoff", "revoke", "disable"} {
t.Run(action, func(t *testing.T) {
runtime, session, forms, gate := consent_runtime_fixture(t)
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
result := make(chan error, 1)
go func() {
_, err := runtime.authorize_remote(ctx, "consent-probe", "controlled", []string{"output-observe", "clipboard-exchange"})
result <- err
}()
var request consentFormRequest
select {
case request = <-forms.requests:
case <-ctx.Done():
t.Fatal("no consent form")
}
if _, err := interaction.NewSession(request.form); err != nil {
t.Fatal("invalid consent form", err)
}
if request.form.Elements[0].ID != "cancel" {
t.Fatal("approval was default action")
}
for _, element := range request.form.Elements {
if element.Checked {
t.Fatal("permission preselected")
}
}
reply := internalUIResult{Activated: "approve", Values: map[string]interaction.Value{"output-observe": {Checked: true}}}
switch action {
case "cancel":
reply.Activated = "cancel"
case "empty":
reply.Values = nil
case "lock":
gate.SetLocked(true, nil, nil, nil, nil, &brailleWorker{})
gate.SetLocked(false, nil, nil, nil, nil, &brailleWorker{})
case "handoff":
gate.SetSuspended(true)
gate.SetSuspended(false)
case "revoke":
runtime.revokeManagedRemote("consent-probe")
case "disable":
session.Disable()
}
request.reply <- reply
select {
case err := <-result:
if action == "approve" && err != nil {
t.Fatal(err)
}
if action != "approve" && err == nil {
t.Fatal("stale/denied consent issued action")
}
case <-ctx.Done():
t.Fatal("consent worker did not finish")
}
if action != "approve" {
return
}
pending, err := session.Invoke(ctx, "open", nil)
if err != nil {
t.Fatal(err)
}
var lease tentaclewire.RemoteLease
select {
case opened := <-pending:
if opened.Err != nil || json.Unmarshal(opened.Value, &lease) != nil {
t.Fatal("approved action failed", opened.Err)
}
case <-ctx.Done():
t.Fatal("open timed out")
}
delivery, err := session.RemoteContext(lease, "output-observe")
if err != nil {
t.Fatal(err)
}
if _, err := session.RemoteContext(lease, "clipboard-exchange"); err == nil {
t.Fatal("unchecked permission granted")
}
gate.SetSuspended(true)
if delivery.Err() == nil {
t.Fatal("handoff did not synchronously cancel delivery")
}
gate.SetSuspended(false)
if _, err := session.RemoteContext(lease, "output-observe"); err == nil {
t.Fatal("handoff resume revived access")
}
})
}
}
func TestRemoteGrantedTentacleUsesLocalForms(t *testing.T) {
runtime, session, forms, _ := consent_runtime_fixture(t)
form := interaction.Form{Title: "Certificate review", Elements: []interaction.Element{{ID: "cancel", Label: "Cancel", Kind: interaction.Button}}}
if _, err := runtime.open_tentacle_form(t.Context(), tentacleCommand{session: session}, form); err != nil {
t.Fatal(err)
}
select {
case <-forms.requests:
default:
t.Fatal("remote form used ordinary input")
}
}
type ordinaryOnlyForms struct{ tentacleForms }
func TestRemoteConsentCannotFallBackToOrdinaryForms(t *testing.T) {
runtime, session, _, _ := consent_runtime_fixture(t)
runtime.forms = ordinaryOnlyForms{runtime.forms}
if _, err := runtime.authorize_remote(t.Context(), "consent-probe", "controlled", []string{"output-observe"}); err == nil {
t.Fatal("consent fell back to ordinary input")
}
if _, err := runtime.open_tentacle_form(t.Context(), tentacleCommand{session: session}, remote_consent_form("consent-probe", "controlled", []string{"output-observe"})); err == nil {
t.Fatal("sensitive form fell back to ordinary input")
}
}
func TestRemotePolicyAttachedToRuntimeManagers(t *testing.T) {
runtime := &tentacleRuntime{remotePolicy: tentacle.NewRemotePolicy(), newManager: func() (*tentacle.Manager, error) { return tentacle.NewManager(tentacle.DefaultLimits(), nil) }}
manager, err := runtime.makeManager()
if err != nil {
t.Fatal(err)
}
if err := manager.UseRemotePolicy(tentacle.NewRemotePolicy()); err == nil {
t.Fatal("reader manager omitted policy")
}
}
func TestRemoteOptionalGrantsRespectInstallationAndLocalSelection(t *testing.T) {
runtime, session, forms, _ := consent_runtime_fixture(t)
type result struct {
action tentaclewire.SessionEvent
err error
}
completed := make(chan result, 1)
go func() {
action, err := runtime.authorize_remote_request(t.Context(), session, tentaclewire.RemoteRequest{Role: "controlled", Grants: []string{"output-observe"}, OptionalGrants: []string{"clipboard-exchange", "keyboard-inject", "braille-stream"}})
completed <- result{action, err}
}()
request := next_remote_form(t, forms)
offered := map[string]bool{}
for _, element := range request.form.Elements {
if element.Kind == interaction.CheckBox {
offered[element.ID] = true
if element.Checked {
t.Fatal("grant preselected")
}
}
}
if !offered["output-observe"] || !offered["clipboard-exchange"] || offered["keyboard-inject"] || offered["braille-stream"] {
t.Fatal("uninstalled access offered", offered)
}
request.reply <- internalUIResult{Activated: "approve", Values: map[string]interaction.Value{"output-observe": {Checked: true}, "keyboard-inject": {Checked: true}}}
select {
case value := <-completed:
if value.err != nil || len(value.action.Grants) != 1 || value.action.Grants[0] != "output-observe" {
t.Fatal("unselected or uninstalled grant authorized", value)
}
case <-time.After(time.Second):
t.Fatal("approval did not finish")
}
}