feat: 完整游戏运维功能
This commit is contained in:
+150
-194
@@ -1,12 +1,8 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -17,36 +13,6 @@ import (
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
type generatedPackageConfig struct {
|
||||
Kind string `json:"kind"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
RunEndpointID string `json:"runEndpointId,omitempty"`
|
||||
ProfileKey string `json:"profileKey,omitempty"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
SecretRef string `json:"secretRef"`
|
||||
KeyGeneration int `json:"keyGeneration"`
|
||||
AuthKey string `json:"authKey"`
|
||||
}
|
||||
|
||||
type generatedClientManagerPackage struct {
|
||||
Kind string `json:"kind"`
|
||||
Checkout clientManagerCheckoutPlan `json:"checkout"`
|
||||
Config generatedPackageConfig `json:"config"`
|
||||
OutputArtifacts []string `json:"outputArtifacts"`
|
||||
BuildLogRef string `json:"buildLogRef"`
|
||||
KeyFingerprint string `json:"keyFingerprint"`
|
||||
}
|
||||
|
||||
type clientManagerCheckoutPlan struct {
|
||||
RepositoryURL string `json:"repositoryUrl"`
|
||||
SourceRevision string `json:"sourceRevision"`
|
||||
CheckoutRef string `json:"checkoutRef"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
}
|
||||
|
||||
func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, request domain.RunDistributionGenerateRequest) (domain.RunDistribution, error) {
|
||||
request = domain.CopyRunDistributionGenerateRequest(request)
|
||||
if strings.TrimSpace(request.IdempotencyKey) == "" {
|
||||
@@ -159,9 +125,6 @@ func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID st
|
||||
if strings.TrimSpace(request.IdempotencyKey) == "" {
|
||||
request.IdempotencyKey = "client-manager-" + request.ServerInstanceID + "-" + request.ProfileKey + "-" + request.TargetOS + "-" + request.TargetArch
|
||||
}
|
||||
if strings.TrimSpace(request.SourceRevision) == "" {
|
||||
request.SourceRevision = "main"
|
||||
}
|
||||
if err := validator.ValidateClientManagerBuildRequest(request); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
@@ -184,6 +147,18 @@ func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID st
|
||||
_ = svc.recordAuditEvent(user.ID, "client-manager.build.denied", "server-instance", instance.ID, domain.AuditResultDenied, "client-manager build denied: unsupported target")
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
profile, err := findRuntimeClientManagerProfile(plugin, request.ProfileKey)
|
||||
if err != nil {
|
||||
_ = svc.recordAuditEvent(user.ID, "client-manager.build.denied", "server-instance", instance.ID, domain.AuditResultDenied, "client-manager build denied: profile is not declared")
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if strings.TrimSpace(request.SourceRevision) == "" {
|
||||
request.SourceRevision = clientManagerProfileRevision(profile)
|
||||
}
|
||||
if !clientManagerProfileSupportsTarget(profile, request.TargetOS, request.TargetArch) || request.RepositoryURL != profile.RepositoryURL || !clientManagerProfileAllowsRevision(profile, request.SourceRevision) {
|
||||
_ = svc.recordAuditEvent(user.ID, "client-manager.build.denied", "server-instance", instance.ID, domain.AuditResultDenied, "client-manager build denied: repository, revision, or target is not declared")
|
||||
return domain.ClientManagerDistribution{}, validationError("client-manager build must match the declared profile repository, revision, and target")
|
||||
}
|
||||
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "client-manager.build.denied"); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
@@ -215,6 +190,7 @@ func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID st
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
ProfileKey: request.ProfileKey,
|
||||
Version: profile.Version,
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
RepositoryURL: request.RepositoryURL,
|
||||
@@ -246,6 +222,7 @@ func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID st
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
ProfileKey: request.ProfileKey,
|
||||
Version: profile.Version,
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
RepositoryURL: request.RepositoryURL,
|
||||
@@ -271,6 +248,9 @@ func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID st
|
||||
}
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if err := svc.ProjectClientManagerDistribution(distribution); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
ID: buildJobID,
|
||||
ServerInstanceID: instance.ID,
|
||||
@@ -288,6 +268,7 @@ func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID st
|
||||
distribution.UpdatedAt = buildJob.UpdatedAt
|
||||
_ = svc.store.ClientManagerBuildJobs().Update(buildJob)
|
||||
_ = svc.store.ClientManagerDistributions().Update(distribution)
|
||||
_ = svc.ProjectClientManagerDistribution(distribution)
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if job.ID != buildJobID || job.Capability != domain.JobCapabilityDistributionBuild {
|
||||
@@ -388,6 +369,11 @@ func (svc *CoreService) ResetComponentKeyForSession(sessionID string, request do
|
||||
if err := svc.revokeComponentDistributions(instance.ID, request.ComponentKind, normalizedComponentKey(request.ComponentKind, request.ComponentKey), nextGeneration); err != nil {
|
||||
return domain.EncryptedComponentKey{}, err
|
||||
}
|
||||
if request.ComponentKind == domain.DistributionComponentClientManager {
|
||||
if err := svc.fenceClientManagerAfterKeyReset(instance.ID, normalizedComponentKey(request.ComponentKind, request.ComponentKey), nextGeneration); err != nil {
|
||||
return domain.EncryptedComponentKey{}, err
|
||||
}
|
||||
}
|
||||
if err := svc.recordAuditEvent(user.ID, "runtime-key.reset", "server-instance", instance.ID, domain.AuditResultSuccess, "reset "+string(request.ComponentKind)+" key; previous packages revoked"); err != nil {
|
||||
return domain.EncryptedComponentKey{}, err
|
||||
}
|
||||
@@ -419,7 +405,7 @@ func (svc *CoreService) AuthenticateComponent(request domain.ComponentAuthentica
|
||||
_ = svc.recordAuditEvent("runtime", "runtime-key.auth", "server-instance", request.ServerInstanceID, domain.AuditResultDenied, "component authentication denied: stale generation")
|
||||
return domain.CopyComponentAuthenticationResult(result), nil
|
||||
}
|
||||
plainKey, err := decryptRuntimeKey(key.EncryptedKey)
|
||||
plainKey, err := svc.decryptRuntimeKey(key.EncryptedKey)
|
||||
if err != nil {
|
||||
return domain.ComponentAuthenticationResult{}, err
|
||||
}
|
||||
@@ -468,8 +454,7 @@ func (svc *CoreService) GetServerRuntimeActionsForSession(sessionID string, serv
|
||||
break
|
||||
}
|
||||
}
|
||||
bindingsComplete := svc.runtimeBindingsComplete(instance.ID)
|
||||
bindingReason := "runtime binding is incomplete"
|
||||
bindingsComplete, bindingReason := svc.runtimeBindingReadiness(instance.ID)
|
||||
actions := domain.ServerRuntimeActions{
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
@@ -489,6 +474,7 @@ func (svc *CoreService) GetServerRuntimeActionsForSession(sessionID string, serv
|
||||
runtimeAction("historical-logs", "Historical logs", endpointSupports(endpoint, domain.JobCapabilityLogsBackfill) && bindingsComplete, fallbackReason(!endpointSupports(endpoint, domain.JobCapabilityLogsBackfill), "run endpoint cannot backfill logs", bindingReason)),
|
||||
},
|
||||
}
|
||||
actions.Actions = append(actions.Actions, svc.clientManagerRuntimeActionProjection(instance, plugin, endpoint, bindingsComplete, bindingReason)...)
|
||||
return domain.CopyServerRuntimeActions(actions), nil
|
||||
}
|
||||
|
||||
@@ -500,11 +486,7 @@ func (svc *CoreService) PushRunUpdateForSession(sessionID string, request domain
|
||||
if err := validateRunUpdateRequest(request); err != nil {
|
||||
return domain.RunUpdateJob{}, err
|
||||
}
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.RunUpdateJob{}, err
|
||||
}
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID)
|
||||
user, instance, err := svc.requireServerOwner(sessionID, request.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.RunUpdateJob{}, err
|
||||
}
|
||||
@@ -533,6 +515,22 @@ func (svc *CoreService) PushRunUpdateForSession(sessionID string, request domain
|
||||
_ = svc.recordAuditEvent(user.ID, "run.update.denied", "server-instance", instance.ID, domain.AuditResultDenied, "run update denied: checksum mismatch")
|
||||
return domain.RunUpdateJob{}, validationError("checksum must match artifact")
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
return domain.RunUpdateJob{}, err
|
||||
}
|
||||
if endpoint.Platform == "" || endpoint.Architecture == "" {
|
||||
return domain.RunUpdateJob{}, validationError("Run endpoint target is not registered")
|
||||
}
|
||||
distributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: instance.ID, Status: domain.DistributionStatusAvailable})
|
||||
if err != nil {
|
||||
return domain.RunUpdateJob{}, err
|
||||
}
|
||||
distribution, err := findRunDistributionForArtifact(distributions, artifact.ID)
|
||||
if err != nil || distribution.RunEndpointID != endpoint.ID || distribution.TargetOS != endpoint.Platform || distribution.TargetArch != endpoint.Architecture || distribution.Checksum != artifact.Checksum || artifact.OwnerKind != domain.ArtifactOwnerKindJob || artifact.OwnerID != distribution.BuildJobID {
|
||||
_ = svc.recordAuditEvent(user.ID, "run.update.denied", "server-instance", instance.ID, domain.AuditResultDenied, "run update denied: artifact is not an approved target-matched Run distribution")
|
||||
return domain.RunUpdateJob{}, validationError("artifact must be an approved target-matched Run distribution")
|
||||
}
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
ID: jobIDFromParts("job-run-update", request.ServerInstanceID, request.IdempotencyKey),
|
||||
ServerInstanceID: instance.ID,
|
||||
@@ -554,9 +552,15 @@ func (svc *CoreService) PushRunUpdateForSession(sessionID string, request domain
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
ArtifactID: artifact.ID,
|
||||
Checksum: artifact.Checksum,
|
||||
TargetOS: distribution.TargetOS,
|
||||
TargetArch: distribution.TargetArch,
|
||||
TargetRelease: distribution.ID,
|
||||
PreviousVersion: endpoint.Version,
|
||||
JobID: job.ID,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
Status: domain.DistributionJobStatusQueued,
|
||||
Phase: domain.RunUpdatePhaseQueued,
|
||||
Message: "Run update queued",
|
||||
CreatedAt: stamp,
|
||||
UpdatedAt: stamp,
|
||||
}
|
||||
@@ -569,7 +573,7 @@ func (svc *CoreService) PushRunUpdateForSession(sessionID string, request domain
|
||||
if getErr != nil {
|
||||
return domain.RunUpdateJob{}, getErr
|
||||
}
|
||||
if !sameRunUpdateJob(existing, updateJob) {
|
||||
if !sameRunUpdateTarget(existing, updateJob) {
|
||||
return domain.RunUpdateJob{}, validationError("run update job already exists with different target")
|
||||
}
|
||||
return domain.CopyRunUpdateJob(existing), nil
|
||||
@@ -585,16 +589,16 @@ func (svc *CoreService) PushRunUpdateForSession(sessionID string, request domain
|
||||
func (svc *CoreService) QueueDependencyJobForSession(sessionID string, request domain.DependencyJobRequest) (domain.Job, error) {
|
||||
request = domain.CopyDependencyJobRequest(request)
|
||||
if strings.TrimSpace(request.IdempotencyKey) == "" {
|
||||
request.IdempotencyKey = "dependencies-" + request.ServerInstanceID + "-" + request.ProbeKey
|
||||
operation := "check"
|
||||
if request.Install {
|
||||
operation = "install-" + request.InstallPlanKey
|
||||
}
|
||||
request.IdempotencyKey = "dependencies-" + operation + "-" + request.ServerInstanceID + "-" + request.ProbeKey
|
||||
}
|
||||
if err := validateDependencyJobRequest(request); err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID)
|
||||
user, instance, err := svc.requireServerOwner(sessionID, request.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
@@ -609,6 +613,35 @@ func (svc *CoreService) QueueDependencyJobForSession(sessionID string, request d
|
||||
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "dependency.install.denied"); err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
resolution, err := svc.resolveDependencyContext(instance.ID)
|
||||
if err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
if request.TargetOS != "" && request.TargetOS != resolution.endpoint.Platform || request.TargetArch != "" && request.TargetArch != resolution.endpoint.Architecture {
|
||||
return domain.Job{}, validationError("dependency request target does not match Run endpoint")
|
||||
}
|
||||
request.TargetOS = resolution.endpoint.Platform
|
||||
request.TargetArch = resolution.endpoint.Architecture
|
||||
probe, err := declaredDependencyProbe(plugin, request.ProbeKey, request.TargetOS)
|
||||
if err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
var plan domain.RuntimeInstallPlan
|
||||
if request.Install {
|
||||
plan, err = declaredInstallPlan(plugin, request.InstallPlanKey, request.TargetOS)
|
||||
if err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
if !planTargetsProbe(plan, probe) {
|
||||
return domain.Job{}, validationError("install plan does not target requested dependency probe")
|
||||
}
|
||||
}
|
||||
expectedDigest := dependencyPlanDigest(resolution, probe, plan)
|
||||
if request.Install && request.PlanDigest != expectedDigest {
|
||||
_ = svc.recordAuditEvent(user.ID, "dependency.install.denied", "server-instance", instance.ID, domain.AuditResultDenied, "dependency install denied: reviewed plan digest is stale or missing")
|
||||
return domain.Job{}, validationError("planDigest must match the current reviewed install plan")
|
||||
}
|
||||
request.PlanDigest = expectedDigest
|
||||
capability := domain.JobCapabilityDependenciesCheck
|
||||
targetKey := "dependencies/" + request.ProbeKey
|
||||
message := "dependency check queued"
|
||||
@@ -634,7 +667,7 @@ func (svc *CoreService) QueueDependencyJobForSession(sessionID string, request d
|
||||
_ = svc.recordAuditEvent(user.ID, auditAction+".denied", "server-instance", instance.ID, domain.AuditResultDenied, "dependency operation denied: endpoint unsupported or offline")
|
||||
return domain.Job{}, err
|
||||
}
|
||||
if err := svc.upsertDependencyStatus(instance, request, state, "queued through platform job"); err != nil {
|
||||
if err := svc.upsertDependencyStatus(instance, request, job.ID, probe.Required, state, "queued through platform job"); err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
if err := svc.recordAuditEvent(user.ID, auditAction, "server-instance", instance.ID, domain.AuditResultQueued, message); err != nil {
|
||||
@@ -706,7 +739,7 @@ func (svc *CoreService) ensureActiveComponentKey(serverInstanceID string, kind d
|
||||
normalized := normalizedComponentKey(kind, componentKey)
|
||||
key, err := svc.activeComponentKey(serverInstanceID, kind, normalized)
|
||||
if err == nil {
|
||||
plainKey, err := decryptRuntimeKey(key.EncryptedKey)
|
||||
plainKey, err := svc.decryptRuntimeKey(key.EncryptedKey)
|
||||
return key, plainKey, err
|
||||
}
|
||||
if !errors.Is(err, repo.ErrNotFound) {
|
||||
@@ -742,7 +775,7 @@ func (svc *CoreService) createEncryptedComponentKey(serverInstanceID string, kin
|
||||
if err != nil {
|
||||
return domain.EncryptedComponentKey{}, "", err
|
||||
}
|
||||
encryptedKey, err := encryptRuntimeKey(plainKey)
|
||||
encryptedKey, err := svc.encryptRuntimeKey(plainKey)
|
||||
if err != nil {
|
||||
return domain.EncryptedComponentKey{}, "", err
|
||||
}
|
||||
@@ -894,9 +927,21 @@ func (svc *CoreService) ensureArtifactPayload(artifactID string, payload []byte,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if existingPayload, err := svc.artifactStore.GetPayload(artifactID); err == nil {
|
||||
if int64(len(existingPayload)) != artifact.SizeBytes || validator.BytesChecksum(existingPayload) != artifact.Checksum {
|
||||
return validationError("artifact payload does not match metadata")
|
||||
}
|
||||
svc.artifactPayloads[artifactID] = domain.CopyBytes(existingPayload)
|
||||
return nil
|
||||
} else if !errors.Is(err, repo.ErrNotFound) {
|
||||
return err
|
||||
}
|
||||
if int64(len(payload)) != artifact.SizeBytes || validator.BytesChecksum(payload) != artifact.Checksum {
|
||||
return validationError("artifact payload does not match metadata")
|
||||
}
|
||||
if err := svc.artifactStore.PutPayload(artifactID, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
svc.artifactPayloads[artifactID] = domain.CopyBytes(payload)
|
||||
return nil
|
||||
}
|
||||
@@ -905,6 +950,7 @@ func sameClientManagerBuildJobArtifacts(existing domain.ClientManagerBuildJob, e
|
||||
return existing.ServerInstanceID == expected.ServerInstanceID &&
|
||||
existing.PluginID == expected.PluginID &&
|
||||
existing.ProfileKey == expected.ProfileKey &&
|
||||
existing.Version == expected.Version &&
|
||||
existing.TargetOS == expected.TargetOS &&
|
||||
existing.TargetArch == expected.TargetArch &&
|
||||
existing.RepositoryURL == expected.RepositoryURL &&
|
||||
@@ -916,17 +962,7 @@ func sameClientManagerBuildJobArtifacts(existing domain.ClientManagerBuildJob, e
|
||||
existing.Status == expected.Status
|
||||
}
|
||||
|
||||
func sameRunUpdateJob(existing domain.RunUpdateJob, expected domain.RunUpdateJob) bool {
|
||||
return existing.ServerInstanceID == expected.ServerInstanceID &&
|
||||
existing.RunEndpointID == expected.RunEndpointID &&
|
||||
existing.ArtifactID == expected.ArtifactID &&
|
||||
existing.Checksum == expected.Checksum &&
|
||||
existing.JobID == expected.JobID &&
|
||||
existing.IdempotencyKey == expected.IdempotencyKey &&
|
||||
existing.Status == expected.Status
|
||||
}
|
||||
|
||||
func (svc *CoreService) upsertDependencyStatus(instance domain.ServerInstance, request domain.DependencyJobRequest, state domain.DependencyState, message string) error {
|
||||
func (svc *CoreService) upsertDependencyStatus(instance domain.ServerInstance, request domain.DependencyJobRequest, jobID string, required bool, state domain.DependencyState, message string) error {
|
||||
statusID := distributionID("dependency-status", instance.ID, request.ProbeKey)
|
||||
stamp := svc.now()
|
||||
status := domain.DependencyStatus{
|
||||
@@ -937,8 +973,10 @@ func (svc *CoreService) upsertDependencyStatus(instance domain.ServerInstance, r
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
State: state,
|
||||
Required: true,
|
||||
Required: required,
|
||||
InstallPlanKey: request.InstallPlanKey,
|
||||
PlanDigest: request.PlanDigest,
|
||||
JobID: jobID,
|
||||
Message: message,
|
||||
CheckedAt: stamp,
|
||||
UpdatedAt: stamp,
|
||||
@@ -955,25 +993,34 @@ func (svc *CoreService) upsertDependencyStatus(instance domain.ServerInstance, r
|
||||
}
|
||||
|
||||
func (svc *CoreService) recordAuditEvent(actorID string, action string, resourceKind string, resourceID string, result domain.AuditResult, summary string) error {
|
||||
_, err := svc.recordAuditEventWithID(actorID, action, resourceKind, resourceID, result, summary)
|
||||
return err
|
||||
}
|
||||
|
||||
func (svc *CoreService) recordAuditEventWithID(actorID string, action string, resourceKind string, resourceID string, result domain.AuditResult, summary string) (string, error) {
|
||||
svc.auditMu.Lock()
|
||||
svc.auditSeq++
|
||||
seq := svc.auditSeq
|
||||
svc.auditMu.Unlock()
|
||||
|
||||
stamp := svc.now()
|
||||
event := domain.AuditEvent{
|
||||
ID: fmt.Sprintf("audit-%s-%d", strings.ReplaceAll(action, ".", "-"), seq),
|
||||
ID: fmt.Sprintf("audit-%s-%d-%d", strings.ReplaceAll(action, ".", "-"), stamp.UnixNano(), seq),
|
||||
ActorID: actorID,
|
||||
Action: action,
|
||||
ResourceKind: resourceKind,
|
||||
ResourceID: resourceID,
|
||||
Result: result,
|
||||
Summary: safeBridgeReason(summary),
|
||||
CreatedAt: svc.now(),
|
||||
CreatedAt: stamp,
|
||||
}
|
||||
if err := validator.ValidateAuditEvent(event); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
return svc.store.AuditEvents().Create(event)
|
||||
if err := svc.store.AuditEvents().Create(event); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return event.ID, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) auditArtifactDownload(sessionID string, artifact domain.Artifact) error {
|
||||
@@ -1085,17 +1132,6 @@ func fingerprintForString(value string) string {
|
||||
return hex.EncodeToString(sum[:])[:12]
|
||||
}
|
||||
|
||||
func clientManagerCheckoutRef(repositoryURL string, sourceRevision string) string {
|
||||
sourceRevision = strings.TrimSpace(sourceRevision)
|
||||
if sourceRevision == "" {
|
||||
sourceRevision = "main"
|
||||
}
|
||||
if looksLikeCommitRevision(sourceRevision) {
|
||||
return "commit/" + sourceRevision
|
||||
}
|
||||
return "branch/" + sanitizeIDPart(sourceRevision)
|
||||
}
|
||||
|
||||
func clientManagerOutputName(profileKey string, targetOS string) string {
|
||||
name := sanitizeIDPart(profileKey)
|
||||
if targetOS == "windows" {
|
||||
@@ -1104,57 +1140,6 @@ func clientManagerOutputName(profileKey string, targetOS string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
func clientManagerBuildLog(checkout clientManagerCheckoutPlan, config generatedPackageConfig, outputs []string) string {
|
||||
lines := []string{
|
||||
"client-manager checkout prepared",
|
||||
"repository=" + checkout.RepositoryURL,
|
||||
"sourceRevision=" + checkout.SourceRevision,
|
||||
"checkoutRef=" + checkout.CheckoutRef,
|
||||
"target=" + checkout.TargetOS + "/" + checkout.TargetArch,
|
||||
"dependencyCheck=typed build profile accepted",
|
||||
"configInjection=secret ref " + config.SecretRef + " generation " + fmt.Sprintf("%d", config.KeyGeneration),
|
||||
"keyFingerprint=" + fingerprintForString(config.AuthKey),
|
||||
"outputs=" + strings.Join(outputs, ","),
|
||||
}
|
||||
return redactDistributionLog(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func looksLikeCommitRevision(value string) bool {
|
||||
if len(value) < 7 || len(value) > 64 {
|
||||
return false
|
||||
}
|
||||
for _, char := range value {
|
||||
if (char >= 'a' && char <= 'f') || (char >= 'A' && char <= 'F') || (char >= '0' && char <= '9') {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func redactDistributionLog(value string) string {
|
||||
replacements := []string{
|
||||
"/Users/", "[host]/",
|
||||
"password=", "password=[redacted]",
|
||||
"api_key=", "api_key=[redacted]",
|
||||
"secret=", "secret=[redacted]",
|
||||
"Bearer ", "Bearer [redacted] ",
|
||||
"sk-", "sk-[redacted]",
|
||||
"unix://", "socket://",
|
||||
"tcp://", "endpoint://",
|
||||
"mysql://", "db://",
|
||||
"sqlite://", "db://",
|
||||
}
|
||||
redacted := value
|
||||
for i := 0; i+1 < len(replacements); i += 2 {
|
||||
redacted = strings.ReplaceAll(redacted, replacements[i], replacements[i+1])
|
||||
}
|
||||
if len(redacted) > 4096 {
|
||||
return redacted[:4096]
|
||||
}
|
||||
return redacted
|
||||
}
|
||||
|
||||
func minInt(a int, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
@@ -1178,24 +1163,43 @@ func fallbackReason(primary bool, primaryReason string, fallback string) string
|
||||
}
|
||||
|
||||
func (svc *CoreService) runtimeBindingsComplete(serverInstanceID string) bool {
|
||||
bindings, err := svc.store.RuntimeBindings().List(domain.RuntimeBindingFilter{ServerInstanceID: serverInstanceID})
|
||||
complete, _ := svc.runtimeBindingReadiness(serverInstanceID)
|
||||
return complete
|
||||
}
|
||||
|
||||
func (svc *CoreService) runtimeBindingReadiness(serverInstanceID string) (bool, string) {
|
||||
binding, err := svc.runtimeBindingForServer(serverInstanceID)
|
||||
if errors.Is(err, repo.ErrNotFound) {
|
||||
return false, "runtime profile is not configured"
|
||||
}
|
||||
if err != nil {
|
||||
return false
|
||||
return false, "runtime binding cannot be verified"
|
||||
}
|
||||
for _, binding := range bindings {
|
||||
if binding.Status == domain.RuntimeBindingStatusIncomplete {
|
||||
return false
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(serverInstanceID)
|
||||
if err != nil || binding.PluginID != instance.PluginID || binding.PluginVersion != instance.PluginVersion {
|
||||
return false, "runtime binding does not match the server plugin"
|
||||
}
|
||||
return true
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return false, "runtime profile cannot be verified"
|
||||
}
|
||||
binding, err = normalizeRuntimeBinding(plugin, binding)
|
||||
if err != nil {
|
||||
return false, "runtime binding cannot be verified"
|
||||
}
|
||||
if binding.Status != domain.RuntimeBindingStatusComplete {
|
||||
return false, "missing logical bindings: " + strings.Join(binding.MissingKeys, ", ")
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (svc *CoreService) requireCompleteRuntimeBindings(actorID string, serverInstanceID string, deniedAction string) error {
|
||||
if svc.runtimeBindingsComplete(serverInstanceID) {
|
||||
complete, reason := svc.runtimeBindingReadiness(serverInstanceID)
|
||||
if complete {
|
||||
return nil
|
||||
}
|
||||
_ = svc.recordAuditEvent(actorID, deniedAction, "server-instance", serverInstanceID, domain.AuditResultDenied, "operation denied: runtime binding is incomplete")
|
||||
return ErrForbidden
|
||||
_ = svc.recordAuditEvent(actorID, deniedAction, "server-instance", serverInstanceID, domain.AuditResultDenied, "operation denied: "+reason)
|
||||
return validationError(reason)
|
||||
}
|
||||
|
||||
func pluginDeclares(plugin domain.GamePlugin, permission string) bool {
|
||||
@@ -1239,6 +1243,9 @@ func validateDependencyJobRequest(request domain.DependencyJobRequest) error {
|
||||
if request.Install && !safeDistributionKey(request.InstallPlanKey) {
|
||||
return validationError("installPlanKey is invalid")
|
||||
}
|
||||
if request.Install && (request.PlanDigest == "" || !strings.HasPrefix(request.PlanDigest, "sha256:") || len(request.PlanDigest) != len("sha256:")+64) {
|
||||
return validationError("planDigest must be a sha256 digest")
|
||||
}
|
||||
if containsUnsafeRequestText(request.IdempotencyKey) || containsUnsafeRequestText(request.TargetOS) || containsUnsafeRequestText(request.TargetArch) {
|
||||
return validationError("dependency request contains unsafe content")
|
||||
}
|
||||
@@ -1291,54 +1298,3 @@ func containsUnsafeRequestText(value string) bool {
|
||||
strings.Contains(lowered, "tcp://") ||
|
||||
strings.Contains(lowered, "/users/")
|
||||
}
|
||||
|
||||
func encryptRuntimeKey(plain string) (string, error) {
|
||||
key := runtimeEncryptionKey()
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
ciphertext := gcm.Seal(nil, nonce, []byte(plain), nil)
|
||||
return "enc:v1:" + base64.RawURLEncoding.EncodeToString(nonce) + ":" + base64.RawURLEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
func decryptRuntimeKey(encrypted string) (string, error) {
|
||||
parts := strings.Split(encrypted, ":")
|
||||
if len(parts) != 4 || parts[0] != "enc" || parts[1] != "v1" {
|
||||
return "", validationError("encrypted key format is invalid")
|
||||
}
|
||||
nonce, err := base64.RawURLEncoding.DecodeString(parts[2])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ciphertext, err := base64.RawURLEncoding.DecodeString(parts[3])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
key := runtimeEncryptionKey()
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
plain, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(plain), nil
|
||||
}
|
||||
|
||||
func runtimeEncryptionKey() [32]byte {
|
||||
return sha256.Sum256([]byte("browser.local/platform/runtime-component-key/v1"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user