Skip to content

Commit

Permalink
Implemented roles for signing keys (#129)
Browse files Browse the repository at this point in the history
[feat] Implemented roles for signing keys
Roles are similar to default user configurations in that they specify the permissions
for a client. These permissions are associated with a signing key. Any user issued
with the specified signing key is only allowed the permissions specified by the account.
  • Loading branch information
aricart authored Dec 17, 2020
1 parent 8d0f36a commit ca66b86
Show file tree
Hide file tree
Showing 8 changed files with 470 additions and 19 deletions.
2 changes: 1 addition & 1 deletion v2/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ test:
go test -v -coverprofile=./coverage.out ./...

cover:
go tool cover -html=coverage.out
go tool cover -html=coverage.out
16 changes: 6 additions & 10 deletions v2/account_claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ type Account struct {
Imports Imports `json:"imports,omitempty"`
Exports Exports `json:"exports,omitempty"`
Limits OperatorLimits `json:"limits,omitempty"`
SigningKeys StringList `json:"signing_keys,omitempty"`
SigningKeys SigningKeys `json:"signing_keys,omitempty"`
Revocations RevocationList `json:"revocations,omitempty"`
DefaultPermissions Permissions `json:"default_permissions,omitempty"`
Info
Expand Down Expand Up @@ -126,12 +126,7 @@ func (a *Account) Validate(acct *AccountClaims, vr *ValidationResults) {
}
}
}

for _, k := range a.SigningKeys {
if !nkeys.IsValidPublicAccountKey(k) {
vr.AddError("%s is not an account public key", k)
}
}
a.SigningKeys.Validate(vr)
a.Info.Validate(vr)
}

Expand All @@ -147,6 +142,7 @@ func NewAccountClaims(subject string) *AccountClaims {
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{
Expand Down Expand Up @@ -221,9 +217,9 @@ func (a *AccountClaims) Claims() *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
func (a *AccountClaims) DidSign(uc Claims) bool {
if uc != nil {
issuer := uc.Claims().Issuer
if issuer == a.Subject {
return true
}
Expand Down
9 changes: 5 additions & 4 deletions v2/account_claims_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,15 +347,16 @@ func TestAccountSigningKeyValidation(t *testing.T) {
if err != nil {
t.Fatal(err)
}

ac2, err := DecodeAccountClaims(token)
if err != nil {
t.Fatal(err)
}
if len(ac2.SigningKeys) != 1 {
t.Fatal("expected claim to have a signing key")
}
if ac.SigningKeys[0] != apk2 {
t.Fatalf("expected signing key to be %s - got %s", apk2, ac.SigningKeys[0])
if !ac.SigningKeys.Contains(apk2) {
t.Fatalf("expected signing key %s", apk2)
}

bkp := createUserNKey(t)
Expand Down Expand Up @@ -388,8 +389,8 @@ func TestAccountSignedBy(t *testing.T) {
if len(ac2.SigningKeys) != 1 {
t.Fatal("expected claim to have a signing key")
}
if ac.SigningKeys[0] != apk2 {
t.Fatalf("expected signing key to be %s - got %s", apk2, ac.SigningKeys[0])
if !ac.SigningKeys.Contains(apk2) {
t.Fatalf("expected signing key %s", apk2)
}

ukp := createUserNKey(t)
Expand Down
6 changes: 5 additions & 1 deletion v2/decoder_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func loadAccount(data []byte, version int) (*AccountClaims, error) {
return v1a.Migrate()
case 2:
var v2a AccountClaims
v2a.SigningKeys = make(SigningKeys)
if err := json.Unmarshal(data, &v2a); err != nil {
return nil, err
}
Expand Down Expand Up @@ -73,7 +74,10 @@ func (oa v1AccountClaims) migrateV1() (*AccountClaims, error) {
a.Account.Limits.AccountLimits = oa.v1NatsAccount.Limits.AccountLimits
a.Account.Limits.NatsLimits = oa.v1NatsAccount.Limits.NatsLimits
a.Account.Limits.JetStreamLimits = JetStreamLimits{0, 0, 0, 0}
a.Account.SigningKeys = oa.v1NatsAccount.SigningKeys
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
Expand Down
196 changes: 196 additions & 0 deletions v2/signingkeys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* 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"
"errors"
"fmt"

"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"`
}

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 {
sv, ok := v.(Scope)
if ok {
sv.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) {
var a []interface{}
for k, v := range sk {
if v != nil {
a = append(a, v)
} else {
a = append(a, k)
}
}
return json.Marshal(a)
}

func (sk SigningKeys) UnmarshalJSON(data []byte) error {
// 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
}

// 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)
}
}
Loading

0 comments on commit ca66b86

Please sign in to comment.