200 lines
8.9 KiB
Go
200 lines
8.9 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"browser.local/platform/domain"
|
|
"browser.local/platform/validator"
|
|
)
|
|
|
|
func (svc *CoreService) ListRemoteAdapterDeclarationsForSession(sessionID string, serverInstanceID string) ([]domain.RemoteAdapterDeclaration, error) {
|
|
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !plugin.Permissions.RemoteAccess {
|
|
return []domain.RemoteAdapterDeclaration{}, nil
|
|
}
|
|
declarations := make([]domain.RemoteAdapterDeclaration, 0, len(plugin.RuntimeProfiles.TransportProfiles))
|
|
for _, profile := range plugin.RuntimeProfiles.TransportProfiles {
|
|
capabilities := intersectRemoteCapabilities(profile.Capabilities, plugin.RemoteAccess.RunCapabilities, endpoint.Capabilities)
|
|
if len(capabilities) == 0 || strings.TrimSpace(profile.TargetKey) == "" {
|
|
continue
|
|
}
|
|
declaration := domain.RemoteAdapterDeclaration{Key: profile.Key, Kind: remoteAdapterKind(profile.Kind), TargetKeys: []string{profile.TargetKey}, Capabilities: capabilities, TimeoutSeconds: 30, MaxAttempts: 3}
|
|
if err := validator.ValidateRemoteAdapterDeclaration(declaration); err != nil {
|
|
return nil, err
|
|
}
|
|
declarations = append(declarations, declaration)
|
|
}
|
|
return domain.CopyRemoteAdapterDeclarations(declarations), nil
|
|
}
|
|
|
|
func (svc *CoreService) RequestRemoteAdapterForSession(sessionID string, request domain.RemoteAdapterRequest) (domain.RemoteAdapterResult, error) {
|
|
request = domain.CopyRemoteAdapterRequest(request)
|
|
if err := validator.ValidateRemoteAdapterRequest(request); err != nil {
|
|
return domain.RemoteAdapterResult{}, err
|
|
}
|
|
if request.Capability == domain.JobCapabilityRemoteRunDBSQLiteProbe && !request.PlatformScheduled {
|
|
return domain.RemoteAdapterResult{}, forbiddenError("schema probe is scheduled by Platform and is not a direct remote-adapter request")
|
|
}
|
|
instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID)
|
|
if err != nil {
|
|
return domain.RemoteAdapterResult{}, err
|
|
}
|
|
declarations, err := svc.ListRemoteAdapterDeclarationsForSession(sessionID, instance.ID)
|
|
if err != nil {
|
|
return domain.RemoteAdapterResult{}, err
|
|
}
|
|
var selected domain.RemoteAdapterDeclaration
|
|
for _, declaration := range declarations {
|
|
if declaration.Key == request.DeclarationKey && containsString(declaration.TargetKeys, request.TargetKey) && containsString(declaration.Capabilities, request.Capability) {
|
|
selected = declaration
|
|
break
|
|
}
|
|
}
|
|
if selected.Key == "" {
|
|
plugin, pluginErr := svc.store.GamePlugins().Get(instance.PluginID)
|
|
endpoint, endpointErr := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
|
if pluginErr == nil && endpointErr == nil && plugin.Permissions.RemoteAccess && containsString(plugin.RemoteAccess.RunCapabilities, request.Capability) && containsString(endpoint.Capabilities, request.Capability) {
|
|
selected = domain.RemoteAdapterDeclaration{Key: "legacy-" + string(remoteAdapterKindForCapability(request.Capability)), Kind: remoteAdapterKindForCapability(request.Capability), TargetKeys: []string{request.TargetKey}, Capabilities: []string{request.Capability}, TimeoutSeconds: 30, MaxAttempts: 3}
|
|
}
|
|
if selected.Key == "" {
|
|
user, _ := svc.GetCurrentUser(sessionID)
|
|
_ = svc.recordAuditEvent(user.ID, "remote-adapter.authorize", "server-instance", instance.ID, domain.AuditResultDenied, "remote adapter declaration, target, or capability was not approved")
|
|
return domain.RemoteAdapterResult{}, ErrForbidden
|
|
}
|
|
}
|
|
timeout := request.TimeoutSeconds
|
|
if timeout == 0 {
|
|
timeout = selected.TimeoutSeconds
|
|
}
|
|
attempts := request.MaxAttempts
|
|
if attempts == 0 {
|
|
attempts = selected.MaxAttempts
|
|
}
|
|
if timeout > selected.TimeoutSeconds || attempts > selected.MaxAttempts {
|
|
return domain.RemoteAdapterResult{}, validationError("remote adapter timeout or retry exceeds declaration")
|
|
}
|
|
inputRef := request.InputRef
|
|
isSchemaProbe := request.Capability == domain.JobCapabilityRemoteRunDBSQLiteProbe && request.PlatformScheduled && request.SQLiteSchemaProbe != nil
|
|
if inputRef == "" && !isSchemaProbe {
|
|
inputRef = fmt.Sprintf("input://remote-adapters/%s/%s", instance.ID, request.DeclarationKey)
|
|
}
|
|
targetKey := request.TargetKey
|
|
executionInput := domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), RemoteAdapterKey: selected.Key, RemoteAdapterKind: string(selected.Kind), TimeoutSeconds: timeout, Inputs: domain.CopyStringMap(request.Inputs), SQLiteSchemaProbe: domain.CopySCUMSchemaProbeRequestPtr(request.SQLiteSchemaProbe)}
|
|
if isSchemaProbe {
|
|
targetKey = sqliteSchemaProbeRunTargetKey(request.TargetKey)
|
|
inputRef = ""
|
|
executionInput.RemoteAdapterKey = ""
|
|
executionInput.RemoteAdapterKind = ""
|
|
executionInput.Inputs = nil
|
|
}
|
|
job := domain.Job{
|
|
ID: jobIDFromParts("job-remote-adapter", instance.ID, request.IdempotencyKey),
|
|
ServerInstanceID: instance.ID,
|
|
RunEndpointID: instance.RunEndpointID,
|
|
Capability: request.Capability,
|
|
TargetKey: targetKey,
|
|
InputRef: inputRef,
|
|
IdempotencyKey: request.IdempotencyKey,
|
|
Progress: domain.JobProgress{Percent: 0, Message: "scoped remote adapter queued"},
|
|
RetryPolicy: domain.JobRetryPolicy{MaxAttempts: attempts, InitialBackoffSeconds: 2, MaxBackoffSeconds: 30},
|
|
ExecutionInput: executionInput,
|
|
}
|
|
created, err := svc.CreateJob(job)
|
|
if err != nil {
|
|
return domain.RemoteAdapterResult{}, err
|
|
}
|
|
user, err := svc.GetCurrentUser(sessionID)
|
|
if err != nil {
|
|
return domain.RemoteAdapterResult{}, err
|
|
}
|
|
auditID, err := svc.recordAuditEventWithID(user.ID, "remote-adapter.authorize", "server-instance", instance.ID, domain.AuditResultQueued, "authorized declared remote adapter target with bounded timeout and retry")
|
|
if err != nil {
|
|
return domain.RemoteAdapterResult{}, err
|
|
}
|
|
return domain.RemoteAdapterResult{RequestID: created.ID, ServerInstanceID: instance.ID, DeclarationKey: selected.Key, TargetKey: request.TargetKey, Kind: selected.Kind, Status: string(created.State), Retryable: attempts > 1, Message: "scoped remote adapter queued", ResultRef: "job://" + created.ID, AuditEventID: auditID}, nil
|
|
}
|
|
|
|
func sqliteSchemaProbeRunTargetKey(targetKey string) string {
|
|
trimmed := strings.TrimSpace(targetKey)
|
|
if strings.HasPrefix(trimmed, "databases/") {
|
|
return trimmed
|
|
}
|
|
return "databases/" + trimmed
|
|
}
|
|
|
|
func intersectRemoteCapabilities(profile []string, declared []string, endpoint []string) []string {
|
|
result := make([]string, 0, len(profile))
|
|
for _, capability := range profile {
|
|
if isRemoteAdapterCapability(capability) && containsString(declared, capability) && containsString(endpoint, capability) {
|
|
result = append(result, capability)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func isRemoteAdapterCapability(capability string) bool {
|
|
switch capability {
|
|
case domain.JobCapabilityRemoteFTPRead, domain.JobCapabilityRemoteFTPWrite,
|
|
domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite,
|
|
domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite,
|
|
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
|
|
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
|
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func remoteAdapterKind(kind string) domain.RemoteAdapterKind {
|
|
switch strings.ToLower(strings.TrimSpace(kind)) {
|
|
case "ftp":
|
|
return domain.RemoteAdapterFTP
|
|
case "rsync":
|
|
return domain.RemoteAdapterRsync
|
|
case "file":
|
|
return domain.RemoteAdapterRunFile
|
|
case "process":
|
|
return domain.RemoteAdapterRunProcess
|
|
case "sqlite", "mysql", "database":
|
|
return domain.RemoteAdapterDatabase
|
|
case "rcon":
|
|
return domain.RemoteAdapterRCON
|
|
default:
|
|
return domain.RemoteAdapterKind(strings.ToLower(strings.TrimSpace(kind)))
|
|
}
|
|
}
|
|
|
|
func remoteAdapterKindForCapability(capability string) domain.RemoteAdapterKind {
|
|
switch capability {
|
|
case domain.JobCapabilityRemoteFTPRead, domain.JobCapabilityRemoteFTPWrite:
|
|
return domain.RemoteAdapterFTP
|
|
case domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite:
|
|
return domain.RemoteAdapterRsync
|
|
case domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite:
|
|
return domain.RemoteAdapterRunFile
|
|
case domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop:
|
|
return domain.RemoteAdapterRunProcess
|
|
case domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery:
|
|
return domain.RemoteAdapterDatabase
|
|
case domain.JobCapabilityRemoteRunRCONCommand:
|
|
return domain.RemoteAdapterRCON
|
|
case domain.JobCapabilityRemoteRunLogsTransfer:
|
|
return domain.RemoteAdapterKind("log-transfer")
|
|
default:
|
|
return ""
|
|
}
|
|
}
|