package delivery import ( "encoding/json" "errors" "fmt" "sneak.berlin/go/webhooker/internal/database" ) // errUnknownTargetTypeForEdit is returned when a stored target has a // type the edit form has no field set for. var errUnknownTargetTypeForEdit = errors.New( "unknown target type", ) // TargetConfigForm is the UNMASKED projection of a target's stored // configuration, for pre-filling the target edit form. // // It is the deliberate exception to the rule TargetView enforces // everywhere else: TargetView exists so that no template can render // a target's stored blob, because a destination URL's path segments // and a header value are both routinely the credential. An operator // cannot correct a value they cannot see, so the edit form — and // only the edit form — is shown the full value. // // Everything that keeps that exception narrow lives at the call // site: the route is behind RequireAuth and the webhook's ownership // check, and its group sets NoCache so the rendered secret is not // written to a shared cache. Do not reach for this type from any // other page. type TargetConfigForm struct { // URL is the destination for an HTTP target and the webhook // URL for a Slack target. URL string // Headers is the HTTP target's configured headers in the // textarea representation, one "Name: value" per line. Headers string // Timeout is the HTTP target's per-request timeout in seconds, // empty when unset. Timeout string // Expiry is the database (archive) target's row expiry. Expiry string } // NewTargetConfigForm parses a target's stored configuration into // the edit form's fields. // // A configuration that does not parse is an error rather than a // zero-valued form that silently looks like a target with no // settings. The caller shows the operator that the stored value // could not be read, so that saving the form is understood as // replacing it rather than preserving it. func NewTargetConfigForm( t *database.Target, ) (TargetConfigForm, error) { switch t.Type { case database.TargetTypeHTTP: cfg, err := parseHTTPConfig(t.Config) if err != nil { return TargetConfigForm{}, err } return TargetConfigForm{ URL: cfg.URL, Headers: FormatTargetHeaders(cfg.Headers), Timeout: FormatTargetTimeout(cfg.Timeout), }, nil case database.TargetTypeSlack: cfg, err := parseSlackConfig(t.Config) if err != nil { return TargetConfigForm{}, err } return TargetConfigForm{URL: cfg.WebhookURL}, nil case database.TargetTypeDatabase: return databaseConfigForm(t.Config) case database.TargetTypeLog: // The log target takes no configuration. return TargetConfigForm{}, nil default: return TargetConfigForm{}, fmt.Errorf( "%w: %q", errUnknownTargetTypeForEdit, t.Type, ) } } // databaseConfigForm parses an archive target's optional expiry. // An absent or empty configuration is the keep-forever default and // yields an empty field, so re-saving the form unchanged stores the // same empty configuration it started with. An expiry that is set // but not a valid duration is an error, not a blank field. func databaseConfigForm( configJSON string, ) (TargetConfigForm, error) { if configJSON == "" { return TargetConfigForm{}, nil } var cfg databaseTargetConfig err := json.Unmarshal([]byte(configJSON), &cfg) if err != nil { return TargetConfigForm{}, fmt.Errorf( "parsing config JSON: %w", err, ) } if cfg.Expiry == "" || cfg.Expiry == archiveExpiryNever { return TargetConfigForm{}, nil } err = ValidateArchiveExpiry(cfg.Expiry) if err != nil { return TargetConfigForm{}, err } return TargetConfigForm{Expiry: cfg.Expiry}, nil }