Tighten Google Chat webhook validation and settings response
CI / ci (push) Successful in 6m36s
CI / ci (pull_request) Successful in 5m55s

Restrict the decoded key and token to a URL-safe ASCII alphabet so the
16-character minimum and the four-character masked tail measure the same
thing; a token of a few multi-byte characters could otherwise be shown in
full (MQ-165-01).

Validate the raw query as exactly key=…&token=…, refusing empty
segments, a second "?", reordered, extra or repeated parameters
(MQ-165-02).

Drop configured and updated_at from the settings response so it is the
four-field contract: space_label, url_masked, last_test_at,
last_test_status; url_masked is null exactly when nothing is stored
(MQ-165-03).

Task: 919e9422-3a41-4f1f-8967-77da17f5f9f3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 14:15:23 -04:00
co-authored by Claude Opus 5.5
parent 0e88ce811e
commit 38b9e5a06e
6 changed files with 55 additions and 38 deletions
+4 -8
View File
@@ -59,9 +59,8 @@ var (
)
// Settings is the stored webhook as the API shows it: never the URL, only
// its masked form.
// its masked form. URLMasked is nil when no webhook is stored.
type Settings struct {
Configured bool `json:"configured"`
SpaceLabel *string `json:"space_label"`
// URLMasked is the webhook URL without its key and with only the last
// few characters of its token.
@@ -70,7 +69,6 @@ type Settings struct {
// test message is sent to this webhook.
LastTestAt *string `json:"last_test_at"`
LastTestStatus *string `json:"last_test_status"`
UpdatedAt *string `json:"updated_at"`
}
// TestResult is the outcome of a test message.
@@ -143,8 +141,8 @@ func NewService(st *store.Store, box *secretbox.Box, publicURL *url.URL, transpo
}, nil
}
// Get returns the stored webhook's settings; Configured is false and every
// other field nil before one is saved.
// Get returns the stored webhook's settings; every field is nil before one
// is saved.
func (s *Service) Get(ctx context.Context) (Settings, error) {
row, err := db.New(s.st.Reader).GetChatSettings(ctx)
if errors.Is(err, sql.ErrNoRows) {
@@ -440,14 +438,12 @@ func (s *Service) timestamp() string {
}
func settingsFrom(row db.ChatSetting, hook Webhook) Settings {
masked, updated := hook.Masked(), row.UpdatedAt
masked := hook.Masked()
return Settings{
Configured: true,
SpaceLabel: nullString(row.SpaceLabel),
URLMasked: &masked,
LastTestAt: nullString(row.LastTestAt),
LastTestStatus: nullString(row.LastTestStatus),
UpdatedAt: &updated,
}
}