Move SCUM lifecycle ownership to plugin
This commit is contained in:
@@ -159,6 +159,7 @@ func ValidateGamePlugin(plugin domain.GamePlugin) error {
|
||||
violations = append(violations, validateGameClientBridgeManifest("gameClientBridge", plugin.GameClientBridge, plugin.DeclaredPermissions, plugin.Pages, plugin.RuntimeProfiles)...)
|
||||
violations = append(violations, validateMapTrajectoryDeclaration("mapTrajectories", plugin.MapTrajectories)...)
|
||||
violations = append(violations, validatePluginCreateFields("createFields", plugin.CreateFields)...)
|
||||
violations = append(violations, validatePluginAssetFiles("lifecycleAssets", plugin.LifecycleAssets)...)
|
||||
violations = append(violations, validateSafePluginStrings("gamePlugin", pluginSafeStrings(plugin))...)
|
||||
return finish(violations)
|
||||
}
|
||||
@@ -229,10 +230,87 @@ func ValidateGamePluginManifestRegistration(registration domain.GamePluginManife
|
||||
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("manifest.runtimeProfiles.logEvents", manifest.RuntimeProfiles, manifest.Permissions)...)
|
||||
violations = append(violations, validateGameClientBridgeManifest("manifest.gameClientBridge", manifest.GameClientBridge, manifest.Permissions, manifest.Pages, manifest.RuntimeProfiles)...)
|
||||
violations = append(violations, validateMapTrajectoryDeclaration("manifest.mapTrajectories", manifest.MapTrajectories)...)
|
||||
violations = append(violations, validatePluginAssetFileDeclarations("manifest.assetFiles", manifest.AssetFiles)...)
|
||||
violations = append(violations, validatePluginAssetFiles("assetFiles", registration.AssetFiles)...)
|
||||
violations = append(violations, validateRegistrationAssetCoverage(registration.Manifest.AssetFiles, registration.AssetFiles)...)
|
||||
violations = append(violations, validateSafePluginStrings("manifest", manifestSafeStrings(registration))...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func validatePluginAssetFileDeclarations(prefix string, files []domain.PluginAssetFile) []string {
|
||||
if len(files) > 64 {
|
||||
return []string{prefix + " has too many files"}
|
||||
}
|
||||
var violations []string
|
||||
seen := map[string]struct{}{}
|
||||
for i, file := range files {
|
||||
field := fmt.Sprintf("%s[%d]", prefix, i)
|
||||
if !validLogicalFileKey(file.Path) {
|
||||
violations = append(violations, field+".path is unsafe")
|
||||
}
|
||||
if _, exists := seen[file.Path]; exists {
|
||||
violations = append(violations, field+".path is duplicated")
|
||||
}
|
||||
seen[file.Path] = struct{}{}
|
||||
if file.Content != "" {
|
||||
violations = append(violations, field+".content must be supplied only in registration assetFiles")
|
||||
}
|
||||
if file.Mode != 0 && file.Mode != 0o600 && file.Mode != 0o700 {
|
||||
violations = append(violations, field+".mode is unsafe")
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validatePluginAssetFiles(prefix string, files []domain.PluginAssetFile) []string {
|
||||
if len(files) > 64 {
|
||||
return []string{prefix + " has too many files"}
|
||||
}
|
||||
var violations []string
|
||||
seen := map[string]struct{}{}
|
||||
for i, file := range files {
|
||||
field := fmt.Sprintf("%s[%d]", prefix, i)
|
||||
if !validLogicalFileKey(file.Path) {
|
||||
violations = append(violations, field+".path is unsafe")
|
||||
}
|
||||
if _, exists := seen[file.Path]; exists {
|
||||
violations = append(violations, field+".path is duplicated")
|
||||
}
|
||||
seen[file.Path] = struct{}{}
|
||||
if len([]byte(file.Content)) > 64*1024 || strings.ContainsRune(file.Content, '\x00') || containsUnsafeRuntimeSecret(file.Content) {
|
||||
violations = append(violations, field+".content is unsafe")
|
||||
}
|
||||
if file.Mode != 0 && (file.Mode < 0o400 || file.Mode > 0o700 || file.Mode&0o022 != 0) {
|
||||
violations = append(violations, field+".mode is unsafe")
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateRegistrationAssetCoverage(declared []domain.PluginAssetFile, payload []domain.PluginAssetFile) []string {
|
||||
if len(declared) == 0 {
|
||||
return nil
|
||||
}
|
||||
allowed := map[string]struct{}{}
|
||||
for _, file := range declared {
|
||||
allowed[file.Path] = struct{}{}
|
||||
}
|
||||
provided := map[string]struct{}{}
|
||||
for _, file := range payload {
|
||||
provided[file.Path] = struct{}{}
|
||||
if _, ok := allowed[file.Path]; !ok {
|
||||
return []string{"assetFiles contains undeclared plugin asset " + file.Path}
|
||||
}
|
||||
}
|
||||
var violations []string
|
||||
for _, file := range declared {
|
||||
if _, ok := provided[file.Path]; !ok {
|
||||
violations = append(violations, "assetFiles is missing declared plugin asset "+file.Path)
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateMapTrajectoryDeclaration(prefix string, value *domain.GameMapTrajectoryDeclaration) []string {
|
||||
if value == nil {
|
||||
return nil
|
||||
@@ -1711,6 +1789,9 @@ func pluginSafeStrings(plugin domain.GamePlugin) []fieldString {
|
||||
values = appendStringSliceFields(values, "remoteAccess.methods", plugin.RemoteAccess.Methods)
|
||||
values = appendStringSliceFields(values, "remoteAccess.runCapabilities", plugin.RemoteAccess.RunCapabilities)
|
||||
values = appendStringSliceFields(values, "remoteAccess.databaseEngines", plugin.RemoteAccess.DatabaseEngines)
|
||||
for i, file := range plugin.LifecycleAssets {
|
||||
values = append(values, fieldString{field: fmt.Sprintf("lifecycleAssets[%d].path", i), value: file.Path})
|
||||
}
|
||||
for i, page := range plugin.Pages {
|
||||
prefix := fmt.Sprintf("pages[%d]", i)
|
||||
values = append(values,
|
||||
@@ -1753,6 +1834,9 @@ func manifestSafeStrings(registration domain.GamePluginManifestRegistration) []f
|
||||
values = appendStringSliceFields(values, "remoteAccess.methods", manifest.RemoteAccess.Methods)
|
||||
values = appendStringSliceFields(values, "remoteAccess.runCapabilities", manifest.RemoteAccess.RunCapabilities)
|
||||
values = appendStringSliceFields(values, "remoteAccess.databaseEngines", manifest.RemoteAccess.DatabaseEngines)
|
||||
for i, file := range manifest.AssetFiles {
|
||||
values = append(values, fieldString{field: fmt.Sprintf("assetFiles[%d].path", i), value: file.Path})
|
||||
}
|
||||
for i, page := range manifest.Pages {
|
||||
prefix := fmt.Sprintf("pages[%d]", i)
|
||||
values = append(values,
|
||||
@@ -1953,7 +2037,7 @@ func validPluginRunCapability(capability string) bool {
|
||||
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand,
|
||||
domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProgram,
|
||||
domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall,
|
||||
domain.JobCapabilityDeploymentPlan, domain.JobCapabilitySCUMDeploymentPlan, domain.JobCapabilityDeploymentShellPosix, domain.JobCapabilityDeploymentShellPowerShell, domain.JobCapabilityDeploymentShellCmd,
|
||||
domain.JobCapabilityDeploymentPlan, domain.JobCapabilityDeploymentShellPosix, domain.JobCapabilityDeploymentShellPowerShell, domain.JobCapabilityDeploymentShellCmd,
|
||||
domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate,
|
||||
domain.JobCapabilityClientManagerRollback, domain.JobCapabilityClientManagerUninstall,
|
||||
"artifacts.read", "artifacts.write", "artifact.read", "artifact.write",
|
||||
|
||||
@@ -74,6 +74,39 @@ func TestValidateGamePluginManifestRegistrationRejectsUnsafeRequests(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGamePluginManifestRegistrationValidatesAssetFileCoverage(t *testing.T) {
|
||||
registration := validGamePluginManifestRegistration()
|
||||
registration.Manifest.AssetFiles = []domain.PluginAssetFile{
|
||||
{Path: "actions/install.json", Mode: 0o600},
|
||||
{Path: "bin/install-server", Mode: 0o700},
|
||||
}
|
||||
registration.AssetFiles = []domain.PluginAssetFile{
|
||||
{Path: "actions/install.json", Content: "{}", Mode: 0o600},
|
||||
{Path: "bin/install-server", Content: "#!/usr/bin/env sh\n", Mode: 0o700},
|
||||
}
|
||||
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
|
||||
t.Fatalf("expected declared asset files with matching content to validate, got %v", err)
|
||||
}
|
||||
|
||||
missing := domain.CopyGamePluginManifestRegistration(registration)
|
||||
missing.AssetFiles = missing.AssetFiles[:1]
|
||||
if err := ValidateGamePluginManifestRegistration(missing); err == nil || !strings.Contains(err.Error(), "missing declared plugin asset bin/install-server") {
|
||||
t.Fatalf("expected missing asset rejection, got %v", err)
|
||||
}
|
||||
|
||||
extra := domain.CopyGamePluginManifestRegistration(registration)
|
||||
extra.AssetFiles = append(extra.AssetFiles, domain.PluginAssetFile{Path: "bin/unregistered", Content: "unused"})
|
||||
if err := ValidateGamePluginManifestRegistration(extra); err == nil || !strings.Contains(err.Error(), "undeclared plugin asset bin/unregistered") {
|
||||
t.Fatalf("expected undeclared asset rejection, got %v", err)
|
||||
}
|
||||
|
||||
inlineContent := domain.CopyGamePluginManifestRegistration(registration)
|
||||
inlineContent.Manifest.AssetFiles[0].Content = "{}"
|
||||
if err := ValidateGamePluginManifestRegistration(inlineContent); err == nil || !strings.Contains(err.Error(), "content must be supplied only") {
|
||||
t.Fatalf("expected manifest inline content rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGamePluginManifestRegistrationValidatesRuntimeProfiles(t *testing.T) {
|
||||
t.Run("unsafe runtime value", func(t *testing.T) {
|
||||
registration := validGamePluginManifestRegistration()
|
||||
|
||||
Reference in New Issue
Block a user