Files
Курнат Андрей 2315f25754 Initial QSfera import
2026-06-07 10:20:04 +03:00

276 lines
8.5 KiB
Go

// Copyright 2018-2025 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 server
import (
"errors"
"fmt"
"net"
"os"
"strings"
"time"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nkeys"
)
// All JWTs once encoded start with this
const jwtPrefix = "eyJ"
// ReadOperatorJWT will read a jwt file for an operator claim. This can be a decorated file.
func ReadOperatorJWT(jwtfile string) (*jwt.OperatorClaims, error) {
_, claim, err := readOperatorJWT(jwtfile)
return claim, err
}
func readOperatorJWT(jwtfile string) (string, *jwt.OperatorClaims, error) {
contents, err := os.ReadFile(jwtfile)
if err != nil {
// Check to see if the JWT has been inlined.
if !strings.HasPrefix(jwtfile, jwtPrefix) {
return "", nil, err
}
// We may have an inline jwt here.
contents = []byte(jwtfile)
}
defer wipeSlice(contents)
theJWT, err := jwt.ParseDecoratedJWT(contents)
if err != nil {
return "", nil, err
}
opc, err := jwt.DecodeOperatorClaims(theJWT)
if err != nil {
return "", nil, err
}
return theJWT, opc, nil
}
// Just wipe slice with 'x', for clearing contents of nkey seed file.
func wipeSlice(buf []byte) {
for i := range buf {
buf[i] = 'x'
}
}
// validateTrustedOperators will check that we do not have conflicts with
// assigned trusted keys and trusted operators. If operators are defined we
// will expand the trusted keys in options.
func validateTrustedOperators(o *Options) error {
if len(o.TrustedOperators) == 0 {
// if we have no operator, default sentinel shouldn't be set
if o.DefaultSentinel != _EMPTY_ {
return fmt.Errorf("default sentinel requires operators and accounts")
}
return nil
}
if o.DefaultSentinel != _EMPTY_ {
juc, err := jwt.DecodeUserClaims(o.DefaultSentinel)
if err != nil {
return fmt.Errorf("default sentinel JWT not valid")
}
if !juc.BearerToken && juc.IssuerAccount != "" && juc.HasEmptyPermissions() {
// we cannot resolve the account yet - but this looks like a scoped user
// it will be rejected at runtime if not valid
} else if !juc.BearerToken {
return fmt.Errorf("default sentinel must be a bearer token")
}
}
if o.AccountResolver == nil {
return fmt.Errorf("operators require an account resolver to be configured")
}
if len(o.Accounts) > 0 {
return fmt.Errorf("operators do not allow Accounts to be configured directly")
}
if len(o.Users) > 0 || len(o.Nkeys) > 0 {
return fmt.Errorf("operators do not allow users to be configured directly")
}
if len(o.TrustedOperators) > 0 && len(o.TrustedKeys) > 0 {
return fmt.Errorf("conflicting options for 'TrustedKeys' and 'TrustedOperators'")
}
if o.SystemAccount != _EMPTY_ {
foundSys := false
foundNonEmpty := false
for _, op := range o.TrustedOperators {
if op.SystemAccount != _EMPTY_ {
foundNonEmpty = true
}
if op.SystemAccount == o.SystemAccount {
foundSys = true
break
}
}
if foundNonEmpty && !foundSys {
return fmt.Errorf("system_account in config and operator JWT must be identical")
}
} else if o.TrustedOperators[0].SystemAccount == _EMPTY_ {
// In case the system account is neither defined in config nor in the first operator.
// If it would be needed due to the nats account resolver, raise an error.
switch o.AccountResolver.(type) {
case *DirAccResolver, *CacheDirAccResolver:
return fmt.Errorf("using nats based account resolver - the system account needs to be specified in configuration or the operator jwt")
}
}
srvMajor, srvMinor, srvUpdate, _ := versionComponents(VERSION)
for _, opc := range o.TrustedOperators {
if major, minor, update, err := jwt.ParseServerVersion(opc.AssertServerVersion); err != nil {
return fmt.Errorf("operator %s expects version %s got error instead: %s",
opc.Subject, opc.AssertServerVersion, err)
} else if major > srvMajor {
return fmt.Errorf("operator %s expected major version %d > server major version %d",
opc.Subject, major, srvMajor)
} else if srvMajor > major {
} else if minor > srvMinor {
return fmt.Errorf("operator %s expected minor version %d > server minor version %d",
opc.Subject, minor, srvMinor)
} else if srvMinor > minor {
} else if update > srvUpdate {
return fmt.Errorf("operator %s expected update version %d > server update version %d",
opc.Subject, update, srvUpdate)
}
}
// If we have operators, fill in the trusted keys.
// FIXME(dlc) - We had TrustedKeys before TrustedOperators. The jwt.OperatorClaims
// has a DidSign(). Use that longer term. For now we can expand in place.
for _, opc := range o.TrustedOperators {
if o.TrustedKeys == nil {
o.TrustedKeys = make([]string, 0, 4)
}
if !opc.StrictSigningKeyUsage {
o.TrustedKeys = append(o.TrustedKeys, opc.Subject)
}
o.TrustedKeys = append(o.TrustedKeys, opc.SigningKeys...)
}
for _, key := range o.TrustedKeys {
if !nkeys.IsValidPublicOperatorKey(key) {
return fmt.Errorf("trusted Keys %q are required to be a valid public operator nkey", key)
}
}
if len(o.resolverPinnedAccounts) > 0 {
for key := range o.resolverPinnedAccounts {
if !nkeys.IsValidPublicAccountKey(key) {
return fmt.Errorf("pinned account key %q is not a valid public account nkey", key)
}
}
// ensure the system account (belonging to the operator can always connect)
if o.SystemAccount != _EMPTY_ {
o.resolverPinnedAccounts[o.SystemAccount] = struct{}{}
}
}
// If we have an auth callout defined make sure we are not in operator mode.
if o.AuthCallout != nil {
return errors.New("operators do not allow authorization callouts to be configured directly")
}
return nil
}
func validateSrc(claims *jwt.UserClaims, host string) bool {
if claims == nil {
return false
} else if len(claims.Src) == 0 {
return true
} else if host == "" {
return false
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
for _, cidr := range claims.Src {
if _, net, err := net.ParseCIDR(cidr); err != nil {
return false // should not happen as this jwt is invalid
} else if net.Contains(ip) {
return true
}
}
return false
}
func validateTimes(claims *jwt.UserClaims) (bool, time.Duration) {
return validateTimesAt(claims, time.Now())
}
func validateTimesAt(claims *jwt.UserClaims, now time.Time) (bool, time.Duration) {
if claims == nil {
return false, time.Duration(0)
} else if len(claims.Times) == 0 {
return true, time.Duration(0)
}
loc := time.Local
if claims.Locale != "" {
var err error
if loc, err = time.LoadLocation(claims.Locale); err != nil {
return false, time.Duration(0) // parsing not expected to fail at this point
}
now = now.In(loc)
}
var ok bool
var validFor time.Duration
for _, timeRange := range claims.Times {
start, err := time.ParseInLocation("15:04:05", timeRange.Start, loc)
if err != nil {
return false, time.Duration(0) // parsing not expected to fail at this point
}
end, err := time.ParseInLocation("15:04:05", timeRange.End, loc)
if err != nil {
return false, time.Duration(0) // parsing not expected to fail at this point
}
y, m, d := now.Date()
start = time.Date(y, m, d, start.Hour(), start.Minute(), start.Second(), 0, loc)
end = time.Date(y, m, d, end.Hour(), end.Minute(), end.Second(), 0, loc)
inRange, expires := validateTimeRangeAt(start, end, now)
if inRange && (!ok || expires > validFor) {
ok = true
validFor = expires
}
}
return ok, validFor
}
// Returns true if now is within `start` and `end`, and
// how much time is left until `end`.
// False if `now` is not within range.
func validateTimeRangeAt(start, end, now time.Time) (bool, time.Duration) {
// Now falls within range.
// For example 11:00-22:00 at 13:00
if start.Before(now) && end.After(now) {
return true, end.Sub(now)
}
// Range crosses midnight.
if start.After(end) {
// Now is after midnight.
// For example 22:00-06:00 at 05:00.
if end.After(now) {
return true, end.Sub(now)
}
// Now is before midnight.
// For example 22:00-06:00 at 23:30.
end = end.AddDate(0, 0, 1)
if start.Before(now) && end.After(now) {
return true, end.Sub(now)
}
}
return false, time.Duration(0)
}