mirror of https://github.com/tidwall/tile38.git
233 lines
7.5 KiB
Go
233 lines
7.5 KiB
Go
/*
|
|
* 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 (
|
|
"errors"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/nats-io/nkeys"
|
|
)
|
|
|
|
// NoLimit is used to indicate a limit field is unlimited in value.
|
|
const NoLimit = -1
|
|
|
|
// OperatorLimits are used to limit access by an account
|
|
type OperatorLimits struct {
|
|
Subs int64 `json:"subs,omitempty"` // Max number of subscriptions
|
|
Conn int64 `json:"conn,omitempty"` // Max number of active connections
|
|
LeafNodeConn int64 `json:"leaf,omitempty"` // Max number of active leaf node connections
|
|
Imports int64 `json:"imports,omitempty"` // Max number of imports
|
|
Exports int64 `json:"exports,omitempty"` // Max number of exports
|
|
Data int64 `json:"data,omitempty"` // Max number of bytes
|
|
Payload int64 `json:"payload,omitempty"` // Max message payload
|
|
WildcardExports bool `json:"wildcards,omitempty"` // Are wildcards allowed in exports
|
|
}
|
|
|
|
// IsEmpty returns true if all of the limits are 0/false.
|
|
func (o *OperatorLimits) IsEmpty() bool {
|
|
return *o == OperatorLimits{}
|
|
}
|
|
|
|
// IsUnlimited returns true if all limits are
|
|
func (o *OperatorLimits) IsUnlimited() bool {
|
|
return *o == OperatorLimits{NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, true}
|
|
}
|
|
|
|
// Validate checks that the operator limits contain valid values
|
|
func (o *OperatorLimits) Validate(vr *ValidationResults) {
|
|
// negative values mean unlimited, so all numbers are valid
|
|
}
|
|
|
|
// Account holds account specific claims data
|
|
type Account struct {
|
|
Imports Imports `json:"imports,omitempty"`
|
|
Exports Exports `json:"exports,omitempty"`
|
|
Identities []Identity `json:"identity,omitempty"`
|
|
Limits OperatorLimits `json:"limits,omitempty"`
|
|
SigningKeys StringList `json:"signing_keys,omitempty"`
|
|
Revocations RevocationList `json:"revocations,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)
|
|
|
|
for _, i := range a.Identities {
|
|
i.Validate(vr)
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, k := range a.SigningKeys {
|
|
if !nkeys.IsValidPublicAccountKey(k) {
|
|
vr.AddError("%s is not an account public key", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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{}
|
|
// Set to unlimited to start. We do it this way so we get compiler
|
|
// errors if we add to the OperatorLimits.
|
|
c.Limits = OperatorLimits{NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, true}
|
|
c.Subject = subject
|
|
return c
|
|
}
|
|
|
|
// Encode converts account claims into a JWT string
|
|
func (a *AccountClaims) Encode(pair nkeys.KeyPair) (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.ClaimsData.Type = AccountClaim
|
|
return a.ClaimsData.Encode(pair, a)
|
|
}
|
|
|
|
// DecodeAccountClaims decodes account claims from a JWT string
|
|
func DecodeAccountClaims(token string) (*AccountClaims, error) {
|
|
v := AccountClaims{}
|
|
if err := Decode(token, &v); err != nil {
|
|
return nil, err
|
|
}
|
|
return &v, 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 len(a.Identities) > 0 {
|
|
vr.AddWarning("self-signed account JWTs shouldn't contain identity proofs")
|
|
}
|
|
if !a.Limits.IsEmpty() {
|
|
vr.AddWarning("self-signed account JWTs shouldn't contain operator limits")
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// DidSign checks the claims against the account's public key and its signing keys
|
|
func (a *AccountClaims) DidSign(op Claims) bool {
|
|
if op != nil {
|
|
issuer := op.Claims().Issuer
|
|
if issuer == a.Subject {
|
|
return true
|
|
}
|
|
return a.SigningKeys.Contains(issuer)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Revoke enters a revocation by publickey 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.
|
|
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)
|
|
}
|
|
|
|
// IsRevokedAt 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) IsRevokedAt(pubKey string, timestamp time.Time) bool {
|
|
return a.Revocations.IsRevoked(pubKey, timestamp)
|
|
}
|
|
|
|
// IsRevoked does not perform a valid check. Use IsRevokedAt instead.
|
|
func (a *AccountClaims) IsRevoked(_ string) bool {
|
|
return true
|
|
}
|
|
|
|
// 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.Revocations.IsRevoked(claim.Subject, time.Unix(claim.IssuedAt, 0))
|
|
}
|