Files
cthulhu/internal/app/tentacle_remote_consent.go

145 lines
5.4 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"slices"
"time"
"git.stormux.org/storm/cthulhu/internal/interaction"
"git.stormux.org/storm/cthulhu/internal/tentacle"
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
)
type localTentacleForms interface {
OpenLocal(context.Context, interaction.Form) (<-chan internalUIResult, error)
}
// authorize_remote reserves a standalone core dialog. Interactive commands use
// authorize_remote_session with their already-reserved slot and exact session.
func (runtime *tentacleRuntime) authorize_remote(ctx context.Context, id, role string, grants []string) (tentaclewire.SessionEvent, error) {
if runtime == nil || runtime.remotePolicy == nil || !tentaclewire.ValidRoleGrants(role, grants) {
return tentaclewire.SessionEvent{}, tentacle.ErrPermission
}
runtime.mutex.Lock()
var target *tentacle.Session
if !runtime.disabled {
for _, session := range runtime.sessions {
if session.TentacleID() == id {
target = session
break
}
}
}
runtime.mutex.Unlock()
if target == nil {
return tentaclewire.SessionEvent{}, tentacle.ErrUnavailable
}
// Reserve the interaction before creating a ticket, so a second request
// rejected as busy cannot invalidate the dialog already on screen.
select {
case runtime.interactionSlots <- struct{}{}:
default:
return tentaclewire.SessionEvent{}, tentacle.ErrBusy
}
defer func() { <-runtime.interactionSlots }()
return runtime.authorize_remote_session(ctx, target, role, grants)
}
// Caller owns interactionSlots. A child request may open a dialog, but only
// local capture and the core-issued ticket can authorize the requesting session.
func (runtime *tentacleRuntime) authorize_remote_session(ctx context.Context, target *tentacle.Session, role string, grants []string) (tentaclewire.SessionEvent, error) {
if runtime == nil || runtime.remotePolicy == nil || target == nil || !tentaclewire.ValidRoleGrants(role, grants) {
return tentaclewire.SessionEvent{}, tentacle.ErrPermission
}
forms, ok := runtime.forms.(localTentacleForms)
if !ok {
return tentaclewire.SessionEvent{}, tentacle.ErrUnavailable
}
runtime.mutex.Lock()
current := !runtime.disabled && slices.Contains(runtime.sessions, target)
runtime.mutex.Unlock()
if !current {
return tentaclewire.SessionEvent{}, tentacle.ErrUnavailable
}
grants = slices.Clone(grants)
installed := target.RemoteGrants()
for _, grant := range grants {
if !slices.Contains(installed, grant) {
return tentaclewire.SessionEvent{}, tentacle.ErrPermission
}
}
approval, err := target.BeginRemoteApproval(role, grants)
if err != nil {
return tentaclewire.SessionEvent{}, err
}
defer approval.Cancel()
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
stopPolicy := context.AfterFunc(approval.Context(), cancel)
defer stopPolicy()
stopSession := context.AfterFunc(target.OutputContext(), cancel)
defer stopSession()
form := remote_consent_form(target.TentacleID(), role, grants)
replies, err := forms.OpenLocal(ctx, form)
if err != nil {
return tentaclewire.SessionEvent{}, err
}
select {
case reply, open := <-replies:
if !open || ctx.Err() != nil || reply.Cancelled || reply.Err != nil || reply.Activated != "approve" {
return tentaclewire.SessionEvent{}, tentacle.ErrPermission
}
var selected []string
for _, grant := range grants {
if reply.Values[grant].Checked {
selected = append(selected, grant)
}
}
if len(selected) == 0 {
return tentaclewire.SessionEvent{}, tentacle.ErrPermission
}
return target.AuthorizeRemoteWithApproval(approval, role, selected)
case <-ctx.Done():
return tentaclewire.SessionEvent{}, tentacle.ErrPermission
}
}
func remote_consent_form(id, role string, grants []string) interaction.Form {
roleText := "control another computer"
if role == "controlled" {
roleText = "allow a helper to access this computer"
}
form := interaction.Form{Title: "Remote access for " + id + ": " + roleText + ". The relay can read session contents. Select access for this session.",
Elements: []interaction.Element{{ID: "cancel", Label: "Cancel", Kind: interaction.Button}}}
labels := map[string]string{
"output-observe": "Share speech with the helper",
"remote-present": "Hear the other computer",
"keyboard-capture": "Keyboard control of the other computer. F11 returns here",
"keyboard-inject": "Allow the helper to type here",
"clipboard-exchange": "Clipboard text, transferred on request",
"braille-stream": "Braille display and routing",
}
for _, grant := range grants {
form.Elements = append(form.Elements, interaction.Element{ID: grant, Label: labels[grant], Kind: interaction.CheckBox})
}
form.Elements = append(form.Elements, interaction.Element{ID: "approve", Label: "Allow selected access and connect", Kind: interaction.Button})
return form
}
func (runtime *tentacleRuntime) authorize_remote_request(ctx context.Context, target *tentacle.Session, request tentaclewire.RemoteRequest) (tentaclewire.SessionEvent, error) {
if target == nil || !tentaclewire.ValidRemoteRequest(request) {
return tentaclewire.SessionEvent{}, tentacle.ErrPermission
}
grants := slices.Clone(request.Grants)
installed := target.RemoteGrants()
for _, grant := range request.OptionalGrants {
if slices.Contains(installed, grant) {
grants = append(grants, grant)
}
}
return runtime.authorize_remote_session(ctx, target, request.Role, grants)
}