Initial QSfera import
This commit is contained in:
+275
@@ -0,0 +1,275 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user