diff --git a/api/types/cluster_alert.go b/api/types/cluster_alert.go index 24bee8f5a20..e0c0bfd18a0 100644 --- a/api/types/cluster_alert.go +++ b/api/types/cluster_alert.go @@ -17,6 +17,7 @@ limitations under the License. package types import ( + "net/url" "regexp" "sort" "time" @@ -28,6 +29,8 @@ import ( // matchStrictLabel is a fairly conservative allowed charset for labels. var matchStrictLabel = regexp.MustCompile(`^[a-z0-9\.\-\/]+$`).MatchString +const validLinkDestination = "goteleport.com" + type alertOptions struct { labels map[string]string severity AlertSeverity @@ -112,7 +115,7 @@ func (c *ClusterAlert) setDefaults() { } } -// CheckAndSetDefaults verfies required fields. +// CheckAndSetDefaults verifies required fields. func (c *ClusterAlert) CheckAndSetDefaults() error { c.setDefaults() if c.Version != V1 { @@ -135,9 +138,20 @@ func (c *ClusterAlert) CheckAndSetDefaults() error { if !matchStrictLabel(key) { return trace.BadParameter("invalid alert label key: %q", key) } - if !matchStrictLabel(val) { + // for links, we relax the conditions on label values + if key != AlertLink && !matchStrictLabel(val) { return trace.BadParameter("invalid alert label value: %q", val) } + + if key == AlertLink { + u, err := url.Parse(val) + if err != nil { + return trace.BadParameter("invalid alert: label link %q is not a valid URL", val) + } + if u.Hostname() != validLinkDestination { + return trace.BadParameter("invalid alert: label link not allowed %q", val) + } + } } return nil } diff --git a/api/types/cluster_alert_test.go b/api/types/cluster_alert_test.go index 575c17f3fb8..8c467ded203 100644 --- a/api/types/cluster_alert_test.go +++ b/api/types/cluster_alert_test.go @@ -82,3 +82,35 @@ func TestAlertSorting(t *testing.T) { require.Equal(t, fmt.Sprintf("%d", i), a.Metadata.Labels["p"]) } } + +// TestCheckAndSetDefaults verifies that only valid URLs are set on the link label. +func TestCheckAndSetDefaultsWithLink(t *testing.T) { + tests := []struct { + link string + assert require.ErrorAssertionFunc + }{ + { + link: "https://goteleport.com/docs", + assert: require.NoError, + }, + { + link: "h{t}tps://goteleport.com/docs", + assert: require.Error, + }, + { + link: "https://google.com", + assert: require.Error, + }, + } + + for i, tt := range tests { + t.Run(tt.link, func(t *testing.T) { + _, err := NewClusterAlert( + fmt.Sprintf("name-%d", i), + fmt.Sprintf("message-%d", i), + WithAlertLabel(AlertLink, tt.link), + ) + tt.assert(t, err) + }) + } +} diff --git a/api/types/constants.go b/api/types/constants.go index df5d672aeb5..0dbd9bef8f3 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -455,6 +455,9 @@ const ( // AlertPermitAll is an internal label that indicates that an alert is suitable for display to all users. AlertPermitAll = "teleport.internal/alert-permit-all" + + // AlertLink is an internal label that indicates that an alert is a link. + AlertLink = "teleport.internal/link" ) // RequestableResourceKinds lists all Teleport resource kinds users can request access to.