439 lines
18 KiB
Go
439 lines
18 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, "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, "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" {
|
|
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, "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.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")
|
|
}
|
|
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)
|
|
}
|