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  }