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  }