The live tests reached one shared bus and assert, read and remove the mesh's own objects by their fixed names, so packages run in parallel deleted what each other read and the suite passed only one package at a time; a red suite read as noise. internal/testbus starts a server per test, linked in at the nats-server release go.mod pins, and a test holds that pin to the catalogue's bus image and to the facts snapshot's bus when there is one, so the tests never run a bus the mesh does not. The waiter test read a timing (the most connections held at one look) and now reads the state it means (the fewest held across the wait). make check runs the packages in parallel under the race detector, with a timeout.
258 lines
7.5 KiB
Go
258 lines
7.5 KiB
Go
/*
|
|
* Copyright 2018-2024 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 (
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/nats-io/nkeys"
|
|
)
|
|
|
|
// Operator specific claims
|
|
type Operator struct {
|
|
// Slice of other operator NKeys that can be used to sign on behalf of the main
|
|
// operator identity.
|
|
SigningKeys StringList `json:"signing_keys,omitempty"`
|
|
// AccountServerURL is a partial URL like "https://host.domain.org:<port>/jwt/v1"
|
|
// tools will use the prefix and build queries by appending /accounts/<account_id>
|
|
// or /operator to the path provided. Note this assumes that the account server
|
|
// can handle requests in a nats-account-server compatible way. See
|
|
// https://github.com/nats-io/nats-account-server.
|
|
AccountServerURL string `json:"account_server_url,omitempty"`
|
|
// A list of NATS urls (tls://host:port) where tools can connect to the server
|
|
// using proper credentials.
|
|
OperatorServiceURLs StringList `json:"operator_service_urls,omitempty"`
|
|
// Identity of the system account
|
|
SystemAccount string `json:"system_account,omitempty"`
|
|
// Min Server version
|
|
AssertServerVersion string `json:"assert_server_version,omitempty"`
|
|
// Signing of subordinate objects will require signing keys
|
|
StrictSigningKeyUsage bool `json:"strict_signing_key_usage,omitempty"`
|
|
GenericFields
|
|
}
|
|
|
|
func ParseServerVersion(version string) (int, int, int, error) {
|
|
if version == "" {
|
|
return 0, 0, 0, nil
|
|
}
|
|
split := strings.Split(version, ".")
|
|
if len(split) != 3 {
|
|
return 0, 0, 0, fmt.Errorf("asserted server version must be of the form <major>.<minor>.<update>")
|
|
} else if major, err := strconv.Atoi(split[0]); err != nil {
|
|
return 0, 0, 0, fmt.Errorf("asserted server version cant parse %s to int", split[0])
|
|
} else if minor, err := strconv.Atoi(split[1]); err != nil {
|
|
return 0, 0, 0, fmt.Errorf("asserted server version cant parse %s to int", split[1])
|
|
} else if update, err := strconv.Atoi(split[2]); err != nil {
|
|
return 0, 0, 0, fmt.Errorf("asserted server version cant parse %s to int", split[2])
|
|
} else if major < 0 || minor < 0 || update < 0 {
|
|
return 0, 0, 0, fmt.Errorf("asserted server version can'b contain negative values: %s", version)
|
|
} else {
|
|
return major, minor, update, nil
|
|
}
|
|
}
|
|
|
|
// Validate checks the validity of the operators contents
|
|
func (o *Operator) Validate(vr *ValidationResults) {
|
|
if err := o.validateAccountServerURL(); err != nil {
|
|
vr.AddError("%s", err.Error())
|
|
}
|
|
|
|
for _, v := range o.validateOperatorServiceURLs() {
|
|
if v != nil {
|
|
vr.AddError("%s", v.Error())
|
|
}
|
|
}
|
|
|
|
for _, k := range o.SigningKeys {
|
|
if !nkeys.IsValidPublicOperatorKey(k) {
|
|
vr.AddError("%s is not an operator public key", k)
|
|
}
|
|
}
|
|
if o.SystemAccount != "" {
|
|
if !nkeys.IsValidPublicAccountKey(o.SystemAccount) {
|
|
vr.AddError("%s is not an account public key", o.SystemAccount)
|
|
}
|
|
}
|
|
if _, _, _, err := ParseServerVersion(o.AssertServerVersion); err != nil {
|
|
vr.AddError("assert server version error: %s", err)
|
|
}
|
|
}
|
|
|
|
func (o *Operator) validateAccountServerURL() error {
|
|
if o.AccountServerURL != "" {
|
|
// We don't care what kind of URL it is so long as it parses
|
|
// and has a protocol. The account server may impose additional
|
|
// constraints on the type of URLs that it is able to notify to
|
|
u, err := url.Parse(o.AccountServerURL)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing account server url: %v", err)
|
|
}
|
|
if u.Scheme == "" {
|
|
return fmt.Errorf("account server url %q requires a protocol", o.AccountServerURL)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateOperatorServiceURL returns an error if the URL is not a valid NATS or TLS url.
|
|
func ValidateOperatorServiceURL(v string) error {
|
|
// should be possible for the service url to not be expressed
|
|
if v == "" {
|
|
return nil
|
|
}
|
|
u, err := url.Parse(v)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing operator service url %q: %v", v, err)
|
|
}
|
|
|
|
if u.User != nil {
|
|
return fmt.Errorf("operator service url %q - credentials are not supported", v)
|
|
}
|
|
|
|
if u.Path != "" {
|
|
return fmt.Errorf("operator service url %q - paths are not supported", v)
|
|
}
|
|
|
|
lcs := strings.ToLower(u.Scheme)
|
|
switch lcs {
|
|
case "nats":
|
|
return nil
|
|
case "tls":
|
|
return nil
|
|
case "ws":
|
|
return nil
|
|
case "wss":
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("operator service url %q - protocol not supported (only 'nats', 'tls', 'ws', 'wss' only)", v)
|
|
}
|
|
}
|
|
|
|
func (o *Operator) validateOperatorServiceURLs() []error {
|
|
var errs []error
|
|
for _, v := range o.OperatorServiceURLs {
|
|
if v != "" {
|
|
if err := ValidateOperatorServiceURL(v); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
}
|
|
return errs
|
|
}
|
|
|
|
// OperatorClaims define the data for an operator JWT
|
|
type OperatorClaims struct {
|
|
ClaimsData
|
|
Operator `json:"nats,omitempty"`
|
|
}
|
|
|
|
// NewOperatorClaims creates a new operator claim with the specified subject, which should be an operator public key
|
|
func NewOperatorClaims(subject string) *OperatorClaims {
|
|
if subject == "" {
|
|
return nil
|
|
}
|
|
c := &OperatorClaims{}
|
|
c.Subject = subject
|
|
c.Issuer = subject
|
|
return c
|
|
}
|
|
|
|
// DidSign checks the claims against the operator's public key and its signing keys
|
|
func (oc *OperatorClaims) DidSign(op Claims) bool {
|
|
if op == nil {
|
|
return false
|
|
}
|
|
issuer := op.Claims().Issuer
|
|
if issuer == oc.Subject {
|
|
if !oc.StrictSigningKeyUsage {
|
|
return true
|
|
}
|
|
return op.Claims().Subject == oc.Subject
|
|
}
|
|
return oc.SigningKeys.Contains(issuer)
|
|
}
|
|
|
|
// Encode the claims into a JWT string
|
|
func (oc *OperatorClaims) Encode(pair nkeys.KeyPair) (string, error) {
|
|
return oc.EncodeWithSigner(pair, nil)
|
|
}
|
|
|
|
func (oc *OperatorClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
|
|
if !nkeys.IsValidPublicOperatorKey(oc.Subject) {
|
|
return "", errors.New("expected subject to be an operator public key")
|
|
}
|
|
err := oc.validateAccountServerURL()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
oc.Type = OperatorClaim
|
|
return oc.ClaimsData.encode(pair, oc, fn)
|
|
}
|
|
|
|
func (oc *OperatorClaims) ClaimType() ClaimType {
|
|
return oc.Type
|
|
}
|
|
|
|
// DecodeOperatorClaims tries to create an operator claims from a JWt string
|
|
func DecodeOperatorClaims(token string) (*OperatorClaims, error) {
|
|
claims, err := Decode(token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
oc, ok := claims.(*OperatorClaims)
|
|
if !ok {
|
|
return nil, errors.New("not operator claim")
|
|
}
|
|
return oc, nil
|
|
}
|
|
|
|
func (oc *OperatorClaims) String() string {
|
|
return oc.ClaimsData.String(oc)
|
|
}
|
|
|
|
// Payload returns the operator specific data for an operator JWT
|
|
func (oc *OperatorClaims) Payload() interface{} {
|
|
return &oc.Operator
|
|
}
|
|
|
|
// Validate the contents of the claims
|
|
func (oc *OperatorClaims) Validate(vr *ValidationResults) {
|
|
oc.ClaimsData.Validate(vr)
|
|
oc.Operator.Validate(vr)
|
|
}
|
|
|
|
// ExpectedPrefixes defines the nkey types that can sign operator claims, operator
|
|
func (oc *OperatorClaims) ExpectedPrefixes() []nkeys.PrefixByte {
|
|
return []nkeys.PrefixByte{nkeys.PrefixByteOperator}
|
|
}
|
|
|
|
// Claims returns the generic claims data
|
|
func (oc *OperatorClaims) Claims() *ClaimsData {
|
|
return &oc.ClaimsData
|
|
}
|
|
|
|
func (oc *OperatorClaims) updateVersion() {
|
|
oc.GenericFields.Version = libVersion
|
|
}
|
|
|
|
func (oc *OperatorClaims) GetTags() TagList {
|
|
return oc.Operator.Tags
|
|
}
|