feat: ship single-file run distribution and list key reset
Compile-time run auth replaces zip sidecars, lengthens run keys, revokes active sessions on reset, and exposes run-key reset in the server list.
This commit is contained in:
@@ -116,6 +116,7 @@ type DistributionBuildInput struct {
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
TargetRelease string
|
||||
PlatformURL string
|
||||
PackageFormat string
|
||||
RepositoryURL string
|
||||
SourceRevision string
|
||||
|
||||
@@ -153,6 +153,7 @@ type DistributionBuildInputResponse struct {
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
TargetRelease string `json:"targetRelease"`
|
||||
PlatformURL string `json:"platformUrl,omitempty"`
|
||||
PackageFormat string `json:"packageFormat"`
|
||||
RepositoryURL string `json:"repositoryUrl,omitempty"`
|
||||
SourceRevision string `json:"sourceRevision,omitempty"`
|
||||
@@ -468,6 +469,7 @@ func DistributionBuildInputFromDomain(input domain.DistributionBuildInput) Distr
|
||||
TargetOS: input.TargetOS,
|
||||
TargetArch: input.TargetArch,
|
||||
TargetRelease: input.TargetRelease,
|
||||
PlatformURL: input.PlatformURL,
|
||||
PackageFormat: input.PackageFormat,
|
||||
RepositoryURL: input.RepositoryURL,
|
||||
SourceRevision: input.SourceRevision,
|
||||
|
||||
@@ -250,6 +250,12 @@ func distributionPackageFilename(base string, targetOS string, targetArch string
|
||||
if arch != "" {
|
||||
name += "-" + arch
|
||||
}
|
||||
if format == "raw-executable" {
|
||||
if os == "windows" {
|
||||
return name + ".exe"
|
||||
}
|
||||
return name
|
||||
}
|
||||
if format == "" {
|
||||
format = "bin"
|
||||
}
|
||||
@@ -262,6 +268,8 @@ func packageContentType(packageFormat string) string {
|
||||
return "application/zip"
|
||||
case "tar.gz", "tgz":
|
||||
return "application/gzip"
|
||||
case "raw-executable":
|
||||
return "application/octet-stream"
|
||||
default:
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
@@ -210,6 +210,49 @@ func (svc *CoreService) currentRunSession(runEndpointID string, sessionToken str
|
||||
return domain.CopyRunControlSession(session), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) revokeRunControlSessionForInstance(instance domain.ServerInstance) error {
|
||||
if strings.TrimSpace(instance.RunEndpointID) == "" {
|
||||
return nil
|
||||
}
|
||||
svc.controlMu.Lock()
|
||||
defer svc.controlMu.Unlock()
|
||||
|
||||
session, err := svc.store.RunControlSessions().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repo.ErrNotFound) {
|
||||
delete(svc.runSessions, instance.RunEndpointID)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if session.Status == domain.AuthSessionStatusActive {
|
||||
stamp := svc.now()
|
||||
session.Status = domain.AuthSessionStatusRevoked
|
||||
session.RevokedAt = stamp
|
||||
session.UpdatedAt = stamp
|
||||
if err := validator.ValidateRunControlSession(session); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := svc.store.RunControlSessions().Update(session); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
delete(svc.runSessions, instance.RunEndpointID)
|
||||
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repo.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
endpoint.Status = domain.RunEndpointStatusOffline
|
||||
if err := validator.ValidateRunEndpoint(endpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.store.RunEndpoints().Update(endpoint)
|
||||
}
|
||||
|
||||
func runAuthenticationError(requireSigned bool) error {
|
||||
if !requireSigned {
|
||||
return validationError("sessionToken is invalid")
|
||||
|
||||
@@ -60,6 +60,7 @@ func (svc *CoreService) GetDistributionBuildInput(request domain.DistributionBui
|
||||
TargetOS: distribution.TargetOS,
|
||||
TargetArch: distribution.TargetArch,
|
||||
TargetRelease: distribution.ID,
|
||||
PlatformURL: runReleasePlatformURL(),
|
||||
PackageFormat: distribution.PackageFormat,
|
||||
ArtifactID: distribution.ArtifactID,
|
||||
OutputFilename: executableFilename("run", distribution.TargetOS),
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
@@ -73,7 +74,7 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
PackageFormat: packageFormatForTarget(request.TargetOS),
|
||||
PackageFormat: runPackageFormatForTarget(request.TargetOS),
|
||||
BuildJobID: buildJobID,
|
||||
ArtifactID: artifactID,
|
||||
KeyGeneration: key.Generation,
|
||||
@@ -374,6 +375,11 @@ func (svc *CoreService) ResetComponentKeyForSession(sessionID string, request do
|
||||
return domain.EncryptedComponentKey{}, err
|
||||
}
|
||||
}
|
||||
if request.ComponentKind == domain.DistributionComponentRun {
|
||||
if err := svc.revokeRunControlSessionForInstance(instance); 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
|
||||
}
|
||||
@@ -773,6 +779,9 @@ func (svc *CoreService) activeComponentKey(serverInstanceID string, kind domain.
|
||||
|
||||
func (svc *CoreService) createEncryptedComponentKey(serverInstanceID string, kind domain.DistributionComponentKind, componentKey string, generation int) (domain.EncryptedComponentKey, string, error) {
|
||||
plainKey, err := randomToken()
|
||||
if kind == domain.DistributionComponentRun {
|
||||
plainKey, err = randomRunComponentKey()
|
||||
}
|
||||
if err != nil {
|
||||
return domain.EncryptedComponentKey{}, "", err
|
||||
}
|
||||
@@ -1057,6 +1066,11 @@ func validatePluginTarget(plugin domain.GamePlugin, targetOS string) error {
|
||||
return validationError("targetOs is not declared by plugin")
|
||||
}
|
||||
|
||||
func runPackageFormatForTarget(targetOS string) string {
|
||||
_ = targetOS
|
||||
return "raw-executable"
|
||||
}
|
||||
|
||||
func packageFormatForTarget(targetOS string) string {
|
||||
if targetOS == "windows" {
|
||||
return "zip"
|
||||
@@ -1064,6 +1078,13 @@ func packageFormatForTarget(targetOS string) string {
|
||||
return "tar.gz"
|
||||
}
|
||||
|
||||
func runReleasePlatformURL() string {
|
||||
if value := strings.TrimSpace(os.Getenv("PLATFORM_RUN_RELEASE_URL")); value != "" {
|
||||
return value
|
||||
}
|
||||
return "https://scum.npc0.com"
|
||||
}
|
||||
|
||||
func distributionID(prefix string, parts ...interface{}) string {
|
||||
values := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing
|
||||
if err != nil {
|
||||
t.Fatalf("generate run distribution: %v", err)
|
||||
}
|
||||
if distribution.KeyGeneration != 1 || distribution.SecretRef == "" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" || distribution.Checksum != "" {
|
||||
if distribution.KeyGeneration != 1 || distribution.SecretRef == "" || distribution.PackageFormat != "raw-executable" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" || distribution.Checksum != "" {
|
||||
t.Fatalf("unexpected run distribution: %+v", distribution)
|
||||
}
|
||||
job, err := svc.GetJob(distribution.BuildJobID)
|
||||
@@ -62,6 +62,9 @@ func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing
|
||||
if config.AuthKey == "" || config.AuthKey == keys[0].EncryptedKey || strings.Contains(distribution.SecretRef, config.AuthKey) {
|
||||
t.Fatalf("run package key leaked through metadata or was not encrypted, config=%+v key=%+v distribution=%+v", config, keys[0], distribution)
|
||||
}
|
||||
if len(config.AuthKey) < 80 {
|
||||
t.Fatalf("expected longer run component key, got length %d", len(config.AuthKey))
|
||||
}
|
||||
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ComponentKind: domain.DistributionComponentRun,
|
||||
@@ -159,6 +162,7 @@ func TestCoreServiceRuntimeActionsGateDependenciesOnPluginPermission(t *testing.
|
||||
}
|
||||
|
||||
func TestCoreServiceDistributionBuildRejectsPrematureSuccessAndCanRetryAfterUpload(t *testing.T) {
|
||||
t.Setenv("PLATFORM_RUN_RELEASE_URL", "https://scum.npc0.com")
|
||||
svc, session, instance := newDistributionTestFixture(t)
|
||||
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
@@ -185,6 +189,19 @@ func TestCoreServiceDistributionBuildRejectsPrematureSuccessAndCanRetryAfterUplo
|
||||
if err != nil || !claim.HasJob || claim.Job.JobID != distribution.BuildJobID {
|
||||
t.Fatalf("claim distribution build job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
buildInput, err := svc.GetDistributionBuildInput(domain.DistributionBuildInputRequest{
|
||||
RunEndpointID: claim.Job.RunEndpointID,
|
||||
SessionToken: hello.SessionToken,
|
||||
JobID: claim.Job.JobID,
|
||||
LeaseToken: claim.Job.LeaseToken,
|
||||
Attempt: claim.Job.Attempt,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get distribution build input: %v", err)
|
||||
}
|
||||
if buildInput.PlatformURL != "https://scum.npc0.com" || buildInput.PackageFormat != "raw-executable" || buildInput.AuthKey == "" || len(buildInput.AuthKey) < 80 {
|
||||
t.Fatalf("expected raw executable build input with release URL and long key, got %+v", buildInput)
|
||||
}
|
||||
result := domain.RunJobResult{
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
SessionToken: hello.SessionToken,
|
||||
@@ -227,14 +244,14 @@ func TestCoreServiceRunDistributionDownloadUsesTargetPackageName(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("generate run distribution: %v", err)
|
||||
}
|
||||
payload := []byte("windows zipped run package")
|
||||
payload := []byte("windows raw run executable")
|
||||
completeDistributionBuild(t, svc, distribution, payload)
|
||||
|
||||
reference, err := svc.OpenArtifactDownloadForSession(session, domain.ArtifactDownloadReferenceRequest{ArtifactID: distribution.ArtifactID})
|
||||
if err != nil {
|
||||
t.Fatalf("open run artifact download: %v", err)
|
||||
}
|
||||
if reference.Filename != "run-windows-amd64.zip" || reference.ContentType != "application/zip" {
|
||||
if reference.Filename != "run-windows-amd64.exe" || reference.ContentType != "application/octet-stream" {
|
||||
t.Fatalf("expected windows run package metadata, got %+v", reference)
|
||||
}
|
||||
|
||||
@@ -275,7 +292,7 @@ func TestCoreServiceRunDistributionRetryReusesPartialArtifact(t *testing.T) {
|
||||
if distribution.ID != distributionID || distribution.ArtifactID == partialArtifact.ID || distribution.Checksum != "" {
|
||||
t.Fatalf("expected real binary build to ignore legacy config artifact, distribution=%+v artifact=%+v", distribution, partialArtifact)
|
||||
}
|
||||
if distribution.PackageFormat != "zip" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" {
|
||||
if distribution.PackageFormat != "raw-executable" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" {
|
||||
t.Fatalf("unexpected recovered distribution: %+v", distribution)
|
||||
}
|
||||
recoveredConfig := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
|
||||
@@ -332,6 +349,17 @@ func TestCoreServiceResetRunKeyRevokesOldPackagesAndRequiresRegeneration(t *test
|
||||
}
|
||||
oldConfig := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
|
||||
distribution = completeDistributionBuild(t, svc, distribution, []byte("compiled run archive before reset"))
|
||||
helloRequest := validRunControlHello()
|
||||
helloRequest.RunEndpointID = instance.RunEndpointID
|
||||
helloRequest.RegistrationToken = oldConfig.AuthKey
|
||||
helloRequest.ServerInstanceID = instance.ID
|
||||
helloRequest.PluginID = instance.PluginID
|
||||
helloRequest.ComponentKind = domain.DistributionComponentRun
|
||||
helloRequest.KeyGeneration = oldConfig.KeyGeneration
|
||||
hello, err := svc.RegisterRunHello(helloRequest)
|
||||
if err != nil || !hello.Accepted {
|
||||
t.Fatalf("register old run before reset: hello=%+v err=%v", hello, err)
|
||||
}
|
||||
|
||||
reset, err := svc.ResetComponentKeyForSession(session, domain.ComponentKeyResetRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
@@ -343,6 +371,27 @@ func TestCoreServiceResetRunKeyRevokesOldPackagesAndRequiresRegeneration(t *test
|
||||
if reset.Generation != 2 || reset.Status != domain.ComponentKeyStatusActive {
|
||||
t.Fatalf("expected reset key generation 2, got %+v", reset)
|
||||
}
|
||||
if _, err := svc.AcceptRunHeartbeat(domain.RunControlHeartbeat{
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
SessionToken: hello.SessionToken,
|
||||
Status: domain.RunEndpointStatusOnline,
|
||||
Version: "stale-run",
|
||||
Capacity: domain.RunCapacity{MaxJobs: 1},
|
||||
CapabilityFingerprint: helloRequest.CapabilityReport.Fingerprint,
|
||||
}); err == nil {
|
||||
t.Fatal("expected reset to revoke the active run control session")
|
||||
}
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
t.Fatalf("get endpoint after reset: %v", err)
|
||||
}
|
||||
if endpoint.Status != domain.RunEndpointStatusOffline {
|
||||
t.Fatalf("expected reset endpoint to be marked offline, got %+v", endpoint)
|
||||
}
|
||||
endpoint.Status = domain.RunEndpointStatusOnline
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatalf("restore endpoint for regeneration: %v", err)
|
||||
}
|
||||
oldDistribution, err := svc.store.RunDistributions().Get(distribution.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get old distribution: %v", err)
|
||||
|
||||
@@ -2401,6 +2401,14 @@ func randomToken() (string, error) {
|
||||
return base64.RawURLEncoding.EncodeToString(token), nil
|
||||
}
|
||||
|
||||
func randomRunComponentKey() (string, error) {
|
||||
token := make([]byte, 64)
|
||||
if _, err := rand.Read(token); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(token), nil
|
||||
}
|
||||
|
||||
func verifyPassword(hash string, password string) bool {
|
||||
parts := strings.Split(hash, "$")
|
||||
if len(parts) != 4 || parts[0] != "pbkdf2-sha256" {
|
||||
|
||||
@@ -110,7 +110,7 @@ func ValidateRunDistribution(distribution domain.RunDistribution) error {
|
||||
if distribution.KeyGeneration <= 0 {
|
||||
violations = append(violations, "keyGeneration must be positive")
|
||||
}
|
||||
if distribution.PackageFormat != "zip" && distribution.PackageFormat != "tar.gz" {
|
||||
if distribution.PackageFormat != "zip" && distribution.PackageFormat != "tar.gz" && distribution.PackageFormat != "raw-executable" {
|
||||
violations = append(violations, "packageFormat is invalid")
|
||||
}
|
||||
if !strings.HasPrefix(distribution.SecretRef, "secret://runtime-keys/") {
|
||||
|
||||
Reference in New Issue
Block a user