github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/types/cluster_alert.go (about) 1 /* 2 Copyright 2022 Gravitational, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package types 18 19 import ( 20 "net/url" 21 "regexp" 22 "sort" 23 "time" 24 "unicode" 25 26 "github.com/gravitational/trace" 27 ) 28 29 // matchAlertLabelKey is a fairly conservative allowed charset for label keys. 30 var matchAlertLabelKey = regexp.MustCompile(`^[a-z0-9\.\-\/]+$`).MatchString 31 32 // matchAlertLabelVal is a slightly more permissive matcher for label values. 33 var matchAlertLabelVal = regexp.MustCompile(`^[a-z0-9\.\-_\/:|]+$`).MatchString 34 35 const validLinkDestination = "goteleport.com" 36 37 type alertOptions struct { 38 labels map[string]string 39 severity AlertSeverity 40 created time.Time 41 expires time.Time 42 } 43 44 // AlertOption is a functional option for alert construction. 45 type AlertOption func(options *alertOptions) 46 47 // WithAlertLabel constructs an alert with the specified label. 48 func WithAlertLabel(key, val string) AlertOption { 49 return func(options *alertOptions) { 50 if options.labels == nil { 51 options.labels = make(map[string]string) 52 } 53 options.labels[key] = val 54 } 55 } 56 57 // WithAlertSeverity sets the severity of an alert (defaults to MEDIUM). 58 func WithAlertSeverity(severity AlertSeverity) AlertOption { 59 return func(options *alertOptions) { 60 options.severity = severity 61 } 62 } 63 64 // WithAlertCreated sets the alert's creation time. Auth server automatically fills 65 // this before inserting the alert in the backend if none is set. 66 func WithAlertCreated(created time.Time) AlertOption { 67 return func(options *alertOptions) { 68 options.created = created.UTC() 69 } 70 } 71 72 // WithAlertExpires sets the alerts expiry time. Auth server automatically applies a 73 // 24h expiry before inserting the alert in the backend if none is set. 74 func WithAlertExpires(expires time.Time) AlertOption { 75 return func(options *alertOptions) { 76 options.expires = expires.UTC() 77 } 78 } 79 80 // NewClusterAlert creates a new cluster alert. 81 func NewClusterAlert(name string, message string, opts ...AlertOption) (ClusterAlert, error) { 82 options := alertOptions{ 83 severity: AlertSeverity_MEDIUM, 84 } 85 for _, opt := range opts { 86 opt(&options) 87 } 88 alert := ClusterAlert{ 89 ResourceHeader: ResourceHeader{ 90 Metadata: Metadata{ 91 Name: name, 92 Labels: options.labels, 93 Expires: &options.expires, 94 }, 95 }, 96 Spec: ClusterAlertSpec{ 97 Severity: options.severity, 98 Message: message, 99 Created: options.created, 100 }, 101 } 102 if err := alert.CheckAndSetDefaults(); err != nil { 103 return ClusterAlert{}, trace.Wrap(err) 104 } 105 return alert, nil 106 } 107 108 // SortClusterAlerts applies the default cluster alert sorting, prioritizing 109 // elements by a combination of severity and creation time. Alerts are sorted 110 // with higher severity alerts first, and alerts of the same priority are sorted 111 // with newer alerts first. 112 func SortClusterAlerts(alerts []ClusterAlert) { 113 sort.Slice(alerts, func(i, j int) bool { 114 if alerts[i].Spec.Severity == alerts[j].Spec.Severity { 115 return alerts[i].Spec.Created.After(alerts[j].Spec.Created) 116 } 117 return alerts[i].Spec.Severity > alerts[j].Spec.Severity 118 }) 119 } 120 121 func (c *ClusterAlert) setDefaults() { 122 if c.Kind == "" { 123 c.Kind = KindClusterAlert 124 } 125 126 if c.Version == "" { 127 c.Version = V1 128 } 129 } 130 131 // CheckAndSetDefaults verifies required fields. 132 func (c *ClusterAlert) CheckAndSetDefaults() error { 133 c.setDefaults() 134 if c.Version != V1 { 135 return trace.BadParameter("unsupported cluster alert version: %s", c.Version) 136 } 137 138 if c.Kind != KindClusterAlert { 139 return trace.BadParameter("expected kind %s, got %q", KindClusterAlert, c.Kind) 140 } 141 142 if c.Metadata.Name == "" { 143 return trace.BadParameter("alert name must be specified") 144 } 145 146 if err := c.CheckMessage(); err != nil { 147 return trace.Wrap(err) 148 } 149 150 for key, val := range c.Metadata.Labels { 151 if !matchAlertLabelKey(key) { 152 return trace.BadParameter("invalid alert label key: %q", key) 153 } 154 // for links, we relax the conditions on label values 155 if key != AlertLink && !matchAlertLabelVal(val) { 156 return trace.BadParameter("invalid alert label value: %q", val) 157 } 158 159 if key == AlertLink { 160 u, err := url.Parse(val) 161 if err != nil { 162 return trace.BadParameter("invalid alert: label link %q is not a valid URL", val) 163 } 164 if u.Hostname() != validLinkDestination { 165 return trace.BadParameter("invalid alert: label link not allowed %q", val) 166 } 167 } 168 } 169 return nil 170 } 171 172 func (c *ClusterAlert) CheckMessage() error { 173 if c.Spec.Message == "" { 174 return trace.BadParameter("alert message must be specified") 175 } 176 177 for _, c := range c.Spec.Message { 178 if unicode.IsControl(c) { 179 return trace.BadParameter("control characters not supported in alerts") 180 } 181 } 182 return nil 183 } 184 185 // Match checks if the given cluster alert matches this query. 186 func (r *GetClusterAlertsRequest) Match(alert ClusterAlert) bool { 187 if alert.Spec.Severity < r.Severity { 188 return false 189 } 190 191 if r.AlertID != "" && r.AlertID != alert.Metadata.Name { 192 return false 193 } 194 195 for key, val := range r.Labels { 196 if alert.Metadata.Labels[key] != val { 197 return false 198 } 199 } 200 201 return true 202 } 203 204 func (ack *AlertAcknowledgement) Check() error { 205 if ack.AlertID == "" { 206 return trace.BadParameter("missing alert id in ack") 207 } 208 209 if ack.Reason == "" { 210 return trace.BadParameter("ack reason must be specified") 211 } 212 213 for _, c := range ack.Reason { 214 if unicode.IsControl(c) { 215 return trace.BadParameter("control characters not supported in ack reason") 216 } 217 } 218 219 if ack.Expires.IsZero() { 220 return trace.BadParameter("missing expiry time") 221 } 222 223 return nil 224 }