Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+18
View File
@@ -0,0 +1,18 @@
.PHONY: test cover
build:
go build
fmt:
gofmt -w -s *.go
goimports -w *.go
go mod tidy
test:
go vet ./...
staticcheck ./...
rm -rf ./coverage.out
go test -v -coverprofile=./coverage.out ./...
cover:
go tool cover -html=coverage.out
+485
View File
@@ -0,0 +1,485 @@
/*
* 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"
"sort"
"time"
"github.com/nats-io/nkeys"
)
// NoLimit is used to indicate a limit field is unlimited in value.
const (
NoLimit = -1
AnyAccount = "*"
)
type AccountLimits struct {
Imports int64 `json:"imports,omitempty"` // Max number of imports
Exports int64 `json:"exports,omitempty"` // Max number of exports
WildcardExports bool `json:"wildcards,omitempty"` // Are wildcards allowed in exports
DisallowBearer bool `json:"disallow_bearer,omitempty"` // User JWT can't be bearer token
Conn int64 `json:"conn,omitempty"` // Max number of active connections
LeafNodeConn int64 `json:"leaf,omitempty"` // Max number of active leaf node connections
}
// IsUnlimited returns true if all limits are unlimited
func (a *AccountLimits) IsUnlimited() bool {
return *a == AccountLimits{NoLimit, NoLimit, true, false, NoLimit, NoLimit}
}
type NatsLimits struct {
Subs int64 `json:"subs,omitempty"` // Max number of subscriptions
Data int64 `json:"data,omitempty"` // Max number of bytes
Payload int64 `json:"payload,omitempty"` // Max message payload
}
// IsUnlimited returns true if all limits are unlimited
func (n *NatsLimits) IsUnlimited() bool {
return *n == NatsLimits{NoLimit, NoLimit, NoLimit}
}
type JetStreamLimits struct {
MemoryStorage int64 `json:"mem_storage,omitempty"` // Max number of bytes stored in memory across all streams. (0 means disabled)
DiskStorage int64 `json:"disk_storage,omitempty"` // Max number of bytes stored on disk across all streams. (0 means disabled)
Streams int64 `json:"streams,omitempty"` // Max number of streams
Consumer int64 `json:"consumer,omitempty"` // Max number of consumers
MaxAckPending int64 `json:"max_ack_pending,omitempty"` // Max ack pending of a Stream
MemoryMaxStreamBytes int64 `json:"mem_max_stream_bytes,omitempty"` // Max bytes a memory backed stream can have. (0 means disabled/unlimited)
DiskMaxStreamBytes int64 `json:"disk_max_stream_bytes,omitempty"` // Max bytes a disk backed stream can have. (0 means disabled/unlimited)
MaxBytesRequired bool `json:"max_bytes_required,omitempty"` // Max bytes required by all Streams
}
// IsUnlimited returns true if all limits are unlimited
func (j *JetStreamLimits) IsUnlimited() bool {
lim := *j
// workaround in case NoLimit was used instead of 0
if lim.MemoryMaxStreamBytes < 0 {
lim.MemoryMaxStreamBytes = 0
}
if lim.DiskMaxStreamBytes < 0 {
lim.DiskMaxStreamBytes = 0
}
if lim.MaxAckPending < 0 {
lim.MaxAckPending = 0
}
return lim == JetStreamLimits{NoLimit, NoLimit, NoLimit, NoLimit, 0, 0, 0, false}
}
type JetStreamTieredLimits map[string]JetStreamLimits
// OperatorLimits are used to limit access by an account
type OperatorLimits struct {
NatsLimits
AccountLimits
JetStreamLimits
JetStreamTieredLimits `json:"tiered_limits,omitempty"`
}
// IsJSEnabled returns if this account claim has JS enabled either through a tier or the non tiered limits.
func (o *OperatorLimits) IsJSEnabled() bool {
if len(o.JetStreamTieredLimits) > 0 {
for _, l := range o.JetStreamTieredLimits {
if l.MemoryStorage != 0 || l.DiskStorage != 0 {
return true
}
}
return false
}
l := o.JetStreamLimits
return l.MemoryStorage != 0 || l.DiskStorage != 0
}
// IsEmpty returns true if all limits are 0/false/empty.
func (o *OperatorLimits) IsEmpty() bool {
return o.NatsLimits == NatsLimits{} &&
o.AccountLimits == AccountLimits{} &&
o.JetStreamLimits == JetStreamLimits{} &&
len(o.JetStreamTieredLimits) == 0
}
// IsUnlimited returns true if all limits are unlimited
func (o *OperatorLimits) IsUnlimited() bool {
return o.AccountLimits.IsUnlimited() && o.NatsLimits.IsUnlimited() &&
o.JetStreamLimits.IsUnlimited() && len(o.JetStreamTieredLimits) == 0
}
// Validate checks that the operator limits contain valid values
func (o *OperatorLimits) Validate(vr *ValidationResults) {
// negative values mean unlimited, so all numbers are valid
if len(o.JetStreamTieredLimits) > 0 {
if (o.JetStreamLimits != JetStreamLimits{}) {
vr.AddError("JetStream Limits and tiered JetStream Limits are mutually exclusive")
}
if _, ok := o.JetStreamTieredLimits[""]; ok {
vr.AddError(`Tiered JetStream Limits can not contain a blank "" tier name`)
}
}
}
// WeightedMapping for publishes
type WeightedMapping struct {
Subject Subject `json:"subject"`
Weight uint8 `json:"weight,omitempty"`
Cluster string `json:"cluster,omitempty"`
}
func (m *WeightedMapping) GetWeight() uint8 {
if m.Weight == 0 {
return 100
}
return m.Weight
}
type Mapping map[Subject][]WeightedMapping
func (m *Mapping) Validate(vr *ValidationResults) {
for ubFrom, wm := range (map[Subject][]WeightedMapping)(*m) {
ubFrom.Validate(vr)
perCluster := make(map[string]uint32)
total := uint32(0)
for _, e := range wm {
e.Subject.Validate(vr)
if e.GetWeight() > 100 {
vr.AddError("Mapping %q has a weight %d that exceeds 100", ubFrom, e.GetWeight())
}
if e.Cluster != "" {
t := perCluster[e.Cluster]
t += uint32(e.GetWeight())
perCluster[e.Cluster] = t
if t > 100 {
vr.AddError("Mapping %q in cluster %q exceeds 100%% among all of it's weighted to mappings", ubFrom, e.Cluster)
}
} else {
total += uint32(e.GetWeight())
}
}
if total > 100 {
vr.AddError("Mapping %q exceeds 100%% among all of it's weighted to mappings", ubFrom)
}
}
}
func (a *Account) AddMapping(sub Subject, to ...WeightedMapping) {
a.Mappings[sub] = to
}
// ExternalAuthorization enables external authorization for account users.
// AuthUsers are those users specified to bypass the authorization callout and should be used for the authorization service itself.
// AllowedAccounts specifies which accounts, if any, that the authorization service can bind an authorized user to.
// The authorization response, a user JWT, will still need to be signed by the correct account.
// If optional XKey is specified, that is the public xkey (x25519) and the server will encrypt the request such that only the
// holder of the private key can decrypt. The auth service can also optionally encrypt the response back to the server using it's
// public xkey which will be in the authorization request.
type ExternalAuthorization struct {
AuthUsers StringList `json:"auth_users,omitempty"`
AllowedAccounts StringList `json:"allowed_accounts,omitempty"`
XKey string `json:"xkey,omitempty"`
}
func (ac *ExternalAuthorization) IsEnabled() bool {
return len(ac.AuthUsers) > 0
}
// HasExternalAuthorization helper function to determine if external authorization is enabled.
func (a *Account) HasExternalAuthorization() bool {
return a.Authorization.IsEnabled()
}
// EnableExternalAuthorization helper function to setup external authorization.
func (a *Account) EnableExternalAuthorization(users ...string) {
a.Authorization.AuthUsers.Add(users...)
}
func (ac *ExternalAuthorization) Validate(vr *ValidationResults) {
if len(ac.AllowedAccounts) > 0 && len(ac.AuthUsers) == 0 {
vr.AddError("External authorization cannot have accounts without users specified")
}
// Make sure users are all valid user nkeys.
// Make sure allowed accounts are all valid account nkeys.
for _, u := range ac.AuthUsers {
if !nkeys.IsValidPublicUserKey(u) {
vr.AddError("AuthUser %q is not a valid user public key", u)
}
}
for _, a := range ac.AllowedAccounts {
if a == AnyAccount && len(ac.AllowedAccounts) > 1 {
vr.AddError("AllowedAccounts can only be a list of accounts or %q", AnyAccount)
continue
} else if a == AnyAccount {
continue
} else if !nkeys.IsValidPublicAccountKey(a) {
vr.AddError("Account %q is not a valid account public key", a)
}
}
if ac.XKey != "" && !nkeys.IsValidPublicCurveKey(ac.XKey) {
vr.AddError("XKey %q is not a valid public xkey", ac.XKey)
}
}
const (
ClusterTrafficSystem = "system"
ClusterTrafficOwner = "owner"
)
type ClusterTraffic string
func (ct ClusterTraffic) Valid() error {
if ct == "" || ct == ClusterTrafficSystem || ct == ClusterTrafficOwner {
return nil
}
return fmt.Errorf("unknown cluster traffic option: %q", ct)
}
// Account holds account specific claims data
type Account struct {
Imports Imports `json:"imports,omitempty"`
Exports Exports `json:"exports,omitempty"`
Limits OperatorLimits `json:"limits,omitempty"`
SigningKeys SigningKeys `json:"signing_keys,omitempty"`
Revocations RevocationList `json:"revocations,omitempty"`
DefaultPermissions Permissions `json:"default_permissions,omitempty"`
Mappings Mapping `json:"mappings,omitempty"`
Authorization ExternalAuthorization `json:"authorization,omitempty"`
Trace *MsgTrace `json:"trace,omitempty"`
ClusterTraffic ClusterTraffic `json:"cluster_traffic,omitempty"`
Info
GenericFields
}
// MsgTrace holds distributed message tracing configuration
type MsgTrace struct {
// Destination is the subject the server will send message traces to
// if the inbound message contains the "traceparent" header and has
// its sampled field indicating that the trace should be triggered.
Destination Subject `json:"dest,omitempty"`
// Sampling is used to set the probability sampling, that is, the
// server will get a random number between 1 and 100 and trigger
// the trace if the number is lower than this Sampling value.
// The valid range is [1..100]. If the value is not set Validate()
// will set the value to 100.
Sampling int `json:"sampling,omitempty"`
}
// Validate checks if the account is valid, based on the wrapper
func (a *Account) Validate(acct *AccountClaims, vr *ValidationResults) {
a.Imports.Validate(acct.Subject, vr)
a.Exports.Validate(vr)
a.Limits.Validate(vr)
a.DefaultPermissions.Validate(vr)
a.Mappings.Validate(vr)
a.Authorization.Validate(vr)
if a.Trace != nil {
tvr := CreateValidationResults()
a.Trace.Destination.Validate(tvr)
if !tvr.IsEmpty() {
vr.AddError("the account Trace.Destination %s", tvr.Issues[0].Description)
}
if a.Trace.Destination.HasWildCards() {
vr.AddError("the account Trace.Destination subject %q is not a valid publish subject", a.Trace.Destination)
}
if a.Trace.Sampling < 0 || a.Trace.Sampling > 100 {
vr.AddError("the account Trace.Sampling value '%d' is not valid, should be in the range [1..100]", a.Trace.Sampling)
} else if a.Trace.Sampling == 0 {
a.Trace.Sampling = 100
}
}
if !a.Limits.IsEmpty() && a.Limits.Imports >= 0 && int64(len(a.Imports)) > a.Limits.Imports {
vr.AddError("the account contains more imports than allowed by the operator")
}
// Check Imports and Exports for limit violations.
if a.Limits.Imports != NoLimit {
if int64(len(a.Imports)) > a.Limits.Imports {
vr.AddError("the account contains more imports than allowed by the operator")
}
}
if a.Limits.Exports != NoLimit {
if int64(len(a.Exports)) > a.Limits.Exports {
vr.AddError("the account contains more exports than allowed by the operator")
}
// Check for wildcard restrictions
if !a.Limits.WildcardExports {
for _, ex := range a.Exports {
if ex.Subject.HasWildCards() {
vr.AddError("the account contains wildcard exports that are not allowed by the operator")
}
}
}
}
a.SigningKeys.Validate(vr)
a.Info.Validate(vr)
if err := a.ClusterTraffic.Valid(); err != nil {
vr.AddError("%s", err.Error())
}
}
// AccountClaims defines the body of an account JWT
type AccountClaims struct {
ClaimsData
Account `json:"nats,omitempty"`
}
// NewAccountClaims creates a new account JWT
func NewAccountClaims(subject string) *AccountClaims {
if subject == "" {
return nil
}
c := &AccountClaims{}
c.SigningKeys = make(SigningKeys)
// Set to unlimited to start. We do it this way so we get compiler
// errors if we add to the OperatorLimits.
c.Limits = OperatorLimits{
NatsLimits{NoLimit, NoLimit, NoLimit},
AccountLimits{NoLimit, NoLimit, true, false, NoLimit, NoLimit},
JetStreamLimits{0, 0, 0, 0, 0, 0, 0, false},
JetStreamTieredLimits{},
}
c.Subject = subject
c.Mappings = Mapping{}
return c
}
// Encode converts account claims into a JWT string
func (a *AccountClaims) Encode(pair nkeys.KeyPair) (string, error) {
return a.EncodeWithSigner(pair, nil)
}
func (a *AccountClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
if !nkeys.IsValidPublicAccountKey(a.Subject) {
return "", errors.New("expected subject to be account public key")
}
sort.Sort(a.Exports)
sort.Sort(a.Imports)
a.Type = AccountClaim
return a.ClaimsData.encode(pair, a, fn)
}
// DecodeAccountClaims decodes account claims from a JWT string
func DecodeAccountClaims(token string) (*AccountClaims, error) {
claims, err := Decode(token)
if err != nil {
return nil, err
}
ac, ok := claims.(*AccountClaims)
if !ok {
return nil, errors.New("not account claim")
}
return ac, nil
}
func (a *AccountClaims) String() string {
return a.ClaimsData.String(a)
}
// Payload pulls the accounts specific payload out of the claims
func (a *AccountClaims) Payload() interface{} {
return &a.Account
}
// Validate checks the accounts contents
func (a *AccountClaims) Validate(vr *ValidationResults) {
a.ClaimsData.Validate(vr)
a.Account.Validate(a, vr)
if nkeys.IsValidPublicAccountKey(a.ClaimsData.Issuer) {
if !a.Limits.IsEmpty() {
vr.AddWarning("self-signed account JWTs shouldn't contain operator limits")
}
}
}
func (a *AccountClaims) ClaimType() ClaimType {
return a.Type
}
func (a *AccountClaims) updateVersion() {
a.GenericFields.Version = libVersion
}
// ExpectedPrefixes defines the types that can encode an account jwt, account and operator
func (a *AccountClaims) ExpectedPrefixes() []nkeys.PrefixByte {
return []nkeys.PrefixByte{nkeys.PrefixByteAccount, nkeys.PrefixByteOperator}
}
// Claims returns the accounts claims data
func (a *AccountClaims) Claims() *ClaimsData {
return &a.ClaimsData
}
func (a *AccountClaims) GetTags() TagList {
return a.Account.Tags
}
// DidSign checks the claims against the account's public key and its signing keys
func (a *AccountClaims) DidSign(c Claims) bool {
if c != nil {
issuer := c.Claims().Issuer
if issuer == a.Subject {
return true
}
uc, ok := c.(*UserClaims)
if ok && uc.IssuerAccount == a.Subject {
return a.SigningKeys.Contains(issuer)
}
at, ok := c.(*ActivationClaims)
if ok && at.IssuerAccount == a.Subject {
return a.SigningKeys.Contains(issuer)
}
}
return false
}
// Revoke enters a revocation by public key using time.Now().
func (a *AccountClaims) Revoke(pubKey string) {
a.RevokeAt(pubKey, time.Now())
}
// RevokeAt enters a revocation by public key and timestamp into this account
// This will revoke all jwt issued for pubKey, prior to timestamp
// If there is already a revocation for this public key that is newer, it is kept.
// The value is expected to be a public key or "*" (means all public keys)
func (a *AccountClaims) RevokeAt(pubKey string, timestamp time.Time) {
if a.Revocations == nil {
a.Revocations = RevocationList{}
}
a.Revocations.Revoke(pubKey, timestamp)
}
// ClearRevocation removes any revocation for the public key
func (a *AccountClaims) ClearRevocation(pubKey string) {
a.Revocations.ClearRevocation(pubKey)
}
// isRevoked checks if the public key is in the revoked list with a timestamp later than the one passed in.
// Generally this method is called with the subject and issue time of the jwt to be tested.
// DO NOT pass time.Now(), it will not produce a stable/expected response.
func (a *AccountClaims) isRevoked(pubKey string, claimIssuedAt time.Time) bool {
return a.Revocations.IsRevoked(pubKey, claimIssuedAt)
}
// IsClaimRevoked checks if the account revoked the claim passed in.
// Invalid claims (nil, no Subject or IssuedAt) will return true.
func (a *AccountClaims) IsClaimRevoked(claim *UserClaims) bool {
if claim == nil || claim.IssuedAt == 0 || claim.Subject == "" {
return true
}
return a.isRevoked(claim.Subject, time.Unix(claim.IssuedAt, 0))
}
+182
View File
@@ -0,0 +1,182 @@
/*
* 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 (
"crypto/sha256"
"encoding/base32"
"errors"
"fmt"
"strings"
"github.com/nats-io/nkeys"
)
// Activation defines the custom parts of an activation claim
type Activation struct {
ImportSubject Subject `json:"subject,omitempty"`
ImportType ExportType `json:"kind,omitempty"`
// IssuerAccount stores the public key for the account the issuer represents.
// When set, the claim was issued by a signing key.
IssuerAccount string `json:"issuer_account,omitempty"`
GenericFields
}
// IsService returns true if an Activation is for a service
func (a *Activation) IsService() bool {
return a.ImportType == Service
}
// IsStream returns true if an Activation is for a stream
func (a *Activation) IsStream() bool {
return a.ImportType == Stream
}
// Validate checks the exports and limits in an activation JWT
func (a *Activation) Validate(vr *ValidationResults) {
if !a.IsService() && !a.IsStream() {
vr.AddError("invalid import type: %q", a.ImportType)
}
a.ImportSubject.Validate(vr)
}
// ActivationClaims holds the data specific to an activation JWT
type ActivationClaims struct {
ClaimsData
Activation `json:"nats,omitempty"`
}
// NewActivationClaims creates a new activation claim with the provided sub
func NewActivationClaims(subject string) *ActivationClaims {
if subject == "" {
return nil
}
ac := &ActivationClaims{}
ac.Subject = subject
return ac
}
// Encode turns an activation claim into a JWT strimg
func (a *ActivationClaims) Encode(pair nkeys.KeyPair) (string, error) {
return a.EncodeWithSigner(pair, nil)
}
func (a *ActivationClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
if !nkeys.IsValidPublicAccountKey(a.ClaimsData.Subject) {
return "", errors.New("expected subject to be an account")
}
a.Type = ActivationClaim
return a.ClaimsData.encode(pair, a, fn)
}
// DecodeActivationClaims tries to create an activation claim from a JWT string
func DecodeActivationClaims(token string) (*ActivationClaims, error) {
claims, err := Decode(token)
if err != nil {
return nil, err
}
ac, ok := claims.(*ActivationClaims)
if !ok {
return nil, errors.New("not activation claim")
}
return ac, nil
}
// Payload returns the activation specific part of the JWT
func (a *ActivationClaims) Payload() interface{} {
return a.Activation
}
// Validate checks the claims
func (a *ActivationClaims) Validate(vr *ValidationResults) {
a.validateWithTimeChecks(vr, true)
}
// Validate checks the claims
func (a *ActivationClaims) validateWithTimeChecks(vr *ValidationResults, timeChecks bool) {
if timeChecks {
a.ClaimsData.Validate(vr)
}
a.Activation.Validate(vr)
if a.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(a.IssuerAccount) {
vr.AddError("account_id is not an account public key")
}
}
func (a *ActivationClaims) ClaimType() ClaimType {
return a.Type
}
func (a *ActivationClaims) updateVersion() {
a.GenericFields.Version = libVersion
}
// ExpectedPrefixes defines the types that can sign an activation jwt, account and oeprator
func (a *ActivationClaims) ExpectedPrefixes() []nkeys.PrefixByte {
return []nkeys.PrefixByte{nkeys.PrefixByteAccount, nkeys.PrefixByteOperator}
}
// Claims returns the generic part of the JWT
func (a *ActivationClaims) Claims() *ClaimsData {
return &a.ClaimsData
}
func (a *ActivationClaims) String() string {
return a.ClaimsData.String(a)
}
// HashID returns a hash of the claims that can be used to identify it.
// The hash is calculated by creating a string with
// issuerPubKey.subjectPubKey.<subject> and constructing the sha-256 hash and base32 encoding that.
// <subject> is the exported subject, minus any wildcards, so foo.* becomes foo.
// the one special case is that if the export start with "*" or is ">" the <subject> "_"
func (a *ActivationClaims) HashID() (string, error) {
if a.Issuer == "" || a.Subject == "" || a.ImportSubject == "" {
return "", fmt.Errorf("not enough data in the activaion claims to create a hash")
}
subject := cleanSubject(string(a.ImportSubject))
base := fmt.Sprintf("%s.%s.%s", a.Issuer, a.Subject, subject)
h := sha256.New()
h.Write([]byte(base))
sha := h.Sum(nil)
hash := base32.StdEncoding.EncodeToString(sha)
return hash, nil
}
func cleanSubject(subject string) string {
split := strings.Split(subject, ".")
cleaned := ""
for i, tok := range split {
if tok == "*" || tok == ">" {
if i == 0 {
cleaned = "_"
break
}
cleaned = strings.Join(split[:i], ".")
break
}
}
if cleaned == "" {
cleaned = subject
}
return cleaned
}
+255
View File
@@ -0,0 +1,255 @@
/*
* Copyright 2022-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"
"github.com/nats-io/nkeys"
)
// ServerID is basic static info for a NATS server.
type ServerID struct {
Name string `json:"name"`
Host string `json:"host"`
ID string `json:"id"`
Version string `json:"version,omitempty"`
Cluster string `json:"cluster,omitempty"`
Tags TagList `json:"tags,omitempty"`
XKey string `json:"xkey,omitempty"`
}
// ClientInformation is information about a client that is trying to authorize.
type ClientInformation struct {
Host string `json:"host,omitempty"`
ID uint64 `json:"id,omitempty"`
User string `json:"user,omitempty"`
Name string `json:"name,omitempty"`
Tags TagList `json:"tags,omitempty"`
NameTag string `json:"name_tag,omitempty"`
Kind string `json:"kind,omitempty"`
Type string `json:"type,omitempty"`
MQTT string `json:"mqtt_id,omitempty"`
Nonce string `json:"nonce,omitempty"`
}
// ConnectOptions represents options that were set in the CONNECT protocol from the client
// during authorization.
type ConnectOptions struct {
JWT string `json:"jwt,omitempty"`
Nkey string `json:"nkey,omitempty"`
SignedNonce string `json:"sig,omitempty"`
Token string `json:"auth_token,omitempty"`
Username string `json:"user,omitempty"`
Password string `json:"pass,omitempty"`
Name string `json:"name,omitempty"`
Lang string `json:"lang,omitempty"`
Version string `json:"version,omitempty"`
Protocol int `json:"protocol"`
}
// ClientTLS is information about TLS state if present, including client certs.
// If the client certs were present and verified they will be under verified chains
// with the client peer cert being VerifiedChains[0]. These are complete and pem encoded.
// If they were not verified, they will be under certs.
type ClientTLS struct {
Version string `json:"version,omitempty"`
Cipher string `json:"cipher,omitempty"`
Certs StringList `json:"certs,omitempty"`
VerifiedChains []StringList `json:"verified_chains,omitempty"`
}
// AuthorizationRequest represents all the information we know about the client that
// will be sent to an external authorization service.
type AuthorizationRequest struct {
Server ServerID `json:"server_id"`
UserNkey string `json:"user_nkey"`
ClientInformation ClientInformation `json:"client_info"`
ConnectOptions ConnectOptions `json:"connect_opts"`
TLS *ClientTLS `json:"client_tls,omitempty"`
RequestNonce string `json:"request_nonce,omitempty"`
GenericFields
}
// AuthorizationRequestClaims defines an external auth request JWT.
// These wil be signed by a NATS server.
type AuthorizationRequestClaims struct {
ClaimsData
AuthorizationRequest `json:"nats"`
}
// NewAuthorizationRequestClaims creates an auth request JWT with the specific subject/public key.
func NewAuthorizationRequestClaims(subject string) *AuthorizationRequestClaims {
if subject == "" {
return nil
}
var ac AuthorizationRequestClaims
ac.Subject = subject
return &ac
}
// Validate checks the generic and specific parts of the auth request jwt.
func (ac *AuthorizationRequestClaims) Validate(vr *ValidationResults) {
if ac.UserNkey == "" {
vr.AddError("User nkey is required")
} else if !nkeys.IsValidPublicUserKey(ac.UserNkey) {
vr.AddError("User nkey %q is not a valid user public key", ac.UserNkey)
}
ac.ClaimsData.Validate(vr)
}
// Encode tries to turn the auth request claims into a JWT string.
func (ac *AuthorizationRequestClaims) Encode(pair nkeys.KeyPair) (string, error) {
return ac.EncodeWithSigner(pair, nil)
}
func (ac *AuthorizationRequestClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
ac.Type = AuthorizationRequestClaim
return ac.ClaimsData.encode(pair, ac, fn)
}
// DecodeAuthorizationRequestClaims tries to parse an auth request claims from a JWT string
func DecodeAuthorizationRequestClaims(token string) (*AuthorizationRequestClaims, error) {
claims, err := Decode(token)
if err != nil {
return nil, err
}
ac, ok := claims.(*AuthorizationRequestClaims)
if !ok {
return nil, errors.New("not an authorization request claim")
}
return ac, nil
}
// ExpectedPrefixes defines the types that can encode an auth request jwt, servers.
func (ac *AuthorizationRequestClaims) ExpectedPrefixes() []nkeys.PrefixByte {
return []nkeys.PrefixByte{nkeys.PrefixByteServer}
}
func (ac *AuthorizationRequestClaims) ClaimType() ClaimType {
return ac.Type
}
// Claims returns the request claims data.
func (ac *AuthorizationRequestClaims) Claims() *ClaimsData {
return &ac.ClaimsData
}
// Payload pulls the request specific payload out of the claims.
func (ac *AuthorizationRequestClaims) Payload() interface{} {
return &ac.AuthorizationRequest
}
func (ac *AuthorizationRequestClaims) String() string {
return ac.ClaimsData.String(ac)
}
func (ac *AuthorizationRequestClaims) updateVersion() {
ac.GenericFields.Version = libVersion
}
type AuthorizationResponse struct {
Jwt string `json:"jwt,omitempty"`
Error string `json:"error,omitempty"`
// IssuerAccount stores the public key for the account the issuer represents.
// When set, the claim was issued by a signing key.
IssuerAccount string `json:"issuer_account,omitempty"`
GenericFields
}
type AuthorizationResponseClaims struct {
ClaimsData
AuthorizationResponse `json:"nats"`
}
func NewAuthorizationResponseClaims(subject string) *AuthorizationResponseClaims {
if subject == "" {
return nil
}
var ac AuthorizationResponseClaims
ac.Subject = subject
return &ac
}
// DecodeAuthorizationResponseClaims tries to parse an auth request claims from a JWT string
func DecodeAuthorizationResponseClaims(token string) (*AuthorizationResponseClaims, error) {
claims, err := Decode(token)
if err != nil {
return nil, err
}
ac, ok := claims.(*AuthorizationResponseClaims)
if !ok {
return nil, errors.New("not an authorization request claim")
}
return ac, nil
}
// ExpectedPrefixes defines the types that can encode an auth request jwt, servers.
func (ar *AuthorizationResponseClaims) ExpectedPrefixes() []nkeys.PrefixByte {
return []nkeys.PrefixByte{nkeys.PrefixByteAccount}
}
func (ar *AuthorizationResponseClaims) ClaimType() ClaimType {
return ar.Type
}
// Claims returns the request claims data.
func (ar *AuthorizationResponseClaims) Claims() *ClaimsData {
return &ar.ClaimsData
}
// Payload pulls the request specific payload out of the claims.
func (ar *AuthorizationResponseClaims) Payload() interface{} {
return &ar.AuthorizationResponse
}
func (ar *AuthorizationResponseClaims) String() string {
return ar.ClaimsData.String(ar)
}
func (ar *AuthorizationResponseClaims) updateVersion() {
ar.GenericFields.Version = libVersion
}
// Validate checks the generic and specific parts of the auth request jwt.
func (ar *AuthorizationResponseClaims) Validate(vr *ValidationResults) {
if !nkeys.IsValidPublicUserKey(ar.Subject) {
vr.AddError("Subject must be a user public key")
}
if !nkeys.IsValidPublicServerKey(ar.Audience) {
vr.AddError("Audience must be a server public key")
}
if ar.Error == "" && ar.Jwt == "" {
vr.AddError("Error or Jwt is required")
}
if ar.Error != "" && ar.Jwt != "" {
vr.AddError("Only Error or Jwt can be set")
}
if ar.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(ar.IssuerAccount) {
vr.AddError("issuer_account is not an account public key")
}
ar.ClaimsData.Validate(vr)
}
// Encode tries to turn the auth request claims into a JWT string.
func (ar *AuthorizationResponseClaims) Encode(pair nkeys.KeyPair) (string, error) {
return ar.EncodeWithSigner(pair, nil)
}
func (ar *AuthorizationResponseClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
ar.Type = AuthorizationResponseClaim
return ar.ClaimsData.encode(pair, ar, fn)
}
+299
View File
@@ -0,0 +1,299 @@
/*
* 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 (
"crypto/sha512"
"encoding/base32"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/nats-io/nkeys"
)
// ClaimType is used to indicate the type of JWT being stored in a Claim
type ClaimType string
const (
// OperatorClaim is the type of an operator JWT
OperatorClaim = "operator"
// AccountClaim is the type of an Account JWT
AccountClaim = "account"
// UserClaim is the type of an user JWT
UserClaim = "user"
// ActivationClaim is the type of an activation JWT
ActivationClaim = "activation"
// AuthorizationRequestClaim is the type of an auth request claim JWT
AuthorizationRequestClaim = "authorization_request"
// AuthorizationResponseClaim is the response for an auth request
AuthorizationResponseClaim = "authorization_response"
// GenericClaim is a type that doesn't match Operator/Account/User/ActionClaim
GenericClaim = "generic"
)
func IsGenericClaimType(s string) bool {
switch s {
case OperatorClaim:
fallthrough
case AccountClaim:
fallthrough
case UserClaim:
fallthrough
case AuthorizationRequestClaim:
fallthrough
case AuthorizationResponseClaim:
fallthrough
case ActivationClaim:
return false
case GenericClaim:
return true
default:
return true
}
}
// SignFn is used in an external sign environment. The function should be
// able to locate the private key for the specified pub key specified and sign the
// specified data returning the signature as generated.
type SignFn func(pub string, data []byte) ([]byte, error)
// Claims is a JWT claims
type Claims interface {
Claims() *ClaimsData
Encode(kp nkeys.KeyPair) (string, error)
EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error)
ExpectedPrefixes() []nkeys.PrefixByte
Payload() interface{}
String() string
Validate(vr *ValidationResults)
ClaimType() ClaimType
verify(payload string, sig []byte) bool
updateVersion()
}
type GenericFields struct {
Tags TagList `json:"tags,omitempty"`
Type ClaimType `json:"type,omitempty"`
Version int `json:"version,omitempty"`
}
// ClaimsData is the base struct for all claims
type ClaimsData struct {
Audience string `json:"aud,omitempty"`
Expires int64 `json:"exp,omitempty"`
ID string `json:"jti,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
Name string `json:"name,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
}
// Prefix holds the prefix byte for an NKey
type Prefix struct {
nkeys.PrefixByte
}
func encodeToString(d []byte) string {
return base64.RawURLEncoding.EncodeToString(d)
}
func decodeString(s string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(s)
}
func serialize(v interface{}) (string, error) {
j, err := json.Marshal(v)
if err != nil {
return "", err
}
return encodeToString(j), nil
}
func (c *ClaimsData) doEncode(header *Header, kp nkeys.KeyPair, claim Claims, fn SignFn) (string, error) {
if header == nil {
return "", errors.New("header is required")
}
if kp == nil {
return "", errors.New("keypair is required")
}
if c != claim.Claims() {
return "", errors.New("claim and claim data do not match")
}
if c.Subject == "" {
return "", errors.New("subject is not set")
}
h, err := serialize(header)
if err != nil {
return "", err
}
issuerBytes, err := kp.PublicKey()
if err != nil {
return "", err
}
prefixes := claim.ExpectedPrefixes()
if prefixes != nil {
ok := false
for _, p := range prefixes {
switch p {
case nkeys.PrefixByteAccount:
if nkeys.IsValidPublicAccountKey(issuerBytes) {
ok = true
}
case nkeys.PrefixByteOperator:
if nkeys.IsValidPublicOperatorKey(issuerBytes) {
ok = true
}
case nkeys.PrefixByteServer:
if nkeys.IsValidPublicServerKey(issuerBytes) {
ok = true
}
case nkeys.PrefixByteCluster:
if nkeys.IsValidPublicClusterKey(issuerBytes) {
ok = true
}
case nkeys.PrefixByteUser:
if nkeys.IsValidPublicUserKey(issuerBytes) {
ok = true
}
}
}
if !ok {
return "", fmt.Errorf("unable to validate expected prefixes - %v", prefixes)
}
}
c.Issuer = issuerBytes
c.IssuedAt = time.Now().UTC().Unix()
c.ID = "" // to create a repeatable hash
c.ID, err = c.hash()
if err != nil {
return "", err
}
claim.updateVersion()
payload, err := serialize(claim)
if err != nil {
return "", err
}
toSign := fmt.Sprintf("%s.%s", h, payload)
eSig := ""
if header.Algorithm == AlgorithmNkeyOld {
return "", errors.New(AlgorithmNkeyOld + " not supported to write jwtV2")
} else if header.Algorithm == AlgorithmNkey {
var sig []byte
if fn != nil {
pk, err := kp.PublicKey()
if err != nil {
return "", err
}
sig, err = fn(pk, []byte(toSign))
if err != nil {
return "", err
}
} else {
sig, err = kp.Sign([]byte(toSign))
if err != nil {
return "", err
}
}
eSig = encodeToString(sig)
} else {
return "", errors.New(header.Algorithm + " not supported to write jwtV2")
}
// hash need no padding
return fmt.Sprintf("%s.%s", toSign, eSig), nil
}
func (c *ClaimsData) hash() (string, error) {
j, err := json.Marshal(c)
if err != nil {
return "", err
}
h := sha512.New512_256()
h.Write(j)
return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(h.Sum(nil)), nil
}
// Encode encodes a claim into a JWT token. The claim is signed with the
// provided nkey's private key
func (c *ClaimsData) encode(kp nkeys.KeyPair, payload Claims, fn SignFn) (string, error) {
return c.doEncode(&Header{TokenTypeJwt, AlgorithmNkey}, kp, payload, fn)
}
// Returns a JSON representation of the claim
func (c *ClaimsData) String(claim interface{}) string {
j, err := json.MarshalIndent(claim, "", " ")
if err != nil {
return ""
}
return string(j)
}
func parseClaims(s string, target Claims) error {
h, err := decodeString(s)
if err != nil {
return err
}
return json.Unmarshal(h, &target)
}
// Verify verifies that the encoded payload was signed by the
// provided public key. Verify is called automatically with
// the claims portion of the token and the public key in the claim.
// Client code need to insure that the public key in the
// claim is trusted.
func (c *ClaimsData) verify(payload string, sig []byte) bool {
// decode the public key
kp, err := nkeys.FromPublicKey(c.Issuer)
if err != nil {
return false
}
if err := kp.Verify([]byte(payload), sig); err != nil {
return false
}
return true
}
// Validate checks a claim to make sure it is valid. Validity checks
// include expiration and not before constraints.
func (c *ClaimsData) Validate(vr *ValidationResults) {
now := time.Now().UTC().Unix()
if c.Expires > 0 && now > c.Expires {
vr.AddTimeCheck("claim is expired")
}
if c.NotBefore > 0 && c.NotBefore > now {
vr.AddTimeCheck("claim is not yet valid")
}
}
// IsSelfSigned returns true if the claims issuer is the subject
func (c *ClaimsData) IsSelfSigned() bool {
return c.Issuer == c.Subject
}
+281
View File
@@ -0,0 +1,281 @@
/*
* Copyright 2019-2020 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 (
"bytes"
"errors"
"fmt"
"regexp"
"strings"
"time"
"github.com/nats-io/nkeys"
)
// DecorateJWT returns a decorated JWT that describes the kind of JWT
func DecorateJWT(jwtString string) ([]byte, error) {
gc, err := Decode(jwtString)
if err != nil {
return nil, err
}
return formatJwt(string(gc.ClaimType()), jwtString)
}
func formatJwt(kind string, jwtString string) ([]byte, error) {
templ := `-----BEGIN NATS %s JWT-----
%s
------END NATS %s JWT------
`
w := bytes.NewBuffer(nil)
kind = strings.ToUpper(kind)
_, err := fmt.Fprintf(w, templ, kind, jwtString, kind)
if err != nil {
return nil, err
}
return w.Bytes(), nil
}
// DecorateSeed takes a seed and returns a string that wraps
// the seed in the form:
//
// ************************* IMPORTANT *************************
// NKEY Seed printed below can be used sign and prove identity.
// NKEYs are sensitive and should be treated as secrets.
//
// -----BEGIN USER NKEY SEED-----
// SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM
// ------END USER NKEY SEED------
func DecorateSeed(seed []byte) ([]byte, error) {
w := bytes.NewBuffer(nil)
ts := bytes.TrimSpace(seed)
if len(ts) < 2 {
return nil, errors.New("seed is too short")
}
pre := string(ts[0:2])
kind := ""
switch pre {
case "SU":
kind = "USER"
case "SA":
kind = "ACCOUNT"
case "SO":
kind = "OPERATOR"
default:
return nil, errors.New("seed is not an operator, account or user seed")
}
header := `************************* IMPORTANT *************************
NKEY Seed printed below can be used to sign and prove identity.
NKEYs are sensitive and should be treated as secrets.
-----BEGIN %s NKEY SEED-----
`
_, err := fmt.Fprintf(w, header, kind)
if err != nil {
return nil, err
}
w.Write(ts)
footer := `
------END %s NKEY SEED------
*************************************************************
`
_, err = fmt.Fprintf(w, footer, kind)
if err != nil {
return nil, err
}
return w.Bytes(), nil
}
var userConfigRE = regexp.MustCompile(`\s*(?:(?:[-]{3,}.*[-]{3,}\r?\n)([\w\-.=]+)(?:\r?\n[-]{3,}.*[-]{3,}(\r?\n|\z)))`)
// An user config file looks like this:
// -----BEGIN NATS USER JWT-----
// eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5...
// ------END NATS USER JWT------
//
// ************************* IMPORTANT *************************
// NKEY Seed printed below can be used sign and prove identity.
// NKEYs are sensitive and should be treated as secrets.
//
// -----BEGIN USER NKEY SEED-----
// SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM
// ------END USER NKEY SEED------
// FormatUserConfig returns a decorated file with a decorated JWT and decorated seed
func FormatUserConfig(jwtString string, seed []byte) ([]byte, error) {
gc, err := Decode(jwtString)
if err != nil {
return nil, err
}
if gc.ClaimType() != UserClaim {
return nil, fmt.Errorf("%q cannot be serialized as a user config", string(gc.ClaimType()))
}
w := bytes.NewBuffer(nil)
jd, err := formatJwt(string(gc.ClaimType()), jwtString)
if err != nil {
return nil, err
}
_, err = w.Write(jd)
if err != nil {
return nil, err
}
if !bytes.HasPrefix(bytes.TrimSpace(seed), []byte("SU")) {
return nil, fmt.Errorf("nkey seed is not an user seed")
}
kp, err := nkeys.FromSeed(seed)
if err != nil {
return nil, err
}
pk, err := kp.PublicKey()
if err != nil {
return nil, err
}
if pk != gc.Claims().Subject {
return nil, fmt.Errorf("nkey seed does not match the jwt subject")
}
d, err := DecorateSeed(seed)
if err != nil {
return nil, err
}
_, err = w.Write(d)
if err != nil {
return nil, err
}
return w.Bytes(), nil
}
// ParseDecoratedJWT takes a creds file and returns the JWT portion.
func ParseDecoratedJWT(contents []byte) (string, error) {
items := userConfigRE.FindAllSubmatch(contents, -1)
if len(items) == 0 {
return string(contents), nil
}
// First result should be the user JWT.
// We copy here so that if the file contained a seed file too we wipe appropriately.
raw := items[0][1]
tmp := make([]byte, len(raw))
copy(tmp, raw)
return string(tmp), nil
}
// ParseDecoratedNKey takes a creds file, finds the NKey portion and creates a
// key pair from it.
func ParseDecoratedNKey(contents []byte) (nkeys.KeyPair, error) {
var seed []byte
items := userConfigRE.FindAllSubmatch(contents, -1)
if len(items) > 1 {
seed = items[1][1]
} else {
lines := bytes.Split(contents, []byte("\n"))
for _, line := range lines {
if bytes.HasPrefix(bytes.TrimSpace(line), []byte("SO")) ||
bytes.HasPrefix(bytes.TrimSpace(line), []byte("SA")) ||
bytes.HasPrefix(bytes.TrimSpace(line), []byte("SU")) {
seed = line
break
}
}
}
if seed == nil {
return nil, errors.New("no nkey seed found")
}
if !bytes.HasPrefix(seed, []byte("SO")) &&
!bytes.HasPrefix(seed, []byte("SA")) &&
!bytes.HasPrefix(seed, []byte("SU")) {
return nil, errors.New("doesn't contain a seed nkey")
}
kp, err := nkeys.FromSeed(seed)
if err != nil {
return nil, err
}
return kp, nil
}
// ParseDecoratedUserNKey takes a creds file, finds the NKey portion and creates a
// key pair from it. Similar to ParseDecoratedNKey but fails for non-user keys.
func ParseDecoratedUserNKey(contents []byte) (nkeys.KeyPair, error) {
nk, err := ParseDecoratedNKey(contents)
if err != nil {
return nil, err
}
seed, err := nk.Seed()
if err != nil {
return nil, err
}
if !bytes.HasPrefix(seed, []byte("SU")) {
return nil, errors.New("doesn't contain an user seed nkey")
}
kp, err := nkeys.FromSeed(seed)
if err != nil {
return nil, err
}
return kp, nil
}
// IssueUserJWT takes an account scoped signing key, account id, and use public key (and optionally a user's name, an expiration duration and tags) and returns a valid signed JWT.
// The scopedSigningKey, is a mandatory account scoped signing nkey pair to sign the generated jwt (note that it _must_ be a signing key attached to the account (and a _scoped_ signing key), not the account's private (seed) key).
// The accountId, is a mandatory public account nkey. Will return error when not set or not account nkey.
// The publicUserKey, is a mandatory public user nkey. Will return error when not set or not user nkey.
// The name, is an optional human-readable name. When absent, default to publicUserKey.
// The expirationDuration, is an optional but recommended duration, when the generated jwt needs to expire. If not set, JWT will not expire.
// The tags, is an optional list of tags to be included in the JWT.
//
// Returns:
// string, resulting jwt.
// error, when issues arose.
func IssueUserJWT(scopedSigningKey nkeys.KeyPair, accountId string, publicUserKey string, name string, expirationDuration time.Duration, tags ...string) (string, error) {
if !nkeys.IsValidPublicAccountKey(accountId) {
return "", errors.New("issueUserJWT requires an account key for the accountId parameter, but got " + nkeys.Prefix(accountId).String())
}
if !nkeys.IsValidPublicUserKey(publicUserKey) {
return "", errors.New("issueUserJWT requires an account key for the publicUserKey parameter, but got " + nkeys.Prefix(publicUserKey).String())
}
claim := NewUserClaims(publicUserKey)
claim.SetScoped(true)
if expirationDuration != 0 {
claim.Expires = time.Now().Add(expirationDuration).UTC().Unix()
}
claim.IssuerAccount = accountId
if name != "" {
claim.Name = name
} else {
claim.Name = publicUserKey
}
claim.Subject = publicUserKey
claim.Tags = tags
encoded, err := claim.Encode(scopedSigningKey)
if err != nil {
return "", errors.New("err encoding claim " + err.Error())
}
return encoded, nil
}
+173
View File
@@ -0,0 +1,173 @@
/*
* Copyright 2020-2022 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 (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/nats-io/nkeys"
)
const libVersion = 2
// MaxTokenSize is the maximum size of a JWT token in bytes
const MaxTokenSize = 1024 * 1024 // 1MB
// ErrTokenTooLarge is returned when a token exceeds MaxTokenSize
var ErrTokenTooLarge = errors.New("token too large")
type identifier struct {
Type ClaimType `json:"type,omitempty"`
GenericFields `json:"nats,omitempty"`
}
func (i *identifier) Kind() ClaimType {
if i.Type != "" {
return i.Type
}
return i.GenericFields.Type
}
func (i *identifier) Version() int {
if i.Type != "" {
return 1
}
return i.GenericFields.Version
}
type v1ClaimsDataDeletedFields struct {
Tags TagList `json:"tags,omitempty"`
Type ClaimType `json:"type,omitempty"`
IssuerAccount string `json:"issuer_account,omitempty"`
}
// Decode takes a JWT string decodes it and validates it
// and return the embedded Claims. If the token header
// doesn't match the expected algorithm, or the claim is
// not valid or verification fails an error is returned.
func Decode(token string) (Claims, error) {
if len(token) > MaxTokenSize {
return nil, fmt.Errorf("token size %d exceeds maximum of %d bytes: %w", len(token), MaxTokenSize, ErrTokenTooLarge)
}
// must have 3 chunks
chunks := strings.Split(token, ".")
if len(chunks) != 3 {
return nil, errors.New("expected 3 chunks")
}
// header
if _, err := parseHeaders(chunks[0]); err != nil {
return nil, err
}
// claim
data, err := decodeString(chunks[1])
if err != nil {
return nil, err
}
ver, claim, err := loadClaims(data)
if err != nil {
return nil, err
}
// sig
sig, err := decodeString(chunks[2])
if err != nil {
return nil, err
}
if ver <= 1 {
if !claim.verify(chunks[1], sig) {
return nil, errors.New("claim failed V1 signature verification")
}
} else {
if !claim.verify(token[:len(chunks[0])+len(chunks[1])+1], sig) {
return nil, errors.New("claim failed V2 signature verification")
}
}
prefixes := claim.ExpectedPrefixes()
if prefixes != nil {
ok := false
issuer := claim.Claims().Issuer
for _, p := range prefixes {
switch p {
case nkeys.PrefixByteAccount:
if nkeys.IsValidPublicAccountKey(issuer) {
ok = true
}
case nkeys.PrefixByteOperator:
if nkeys.IsValidPublicOperatorKey(issuer) {
ok = true
}
case nkeys.PrefixByteUser:
if nkeys.IsValidPublicUserKey(issuer) {
ok = true
}
case nkeys.PrefixByteServer:
if nkeys.IsValidPublicServerKey(issuer) {
ok = true
}
}
}
if !ok {
return nil, fmt.Errorf("unable to validate expected prefixes - %v", prefixes)
}
}
return claim, nil
}
func loadClaims(data []byte) (int, Claims, error) {
var id identifier
if err := json.Unmarshal(data, &id); err != nil {
return -1, nil, err
}
if id.Version() > libVersion {
return -1, nil, errors.New("JWT was generated by a newer version ")
}
var claim Claims
var err error
switch id.Kind() {
case OperatorClaim:
claim, err = loadOperator(data, id.Version())
case AccountClaim:
claim, err = loadAccount(data, id.Version())
case UserClaim:
claim, err = loadUser(data, id.Version())
case ActivationClaim:
claim, err = loadActivation(data, id.Version())
case AuthorizationRequestClaim:
claim, err = loadAuthorizationRequest(data, id.Version())
case AuthorizationResponseClaim:
claim, err = loadAuthorizationResponse(data, id.Version())
case "cluster":
return -1, nil, errors.New("ClusterClaims are not supported")
case "server":
return -1, nil, errors.New("ServerClaims are not supported")
default:
var gc GenericClaims
if err := json.Unmarshal(data, &gc); err != nil {
return -1, nil, err
}
return -1, &gc, nil
}
return id.Version(), claim, err
}
+87
View File
@@ -0,0 +1,87 @@
/*
* Copyright 2020 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 (
"encoding/json"
"fmt"
)
type v1NatsAccount struct {
Imports Imports `json:"imports,omitempty"`
Exports Exports `json:"exports,omitempty"`
Limits struct {
NatsLimits
AccountLimits
} `json:"limits,omitempty"`
SigningKeys StringList `json:"signing_keys,omitempty"`
Revocations RevocationList `json:"revocations,omitempty"`
}
func loadAccount(data []byte, version int) (*AccountClaims, error) {
switch version {
case 1:
var v1a v1AccountClaims
if err := json.Unmarshal(data, &v1a); err != nil {
return nil, err
}
return v1a.Migrate()
case 2:
var v2a AccountClaims
v2a.SigningKeys = make(SigningKeys)
if err := json.Unmarshal(data, &v2a); err != nil {
return nil, err
}
if len(v2a.Limits.JetStreamTieredLimits) > 0 {
v2a.Limits.JetStreamLimits = JetStreamLimits{}
}
return &v2a, nil
default:
return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version)
}
}
type v1AccountClaims struct {
ClaimsData
v1ClaimsDataDeletedFields
v1NatsAccount `json:"nats,omitempty"`
}
func (oa v1AccountClaims) Migrate() (*AccountClaims, error) {
return oa.migrateV1()
}
func (oa v1AccountClaims) migrateV1() (*AccountClaims, error) {
var a AccountClaims
// copy the base claim
a.ClaimsData = oa.ClaimsData
// move the moved fields
a.Account.Type = oa.v1ClaimsDataDeletedFields.Type
a.Account.Tags = oa.v1ClaimsDataDeletedFields.Tags
// copy the account data
a.Account.Imports = oa.v1NatsAccount.Imports
a.Account.Exports = oa.v1NatsAccount.Exports
a.Account.Limits.AccountLimits = oa.v1NatsAccount.Limits.AccountLimits
a.Account.Limits.NatsLimits = oa.v1NatsAccount.Limits.NatsLimits
a.Account.Limits.JetStreamLimits = JetStreamLimits{}
a.Account.SigningKeys = make(SigningKeys)
for _, v := range oa.SigningKeys {
a.Account.SigningKeys.Add(v)
}
a.Account.Revocations = oa.v1NatsAccount.Revocations
a.Version = 1
return &a, nil
}
+77
View File
@@ -0,0 +1,77 @@
/*
* Copyright 2020 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 (
"encoding/json"
"fmt"
)
// Migration adds GenericFields
type v1NatsActivation struct {
ImportSubject Subject `json:"subject,omitempty"`
ImportType ExportType `json:"type,omitempty"`
// Limit values deprecated inv v2
Max int64 `json:"max,omitempty"`
Payload int64 `json:"payload,omitempty"`
Src string `json:"src,omitempty"`
Times []TimeRange `json:"times,omitempty"`
}
type v1ActivationClaims struct {
ClaimsData
v1ClaimsDataDeletedFields
v1NatsActivation `json:"nats,omitempty"`
}
func loadActivation(data []byte, version int) (*ActivationClaims, error) {
switch version {
case 1:
var v1a v1ActivationClaims
v1a.Max = NoLimit
v1a.Payload = NoLimit
if err := json.Unmarshal(data, &v1a); err != nil {
return nil, err
}
return v1a.Migrate()
case 2:
var v2a ActivationClaims
if err := json.Unmarshal(data, &v2a); err != nil {
return nil, err
}
return &v2a, nil
default:
return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version)
}
}
func (oa v1ActivationClaims) Migrate() (*ActivationClaims, error) {
return oa.migrateV1()
}
func (oa v1ActivationClaims) migrateV1() (*ActivationClaims, error) {
var a ActivationClaims
// copy the base claim
a.ClaimsData = oa.ClaimsData
// move the moved fields
a.Activation.Type = oa.v1ClaimsDataDeletedFields.Type
a.Activation.Tags = oa.v1ClaimsDataDeletedFields.Tags
a.Activation.IssuerAccount = oa.v1ClaimsDataDeletedFields.IssuerAccount
// copy the activation data
a.ImportSubject = oa.ImportSubject
a.ImportType = oa.ImportType
a.Version = 1
return &a, nil
}
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright 2022 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 (
"encoding/json"
)
func loadAuthorizationRequest(data []byte, version int) (*AuthorizationRequestClaims, error) {
var ac AuthorizationRequestClaims
if err := json.Unmarshal(data, &ac); err != nil {
return nil, err
}
return &ac, nil
}
func loadAuthorizationResponse(data []byte, version int) (*AuthorizationResponseClaims, error) {
var ac AuthorizationResponseClaims
if err := json.Unmarshal(data, &ac); err != nil {
return nil, err
}
return &ac, nil
}
+73
View File
@@ -0,0 +1,73 @@
/*
* Copyright 2020 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 (
"encoding/json"
"fmt"
)
type v1NatsOperator struct {
SigningKeys StringList `json:"signing_keys,omitempty"`
AccountServerURL string `json:"account_server_url,omitempty"`
OperatorServiceURLs StringList `json:"operator_service_urls,omitempty"`
SystemAccount string `json:"system_account,omitempty"`
}
func loadOperator(data []byte, version int) (*OperatorClaims, error) {
switch version {
case 1:
var v1a v1OperatorClaims
if err := json.Unmarshal(data, &v1a); err != nil {
return nil, err
}
return v1a.Migrate()
case 2:
var v2a OperatorClaims
if err := json.Unmarshal(data, &v2a); err != nil {
return nil, err
}
return &v2a, nil
default:
return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version)
}
}
type v1OperatorClaims struct {
ClaimsData
v1ClaimsDataDeletedFields
v1NatsOperator `json:"nats,omitempty"`
}
func (oa v1OperatorClaims) Migrate() (*OperatorClaims, error) {
return oa.migrateV1()
}
func (oa v1OperatorClaims) migrateV1() (*OperatorClaims, error) {
var a OperatorClaims
// copy the base claim
a.ClaimsData = oa.ClaimsData
// move the moved fields
a.Operator.Type = oa.v1ClaimsDataDeletedFields.Type
a.Operator.Tags = oa.v1ClaimsDataDeletedFields.Tags
// copy the account data
a.Operator.SigningKeys = oa.v1NatsOperator.SigningKeys
a.Operator.AccountServerURL = oa.v1NatsOperator.AccountServerURL
a.Operator.OperatorServiceURLs = oa.v1NatsOperator.OperatorServiceURLs
a.Operator.SystemAccount = oa.v1NatsOperator.SystemAccount
a.Version = 1
return &a, nil
}
+81
View File
@@ -0,0 +1,81 @@
/*
* Copyright 2020 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 (
"encoding/json"
"fmt"
)
type v1User struct {
Permissions
Limits
BearerToken bool `json:"bearer_token,omitempty"`
// Limit values deprecated inv v2
Max int64 `json:"max,omitempty"`
}
type v1UserClaimsDataDeletedFields struct {
v1ClaimsDataDeletedFields
IssuerAccount string `json:"issuer_account,omitempty"`
}
type v1UserClaims struct {
ClaimsData
v1UserClaimsDataDeletedFields
v1User `json:"nats,omitempty"`
}
func loadUser(data []byte, version int) (*UserClaims, error) {
switch version {
case 1:
var v1a v1UserClaims
v1a.Limits = Limits{NatsLimits: NatsLimits{NoLimit, NoLimit, NoLimit}}
v1a.Max = NoLimit
if err := json.Unmarshal(data, &v1a); err != nil {
return nil, err
}
return v1a.Migrate()
case 2:
var v2a UserClaims
if err := json.Unmarshal(data, &v2a); err != nil {
return nil, err
}
return &v2a, nil
default:
return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version)
}
}
func (oa v1UserClaims) Migrate() (*UserClaims, error) {
return oa.migrateV1()
}
func (oa v1UserClaims) migrateV1() (*UserClaims, error) {
var u UserClaims
// copy the base claim
u.ClaimsData = oa.ClaimsData
// move the moved fields
u.User.Type = oa.v1ClaimsDataDeletedFields.Type
u.User.Tags = oa.v1ClaimsDataDeletedFields.Tags
u.User.IssuerAccount = oa.IssuerAccount
// copy the user data
u.User.Permissions = oa.v1User.Permissions
u.User.Limits = oa.v1User.Limits
u.User.BearerToken = oa.v1User.BearerToken
u.Version = 1
return &u, nil
}
+317
View File
@@ -0,0 +1,317 @@
/*
* Copyright 2018-2019 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 (
"encoding/json"
"fmt"
"strings"
"time"
)
// ResponseType is used to store an export response type
type ResponseType string
const (
// ResponseTypeSingleton is used for a service that sends a single response only
ResponseTypeSingleton = "Singleton"
// ResponseTypeStream is used for a service that will send multiple responses
ResponseTypeStream = "Stream"
// ResponseTypeChunked is used for a service that sends a single response in chunks (so not quite a stream)
ResponseTypeChunked = "Chunked"
)
// ServiceLatency is used when observing and exported service for
// latency measurements.
// Sampling 1-100, represents sampling rate, defaults to 100.
// Results is the subject where the latency metrics are published.
// A metric will be defined by the nats-server's ServiceLatency. Time durations
// are in nanoseconds.
// see https://github.com/nats-io/nats-server/blob/main/server/accounts.go#L524
// e.g.
//
// {
// "app": "dlc22",
// "start": "2019-09-16T21:46:23.636869585-07:00",
// "svc": 219732,
// "nats": {
// "req": 320415,
// "resp": 228268,
// "sys": 0
// },
// "total": 768415
// }
type ServiceLatency struct {
Sampling SamplingRate `json:"sampling"`
Results Subject `json:"results"`
}
type SamplingRate int
const Headers = SamplingRate(0)
// MarshalJSON marshals the field as "headers" or percentages
func (r *SamplingRate) MarshalJSON() ([]byte, error) {
sr := *r
if sr == 0 {
return []byte(`"headers"`), nil
}
if sr >= 1 && sr <= 100 {
return []byte(fmt.Sprintf("%d", sr)), nil
}
return nil, fmt.Errorf("unknown sampling rate")
}
// UnmarshalJSON unmashals numbers as percentages or "headers"
func (t *SamplingRate) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
return fmt.Errorf("empty sampling rate")
}
if strings.ToLower(string(b)) == `"headers"` {
*t = Headers
return nil
}
var j int
err := json.Unmarshal(b, &j)
if err != nil {
return err
}
*t = SamplingRate(j)
return nil
}
func (sl *ServiceLatency) Validate(vr *ValidationResults) {
if sl.Sampling != 0 {
if sl.Sampling < 1 || sl.Sampling > 100 {
vr.AddError("sampling percentage needs to be between 1-100")
}
}
sl.Results.Validate(vr)
if sl.Results.HasWildCards() {
vr.AddError("results subject can not contain wildcards")
}
}
// Export represents a single export
type Export struct {
Name string `json:"name,omitempty"`
Subject Subject `json:"subject,omitempty"`
Type ExportType `json:"type,omitempty"`
TokenReq bool `json:"token_req,omitempty"`
Revocations RevocationList `json:"revocations,omitempty"`
ResponseType ResponseType `json:"response_type,omitempty"`
ResponseThreshold time.Duration `json:"response_threshold,omitempty"`
Latency *ServiceLatency `json:"service_latency,omitempty"`
AccountTokenPosition uint `json:"account_token_position,omitempty"`
Advertise bool `json:"advertise,omitempty"`
AllowTrace bool `json:"allow_trace,omitempty"`
Info
}
// IsService returns true if an export is for a service
func (e *Export) IsService() bool {
return e.Type == Service
}
// IsStream returns true if an export is for a stream
func (e *Export) IsStream() bool {
return e.Type == Stream
}
// IsSingleResponse returns true if an export has a single response
// or no response type is set, also checks that the type is service
func (e *Export) IsSingleResponse() bool {
return e.Type == Service && (e.ResponseType == ResponseTypeSingleton || e.ResponseType == "")
}
// IsChunkedResponse returns true if an export has a chunked response
func (e *Export) IsChunkedResponse() bool {
return e.Type == Service && e.ResponseType == ResponseTypeChunked
}
// IsStreamResponse returns true if an export has a chunked response
func (e *Export) IsStreamResponse() bool {
return e.Type == Service && e.ResponseType == ResponseTypeStream
}
// Validate appends validation issues to the passed in results list
func (e *Export) Validate(vr *ValidationResults) {
if e == nil {
vr.AddError("null export is not allowed")
return
}
if !e.IsService() && !e.IsStream() {
vr.AddError("invalid export type: %q", e.Type)
}
if e.IsService() && !e.IsSingleResponse() && !e.IsChunkedResponse() && !e.IsStreamResponse() {
vr.AddError("invalid response type for service: %q", e.ResponseType)
}
if e.IsStream() {
if e.ResponseType != "" {
vr.AddError("invalid response type for stream: %q", e.ResponseType)
}
if e.AllowTrace {
vr.AddError("AllowTrace only valid for service export")
}
}
if e.Latency != nil {
if !e.IsService() {
vr.AddError("latency tracking only permitted for services")
}
e.Latency.Validate(vr)
}
if e.ResponseThreshold.Nanoseconds() < 0 {
vr.AddError("negative response threshold is invalid")
}
if e.ResponseThreshold.Nanoseconds() > 0 && !e.IsService() {
vr.AddError("response threshold only valid for services")
}
e.Subject.Validate(vr)
if e.AccountTokenPosition > 0 {
if !e.Subject.HasWildCards() {
vr.AddError("Account Token Position can only be used with wildcard subjects: %s", e.Subject)
} else {
subj := string(e.Subject)
token := strings.Split(subj, ".")
tkCnt := uint(len(token))
if e.AccountTokenPosition > tkCnt {
vr.AddError("Account Token Position %d exceeds length of subject '%s'",
e.AccountTokenPosition, e.Subject)
} else if tk := token[e.AccountTokenPosition-1]; tk != "*" {
vr.AddError("Account Token Position %d matches '%s' but must match a * in: %s",
e.AccountTokenPosition, tk, e.Subject)
}
}
}
e.Info.Validate(vr)
}
// Revoke enters a revocation by publickey using time.Now().
func (e *Export) Revoke(pubKey string) {
e.RevokeAt(pubKey, time.Now())
}
// RevokeAt enters a revocation by publickey and timestamp into this export
// If there is already a revocation for this public key that is newer, it is kept.
func (e *Export) RevokeAt(pubKey string, timestamp time.Time) {
if e.Revocations == nil {
e.Revocations = RevocationList{}
}
e.Revocations.Revoke(pubKey, timestamp)
}
// ClearRevocation removes any revocation for the public key
func (e *Export) ClearRevocation(pubKey string) {
e.Revocations.ClearRevocation(pubKey)
}
// isRevoked checks if the public key is in the revoked list with a timestamp later than the one passed in.
// Generally this method is called with the subject and issue time of the jwt to be tested.
// DO NOT pass time.Now(), it will not produce a stable/expected response.
func (e *Export) isRevoked(pubKey string, claimIssuedAt time.Time) bool {
return e.Revocations.IsRevoked(pubKey, claimIssuedAt)
}
// IsClaimRevoked checks if the activation revoked the claim passed in.
// Invalid claims (nil, no Subject or IssuedAt) will return true.
func (e *Export) IsClaimRevoked(claim *ActivationClaims) bool {
if claim == nil || claim.IssuedAt == 0 || claim.Subject == "" {
return true
}
return e.isRevoked(claim.Subject, time.Unix(claim.IssuedAt, 0))
}
// Exports is a slice of exports
type Exports []*Export
// Add appends exports to the list
func (e *Exports) Add(i ...*Export) {
*e = append(*e, i...)
}
func isContainedIn(kind ExportType, subjects []Subject, vr *ValidationResults) {
m := make(map[string]string)
for i, ns := range subjects {
for j, s := range subjects {
if i == j {
continue
}
if ns.IsContainedIn(s) {
str := string(s)
_, ok := m[str]
if !ok {
m[str] = string(ns)
}
}
}
}
if len(m) != 0 {
for k, v := range m {
var vi ValidationIssue
vi.Blocking = true
vi.Description = fmt.Sprintf("%s export subject %q already exports %q", kind, k, v)
vr.Add(&vi)
}
}
}
// Validate calls validate on all of the exports
func (e *Exports) Validate(vr *ValidationResults) {
var serviceSubjects []Subject
var streamSubjects []Subject
for _, v := range *e {
if v == nil {
vr.AddError("null export is not allowed")
continue
}
if v.IsService() {
serviceSubjects = append(serviceSubjects, v.Subject)
} else {
streamSubjects = append(streamSubjects, v.Subject)
}
v.Validate(vr)
}
isContainedIn(Service, serviceSubjects, vr)
isContainedIn(Stream, streamSubjects, vr)
}
// HasExportContainingSubject checks if the export list has an export with the provided subject
func (e *Exports) HasExportContainingSubject(subject Subject) bool {
for _, s := range *e {
if subject.IsContainedIn(s.Subject) {
return true
}
}
return false
}
func (e Exports) Len() int {
return len(e)
}
func (e Exports) Swap(i, j int) {
e[i], e[j] = e[j], e[i]
}
func (e Exports) Less(i, j int) bool {
return e[i].Subject < e[j].Subject
}
+161
View File
@@ -0,0 +1,161 @@
/*
* 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 (
"encoding/json"
"errors"
"strings"
"github.com/nats-io/nkeys"
)
// GenericClaims can be used to read a JWT as a map for any non-generic fields
type GenericClaims struct {
ClaimsData
Data map[string]interface{} `json:"nats,omitempty"`
}
// NewGenericClaims creates a map-based Claims
func NewGenericClaims(subject string) *GenericClaims {
if subject == "" {
return nil
}
c := GenericClaims{}
c.Subject = subject
c.Data = make(map[string]interface{})
return &c
}
// DecodeGeneric takes a JWT string and decodes it into a ClaimsData and map
func DecodeGeneric(token string) (*GenericClaims, error) {
// must have 3 chunks
chunks := strings.Split(token, ".")
if len(chunks) != 3 {
return nil, errors.New("expected 3 chunks")
}
// header
header, err := parseHeaders(chunks[0])
if err != nil {
return nil, err
}
// claim
data, err := decodeString(chunks[1])
if err != nil {
return nil, err
}
gc := struct {
GenericClaims
GenericFields
}{}
if err := json.Unmarshal(data, &gc); err != nil {
return nil, err
}
// sig
sig, err := decodeString(chunks[2])
if err != nil {
return nil, err
}
if header.Algorithm == AlgorithmNkeyOld {
if !gc.verify(chunks[1], sig) {
return nil, errors.New("claim failed V1 signature verification")
}
if tp := gc.GenericFields.Type; tp != "" {
// the conversion needs to be from a string because
// on custom types the type is not going to be one of
// the constants
gc.GenericClaims.Data["type"] = string(tp)
}
if tp := gc.GenericFields.Tags; len(tp) != 0 {
gc.GenericClaims.Data["tags"] = tp
}
} else {
if !gc.verify(token[:len(chunks[0])+len(chunks[1])+1], sig) {
return nil, errors.New("claim failed V2 signature verification")
}
}
return &gc.GenericClaims, nil
}
// Claims returns the standard part of the generic claim
func (gc *GenericClaims) Claims() *ClaimsData {
return &gc.ClaimsData
}
// Payload returns the custom part of the claims data
func (gc *GenericClaims) Payload() interface{} {
return &gc.Data
}
// Encode takes a generic claims and creates a JWT string
func (gc *GenericClaims) Encode(pair nkeys.KeyPair) (string, error) {
return gc.ClaimsData.encode(pair, gc, nil)
}
func (gc *GenericClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
return gc.ClaimsData.encode(pair, gc, fn)
}
// Validate checks the generic part of the claims data
func (gc *GenericClaims) Validate(vr *ValidationResults) {
gc.ClaimsData.Validate(vr)
}
func (gc *GenericClaims) String() string {
return gc.ClaimsData.String(gc)
}
// ExpectedPrefixes returns the types allowed to encode a generic JWT, which is nil for all
func (gc *GenericClaims) ExpectedPrefixes() []nkeys.PrefixByte {
return nil
}
func (gc *GenericClaims) ClaimType() ClaimType {
v, ok := gc.Data["type"]
if !ok {
v, ok = gc.Data["nats"]
if ok {
m, ok := v.(map[string]interface{})
if ok {
v = m["type"]
}
}
}
switch ct := v.(type) {
case string:
if IsGenericClaimType(ct) {
return GenericClaim
}
return ClaimType(ct)
case ClaimType:
return ct
default:
return ""
}
}
func (gc *GenericClaims) updateVersion() {
if gc.Data != nil {
// store as float as that is what decoding with json does too
gc.Data["version"] = float64(libVersion)
}
}
+78
View File
@@ -0,0 +1,78 @@
/*
* Copyright 2018-2019 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 (
"encoding/json"
"fmt"
"strings"
)
const (
// Version is semantic version.
Version = "2.4.0"
// TokenTypeJwt is the JWT token type supported JWT tokens
// encoded and decoded by this library
// from RFC7519 5.1 "typ":
// it is RECOMMENDED that "JWT" always be spelled using uppercase characters for compatibility
TokenTypeJwt = "JWT"
// AlgorithmNkey is the algorithm supported by JWT tokens
// encoded and decoded by this library
AlgorithmNkeyOld = "ed25519"
AlgorithmNkey = AlgorithmNkeyOld + "-nkey"
)
// Header is a JWT Jose Header
type Header struct {
Type string `json:"typ"`
Algorithm string `json:"alg"`
}
// Parses a header JWT token
func parseHeaders(s string) (*Header, error) {
h, err := decodeString(s)
if err != nil {
return nil, err
}
header := Header{}
if err := json.Unmarshal(h, &header); err != nil {
return nil, err
}
if err := header.Valid(); err != nil {
return nil, err
}
return &header, nil
}
// Valid validates the Header. It returns nil if the Header is
// a JWT header, and the algorithm used is the NKEY algorithm.
func (h *Header) Valid() error {
if TokenTypeJwt != strings.ToUpper(h.Type) {
return fmt.Errorf("not supported type %q", h.Type)
}
alg := strings.ToLower(h.Algorithm)
if !strings.HasPrefix(alg, AlgorithmNkeyOld) {
return fmt.Errorf("unexpected %q algorithm", h.Algorithm)
}
if AlgorithmNkeyOld != alg && AlgorithmNkey != alg {
return fmt.Errorf("unexpected %q algorithm", h.Algorithm)
}
return nil
}
+177
View File
@@ -0,0 +1,177 @@
/*
* Copyright 2018-2020 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 describes a mapping from another account into this one
type Import struct {
Name string `json:"name,omitempty"`
// Subject field in an import is always from the perspective of the
// initial publisher - in the case of a stream it is the account owning
// the stream (the exporter), and in the case of a service it is the
// account making the request (the importer).
Subject Subject `json:"subject,omitempty"`
Account string `json:"account,omitempty"`
Token string `json:"token,omitempty"`
// Deprecated: use LocalSubject instead
// To field in an import is always from the perspective of the subscriber
// in the case of a stream it is the client of the stream (the importer),
// from the perspective of a service, it is the subscription waiting for
// requests (the exporter). If the field is empty, it will default to the
// value in the Subject field.
To Subject `json:"to,omitempty"`
// Local subject used to subscribe (for streams) and publish (for services) to.
// This value only needs setting if you want to change the value of Subject.
// If the value of Subject ends in > then LocalSubject needs to end in > as well.
// LocalSubject can contain $<number> wildcard references where number references the nth wildcard in Subject.
// The sum of wildcard reference and * tokens needs to match the number of * token in Subject.
LocalSubject RenamingSubject `json:"local_subject,omitempty"`
Type ExportType `json:"type,omitempty"`
Share bool `json:"share,omitempty"`
AllowTrace bool `json:"allow_trace,omitempty"`
}
// IsService returns true if the import is of type service
func (i *Import) IsService() bool {
return i.Type == Service
}
// IsStream returns true if the import is of type stream
func (i *Import) IsStream() bool {
return i.Type == Stream
}
// Returns the value of To without triggering the deprecation warning for a read
func (i *Import) GetTo() string {
return string(i.To)
}
// Validate checks if an import is valid for the wrapping account
func (i *Import) Validate(actPubKey string, vr *ValidationResults) {
if i == nil {
vr.AddError("null import is not allowed")
return
}
if !i.IsService() && !i.IsStream() {
vr.AddError("invalid import type: %q", i.Type)
}
if i.IsService() && i.AllowTrace {
vr.AddError("AllowTrace only valid for stream import")
}
if i.Account == "" {
vr.AddError("account to import from is not specified")
}
if i.GetTo() != "" {
vr.AddWarning("the field to has been deprecated (use LocalSubject instead)")
}
i.Subject.Validate(vr)
if i.LocalSubject != "" {
i.LocalSubject.Validate(i.Subject, vr)
if i.To != "" {
vr.AddError("Local Subject replaces To")
}
}
if i.Share && !i.IsService() {
vr.AddError("sharing information (for latency tracking) is only valid for services: %q", i.Subject)
}
var act *ActivationClaims
if i.Token != "" {
var err error
act, err = DecodeActivationClaims(i.Token)
if err != nil {
vr.AddError("import %q contains an invalid activation token", i.Subject)
}
}
if act != nil {
if !(act.Issuer == i.Account || act.IssuerAccount == i.Account) {
vr.AddError("activation token doesn't match account for import %q", i.Subject)
}
if act.ClaimsData.Subject != actPubKey {
vr.AddError("activation token doesn't match account it is being included in, %q", i.Subject)
}
if act.ImportType != i.Type {
vr.AddError("mismatch between token import type %s and type of import %s", act.ImportType, i.Type)
}
act.validateWithTimeChecks(vr, false)
subj := i.Subject
if i.IsService() && i.To != "" {
subj = i.To
}
if !subj.IsContainedIn(act.ImportSubject) {
vr.AddError("activation token import subject %q doesn't match import %q", act.ImportSubject, i.Subject)
}
}
}
// Imports is a list of import structs
type Imports []*Import
// Validate checks if an import is valid for the wrapping account
func (i *Imports) Validate(acctPubKey string, vr *ValidationResults) {
// Group subjects by account to check for overlaps only within the same account
subsByAcct := make(map[string]map[Subject]struct{}, len(*i))
for _, v := range *i {
if v == nil {
vr.AddError("null import is not allowed")
continue
}
if v.Type == Service {
sub := v.To
if sub == "" {
sub = v.LocalSubject.ToSubject()
}
if sub == "" {
sub = v.Subject
}
// Check for overlapping subjects only within the same account
for subOther := range subsByAcct[v.Account] {
if sub.IsContainedIn(subOther) || subOther.IsContainedIn(sub) {
vr.AddError("overlapping subject namespace for %q and %q in same account %q", sub, subOther, v.Account)
}
}
if subsByAcct[v.Account] == nil {
subsByAcct[v.Account] = make(map[Subject]struct{}, len(*i))
}
if _, ok := subsByAcct[v.Account][sub]; ok {
vr.AddError("overlapping subject namespace for %q in account %q", sub, v.Account)
}
subsByAcct[v.Account][sub] = struct{}{}
}
v.Validate(acctPubKey, vr)
}
}
// Add is a simple way to add imports
func (i *Imports) Add(a ...*Import) {
*i = append(*i, a...)
}
func (i Imports) Len() int {
return len(i)
}
func (i Imports) Swap(j, k int) {
i[j], i[k] = i[k], i[j]
}
func (i Imports) Less(j, k int) bool {
return i[j].Subject < i[k].Subject
}
+257
View File
@@ -0,0 +1,257 @@
/*
* 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
}
+84
View File
@@ -0,0 +1,84 @@
/*
* Copyright 2020 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 (
"time"
)
const All = "*"
// RevocationList is used to store a mapping of public keys to unix timestamps
type RevocationList map[string]int64
type RevocationEntry struct {
PublicKey string
TimeStamp int64
}
// Revoke enters a revocation by publickey and timestamp into this export
// If there is already a revocation for this public key that is newer, it is kept.
func (r RevocationList) Revoke(pubKey string, timestamp time.Time) {
newTS := timestamp.Unix()
// cannot move a revocation into the future - only into the past
if ts, ok := r[pubKey]; ok && ts > newTS {
return
}
r[pubKey] = newTS
}
// MaybeCompact will compact the revocation list if jwt.All is found. Any
// revocation that is covered by a jwt.All revocation will be deleted, thus
// reducing the size of the JWT. Returns a slice of entries that were removed
// during the process.
func (r RevocationList) MaybeCompact() []RevocationEntry {
var deleted []RevocationEntry
ats, ok := r[All]
if ok {
for k, ts := range r {
if k != All && ats >= ts {
deleted = append(deleted, RevocationEntry{
PublicKey: k,
TimeStamp: ts,
})
delete(r, k)
}
}
}
return deleted
}
// ClearRevocation removes any revocation for the public key
func (r RevocationList) ClearRevocation(pubKey string) {
delete(r, pubKey)
}
// IsRevoked checks if the public key is in the revoked list with a timestamp later than
// the one passed in. Generally this method is called with an issue time but other time's can
// be used for testing.
func (r RevocationList) IsRevoked(pubKey string, timestamp time.Time) bool {
if r.allRevoked(timestamp) {
return true
}
ts, ok := r[pubKey]
return ok && ts >= timestamp.Unix()
}
// allRevoked returns true if All is set and the timestamp is later or same as the
// one passed. This is called by IsRevoked.
func (r RevocationList) allRevoked(timestamp time.Time) bool {
ts, ok := r[All]
return ok && ts >= timestamp.Unix()
}
+213
View File
@@ -0,0 +1,213 @@
/*
* Copyright 2020-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 (
"encoding/json"
"errors"
"fmt"
"sort"
"github.com/nats-io/nkeys"
)
type Scope interface {
SigningKey() string
ValidateScopedSigner(claim Claims) error
Validate(vr *ValidationResults)
}
type ScopeType int
const (
UserScopeType ScopeType = iota + 1
)
func (t ScopeType) String() string {
switch t {
case UserScopeType:
return "user_scope"
}
return "unknown"
}
func (t *ScopeType) MarshalJSON() ([]byte, error) {
switch *t {
case UserScopeType:
return []byte("\"user_scope\""), nil
}
return nil, fmt.Errorf("unknown scope type %q", t)
}
func (t *ScopeType) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
switch s {
case "user_scope":
*t = UserScopeType
return nil
}
return fmt.Errorf("unknown scope type %q", t)
}
type UserScope struct {
Kind ScopeType `json:"kind"`
Key string `json:"key"`
Role string `json:"role"`
Template UserPermissionLimits `json:"template"`
Description string `json:"description"`
}
func NewUserScope() *UserScope {
var s UserScope
s.Kind = UserScopeType
s.Template.NatsLimits = NatsLimits{NoLimit, NoLimit, NoLimit}
return &s
}
func (us UserScope) SigningKey() string {
return us.Key
}
func (us UserScope) Validate(vr *ValidationResults) {
if !nkeys.IsValidPublicAccountKey(us.Key) {
vr.AddError("%s is not an account public key", us.Key)
}
}
func (us UserScope) ValidateScopedSigner(c Claims) error {
uc, ok := c.(*UserClaims)
if !ok {
return fmt.Errorf("not an user claim - scoped signing key requires user claim")
}
if uc.Claims().Issuer != us.Key {
return errors.New("issuer not the scoped signer")
}
if !uc.HasEmptyPermissions() {
return errors.New("scoped users require no permissions or limits set")
}
return nil
}
// SigningKeys is a map keyed by a public account key
type SigningKeys map[string]Scope
func (sk SigningKeys) Validate(vr *ValidationResults) {
for k, v := range sk {
// regular signing keys won't have a scope
if v != nil {
v.Validate(vr)
} else {
if !nkeys.IsValidPublicAccountKey(k) {
vr.AddError("%q is not a valid account signing key", k)
}
}
}
}
// MarshalJSON serializes the scoped signing keys as an array
func (sk *SigningKeys) MarshalJSON() ([]byte, error) {
if sk == nil {
return nil, nil
}
keys := sk.Keys()
sort.Strings(keys)
var a []interface{}
for _, k := range keys {
if (*sk)[k] != nil {
a = append(a, (*sk)[k])
} else {
a = append(a, k)
}
}
return json.Marshal(a)
}
func (sk *SigningKeys) UnmarshalJSON(data []byte) error {
if *sk == nil {
*sk = make(SigningKeys)
}
// read an array - we can have a string or an map
var a []interface{}
if err := json.Unmarshal(data, &a); err != nil {
return err
}
for _, i := range a {
switch v := i.(type) {
case string:
(*sk)[v] = nil
case map[string]interface{}:
d, err := json.Marshal(v)
if err != nil {
return err
}
switch v["kind"] {
case UserScopeType.String():
us := NewUserScope()
if err := json.Unmarshal(d, &us); err != nil {
return err
}
(*sk)[us.Key] = us
default:
return fmt.Errorf("unknown signing key scope %q", v["type"])
}
}
}
return nil
}
func (sk SigningKeys) Keys() []string {
var keys []string
for k := range sk {
keys = append(keys, k)
}
return keys
}
// GetScope returns nil if the key is not associated
func (sk SigningKeys) GetScope(k string) (Scope, bool) {
v, ok := sk[k]
if !ok {
return nil, false
}
return v, true
}
func (sk SigningKeys) Contains(k string) bool {
_, ok := sk[k]
return ok
}
func (sk SigningKeys) Add(keys ...string) {
for _, k := range keys {
sk[k] = nil
}
}
func (sk SigningKeys) AddScopedSigner(s Scope) {
sk[s.SigningKey()] = s
}
func (sk SigningKeys) Remove(keys ...string) {
for _, k := range keys {
delete(sk, k)
}
}
+495
View File
@@ -0,0 +1,495 @@
/*
* Copyright 2018-2019 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 (
"encoding/json"
"fmt"
"net"
"net/url"
"reflect"
"strconv"
"strings"
"time"
)
const MaxInfoLength = 8 * 1024
type Info struct {
Description string `json:"description,omitempty"`
InfoURL string `json:"info_url,omitempty"`
}
func (s Info) Validate(vr *ValidationResults) {
if len(s.Description) > MaxInfoLength {
vr.AddError("Description is too long")
}
if s.InfoURL != "" {
if len(s.InfoURL) > MaxInfoLength {
vr.AddError("Info URL is too long")
}
u, err := url.Parse(s.InfoURL)
if err == nil && (u.Hostname() == "" || u.Scheme == "") {
err = fmt.Errorf("no hostname or scheme")
}
if err != nil {
vr.AddError("error parsing info url: %v", err)
}
}
}
// ExportType defines the type of import/export.
type ExportType int
const (
// Unknown is used if we don't know the type
Unknown ExportType = iota
// Stream defines the type field value for a stream "stream"
Stream
// Service defines the type field value for a service "service"
Service
)
func (t ExportType) String() string {
switch t {
case Stream:
return "stream"
case Service:
return "service"
}
return "unknown"
}
// MarshalJSON marshals the enum as a quoted json string
func (t *ExportType) MarshalJSON() ([]byte, error) {
switch *t {
case Stream:
return []byte("\"stream\""), nil
case Service:
return []byte("\"service\""), nil
}
return nil, fmt.Errorf("unknown export type")
}
// UnmarshalJSON unmashals a quoted json string to the enum value
func (t *ExportType) UnmarshalJSON(b []byte) error {
var j string
err := json.Unmarshal(b, &j)
if err != nil {
return err
}
switch j {
case "stream":
*t = Stream
return nil
case "service":
*t = Service
return nil
}
return fmt.Errorf("unknown export type %q", j)
}
type RenamingSubject Subject
func (s RenamingSubject) Validate(from Subject, vr *ValidationResults) {
v := Subject(s)
v.Validate(vr)
if from == "" {
vr.AddError("subject cannot be empty")
}
if strings.Contains(string(s), " ") {
vr.AddError("subject %q cannot have spaces", v)
}
matchesSuffix := func(s Subject) bool {
return s == ">" || strings.HasSuffix(string(s), ".>")
}
if matchesSuffix(v) != matchesSuffix(from) {
vr.AddError("both, renaming subject and subject, need to end or not end in >")
}
fromCnt := from.countTokenWildcards()
refCnt := 0
for _, tk := range strings.Split(string(v), ".") {
if tk == "*" {
refCnt++
}
if len(tk) < 2 {
continue
}
if tk[0] == '$' {
if idx, err := strconv.Atoi(tk[1:]); err == nil {
if idx > fromCnt {
vr.AddError("Reference $%d in %q reference * in %q that do not exist", idx, s, from)
} else {
refCnt++
}
}
}
}
if refCnt != fromCnt {
vr.AddError("subject does not contain enough * or reference wildcards $[0-9]")
}
}
// Replaces reference tokens with *
func (s RenamingSubject) ToSubject() Subject {
if !strings.Contains(string(s), "$") {
return Subject(s)
}
bldr := strings.Builder{}
tokens := strings.Split(string(s), ".")
for i, tk := range tokens {
convert := false
if len(tk) > 1 && tk[0] == '$' {
if _, err := strconv.Atoi(tk[1:]); err == nil {
convert = true
}
}
if convert {
bldr.WriteString("*")
} else {
bldr.WriteString(tk)
}
if i != len(tokens)-1 {
bldr.WriteString(".")
}
}
return Subject(bldr.String())
}
// Subject is a string that represents a NATS subject
type Subject string
// Validate checks that a subject string is valid, ie not empty and without spaces
func (s Subject) Validate(vr *ValidationResults) {
v := string(s)
if v == "" {
vr.AddError("subject cannot be empty")
// No other checks after that make sense
return
}
if strings.Contains(v, " ") {
vr.AddError("subject %q cannot have spaces", v)
}
if v[0] == '.' || v[len(v)-1] == '.' {
vr.AddError("subject %q cannot start or end with a `.`", v)
}
if strings.Contains(v, "..") {
vr.AddError("subject %q cannot contain consecutive `.`", v)
}
}
func (s Subject) countTokenWildcards() int {
v := string(s)
if v == "*" {
return 1
}
cnt := 0
for _, t := range strings.Split(v, ".") {
if t == "*" {
cnt++
}
}
return cnt
}
// HasWildCards is used to check if a subject contains a > or *
func (s Subject) HasWildCards() bool {
v := string(s)
return strings.HasSuffix(v, ".>") ||
strings.Contains(v, ".*.") ||
strings.HasSuffix(v, ".*") ||
strings.HasPrefix(v, "*.") ||
v == "*" ||
v == ">"
}
// IsContainedIn does a simple test to see if the subject is contained in another subject
func (s Subject) IsContainedIn(other Subject) bool {
otherArray := strings.Split(string(other), ".")
myArray := strings.Split(string(s), ".")
if len(myArray) > len(otherArray) && otherArray[len(otherArray)-1] != ">" {
return false
}
if len(myArray) < len(otherArray) {
return false
}
for ind, tok := range otherArray {
myTok := myArray[ind]
if ind == len(otherArray)-1 && tok == ">" {
return true
}
if tok != myTok && tok != "*" {
return false
}
}
return true
}
// TimeRange is used to represent a start and end time
type TimeRange struct {
Start string `json:"start,omitempty"`
End string `json:"end,omitempty"`
}
// Validate checks the values in a time range struct
func (tr *TimeRange) Validate(vr *ValidationResults) {
format := "15:04:05"
if tr.Start == "" {
vr.AddError("time ranges start must contain a start")
} else {
_, err := time.Parse(format, tr.Start)
if err != nil {
vr.AddError("start in time range is invalid %q", tr.Start)
}
}
if tr.End == "" {
vr.AddError("time ranges end must contain an end")
} else {
_, err := time.Parse(format, tr.End)
if err != nil {
vr.AddError("end in time range is invalid %q", tr.End)
}
}
}
// Src is a comma separated list of CIDR specifications
type UserLimits struct {
Src CIDRList `json:"src,omitempty"`
Times []TimeRange `json:"times,omitempty"`
Locale string `json:"times_location,omitempty"`
}
func (u *UserLimits) Empty() bool {
return reflect.DeepEqual(*u, UserLimits{})
}
func (u *UserLimits) IsUnlimited() bool {
return len(u.Src) == 0 && len(u.Times) == 0
}
// Limits are used to control acccess for users and importing accounts
type Limits struct {
UserLimits
NatsLimits
}
func (l *Limits) IsUnlimited() bool {
return l.UserLimits.IsUnlimited() && l.NatsLimits.IsUnlimited()
}
// Validate checks the values in a limit struct
func (l *Limits) Validate(vr *ValidationResults) {
if len(l.Src) != 0 {
for _, cidr := range l.Src {
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil || ipNet == nil {
vr.AddError("invalid cidr %q in user src limits", cidr)
}
}
}
if len(l.Times) > 0 {
for _, t := range l.Times {
t.Validate(vr)
}
}
if l.Locale != "" {
if _, err := time.LoadLocation(l.Locale); err != nil {
vr.AddError("could not parse iana time zone by name: %v", err)
}
}
}
// Permission defines allow/deny subjects
type Permission struct {
Allow StringList `json:"allow,omitempty"`
Deny StringList `json:"deny,omitempty"`
}
func (p *Permission) Empty() bool {
return len(p.Allow) == 0 && len(p.Deny) == 0
}
func checkPermission(vr *ValidationResults, subj string, permitQueue bool) {
tk := strings.Split(subj, " ")
switch len(tk) {
case 1:
Subject(tk[0]).Validate(vr)
case 2:
Subject(tk[0]).Validate(vr)
Subject(tk[1]).Validate(vr)
if !permitQueue {
vr.AddError(`Permission Subject "%s" is not allowed to contain queue`, subj)
}
default:
vr.AddError(`Permission Subject "%s" contains too many spaces`, subj)
}
}
// Validate the allow, deny elements of a permission
func (p *Permission) Validate(vr *ValidationResults, permitQueue bool) {
for _, subj := range p.Allow {
checkPermission(vr, subj, permitQueue)
}
for _, subj := range p.Deny {
checkPermission(vr, subj, permitQueue)
}
}
// ResponsePermission can be used to allow responses to any reply subject
// that is received on a valid subscription.
type ResponsePermission struct {
MaxMsgs int `json:"max"`
Expires time.Duration `json:"ttl"`
}
// Validate the response permission.
func (p *ResponsePermission) Validate(_ *ValidationResults) {
// Any values can be valid for now.
}
// Permissions are used to restrict subject access, either on a user or for everyone on a server by default
type Permissions struct {
Pub Permission `json:"pub,omitempty"`
Sub Permission `json:"sub,omitempty"`
Resp *ResponsePermission `json:"resp,omitempty"`
}
// Validate the pub and sub fields in the permissions list
func (p *Permissions) Validate(vr *ValidationResults) {
if p.Resp != nil {
p.Resp.Validate(vr)
}
p.Sub.Validate(vr, true)
p.Pub.Validate(vr, false)
}
// StringList is a wrapper for an array of strings
type StringList []string
// Contains returns true if the list contains the string
func (u *StringList) Contains(p string) bool {
for _, t := range *u {
if t == p {
return true
}
}
return false
}
// Add appends 1 or more strings to a list
func (u *StringList) Add(p ...string) {
for _, v := range p {
if !u.Contains(v) && v != "" {
*u = append(*u, v)
}
}
}
// Remove removes 1 or more strings from a list
func (u *StringList) Remove(p ...string) {
for _, v := range p {
for i, t := range *u {
if t == v {
a := *u
*u = append(a[:i], a[i+1:]...)
break
}
}
}
}
// TagList is a unique array of lower case strings
// All tag list methods lower case the strings in the arguments
type TagList []string
// Contains returns true if the list contains the tags
func (u *TagList) Contains(p string) bool {
p = strings.ToLower(strings.TrimSpace(p))
for _, t := range *u {
if t == p {
return true
}
}
return false
}
// Add appends 1 or more tags to a list
func (u *TagList) Add(p ...string) {
for _, v := range p {
v = strings.ToLower(strings.TrimSpace(v))
if !u.Contains(v) && v != "" {
*u = append(*u, v)
}
}
}
// Remove removes 1 or more tags from a list
func (u *TagList) Remove(p ...string) {
for _, v := range p {
v = strings.ToLower(strings.TrimSpace(v))
for i, t := range *u {
if t == v {
a := *u
*u = append(a[:i], a[i+1:]...)
break
}
}
}
}
type CIDRList TagList
func (c *CIDRList) Contains(p string) bool {
return (*TagList)(c).Contains(p)
}
func (c *CIDRList) Add(p ...string) {
(*TagList)(c).Add(p...)
}
func (c *CIDRList) Remove(p ...string) {
(*TagList)(c).Remove(p...)
}
func (c *CIDRList) Set(values string) {
*c = CIDRList{}
c.Add(strings.Split(strings.ToLower(values), ",")...)
}
func (c *CIDRList) UnmarshalJSON(body []byte) (err error) {
// parse either as array of strings or comma separate list
var request []string
var list string
if err := json.Unmarshal(body, &request); err == nil {
*c = request
return nil
} else if err := json.Unmarshal(body, &list); err == nil {
c.Set(list)
return nil
} else {
return err
}
}
+163
View File
@@ -0,0 +1,163 @@
/*
* 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"
"reflect"
"github.com/nats-io/nkeys"
)
const (
ConnectionTypeStandard = "STANDARD"
ConnectionTypeWebsocket = "WEBSOCKET"
ConnectionTypeLeafnode = "LEAFNODE"
ConnectionTypeLeafnodeWS = "LEAFNODE_WS"
ConnectionTypeMqtt = "MQTT"
ConnectionTypeMqttWS = "MQTT_WS"
ConnectionTypeInProcess = "IN_PROCESS"
)
type UserPermissionLimits struct {
Permissions
Limits
BearerToken bool `json:"bearer_token,omitempty"`
ProxyRequired bool `json:"proxy_required,omitempty"`
AllowedConnectionTypes StringList `json:"allowed_connection_types,omitempty"`
}
// User defines the user specific data in a user JWT
type User struct {
UserPermissionLimits
// IssuerAccount stores the public key for the account the issuer represents.
// When set, the claim was issued by a signing key.
IssuerAccount string `json:"issuer_account,omitempty"`
GenericFields
}
// Validate checks the permissions and limits in a User jwt
func (u *User) Validate(vr *ValidationResults) {
u.Permissions.Validate(vr)
u.Limits.Validate(vr)
// When BearerToken is true server will ignore any nonce-signing verification
}
// UserClaims defines a user JWT
type UserClaims struct {
ClaimsData
User `json:"nats,omitempty"`
}
// NewUserClaims creates a user JWT with the specific subject/public key
func NewUserClaims(subject string) *UserClaims {
if subject == "" {
return nil
}
c := &UserClaims{}
c.Subject = subject
c.Limits = Limits{
UserLimits{CIDRList{}, nil, ""},
NatsLimits{NoLimit, NoLimit, NoLimit},
}
return c
}
func (u *UserClaims) SetScoped(t bool) {
if t {
u.UserPermissionLimits = UserPermissionLimits{}
} else {
u.Limits = Limits{
UserLimits{CIDRList{}, nil, ""},
NatsLimits{NoLimit, NoLimit, NoLimit},
}
}
}
func (u *UserClaims) HasEmptyPermissions() bool {
return reflect.DeepEqual(u.UserPermissionLimits, UserPermissionLimits{})
}
// Encode tries to turn the user claims into a JWT string
func (u *UserClaims) Encode(pair nkeys.KeyPair) (string, error) {
return u.EncodeWithSigner(pair, nil)
}
func (u *UserClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
if !nkeys.IsValidPublicUserKey(u.Subject) {
return "", errors.New("expected subject to be user public key")
}
u.Type = UserClaim
return u.ClaimsData.encode(pair, u, fn)
}
// DecodeUserClaims tries to parse a user claims from a JWT string
func DecodeUserClaims(token string) (*UserClaims, error) {
claims, err := Decode(token)
if err != nil {
return nil, err
}
ac, ok := claims.(*UserClaims)
if !ok {
return nil, errors.New("not user claim")
}
return ac, nil
}
func (u *UserClaims) ClaimType() ClaimType {
return u.Type
}
// Validate checks the generic and specific parts of the user jwt
func (u *UserClaims) Validate(vr *ValidationResults) {
u.ClaimsData.Validate(vr)
u.User.Validate(vr)
if u.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(u.IssuerAccount) {
vr.AddError("account_id is not an account public key")
}
}
// ExpectedPrefixes defines the types that can encode a user JWT, account
func (u *UserClaims) ExpectedPrefixes() []nkeys.PrefixByte {
return []nkeys.PrefixByte{nkeys.PrefixByteAccount}
}
// Claims returns the generic data from a user jwt
func (u *UserClaims) Claims() *ClaimsData {
return &u.ClaimsData
}
// Payload returns the user specific data from a user JWT
func (u *UserClaims) Payload() interface{} {
return &u.User
}
func (u *UserClaims) String() string {
return u.ClaimsData.String(u)
}
func (u *UserClaims) updateVersion() {
u.GenericFields.Version = libVersion
}
// IsBearerToken returns true if nonce-signing requirements should be skipped
func (u *UserClaims) IsBearerToken() bool {
return u.BearerToken
}
func (u *UserClaims) GetTags() TagList {
return u.User.Tags
}
+118
View File
@@ -0,0 +1,118 @@
/*
* Copyright 2018 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"
)
// ValidationIssue represents an issue during JWT validation, it may or may not be a blocking error
type ValidationIssue struct {
Description string
Blocking bool
TimeCheck bool
}
func (ve *ValidationIssue) Error() string {
return ve.Description
}
// ValidationResults is a list of ValidationIssue pointers
type ValidationResults struct {
Issues []*ValidationIssue
}
// CreateValidationResults creates an empty list of validation issues
func CreateValidationResults() *ValidationResults {
var issues []*ValidationIssue
return &ValidationResults{
Issues: issues,
}
}
// Add appends an issue to the list
func (v *ValidationResults) Add(vi *ValidationIssue) {
v.Issues = append(v.Issues, vi)
}
// AddError creates a new validation error and adds it to the list
func (v *ValidationResults) AddError(format string, args ...interface{}) {
v.Add(&ValidationIssue{
Description: fmt.Sprintf(format, args...),
Blocking: true,
TimeCheck: false,
})
}
// AddTimeCheck creates a new validation issue related to a time check and adds it to the list
func (v *ValidationResults) AddTimeCheck(format string, args ...interface{}) {
v.Add(&ValidationIssue{
Description: fmt.Sprintf(format, args...),
Blocking: false,
TimeCheck: true,
})
}
// AddWarning creates a new validation warning and adds it to the list
func (v *ValidationResults) AddWarning(format string, args ...interface{}) {
v.Add(&ValidationIssue{
Description: fmt.Sprintf(format, args...),
Blocking: false,
TimeCheck: false,
})
}
// IsBlocking returns true if the list contains a blocking error
func (v *ValidationResults) IsBlocking(includeTimeChecks bool) bool {
for _, i := range v.Issues {
if i.Blocking {
return true
}
if includeTimeChecks && i.TimeCheck {
return true
}
}
return false
}
// IsEmpty returns true if the list is empty
func (v *ValidationResults) IsEmpty() bool {
return len(v.Issues) == 0
}
// Errors returns only blocking issues as errors
func (v *ValidationResults) Errors() []error {
var errs []error
for _, v := range v.Issues {
if v.Blocking {
errs = append(errs, errors.New(v.Description))
}
}
return errs
}
// Warnings returns only non blocking issues as strings
func (v *ValidationResults) Warnings() []string {
var errs []string
for _, v := range v.Issues {
if !v.Blocking {
errs = append(errs, v.Description)
}
}
return errs
}