/* * 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:/jwt/v1" // tools will use the prefix and build queries by appending /accounts/ // 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 ..") } 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 }