246 lines
8.2 KiB
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")
|
|
}
|
|
}
|