136 lines
4.5 KiB
Go
136 lines
4.5 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package tentacle
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"sync"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
|
|
)
|
|
|
|
// RemotePolicy belongs to the reader's focus/lock owner. Its cancellation is
|
|
// synchronous and does not acquire a session, command, output or registry lock.
|
|
// Restoring availability creates a fresh lifetime; it never revives consent.
|
|
type RemotePolicy struct {
|
|
mutex sync.Mutex
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
available bool
|
|
}
|
|
|
|
// RemoteApproval is a single-use core ticket captured BEFORE showing local
|
|
// consent. It is not serializable and cannot be supplied by a Tentacle RPC.
|
|
type RemoteApproval struct {
|
|
policy *RemotePolicy
|
|
ctx context.Context
|
|
lifetime context.Context
|
|
cancel context.CancelFunc
|
|
session *Session
|
|
role string
|
|
grants []string
|
|
}
|
|
|
|
func NewRemotePolicy() *RemotePolicy {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
return &RemotePolicy{ctx: ctx, cancel: cancel, available: true}
|
|
}
|
|
|
|
func (policy *RemotePolicy) SetAvailable(available bool) {
|
|
policy.mutex.Lock()
|
|
defer policy.mutex.Unlock()
|
|
if policy.available == available {
|
|
return
|
|
}
|
|
policy.cancel()
|
|
policy.available = available
|
|
if available {
|
|
policy.ctx, policy.cancel = context.WithCancel(context.Background())
|
|
}
|
|
}
|
|
|
|
// Context captures the current availability lifetime for sensitive forms. A
|
|
// lock/handoff round trip cancels that lifetime even if native polling misses
|
|
// the short transition; callers must never replace it with the resumed one.
|
|
func (policy *RemotePolicy) Context() context.Context {
|
|
policy.mutex.Lock()
|
|
defer policy.mutex.Unlock()
|
|
return policy.ctx
|
|
}
|
|
|
|
func (session *Session) BeginRemoteApproval(role string, grants []string) (*RemoteApproval, error) {
|
|
session.mutex.Lock()
|
|
defer session.mutex.Unlock()
|
|
policy := session.manager.remotePolicy
|
|
if policy == nil || session.state != Active || session.ctx.Err() != nil || !session.spec.SessionEvents || !tentaclewire.ValidRoleGrants(role, grants) {
|
|
return nil, ErrPermission
|
|
}
|
|
for _, grant := range grants {
|
|
if !slices.Contains(session.spec.RemoteGrants, grant) {
|
|
return nil, ErrPermission
|
|
}
|
|
}
|
|
policy.mutex.Lock()
|
|
defer policy.mutex.Unlock()
|
|
if !policy.available {
|
|
return nil, ErrPermission
|
|
}
|
|
session.revoke_remote_locked("replaced", true)
|
|
if session.state != Active {
|
|
return nil, ErrUnavailable
|
|
}
|
|
ctx, cancel := context.WithCancel(policy.ctx)
|
|
approval := &RemoteApproval{policy: policy, ctx: ctx, lifetime: policy.ctx, cancel: cancel, session: session, role: role, grants: slices.Clone(grants)}
|
|
session.remoteApproval = approval
|
|
return approval, nil
|
|
}
|
|
|
|
func (approval *RemoteApproval) Context() context.Context { return approval.ctx }
|
|
|
|
// Cancel only retires this pending dialog, never an issued lease or a newer
|
|
// dialog. Package changes and session shutdown also cancel pending approvals.
|
|
func (approval *RemoteApproval) Cancel() {
|
|
approval.session.mutex.Lock()
|
|
defer approval.session.mutex.Unlock()
|
|
if approval.session.remoteApproval == approval {
|
|
approval.session.remoteApproval = nil
|
|
}
|
|
approval.cancel()
|
|
}
|
|
|
|
// UseRemotePolicy must run before starting any children. A production manager
|
|
// with a policy rejects the unrestricted authorizer used by protocol fixtures.
|
|
func (manager *Manager) UseRemotePolicy(policy *RemotePolicy) error {
|
|
manager.mutex.Lock()
|
|
defer manager.mutex.Unlock()
|
|
if policy == nil || manager.remotePolicy != nil || len(manager.sessions) != 0 || manager.closed {
|
|
return ErrUnavailable
|
|
}
|
|
manager.remotePolicy = policy
|
|
return nil
|
|
}
|
|
|
|
func (session *Session) AuthorizeRemoteWithApproval(approval *RemoteApproval, role string, grants []string) (tentaclewire.SessionEvent, error) {
|
|
session.mutex.Lock()
|
|
defer session.mutex.Unlock()
|
|
policy := session.manager.remotePolicy
|
|
if policy == nil || approval == nil || approval.policy != policy || session.remoteApproval != approval || approval.session != session {
|
|
return tentaclewire.SessionEvent{}, ErrPermission
|
|
}
|
|
policy.mutex.Lock()
|
|
defer policy.mutex.Unlock()
|
|
if !policy.available || approval.lifetime != policy.ctx || approval.ctx.Err() != nil || role != approval.role || !tentaclewire.ValidRoleGrants(role, grants) {
|
|
return tentaclewire.SessionEvent{}, ErrPermission
|
|
}
|
|
for _, grant := range grants {
|
|
if !slices.Contains(approval.grants, grant) {
|
|
return tentaclewire.SessionEvent{}, ErrPermission
|
|
}
|
|
}
|
|
session.remoteApproval = nil
|
|
defer approval.cancel()
|
|
return session.authorize_remote_locked(role, grants, approval.lifetime)
|
|
}
|