Complete platform management workflows
This commit is contained in:
@@ -14,6 +14,16 @@ func ValidateRunControlHello(hello domain.RunControlHello) error {
|
||||
violations = appendRequired(violations, "displayName", hello.DisplayName)
|
||||
violations = appendRequired(violations, "version", hello.Version)
|
||||
violations = appendRequired(violations, "capabilityReport.fingerprint", hello.CapabilityReport.Fingerprint)
|
||||
if hello.ServerInstanceID != "" || hello.PluginID != "" || hello.ComponentKind != "" || hello.ComponentKey != "" || hello.KeyGeneration != 0 {
|
||||
violations = appendRequired(violations, "serverInstanceId", hello.ServerInstanceID)
|
||||
violations = appendRequired(violations, "pluginId", hello.PluginID)
|
||||
if hello.ComponentKind != domain.DistributionComponentRun && hello.ComponentKind != domain.DistributionComponentClientManager {
|
||||
violations = append(violations, "componentKind is invalid")
|
||||
}
|
||||
if hello.KeyGeneration <= 0 {
|
||||
violations = append(violations, "keyGeneration must be positive")
|
||||
}
|
||||
}
|
||||
if !validRunControlStatus(hello.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
const maxDistributionMessageLength = 256
|
||||
|
||||
func ValidateRuntimeBinding(binding domain.RuntimeBinding) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", binding.ID)
|
||||
violations = appendRequired(violations, "serverInstanceId", binding.ServerInstanceID)
|
||||
violations = appendRequired(violations, "pluginId", binding.PluginID)
|
||||
violations = appendRequired(violations, "profileKey", binding.ProfileKey)
|
||||
violations = appendRequired(violations, "mode", binding.Mode)
|
||||
if !validRuntimeBindingStatus(binding.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
for key, value := range binding.Bindings {
|
||||
if !validDistributionLogicalKey(key) {
|
||||
violations = append(violations, "bindings key is invalid")
|
||||
}
|
||||
if containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || strings.Contains(strings.ToLower(value), "://") && !strings.HasPrefix(value, "secret://") {
|
||||
violations = append(violations, "bindings."+key+" must use safe logical or secret refs")
|
||||
}
|
||||
}
|
||||
for i, key := range binding.MissingKeys {
|
||||
if !validDistributionLogicalKey(key) {
|
||||
violations = append(violations, fmt.Sprintf("missingKeys[%d] is invalid", i))
|
||||
}
|
||||
}
|
||||
if binding.CreatedAt.IsZero() {
|
||||
violations = append(violations, "createdAt is required")
|
||||
}
|
||||
if binding.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "updatedAt is required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateEncryptedComponentKey(key domain.EncryptedComponentKey) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", key.ID)
|
||||
violations = appendRequired(violations, "serverInstanceId", key.ServerInstanceID)
|
||||
violations = appendRequired(violations, "encryptedKey", key.EncryptedKey)
|
||||
violations = appendRequired(violations, "keyHash", key.KeyHash)
|
||||
violations = appendRequired(violations, "fingerprint", key.Fingerprint)
|
||||
violations = appendRequired(violations, "secretRef", key.SecretRef)
|
||||
if !validDistributionComponentKind(key.ComponentKind) {
|
||||
violations = append(violations, "componentKind is invalid")
|
||||
}
|
||||
if key.ComponentKind == domain.DistributionComponentClientManager && strings.TrimSpace(key.ComponentKey) == "" {
|
||||
violations = append(violations, "componentKey is required for client-manager keys")
|
||||
}
|
||||
if key.ComponentKey != "" && !validDistributionLogicalKey(key.ComponentKey) {
|
||||
violations = append(violations, "componentKey is invalid")
|
||||
}
|
||||
if !strings.HasPrefix(key.EncryptedKey, "enc:v1:") {
|
||||
violations = append(violations, "encryptedKey must be encrypted")
|
||||
}
|
||||
if key.KeyHash != "" && !validSHA256Checksum(key.KeyHash) {
|
||||
violations = append(violations, "keyHash must be sha256:<hex>")
|
||||
}
|
||||
if !strings.HasPrefix(key.SecretRef, "secret://runtime-keys/") {
|
||||
violations = append(violations, "secretRef must be a runtime key secret ref")
|
||||
}
|
||||
if key.Generation <= 0 {
|
||||
violations = append(violations, "generation must be positive")
|
||||
}
|
||||
if !validComponentKeyStatus(key.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if key.CreatedAt.IsZero() {
|
||||
violations = append(violations, "createdAt is required")
|
||||
}
|
||||
if key.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "updatedAt is required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateRunDistribution(distribution domain.RunDistribution) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", distribution.ID)
|
||||
violations = appendRequired(violations, "serverInstanceId", distribution.ServerInstanceID)
|
||||
violations = appendRequired(violations, "pluginId", distribution.PluginID)
|
||||
violations = appendRequired(violations, "runEndpointId", distribution.RunEndpointID)
|
||||
violations = appendRequired(violations, "targetOs", distribution.TargetOS)
|
||||
violations = appendRequired(violations, "targetArch", distribution.TargetArch)
|
||||
violations = appendRequired(violations, "packageFormat", distribution.PackageFormat)
|
||||
violations = appendRequired(violations, "artifactId", distribution.ArtifactID)
|
||||
violations = appendRequired(violations, "checksum", distribution.Checksum)
|
||||
violations = appendRequired(violations, "secretRef", distribution.SecretRef)
|
||||
violations = appendDistributionTargetViolations(violations, distribution.TargetOS, distribution.TargetArch)
|
||||
violations = appendDistributionStatusViolations(violations, distribution.Status)
|
||||
if distribution.Checksum != "" && !validSHA256Checksum(distribution.Checksum) {
|
||||
violations = append(violations, "checksum must be sha256:<hex>")
|
||||
}
|
||||
if distribution.KeyGeneration <= 0 {
|
||||
violations = append(violations, "keyGeneration must be positive")
|
||||
}
|
||||
if distribution.PackageFormat != "zip" && distribution.PackageFormat != "tar.gz" {
|
||||
violations = append(violations, "packageFormat is invalid")
|
||||
}
|
||||
if !strings.HasPrefix(distribution.SecretRef, "secret://runtime-keys/") {
|
||||
violations = append(violations, "secretRef must be redacted runtime key ref")
|
||||
}
|
||||
if distribution.CreatedAt.IsZero() {
|
||||
violations = append(violations, "createdAt is required")
|
||||
}
|
||||
if distribution.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "updatedAt is required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerDistribution(distribution domain.ClientManagerDistribution) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", distribution.ID)
|
||||
violations = appendRequired(violations, "serverInstanceId", distribution.ServerInstanceID)
|
||||
violations = appendRequired(violations, "pluginId", distribution.PluginID)
|
||||
violations = appendRequired(violations, "profileKey", distribution.ProfileKey)
|
||||
violations = appendRequired(violations, "repositoryUrl", distribution.RepositoryURL)
|
||||
violations = appendRequired(violations, "sourceRevision", distribution.SourceRevision)
|
||||
violations = appendRequired(violations, "buildJobId", distribution.BuildJobID)
|
||||
violations = appendRequired(violations, "artifactId", distribution.ArtifactID)
|
||||
violations = appendRequired(violations, "checksum", distribution.Checksum)
|
||||
violations = appendRequired(violations, "secretRef", distribution.SecretRef)
|
||||
violations = appendDistributionTargetViolations(violations, distribution.TargetOS, distribution.TargetArch)
|
||||
violations = appendDistributionStatusViolations(violations, distribution.Status)
|
||||
if !validDistributionLogicalKey(distribution.ProfileKey) {
|
||||
violations = append(violations, "profileKey is invalid")
|
||||
}
|
||||
if distribution.Checksum != "" && !validSHA256Checksum(distribution.Checksum) {
|
||||
violations = append(violations, "checksum must be sha256:<hex>")
|
||||
}
|
||||
if distribution.KeyGeneration <= 0 {
|
||||
violations = append(violations, "keyGeneration must be positive")
|
||||
}
|
||||
violations = append(violations, validateRepositoryURL("repositoryUrl", distribution.RepositoryURL)...)
|
||||
if !strings.HasPrefix(distribution.SecretRef, "secret://runtime-keys/") {
|
||||
violations = append(violations, "secretRef must be redacted runtime key ref")
|
||||
}
|
||||
if distribution.CreatedAt.IsZero() {
|
||||
violations = append(violations, "createdAt is required")
|
||||
}
|
||||
if distribution.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "updatedAt is required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateDependencyStatus(status domain.DependencyStatus) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", status.ID)
|
||||
violations = appendRequired(violations, "serverInstanceId", status.ServerInstanceID)
|
||||
violations = appendRequired(violations, "pluginId", status.PluginID)
|
||||
violations = appendRequired(violations, "probeKey", status.ProbeKey)
|
||||
if !validDistributionLogicalKey(status.ProbeKey) {
|
||||
violations = append(violations, "probeKey is invalid")
|
||||
}
|
||||
if status.TargetOS != "" || status.TargetArch != "" {
|
||||
violations = appendDistributionTargetViolations(violations, status.TargetOS, status.TargetArch)
|
||||
}
|
||||
if !validDependencyState(status.State) {
|
||||
violations = append(violations, "state is invalid")
|
||||
}
|
||||
if status.InstallPlanKey != "" && !validDistributionLogicalKey(status.InstallPlanKey) {
|
||||
violations = append(violations, "installPlanKey is invalid")
|
||||
}
|
||||
if len(status.Message) > maxDistributionMessageLength || containsUnsafeRuntimeSecret(status.Message) || looksLikeRawHostPath(status.Message) {
|
||||
violations = append(violations, "message is unsafe or too long")
|
||||
}
|
||||
if status.CheckedAt.IsZero() {
|
||||
violations = append(violations, "checkedAt is required")
|
||||
}
|
||||
if status.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "updatedAt is required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerBuildJob(job domain.ClientManagerBuildJob) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", job.ID)
|
||||
violations = appendRequired(violations, "serverInstanceId", job.ServerInstanceID)
|
||||
violations = appendRequired(violations, "pluginId", job.PluginID)
|
||||
violations = appendRequired(violations, "profileKey", job.ProfileKey)
|
||||
violations = appendRequired(violations, "repositoryUrl", job.RepositoryURL)
|
||||
violations = appendRequired(violations, "sourceRevision", job.SourceRevision)
|
||||
violations = appendDistributionTargetViolations(violations, job.TargetOS, job.TargetArch)
|
||||
if !validDistributionLogicalKey(job.ProfileKey) {
|
||||
violations = append(violations, "profileKey is invalid")
|
||||
}
|
||||
if job.Checksum != "" && !validSHA256Checksum(job.Checksum) {
|
||||
violations = append(violations, "checksum must be sha256:<hex>")
|
||||
}
|
||||
if job.LogsRef != "" && !validScopedInputRef(job.LogsRef) {
|
||||
violations = append(violations, "logsRef is not allowed")
|
||||
}
|
||||
if job.KeyGeneration < 0 {
|
||||
violations = append(violations, "keyGeneration must not be negative")
|
||||
}
|
||||
if !validDistributionJobStatus(job.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
violations = append(violations, validateRepositoryURL("repositoryUrl", job.RepositoryURL)...)
|
||||
if job.CreatedAt.IsZero() {
|
||||
violations = append(violations, "createdAt is required")
|
||||
}
|
||||
if job.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "updatedAt is required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateRunUpdateJob(job domain.RunUpdateJob) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", job.ID)
|
||||
violations = appendRequired(violations, "serverInstanceId", job.ServerInstanceID)
|
||||
violations = appendRequired(violations, "runEndpointId", job.RunEndpointID)
|
||||
violations = appendRequired(violations, "artifactId", job.ArtifactID)
|
||||
violations = appendRequired(violations, "checksum", job.Checksum)
|
||||
violations = appendRequired(violations, "idempotencyKey", job.IdempotencyKey)
|
||||
if job.Checksum != "" && !validSHA256Checksum(job.Checksum) {
|
||||
violations = append(violations, "checksum must be sha256:<hex>")
|
||||
}
|
||||
if !validDistributionJobStatus(job.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if containsUnsafeRuntimeSecret(job.IdempotencyKey) || looksLikeRawHostPath(job.IdempotencyKey) {
|
||||
violations = append(violations, "idempotencyKey is unsafe")
|
||||
}
|
||||
if job.CreatedAt.IsZero() {
|
||||
violations = append(violations, "createdAt is required")
|
||||
}
|
||||
if job.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "updatedAt is required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateRunDistributionGenerateRequest(request domain.RunDistributionGenerateRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
|
||||
violations = appendRequired(violations, "targetOs", request.TargetOS)
|
||||
violations = appendRequired(violations, "targetArch", request.TargetArch)
|
||||
violations = appendRequired(violations, "idempotencyKey", request.IdempotencyKey)
|
||||
violations = appendDistributionTargetViolations(violations, request.TargetOS, request.TargetArch)
|
||||
if containsUnsafeRuntimeSecret(request.IdempotencyKey) || looksLikeRawHostPath(request.IdempotencyKey) {
|
||||
violations = append(violations, "idempotencyKey is unsafe")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerBuildRequest(request domain.ClientManagerBuildRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
|
||||
violations = appendRequired(violations, "profileKey", request.ProfileKey)
|
||||
violations = appendRequired(violations, "targetOs", request.TargetOS)
|
||||
violations = appendRequired(violations, "targetArch", request.TargetArch)
|
||||
violations = appendRequired(violations, "repositoryUrl", request.RepositoryURL)
|
||||
violations = appendRequired(violations, "idempotencyKey", request.IdempotencyKey)
|
||||
violations = appendDistributionTargetViolations(violations, request.TargetOS, request.TargetArch)
|
||||
if !validDistributionLogicalKey(request.ProfileKey) {
|
||||
violations = append(violations, "profileKey is invalid")
|
||||
}
|
||||
if request.SourceRevision != "" && !validDistributionLogicalKey(request.SourceRevision) {
|
||||
violations = append(violations, "sourceRevision is invalid")
|
||||
}
|
||||
if containsUnsafeRuntimeSecret(request.IdempotencyKey) || looksLikeRawHostPath(request.IdempotencyKey) {
|
||||
violations = append(violations, "idempotencyKey is unsafe")
|
||||
}
|
||||
violations = append(violations, validateRepositoryURL("repositoryUrl", request.RepositoryURL)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateComponentKeyResetRequest(request domain.ComponentKeyResetRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
|
||||
if !validDistributionComponentKind(request.ComponentKind) {
|
||||
violations = append(violations, "componentKind is invalid")
|
||||
}
|
||||
if request.ComponentKind == domain.DistributionComponentClientManager && strings.TrimSpace(request.ComponentKey) == "" {
|
||||
violations = append(violations, "componentKey is required for client-manager")
|
||||
}
|
||||
if request.ComponentKey != "" && !validDistributionLogicalKey(request.ComponentKey) {
|
||||
violations = append(violations, "componentKey is invalid")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateComponentAuthenticationRequest(request domain.ComponentAuthenticationRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
|
||||
violations = appendRequired(violations, "key", request.Key)
|
||||
if !validDistributionComponentKind(request.ComponentKind) {
|
||||
violations = append(violations, "componentKind is invalid")
|
||||
}
|
||||
if request.ComponentKind == domain.DistributionComponentClientManager && strings.TrimSpace(request.ComponentKey) == "" {
|
||||
violations = append(violations, "componentKey is required for client-manager")
|
||||
}
|
||||
if request.ComponentKey != "" && !validDistributionLogicalKey(request.ComponentKey) {
|
||||
violations = append(violations, "componentKey is invalid")
|
||||
}
|
||||
if request.Generation <= 0 {
|
||||
violations = append(violations, "generation must be positive")
|
||||
}
|
||||
if len(request.Key) > 256 || looksLikeRawHostPath(request.Key) || strings.Contains(strings.ToLower(request.Key), "://") {
|
||||
violations = append(violations, "key is unsafe")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func appendDistributionTargetViolations(violations []string, targetOS string, targetArch string) []string {
|
||||
if !validDistributionTargetOS(targetOS) {
|
||||
violations = append(violations, "targetOs is invalid")
|
||||
}
|
||||
if !validDistributionTargetArch(targetArch) {
|
||||
violations = append(violations, "targetArch is invalid")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func appendDistributionStatusViolations(violations []string, status domain.DistributionStatus) []string {
|
||||
if !validDistributionStatus(status) {
|
||||
return append(violations, "status is invalid")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateRepositoryURL(field string, value string) []string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
lowered := strings.ToLower(strings.TrimSpace(value))
|
||||
if !strings.HasPrefix(lowered, "https://") || !strings.HasSuffix(lowered, ".git") {
|
||||
return []string{field + " must be an HTTPS git repository URL"}
|
||||
}
|
||||
for _, reason := range unsafePluginStringReasons(value) {
|
||||
return []string{field + ": " + reason}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validDistributionComponentKind(kind domain.DistributionComponentKind) bool {
|
||||
switch kind {
|
||||
case domain.DistributionComponentRun, domain.DistributionComponentClientManager:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validComponentKeyStatus(status domain.ComponentKeyStatus) bool {
|
||||
switch status {
|
||||
case domain.ComponentKeyStatusActive, domain.ComponentKeyStatusRevoked:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validDistributionStatus(status domain.DistributionStatus) bool {
|
||||
switch status {
|
||||
case domain.DistributionStatusAvailable, domain.DistributionStatusRevoked, domain.DistributionStatusBuilding, domain.DistributionStatusFailed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validRuntimeBindingStatus(status domain.RuntimeBindingStatus) bool {
|
||||
switch status {
|
||||
case domain.RuntimeBindingStatusComplete, domain.RuntimeBindingStatusIncomplete:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validDependencyState(state domain.DependencyState) bool {
|
||||
switch state {
|
||||
case domain.DependencyStateUnknown, domain.DependencyStatePresent, domain.DependencyStateMissing, domain.DependencyStateInstalling, domain.DependencyStateFailed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validDistributionJobStatus(status domain.DistributionJobStatus) bool {
|
||||
switch status {
|
||||
case domain.DistributionJobStatusQueued, domain.DistributionJobStatusRunning, domain.DistributionJobStatusSucceeded, domain.DistributionJobStatusFailed, domain.DistributionJobStatusDenied:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validDistributionTargetOS(targetOS string) bool {
|
||||
switch targetOS {
|
||||
case "linux", "windows", "darwin":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validDistributionTargetArch(targetArch string) bool {
|
||||
switch targetArch {
|
||||
case "amd64", "arm64":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validDistributionLogicalKey(value string) bool {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" || trimmed != value || len([]rune(value)) > 96 {
|
||||
return false
|
||||
}
|
||||
for _, char := range value {
|
||||
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' || char == '/' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return !strings.Contains(value, "..") && !strings.Contains(value, "://") && !looksLikeRawHostPath(value) && !containsUnsafeRuntimeSecret(value)
|
||||
}
|
||||
@@ -145,6 +145,7 @@ func ValidateGamePlugin(plugin domain.GamePlugin) error {
|
||||
violations = append(violations, validatePluginPages(plugin.Pages)...)
|
||||
violations = append(violations, duplicateViolations("tags", plugin.Tags)...)
|
||||
violations = append(violations, validateAIPurposes(plugin.AIPurposes)...)
|
||||
violations = append(violations, validateRemoteAccess("remoteAccess", plugin.RemoteAccess, plugin.RequiredRunCapabilities)...)
|
||||
violations = append(violations, validateSafePluginStrings("gamePlugin", pluginSafeStrings(plugin))...)
|
||||
return finish(violations)
|
||||
}
|
||||
@@ -196,6 +197,7 @@ func ValidateGamePluginManifestRegistration(registration domain.GamePluginManife
|
||||
violations = append(violations, validatePluginPages(manifest.Pages)...)
|
||||
violations = append(violations, duplicateViolations("manifest.tags", manifest.Tags)...)
|
||||
violations = append(violations, validateAIPurposes(manifest.AI.Purposes)...)
|
||||
violations = append(violations, validateRemoteAccess("manifest.remoteAccess", manifest.RemoteAccess, manifest.Capabilities)...)
|
||||
violations = append(violations, validateSafePluginStrings("manifest", manifestSafeStrings(registration))...)
|
||||
return finish(violations)
|
||||
}
|
||||
@@ -348,6 +350,7 @@ func validatePluginMarketplacePlugin(prefix string, plugin domain.PluginMarketpl
|
||||
violations = append(violations, validatePluginPages(plugin.Pages)...)
|
||||
violations = append(violations, duplicateViolations(prefix+".tags", plugin.Tags)...)
|
||||
violations = append(violations, validateAIPurposes(plugin.AIPurposes)...)
|
||||
violations = append(violations, validateRemoteAccess(prefix+".remoteAccess", plugin.RemoteAccess, plugin.Capabilities)...)
|
||||
violations = append(violations, validateSafePluginStrings(prefix, marketplacePluginSafeStrings(plugin))...)
|
||||
return violations
|
||||
}
|
||||
@@ -401,6 +404,14 @@ func AuthorizePluginBridgeAction(plugin domain.GamePlugin, request domain.Plugin
|
||||
}
|
||||
|
||||
func ValidateServerInstance(instance domain.ServerInstance) error {
|
||||
return validateServerInstance(instance, false)
|
||||
}
|
||||
|
||||
func ValidateStoredServerInstance(instance domain.ServerInstance) error {
|
||||
return validateServerInstance(instance, true)
|
||||
}
|
||||
|
||||
func validateServerInstance(instance domain.ServerInstance, allowDeleted bool) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", instance.ID)
|
||||
violations = appendRequired(violations, "pluginId", instance.PluginID)
|
||||
@@ -425,7 +436,7 @@ func ValidateServerInstance(instance domain.ServerInstance) error {
|
||||
if !validServerInstanceState(instance.State) {
|
||||
violations = append(violations, "state is invalid")
|
||||
}
|
||||
if instance.State == domain.ServerInstanceStateDeleted {
|
||||
if instance.State == domain.ServerInstanceStateDeleted && !allowDeleted {
|
||||
violations = append(violations, "state must not be deleted on create")
|
||||
}
|
||||
if instance.ConfigVersion < 0 {
|
||||
@@ -434,6 +445,20 @@ func ValidateServerInstance(instance domain.ServerInstance) error {
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateServerInstanceUpdate(update domain.ServerInstanceUpdate) error {
|
||||
var violations []string
|
||||
if update.Name != nil {
|
||||
name := strings.TrimSpace(*update.Name)
|
||||
if name == "" {
|
||||
violations = append(violations, "name is required")
|
||||
}
|
||||
if name != *update.Name {
|
||||
violations = append(violations, "name must not have surrounding whitespace")
|
||||
}
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateServerInstanceDependencies(instance domain.ServerInstance, plugin domain.GamePlugin, endpoint domain.RunEndpoint) error {
|
||||
var violations []string
|
||||
if plugin.ID == "" {
|
||||
@@ -698,6 +723,17 @@ func ValidateJob(job domain.Job) error {
|
||||
violations = append(violations, "inputRef is required for scoped write jobs")
|
||||
}
|
||||
}
|
||||
if isRemoteRunCapability(job.Capability) {
|
||||
if job.ServerInstanceID == "" {
|
||||
violations = append(violations, "serverInstanceId is required for remote access jobs")
|
||||
}
|
||||
if remoteCapabilityRequiresTargetKey(job.Capability) && job.TargetKey == "" {
|
||||
violations = append(violations, "targetKey is required for remote access jobs")
|
||||
}
|
||||
if remoteCapabilityRequiresInputRef(job.Capability) && job.InputRef == "" {
|
||||
violations = append(violations, "inputRef is required for remote access jobs")
|
||||
}
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
@@ -877,6 +913,63 @@ func validateAIPurposes(purposes []string) []string {
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateRemoteAccess(field string, remote domain.GamePluginRemoteAccess, declaredCapabilities []string) []string {
|
||||
var violations []string
|
||||
if len(remote.Methods) == 0 && len(remote.RunCapabilities) == 0 && len(remote.DatabaseEngines) == 0 && !remote.RCON && !remote.LogTransfer {
|
||||
return violations
|
||||
}
|
||||
if len(remote.Methods) == 0 {
|
||||
violations = append(violations, field+".methods must not be empty when remote access is declared")
|
||||
}
|
||||
for i, method := range remote.Methods {
|
||||
if !validRemoteAccessMethod(method) {
|
||||
violations = append(violations, fmt.Sprintf("%s.methods[%d] is not allowed", field, i))
|
||||
}
|
||||
}
|
||||
violations = append(violations, duplicateViolations(field+".methods", remote.Methods)...)
|
||||
for i, capability := range remote.RunCapabilities {
|
||||
if !validPluginRunCapability(capability) || !isRemoteRunCapability(capability) {
|
||||
violations = append(violations, fmt.Sprintf("%s.runCapabilities[%d] is not allowed", field, i))
|
||||
continue
|
||||
}
|
||||
if !containsString(declaredCapabilities, capability) {
|
||||
violations = append(violations, fmt.Sprintf("%s.runCapabilities[%d] must also be declared in capabilities", field, i))
|
||||
}
|
||||
}
|
||||
violations = append(violations, duplicateViolations(field+".runCapabilities", remote.RunCapabilities)...)
|
||||
for i, engine := range remote.DatabaseEngines {
|
||||
if !validRemoteDatabaseEngine(engine) {
|
||||
violations = append(violations, fmt.Sprintf("%s.databaseEngines[%d] is not allowed", field, i))
|
||||
}
|
||||
}
|
||||
violations = append(violations, duplicateViolations(field+".databaseEngines", remote.DatabaseEngines)...)
|
||||
if containsString(remote.Methods, "run") && len(remote.RunCapabilities) == 0 {
|
||||
violations = append(violations, field+".runCapabilities must not be empty when run access is declared")
|
||||
}
|
||||
if containsString(remote.Methods, "ftp") && !containsAny(declaredCapabilities, []string{domain.JobCapabilityRemoteFTPRead, domain.JobCapabilityRemoteFTPWrite}) {
|
||||
violations = append(violations, field+" requires remote.ftp.read or remote.ftp.write when ftp is declared")
|
||||
}
|
||||
if containsString(remote.Methods, "rsync") && !containsAny(declaredCapabilities, []string{domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite}) {
|
||||
violations = append(violations, field+" requires remote.rsync.read or remote.rsync.write when rsync is declared")
|
||||
}
|
||||
if remote.RCON && !containsString(remote.RunCapabilities, domain.JobCapabilityRemoteRunRCONCommand) {
|
||||
violations = append(violations, field+".rcon requires remote.run.rcon.command")
|
||||
}
|
||||
if remote.LogTransfer && !containsString(remote.RunCapabilities, domain.JobCapabilityRemoteRunLogsTransfer) {
|
||||
violations = append(violations, field+".logTransfer requires remote.run.logs.transfer")
|
||||
}
|
||||
for _, engine := range remote.DatabaseEngines {
|
||||
required := domain.JobCapabilityRemoteRunDBMySQLQuery
|
||||
if engine == "sqlite" {
|
||||
required = domain.JobCapabilityRemoteRunDBSQLiteQuery
|
||||
}
|
||||
if !containsString(remote.RunCapabilities, required) {
|
||||
violations = append(violations, fmt.Sprintf("%s.databaseEngines requires %s", field, required))
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSafePluginStrings(prefix string, values []fieldString) []string {
|
||||
var violations []string
|
||||
for _, value := range values {
|
||||
@@ -908,6 +1001,9 @@ func pluginSafeStrings(plugin domain.GamePlugin) []fieldString {
|
||||
values = appendStringSliceFields(values, "tags", plugin.Tags)
|
||||
values = appendStringSliceFields(values, "aiPurposes", plugin.AIPurposes)
|
||||
values = appendStringSliceFields(values, "bridgeActions", plugin.BridgeActions)
|
||||
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, page := range plugin.Pages {
|
||||
prefix := fmt.Sprintf("pages[%d]", i)
|
||||
values = append(values,
|
||||
@@ -944,6 +1040,9 @@ func manifestSafeStrings(registration domain.GamePluginManifestRegistration) []f
|
||||
values = appendStringSliceFields(values, "capabilities", manifest.Capabilities)
|
||||
values = appendStringSliceFields(values, "permissions", manifest.Permissions)
|
||||
values = appendStringSliceFields(values, "ai.purposes", manifest.AI.Purposes)
|
||||
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, page := range manifest.Pages {
|
||||
prefix := fmt.Sprintf("pages[%d]", i)
|
||||
values = append(values,
|
||||
@@ -979,6 +1078,9 @@ func marketplacePluginSafeStrings(plugin domain.PluginMarketplacePlugin) []field
|
||||
values = appendStringSliceFields(values, "tags", plugin.Tags)
|
||||
values = appendStringSliceFields(values, "aiPurposes", plugin.AIPurposes)
|
||||
values = appendStringSliceFields(values, "bridgeActions", plugin.BridgeActions)
|
||||
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, page := range plugin.Pages {
|
||||
prefix := fmt.Sprintf("pages[%d]", i)
|
||||
values = append(values,
|
||||
@@ -1132,7 +1234,14 @@ func validPluginRunCapability(capability string) bool {
|
||||
"config.write",
|
||||
"files.list", "files.read", "files.write", "files.patch",
|
||||
"file.list", "file.read", "file.write", "file.patch",
|
||||
"logs.read", "log.query",
|
||||
"logs.read", "log.query", domain.JobCapabilityLogsBackfill,
|
||||
domain.JobCapabilityRemoteFTPRead, domain.JobCapabilityRemoteFTPWrite,
|
||||
domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite,
|
||||
domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite,
|
||||
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand,
|
||||
domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall,
|
||||
"artifacts.read", "artifacts.write", "artifact.read", "artifact.write",
|
||||
"ai.invoke":
|
||||
return true
|
||||
@@ -1141,6 +1250,51 @@ func validPluginRunCapability(capability string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func isRemoteRunCapability(capability string) bool {
|
||||
return strings.HasPrefix(capability, "remote.")
|
||||
}
|
||||
|
||||
func remoteCapabilityRequiresTargetKey(capability string) bool {
|
||||
switch capability {
|
||||
case domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop:
|
||||
return false
|
||||
default:
|
||||
return isRemoteRunCapability(capability)
|
||||
}
|
||||
}
|
||||
|
||||
func remoteCapabilityRequiresInputRef(capability string) bool {
|
||||
switch capability {
|
||||
case domain.JobCapabilityRemoteFTPWrite,
|
||||
domain.JobCapabilityRemoteRsyncWrite,
|
||||
domain.JobCapabilityRemoteRunFilesWrite,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery,
|
||||
domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunRCONCommand:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validRemoteAccessMethod(method string) bool {
|
||||
switch method {
|
||||
case "ftp", "rsync", "run":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validRemoteDatabaseEngine(engine string) bool {
|
||||
switch engine {
|
||||
case "mysql", "sqlite":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validFileOperationKind(operation domain.FileOperationKind) bool {
|
||||
switch operation {
|
||||
case domain.FileOperationRead, domain.FileOperationWrite:
|
||||
@@ -1186,7 +1340,7 @@ func validScopedInputRef(ref string) bool {
|
||||
|
||||
func validPluginPermission(permission string) bool {
|
||||
switch permission {
|
||||
case "server.create", "server.read", "server.lifecycle", "server.files.read", "server.files.write", "server.logs.read", "server.artifacts.read", "server.artifacts.write", "ai.invoke":
|
||||
case "server.create", "server.read", "server.lifecycle", "server.files.read", "server.files.write", "server.logs.read", "server.artifacts.read", "server.artifacts.write", "server.remote.access", "server.run.distribution", "server.dependencies.manage", "server.client-manager.manage", "ai.invoke":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -1200,6 +1354,11 @@ func validPluginBridgeAction(action domain.PluginBridgeAction) bool {
|
||||
domain.PluginBridgeActionLogsQuery,
|
||||
domain.PluginBridgeActionArtifactsOpen,
|
||||
domain.PluginBridgeActionFilesRequest,
|
||||
domain.PluginBridgeActionRemoteAccessRequest,
|
||||
domain.PluginBridgeActionRunDistribution,
|
||||
domain.PluginBridgeActionDependenciesRequest,
|
||||
domain.PluginBridgeActionLogsBackfillRequest,
|
||||
domain.PluginBridgeActionClientManager,
|
||||
domain.PluginBridgeActionAIInvoke:
|
||||
return true
|
||||
default:
|
||||
@@ -1219,6 +1378,16 @@ func requiredBridgePermissions(action domain.PluginBridgeAction) []string {
|
||||
return []string{"server.artifacts.read"}
|
||||
case domain.PluginBridgeActionFilesRequest:
|
||||
return []string{"server.files.read"}
|
||||
case domain.PluginBridgeActionRemoteAccessRequest:
|
||||
return []string{"server.remote.access"}
|
||||
case domain.PluginBridgeActionRunDistribution:
|
||||
return []string{"server.run.distribution"}
|
||||
case domain.PluginBridgeActionDependenciesRequest:
|
||||
return []string{"server.dependencies.manage"}
|
||||
case domain.PluginBridgeActionLogsBackfillRequest:
|
||||
return []string{"server.logs.read"}
|
||||
case domain.PluginBridgeActionClientManager:
|
||||
return []string{"server.client-manager.manage"}
|
||||
case domain.PluginBridgeActionAIInvoke:
|
||||
return []string{"ai.invoke"}
|
||||
default:
|
||||
@@ -1259,6 +1428,15 @@ func containsAll(values []string, required []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func containsAny(values []string, candidates []string) bool {
|
||||
for _, candidate := range candidates {
|
||||
if containsString(values, candidate) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsString(values []string, target string) bool {
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
|
||||
Reference in New Issue
Block a user