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 }