/* * Copyright 2018-2019 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jwt import ( "encoding/json" "fmt" "strings" "time" ) // ResponseType is used to store an export response type type ResponseType string const ( // ResponseTypeSingleton is used for a service that sends a single response only ResponseTypeSingleton = "Singleton" // ResponseTypeStream is used for a service that will send multiple responses ResponseTypeStream = "Stream" // ResponseTypeChunked is used for a service that sends a single response in chunks (so not quite a stream) ResponseTypeChunked = "Chunked" ) // ServiceLatency is used when observing and exported service for // latency measurements. // Sampling 1-100, represents sampling rate, defaults to 100. // Results is the subject where the latency metrics are published. // A metric will be defined by the nats-server's ServiceLatency. Time durations // are in nanoseconds. // see https://github.com/nats-io/nats-server/blob/main/server/accounts.go#L524 // e.g. // // { // "app": "dlc22", // "start": "2019-09-16T21:46:23.636869585-07:00", // "svc": 219732, // "nats": { // "req": 320415, // "resp": 228268, // "sys": 0 // }, // "total": 768415 // } type ServiceLatency struct { Sampling SamplingRate `json:"sampling"` Results Subject `json:"results"` } type SamplingRate int const Headers = SamplingRate(0) // MarshalJSON marshals the field as "headers" or percentages func (r *SamplingRate) MarshalJSON() ([]byte, error) { sr := *r if sr == 0 { return []byte(`"headers"`), nil } if sr >= 1 && sr <= 100 { return []byte(fmt.Sprintf("%d", sr)), nil } return nil, fmt.Errorf("unknown sampling rate") } // UnmarshalJSON unmashals numbers as percentages or "headers" func (t *SamplingRate) UnmarshalJSON(b []byte) error { if len(b) == 0 { return fmt.Errorf("empty sampling rate") } if strings.ToLower(string(b)) == `"headers"` { *t = Headers return nil } var j int err := json.Unmarshal(b, &j) if err != nil { return err } *t = SamplingRate(j) return nil } func (sl *ServiceLatency) Validate(vr *ValidationResults) { if sl.Sampling != 0 { if sl.Sampling < 1 || sl.Sampling > 100 { vr.AddError("sampling percentage needs to be between 1-100") } } sl.Results.Validate(vr) if sl.Results.HasWildCards() { vr.AddError("results subject can not contain wildcards") } } // Export represents a single export type Export struct { Name string `json:"name,omitempty"` Subject Subject `json:"subject,omitempty"` Type ExportType `json:"type,omitempty"` TokenReq bool `json:"token_req,omitempty"` Revocations RevocationList `json:"revocations,omitempty"` ResponseType ResponseType `json:"response_type,omitempty"` ResponseThreshold time.Duration `json:"response_threshold,omitempty"` Latency *ServiceLatency `json:"service_latency,omitempty"` AccountTokenPosition uint `json:"account_token_position,omitempty"` Advertise bool `json:"advertise,omitempty"` AllowTrace bool `json:"allow_trace,omitempty"` Info } // IsService returns true if an export is for a service func (e *Export) IsService() bool { return e.Type == Service } // IsStream returns true if an export is for a stream func (e *Export) IsStream() bool { return e.Type == Stream } // IsSingleResponse returns true if an export has a single response // or no response type is set, also checks that the type is service func (e *Export) IsSingleResponse() bool { return e.Type == Service && (e.ResponseType == ResponseTypeSingleton || e.ResponseType == "") } // IsChunkedResponse returns true if an export has a chunked response func (e *Export) IsChunkedResponse() bool { return e.Type == Service && e.ResponseType == ResponseTypeChunked } // IsStreamResponse returns true if an export has a chunked response func (e *Export) IsStreamResponse() bool { return e.Type == Service && e.ResponseType == ResponseTypeStream } // Validate appends validation issues to the passed in results list func (e *Export) Validate(vr *ValidationResults) { if e == nil { vr.AddError("null export is not allowed") return } if !e.IsService() && !e.IsStream() { vr.AddError("invalid export type: %q", e.Type) } if e.IsService() && !e.IsSingleResponse() && !e.IsChunkedResponse() && !e.IsStreamResponse() { vr.AddError("invalid response type for service: %q", e.ResponseType) } if e.IsStream() { if e.ResponseType != "" { vr.AddError("invalid response type for stream: %q", e.ResponseType) } if e.AllowTrace { vr.AddError("AllowTrace only valid for service export") } } if e.Latency != nil { if !e.IsService() { vr.AddError("latency tracking only permitted for services") } e.Latency.Validate(vr) } if e.ResponseThreshold.Nanoseconds() < 0 { vr.AddError("negative response threshold is invalid") } if e.ResponseThreshold.Nanoseconds() > 0 && !e.IsService() { vr.AddError("response threshold only valid for services") } e.Subject.Validate(vr) if e.AccountTokenPosition > 0 { if !e.Subject.HasWildCards() { vr.AddError("Account Token Position can only be used with wildcard subjects: %s", e.Subject) } else { subj := string(e.Subject) token := strings.Split(subj, ".") tkCnt := uint(len(token)) if e.AccountTokenPosition > tkCnt { vr.AddError("Account Token Position %d exceeds length of subject '%s'", e.AccountTokenPosition, e.Subject) } else if tk := token[e.AccountTokenPosition-1]; tk != "*" { vr.AddError("Account Token Position %d matches '%s' but must match a * in: %s", e.AccountTokenPosition, tk, e.Subject) } } } e.Info.Validate(vr) } // Revoke enters a revocation by publickey using time.Now(). func (e *Export) Revoke(pubKey string) { e.RevokeAt(pubKey, time.Now()) } // RevokeAt enters a revocation by publickey and timestamp into this export // If there is already a revocation for this public key that is newer, it is kept. func (e *Export) RevokeAt(pubKey string, timestamp time.Time) { if e.Revocations == nil { e.Revocations = RevocationList{} } e.Revocations.Revoke(pubKey, timestamp) } // ClearRevocation removes any revocation for the public key func (e *Export) ClearRevocation(pubKey string) { e.Revocations.ClearRevocation(pubKey) } // isRevoked checks if the public key is in the revoked list with a timestamp later than the one passed in. // Generally this method is called with the subject and issue time of the jwt to be tested. // DO NOT pass time.Now(), it will not produce a stable/expected response. func (e *Export) isRevoked(pubKey string, claimIssuedAt time.Time) bool { return e.Revocations.IsRevoked(pubKey, claimIssuedAt) } // IsClaimRevoked checks if the activation revoked the claim passed in. // Invalid claims (nil, no Subject or IssuedAt) will return true. func (e *Export) IsClaimRevoked(claim *ActivationClaims) bool { if claim == nil || claim.IssuedAt == 0 || claim.Subject == "" { return true } return e.isRevoked(claim.Subject, time.Unix(claim.IssuedAt, 0)) } // Exports is a slice of exports type Exports []*Export // Add appends exports to the list func (e *Exports) Add(i ...*Export) { *e = append(*e, i...) } func isContainedIn(kind ExportType, subjects []Subject, vr *ValidationResults) { m := make(map[string]string) for i, ns := range subjects { for j, s := range subjects { if i == j { continue } if ns.IsContainedIn(s) { str := string(s) _, ok := m[str] if !ok { m[str] = string(ns) } } } } if len(m) != 0 { for k, v := range m { var vi ValidationIssue vi.Blocking = true vi.Description = fmt.Sprintf("%s export subject %q already exports %q", kind, k, v) vr.Add(&vi) } } } // Validate calls validate on all of the exports func (e *Exports) Validate(vr *ValidationResults) { var serviceSubjects []Subject var streamSubjects []Subject for _, v := range *e { if v == nil { vr.AddError("null export is not allowed") continue } if v.IsService() { serviceSubjects = append(serviceSubjects, v.Subject) } else { streamSubjects = append(streamSubjects, v.Subject) } v.Validate(vr) } isContainedIn(Service, serviceSubjects, vr) isContainedIn(Stream, streamSubjects, vr) } // HasExportContainingSubject checks if the export list has an export with the provided subject func (e *Exports) HasExportContainingSubject(subject Subject) bool { for _, s := range *e { if subject.IsContainedIn(s.Subject) { return true } } return false } func (e Exports) Len() int { return len(e) } func (e Exports) Swap(i, j int) { e[i], e[j] = e[j], e[i] } func (e Exports) Less(i, j int) bool { return e[i].Subject < e[j].Subject }