Files

363 lines
15 KiB
Go

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, "pluginVersion", binding.PluginVersion)
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")
}
trimmed := strings.TrimSpace(value)
lowerKey := strings.ToLower(key)
sensitiveKey := strings.Contains(lowerKey, "password") || strings.Contains(lowerKey, "credential") || strings.Contains(lowerKey, "secret") || strings.Contains(lowerKey, "token") || strings.Contains(lowerKey, "dsn")
if trimmed != value || strings.HasPrefix(value, "/") || strings.HasPrefix(value, `\`) || containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || strings.Contains(strings.ToLower(value), "://") && !strings.HasPrefix(value, "secret://") || sensitiveKey && 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.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, "buildJobId", distribution.BuildJobID)
violations = appendRequired(violations, "artifactId", distribution.ArtifactID)
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.Status == domain.DistributionStatusAvailable && distribution.Checksum == "" {
violations = append(violations, "checksum is required when distribution is available")
}
if distribution.KeyGeneration <= 0 {
violations = append(violations, "keyGeneration must be positive")
}
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/") {
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 status.PlanDigest != "" && !validSHA256Checksum(status.PlanDigest) {
violations = append(violations, "planDigest must be sha256:<hex>")
}
if len(status.JobID) > 180 || containsUnsafeRuntimeSecret(status.JobID) || looksLikeRawHostPath(status.JobID) {
violations = append(violations, "jobId is unsafe or too long")
}
if status.CompletedSteps < 0 || status.CompletedSteps > 64 {
violations = append(violations, "completedSteps is out of bounds")
}
if len(status.Evidence) > maxDistributionMessageLength || containsUnsafeRuntimeSecret(status.Evidence) || looksLikeRawHostPath(status.Evidence) {
violations = append(violations, "evidence is unsafe or too long")
}
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 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, "targetOs", job.TargetOS)
violations = appendRequired(violations, "targetArch", job.TargetArch)
violations = appendRequired(violations, "targetRelease", job.TargetRelease)
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")
}
violations = appendDistributionTargetViolations(violations, job.TargetOS, job.TargetArch)
if !validRunUpdatePhase(job.Phase) {
violations = append(violations, "phase is invalid")
}
if len(job.Message) > maxDistributionMessageLength || containsUnsafeRuntimeSecret(job.Message) || looksLikeRawHostPath(job.Message) {
violations = append(violations, "message is unsafe or too long")
}
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 validRunUpdatePhase(phase domain.RunUpdatePhase) bool {
switch phase {
case domain.RunUpdatePhaseQueued, domain.RunUpdatePhaseDownloading, domain.RunUpdatePhaseStaged, domain.RunUpdatePhaseRestartRequested, domain.RunUpdatePhaseActivating, domain.RunUpdatePhaseSucceeded, domain.RunUpdatePhaseRolledBack, domain.RunUpdatePhaseFailed:
return true
default:
return false
}
}
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 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.DistributionComponentRun {
violations = append(violations, "componentKind must be run")
}
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.DistributionComponentRun {
violations = append(violations, "componentKind must be run")
}
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 validDistributionComponentKind(kind domain.DistributionComponentKind) bool {
switch kind {
case domain.DistributionComponentRun:
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)
}