github.com/nats-io/jwt/v2@v2.5.6/exports.go (about) 1 /* 2 * Copyright 2018-2019 The NATS Authors 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package jwt 17 18 import ( 19 "encoding/json" 20 "fmt" 21 "strings" 22 "time" 23 ) 24 25 // ResponseType is used to store an export response type 26 type ResponseType string 27 28 const ( 29 // ResponseTypeSingleton is used for a service that sends a single response only 30 ResponseTypeSingleton = "Singleton" 31 32 // ResponseTypeStream is used for a service that will send multiple responses 33 ResponseTypeStream = "Stream" 34 35 // ResponseTypeChunked is used for a service that sends a single response in chunks (so not quite a stream) 36 ResponseTypeChunked = "Chunked" 37 ) 38 39 // ServiceLatency is used when observing and exported service for 40 // latency measurements. 41 // Sampling 1-100, represents sampling rate, defaults to 100. 42 // Results is the subject where the latency metrics are published. 43 // A metric will be defined by the nats-server's ServiceLatency. Time durations 44 // are in nanoseconds. 45 // see https://github.com/nats-io/nats-server/blob/main/server/accounts.go#L524 46 // e.g. 47 // 48 // { 49 // "app": "dlc22", 50 // "start": "2019-09-16T21:46:23.636869585-07:00", 51 // "svc": 219732, 52 // "nats": { 53 // "req": 320415, 54 // "resp": 228268, 55 // "sys": 0 56 // }, 57 // "total": 768415 58 // } 59 type ServiceLatency struct { 60 Sampling SamplingRate `json:"sampling"` 61 Results Subject `json:"results"` 62 } 63 64 type SamplingRate int 65 66 const Headers = SamplingRate(0) 67 68 // MarshalJSON marshals the field as "headers" or percentages 69 func (r *SamplingRate) MarshalJSON() ([]byte, error) { 70 sr := *r 71 if sr == 0 { 72 return []byte(`"headers"`), nil 73 } 74 if sr >= 1 && sr <= 100 { 75 return []byte(fmt.Sprintf("%d", sr)), nil 76 } 77 return nil, fmt.Errorf("unknown sampling rate") 78 } 79 80 // UnmarshalJSON unmashals numbers as percentages or "headers" 81 func (t *SamplingRate) UnmarshalJSON(b []byte) error { 82 if len(b) == 0 { 83 return fmt.Errorf("empty sampling rate") 84 } 85 if strings.ToLower(string(b)) == `"headers"` { 86 *t = Headers 87 return nil 88 } 89 var j int 90 err := json.Unmarshal(b, &j) 91 if err != nil { 92 return err 93 } 94 *t = SamplingRate(j) 95 return nil 96 } 97 98 func (sl *ServiceLatency) Validate(vr *ValidationResults) { 99 if sl.Sampling != 0 { 100 if sl.Sampling < 1 || sl.Sampling > 100 { 101 vr.AddError("sampling percentage needs to be between 1-100") 102 } 103 } 104 sl.Results.Validate(vr) 105 if sl.Results.HasWildCards() { 106 vr.AddError("results subject can not contain wildcards") 107 } 108 } 109 110 // Export represents a single export 111 type Export struct { 112 Name string `json:"name,omitempty"` 113 Subject Subject `json:"subject,omitempty"` 114 Type ExportType `json:"type,omitempty"` 115 TokenReq bool `json:"token_req,omitempty"` 116 Revocations RevocationList `json:"revocations,omitempty"` 117 ResponseType ResponseType `json:"response_type,omitempty"` 118 ResponseThreshold time.Duration `json:"response_threshold,omitempty"` 119 Latency *ServiceLatency `json:"service_latency,omitempty"` 120 AccountTokenPosition uint `json:"account_token_position,omitempty"` 121 Advertise bool `json:"advertise,omitempty"` 122 AllowTrace bool `json:"allow_trace,omitempty"` 123 Info 124 } 125 126 // IsService returns true if an export is for a service 127 func (e *Export) IsService() bool { 128 return e.Type == Service 129 } 130 131 // IsStream returns true if an export is for a stream 132 func (e *Export) IsStream() bool { 133 return e.Type == Stream 134 } 135 136 // IsSingleResponse returns true if an export has a single response 137 // or no response type is set, also checks that the type is service 138 func (e *Export) IsSingleResponse() bool { 139 return e.Type == Service && (e.ResponseType == ResponseTypeSingleton || e.ResponseType == "") 140 } 141 142 // IsChunkedResponse returns true if an export has a chunked response 143 func (e *Export) IsChunkedResponse() bool { 144 return e.Type == Service && e.ResponseType == ResponseTypeChunked 145 } 146 147 // IsStreamResponse returns true if an export has a chunked response 148 func (e *Export) IsStreamResponse() bool { 149 return e.Type == Service && e.ResponseType == ResponseTypeStream 150 } 151 152 // Validate appends validation issues to the passed in results list 153 func (e *Export) Validate(vr *ValidationResults) { 154 if e == nil { 155 vr.AddError("null export is not allowed") 156 return 157 } 158 if !e.IsService() && !e.IsStream() { 159 vr.AddError("invalid export type: %q", e.Type) 160 } 161 if e.IsService() && !e.IsSingleResponse() && !e.IsChunkedResponse() && !e.IsStreamResponse() { 162 vr.AddError("invalid response type for service: %q", e.ResponseType) 163 } 164 if e.IsStream() { 165 if e.ResponseType != "" { 166 vr.AddError("invalid response type for stream: %q", e.ResponseType) 167 } 168 if e.AllowTrace { 169 vr.AddError("AllowTrace only valid for service export") 170 } 171 } 172 if e.Latency != nil { 173 if !e.IsService() { 174 vr.AddError("latency tracking only permitted for services") 175 } 176 e.Latency.Validate(vr) 177 } 178 if e.ResponseThreshold.Nanoseconds() < 0 { 179 vr.AddError("negative response threshold is invalid") 180 } 181 if e.ResponseThreshold.Nanoseconds() > 0 && !e.IsService() { 182 vr.AddError("response threshold only valid for services") 183 } 184 e.Subject.Validate(vr) 185 if e.AccountTokenPosition > 0 { 186 if !e.Subject.HasWildCards() { 187 vr.AddError("Account Token Position can only be used with wildcard subjects: %s", e.Subject) 188 } else { 189 subj := string(e.Subject) 190 token := strings.Split(subj, ".") 191 tkCnt := uint(len(token)) 192 if e.AccountTokenPosition > tkCnt { 193 vr.AddError("Account Token Position %d exceeds length of subject '%s'", 194 e.AccountTokenPosition, e.Subject) 195 } else if tk := token[e.AccountTokenPosition-1]; tk != "*" { 196 vr.AddError("Account Token Position %d matches '%s' but must match a * in: %s", 197 e.AccountTokenPosition, tk, e.Subject) 198 } 199 } 200 } 201 e.Info.Validate(vr) 202 } 203 204 // Revoke enters a revocation by publickey using time.Now(). 205 func (e *Export) Revoke(pubKey string) { 206 e.RevokeAt(pubKey, time.Now()) 207 } 208 209 // RevokeAt enters a revocation by publickey and timestamp into this export 210 // If there is already a revocation for this public key that is newer, it is kept. 211 func (e *Export) RevokeAt(pubKey string, timestamp time.Time) { 212 if e.Revocations == nil { 213 e.Revocations = RevocationList{} 214 } 215 216 e.Revocations.Revoke(pubKey, timestamp) 217 } 218 219 // ClearRevocation removes any revocation for the public key 220 func (e *Export) ClearRevocation(pubKey string) { 221 e.Revocations.ClearRevocation(pubKey) 222 } 223 224 // isRevoked checks if the public key is in the revoked list with a timestamp later than the one passed in. 225 // Generally this method is called with the subject and issue time of the jwt to be tested. 226 // DO NOT pass time.Now(), it will not produce a stable/expected response. 227 func (e *Export) isRevoked(pubKey string, claimIssuedAt time.Time) bool { 228 return e.Revocations.IsRevoked(pubKey, claimIssuedAt) 229 } 230 231 // IsClaimRevoked checks if the activation revoked the claim passed in. 232 // Invalid claims (nil, no Subject or IssuedAt) will return true. 233 func (e *Export) IsClaimRevoked(claim *ActivationClaims) bool { 234 if claim == nil || claim.IssuedAt == 0 || claim.Subject == "" { 235 return true 236 } 237 return e.isRevoked(claim.Subject, time.Unix(claim.IssuedAt, 0)) 238 } 239 240 // Exports is a slice of exports 241 type Exports []*Export 242 243 // Add appends exports to the list 244 func (e *Exports) Add(i ...*Export) { 245 *e = append(*e, i...) 246 } 247 248 func isContainedIn(kind ExportType, subjects []Subject, vr *ValidationResults) { 249 m := make(map[string]string) 250 for i, ns := range subjects { 251 for j, s := range subjects { 252 if i == j { 253 continue 254 } 255 if ns.IsContainedIn(s) { 256 str := string(s) 257 _, ok := m[str] 258 if !ok { 259 m[str] = string(ns) 260 } 261 } 262 } 263 } 264 265 if len(m) != 0 { 266 for k, v := range m { 267 var vi ValidationIssue 268 vi.Blocking = true 269 vi.Description = fmt.Sprintf("%s export subject %q already exports %q", kind, k, v) 270 vr.Add(&vi) 271 } 272 } 273 } 274 275 // Validate calls validate on all of the exports 276 func (e *Exports) Validate(vr *ValidationResults) error { 277 var serviceSubjects []Subject 278 var streamSubjects []Subject 279 280 for _, v := range *e { 281 if v == nil { 282 vr.AddError("null export is not allowed") 283 continue 284 } 285 if v.IsService() { 286 serviceSubjects = append(serviceSubjects, v.Subject) 287 } else { 288 streamSubjects = append(streamSubjects, v.Subject) 289 } 290 v.Validate(vr) 291 } 292 293 isContainedIn(Service, serviceSubjects, vr) 294 isContainedIn(Stream, streamSubjects, vr) 295 296 return nil 297 } 298 299 // HasExportContainingSubject checks if the export list has an export with the provided subject 300 func (e *Exports) HasExportContainingSubject(subject Subject) bool { 301 for _, s := range *e { 302 if subject.IsContainedIn(s.Subject) { 303 return true 304 } 305 } 306 return false 307 } 308 309 func (e Exports) Len() int { 310 return len(e) 311 } 312 313 func (e Exports) Swap(i, j int) { 314 e[i], e[j] = e[j], e[i] 315 } 316 317 func (e Exports) Less(i, j int) bool { 318 return e[i].Subject < e[j].Subject 319 }