Validate portable workspace identifiers

This commit is contained in:
2026-08-10 17:35:52 +00:00
parent 0b40cf8026
commit 1dccf5f140
31 changed files with 490 additions and 48 deletions

View File

@@ -0,0 +1,25 @@
package config
import (
"strings"
"testing"
)
func TestIdentityValidationRejectsUnsafeSegments(t *testing.T) {
unsafe := []string{"../escape", `campaign\name`, `C:\campaign`, "café", "campaign name", ".", ".."}
for _, value := range unsafe {
t.Run("campaign/"+strings.ReplaceAll(value, "/", "_"), func(t *testing.T) {
cfg := &CampaignConfig{CampaignID: value, Inputs: CampaignInputsConfig{
SpeakersFile: "speakers.yml", AutocorrectFile: "autocorrect.yml", GlossaryFile: "glossary.yml", PlayersFile: "players.yml", PartyFile: "party.yml",
}}
if err := validateCampaign(cfg); err == nil {
t.Fatalf("validateCampaign(%q) error = nil, want rejection", value)
}
})
t.Run("session/"+strings.ReplaceAll(value, "/", "_"), func(t *testing.T) {
if err := validateSessionIdentifier("session.session_id", value, true); err == nil {
t.Fatalf("validateSessionIdentifier(%q) error = nil, want rejection", value)
}
})
}
}

View File

@@ -53,6 +53,9 @@ func validateCampaign(cfg *CampaignConfig) error {
if CampaignID(cfg) == "" {
return fmt.Errorf("campaign.campaign_id is required")
}
if err := pathsafe.ValidateOpaqueSegment(cfg.CampaignID); err != nil {
return fmt.Errorf("campaign.campaign_id: %w", err)
}
if strings.TrimSpace(cfg.Inputs.SpeakersFile) == "" {
return fmt.Errorf("campaign.inputs.speakers_file is required")
}
@@ -75,6 +78,11 @@ func validatePipeline(cfg *PipelineConfig) error {
if strings.TrimSpace(cfg.Workspace.Root) == "" {
return fmt.Errorf("pipeline.workspace.root is required")
}
if campaignID := cfg.Campaigns.DefaultCampaignID; campaignID != "" {
if err := pathsafe.ValidateOpaqueSegment(campaignID); err != nil {
return fmt.Errorf("pipeline.campaigns.default_campaign_id: %w", err)
}
}
if err := validateSecrets(cfg.Secrets); err != nil {
return err
}
@@ -717,6 +725,9 @@ func validateSession(cfg *SessionConfig) error {
if strings.TrimSpace(cfg.Campaign) == "" {
return fmt.Errorf("session.campaign is required")
}
if err := pathsafe.ValidateOpaqueSegment(cfg.Campaign); err != nil {
return fmt.Errorf("session.campaign: %w", err)
}
if strings.TrimSpace(cfg.Inputs.SpeakersFile) == "" {
return fmt.Errorf("session.inputs.speakers_file is required")
@@ -762,6 +773,9 @@ func validateSessionIdentifier(fieldName, value string, required bool) error {
}
return nil
}
if err := pathsafe.ValidateOpaqueSegment(value); err != nil {
return fmt.Errorf("%s: %w", fieldName, err)
}
return nil
}