github.com/xmidt-org/webpa-common@v1.11.9/basculechecks/metricvalidator.go (about)

     1  /**
     2   * Copyright 2020 Comcast Cable Communications Management, LLC
     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  
    18  package basculechecks
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"regexp"
    24  
    25  	"github.com/go-kit/kit/log"
    26  	"github.com/spf13/cast"
    27  	"github.com/xmidt-org/bascule"
    28  )
    29  
    30  var defaultLogger = log.NewNopLogger()
    31  
    32  // CapabilitiesChecker is an object that can determine if a request is
    33  // authorized given a bascule.Authentication object.  If it's not authorized, a
    34  // reason and error are given for logging and metrics.
    35  type CapabilitiesChecker interface {
    36  	Check(auth bascule.Authentication, vals ParsedValues) (string, error)
    37  }
    38  
    39  // ParsedValues are values determined from the bascule Authentication.
    40  type ParsedValues struct {
    41  	// Endpoint is the string representation of a regular expression that
    42  	// matches the URL for the request.  The main benefit of this string is it
    43  	// most likely won't include strings that change from one request to the
    44  	// next (ie, device ID).
    45  	Endpoint string
    46  	// Partner is a string representation of the list of partners found in the
    47  	// JWT, where:
    48  	//   - any list including "*" as a partner is determined to be "wildcard".
    49  	//   - when the list is <1 item, the partner is determined to be "none".
    50  	//   - when the list is >1 item, the partner is determined to be "many".
    51  	//   - when the list is only one item, that is the partner value.
    52  	Partner string
    53  }
    54  
    55  // MetricValidator determines if a request is authorized and then updates a
    56  // metric to show those results.
    57  type MetricValidator struct {
    58  	C         CapabilitiesChecker
    59  	Measures  *AuthCapabilityCheckMeasures
    60  	Endpoints []*regexp.Regexp
    61  }
    62  
    63  // CreateValidator provides a function for authorization middleware.  The
    64  // function parses the information needed for the CapabilitiesChecker, calls it
    65  // to determine if the request is authorized, and maintains the results in a
    66  // metric.  The function can actually mark the request as unauthorized or just
    67  // update the metric and allow the request, depending on configuration.  This
    68  // allows for monitoring before being more strict with authorization.
    69  func (m MetricValidator) CreateValidator(errorOut bool) bascule.ValidatorFunc {
    70  	return func(ctx context.Context, _ bascule.Token) error {
    71  		// if we're not supposed to error out, the outcome should be accepted on failure
    72  		failureOutcome := AcceptedOutcome
    73  		if errorOut {
    74  			// if we actually error out, the outcome is the request being rejected
    75  			failureOutcome = RejectedOutcome
    76  		}
    77  
    78  		auth, ok := bascule.FromContext(ctx)
    79  		if !ok {
    80  			m.Measures.CapabilityCheckOutcome.With(OutcomeLabel, failureOutcome, ReasonLabel, TokenMissing, ClientIDLabel, "", PartnerIDLabel, "", EndpointLabel, "").Add(1)
    81  			if errorOut {
    82  				return ErrNoAuth
    83  			}
    84  			return nil
    85  		}
    86  
    87  		client, partnerID, endpoint, reason, err := m.prepMetrics(auth)
    88  		labels := []string{ClientIDLabel, client, PartnerIDLabel, partnerID, EndpointLabel, endpoint}
    89  		if err != nil {
    90  			labels = append(labels, OutcomeLabel, failureOutcome, ReasonLabel, reason)
    91  			m.Measures.CapabilityCheckOutcome.With(labels...).Add(1)
    92  			if errorOut {
    93  				return err
    94  			}
    95  			return nil
    96  		}
    97  
    98  		v := ParsedValues{
    99  			Endpoint: endpoint,
   100  			Partner:  partnerID,
   101  		}
   102  
   103  		reason, err = m.C.Check(auth, v)
   104  		if err != nil {
   105  			labels = append(labels, OutcomeLabel, failureOutcome, ReasonLabel, reason)
   106  			m.Measures.CapabilityCheckOutcome.With(labels...).Add(1)
   107  			if errorOut {
   108  				return err
   109  			}
   110  			return nil
   111  		}
   112  
   113  		labels = append(labels, OutcomeLabel, AcceptedOutcome, ReasonLabel, "")
   114  		m.Measures.CapabilityCheckOutcome.With(labels...).Add(1)
   115  		return nil
   116  	}
   117  }
   118  
   119  // prepMetrics gathers the information needed for metric label information.  It
   120  // gathers the client ID, partnerID, and endpoint (bucketed) for more information
   121  // on the metric when a request is unauthorized.
   122  func (m MetricValidator) prepMetrics(auth bascule.Authentication) (string, string, string, string, error) {
   123  	if auth.Token == nil {
   124  		return "", "", "", TokenMissingValues, ErrNoToken
   125  	}
   126  	client := auth.Token.Principal()
   127  	if auth.Token.Attributes() == nil {
   128  		return client, "", "", TokenMissingValues, ErrNilAttributes
   129  	}
   130  
   131  	partnerVal, ok := bascule.GetNestedAttribute(auth.Token.Attributes(), PartnerKeys()...)
   132  	if !ok {
   133  		return client, "", "", UndeterminedPartnerID, fmt.Errorf("couldn't get partner IDs from attributes using keys %v", PartnerKeys())
   134  	}
   135  	partnerIDs, err := cast.ToStringSliceE(partnerVal)
   136  	if err != nil {
   137  		return client, "", "", UndeterminedPartnerID, fmt.Errorf("partner IDs \"%v\" couldn't be cast to string slice: %v", partnerVal, err)
   138  	}
   139  	partnerID := DeterminePartnerMetric(partnerIDs)
   140  
   141  	if auth.Request.URL == nil {
   142  		return client, partnerID, "", TokenMissingValues, ErrNoURL
   143  	}
   144  	escapedURL := auth.Request.URL.EscapedPath()
   145  	endpoint := determineEndpointMetric(m.Endpoints, escapedURL)
   146  	return client, partnerID, endpoint, "", nil
   147  }
   148  
   149  // DeterminePartnerMetric takes a list of partners and decides what the partner
   150  // metric label should be.
   151  func DeterminePartnerMetric(partners []string) string {
   152  	if len(partners) < 1 {
   153  		return "none"
   154  	}
   155  	if len(partners) == 1 {
   156  		if partners[0] == "*" {
   157  			return "wildcard"
   158  		}
   159  		return partners[0]
   160  	}
   161  	for _, partner := range partners {
   162  		if partner == "*" {
   163  			return "wildcard"
   164  		}
   165  	}
   166  	return "many"
   167  }
   168  
   169  // determineEndpointMetric takes a list of regular expressions and applies them
   170  // to the url of the request to decide what the endpoint metric label should be.
   171  func determineEndpointMetric(endpoints []*regexp.Regexp, urlHit string) string {
   172  	for _, r := range endpoints {
   173  		idxs := r.FindStringIndex(urlHit)
   174  		if idxs == nil {
   175  			continue
   176  		}
   177  		if idxs[0] == 0 {
   178  			return r.String()
   179  		}
   180  	}
   181  	return "not_recognized"
   182  }