Initial QSfera import
This commit is contained in:
+201
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user