145 lines
5.4 KiB
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)
|
|
}
|