mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 03:38:42 -07:00
Basic nkey support and nonce handling
Signed-off-by: Derek Collison <derek@nats.io>
This commit is contained in:
@@ -5,6 +5,7 @@ go:
|
||||
- 1.11
|
||||
install:
|
||||
- go get github.com/nats-io/go-nats
|
||||
- go get github.com/nats-io/nkeys
|
||||
- go get github.com/mattn/goveralls
|
||||
- go get github.com/wadey/gocovmerge
|
||||
- go get -u honnef.co/go/tools/cmd/megacheck
|
||||
|
||||
132
server/auth.go
132
server/auth.go
@@ -15,9 +15,11 @@ package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/nats-io/nkeys"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -37,6 +39,12 @@ type ClientAuthentication interface {
|
||||
RegisterUser(*User)
|
||||
}
|
||||
|
||||
// Nkey is for multiple nkey based users
|
||||
type NkeyUser struct {
|
||||
Nkey string `json:"user"`
|
||||
Permissions *Permissions `json:"permissions"`
|
||||
}
|
||||
|
||||
// User is for multiple accounts/users.
|
||||
type User struct {
|
||||
Username string `json:"user"`
|
||||
@@ -56,6 +64,18 @@ func (u *User) clone() *User {
|
||||
return clone
|
||||
}
|
||||
|
||||
// clone performs a deep copy of the NkeyUser struct, returning a new clone with
|
||||
// all values copied.
|
||||
func (n *NkeyUser) clone() *NkeyUser {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
clone := &NkeyUser{}
|
||||
*clone = *n
|
||||
clone.Permissions = n.Permissions.clone()
|
||||
return clone
|
||||
}
|
||||
|
||||
// SubjectPermission is an individual allow and deny struct for publish
|
||||
// and subscribe authorizations.
|
||||
type SubjectPermission struct {
|
||||
@@ -125,10 +145,19 @@ func (s *Server) configureAuthorization() {
|
||||
// This just checks and sets up the user map if we have multiple users.
|
||||
if opts.CustomClientAuthentication != nil {
|
||||
s.info.AuthRequired = true
|
||||
} else if opts.Users != nil {
|
||||
s.users = make(map[string]*User)
|
||||
for _, u := range opts.Users {
|
||||
s.users[u.Username] = u
|
||||
} else if opts.Nkeys != nil || opts.Users != nil {
|
||||
// Support both at the same time.
|
||||
if opts.Nkeys != nil {
|
||||
s.nkeys = make(map[string]*NkeyUser)
|
||||
for _, u := range opts.Nkeys {
|
||||
s.nkeys[u.Nkey] = u
|
||||
}
|
||||
}
|
||||
if opts.Users != nil {
|
||||
s.users = make(map[string]*User)
|
||||
for _, u := range opts.Users {
|
||||
s.users[u.Username] = u
|
||||
}
|
||||
}
|
||||
s.info.AuthRequired = true
|
||||
} else if opts.Username != "" || opts.Authorization != "" {
|
||||
@@ -152,31 +181,72 @@ func (s *Server) checkAuthorization(c *client) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// hasUsers leyt's us know if we have a users array.
|
||||
func (s *Server) hasUsers() bool {
|
||||
s.mu.Lock()
|
||||
hu := s.users != nil
|
||||
s.mu.Unlock()
|
||||
return hu
|
||||
}
|
||||
|
||||
// isClientAuthorized will check the client against the proper authorization method and data.
|
||||
// This could be token or username/password based.
|
||||
// This could be nkey, token, or username/password based.
|
||||
func (s *Server) isClientAuthorized(c *client) bool {
|
||||
// Snapshot server options.
|
||||
opts := s.getOpts()
|
||||
// Snapshot server options by hand and only grab what we really need.
|
||||
s.optsMu.RLock()
|
||||
customClientAuthentication := s.opts.CustomClientAuthentication
|
||||
authorization := s.opts.Authorization
|
||||
username := s.opts.Username
|
||||
password := s.opts.Password
|
||||
s.optsMu.RUnlock()
|
||||
|
||||
// Check custom auth first, then multiple users, then token, then single user/pass.
|
||||
if opts.CustomClientAuthentication != nil {
|
||||
return opts.CustomClientAuthentication.Check(c)
|
||||
} else if s.hasUsers() {
|
||||
s.mu.Lock()
|
||||
user, ok := s.users[c.opts.Username]
|
||||
// Check custom auth first, then nkeys, then multiple users, then token, then single user/pass.
|
||||
if customClientAuthentication != nil {
|
||||
return customClientAuthentication.Check(c)
|
||||
}
|
||||
|
||||
var nkey *NkeyUser
|
||||
var user *User
|
||||
var ok bool
|
||||
|
||||
s.mu.Lock()
|
||||
authRequired := s.info.AuthRequired
|
||||
if !authRequired {
|
||||
// TODO(dlc) - If they send us credentials should we fail?
|
||||
s.mu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if we have nkeys or users for client.
|
||||
hasNkeys := s.nkeys != nil
|
||||
hasUsers := s.users != nil
|
||||
if hasNkeys && c.opts.Nkey != "" {
|
||||
nkey, ok = s.nkeys[c.opts.Nkey]
|
||||
if !ok {
|
||||
s.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
} else if hasUsers && c.opts.Username != "" {
|
||||
user, ok = s.users[c.opts.Username]
|
||||
if !ok {
|
||||
s.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
// Verify the signature against the nonce.
|
||||
if nkey != nil {
|
||||
if c.opts.Sig == "" {
|
||||
return false
|
||||
}
|
||||
sig, err := base64.RawURLEncoding.DecodeString(c.opts.Sig)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
pub, err := nkeys.FromPublicKey(c.opts.Nkey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if err := pub.Verify(c.nonce, sig); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
ok = comparePasswords(user.Password, c.opts.Password)
|
||||
// If we are authorized, register the user which will properly setup any permissions
|
||||
// for pub/sub authorizations.
|
||||
@@ -184,18 +254,18 @@ func (s *Server) isClientAuthorized(c *client) bool {
|
||||
c.RegisterUser(user)
|
||||
}
|
||||
return ok
|
||||
|
||||
} else if opts.Authorization != "" {
|
||||
return comparePasswords(opts.Authorization, c.opts.Authorization)
|
||||
|
||||
} else if opts.Username != "" {
|
||||
if opts.Username != c.opts.Username {
|
||||
return false
|
||||
}
|
||||
return comparePasswords(opts.Password, c.opts.Password)
|
||||
}
|
||||
|
||||
return true
|
||||
if authorization != "" {
|
||||
return comparePasswords(authorization, c.opts.Authorization)
|
||||
} else if username != "" {
|
||||
if username != c.opts.Username {
|
||||
return false
|
||||
}
|
||||
return comparePasswords(password, c.opts.Password)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// checkRouterAuth checks optional router authorization which can be nil or username/password.
|
||||
|
||||
@@ -137,6 +137,7 @@ type client struct {
|
||||
cid uint64
|
||||
opts clientOpts
|
||||
start time.Time
|
||||
nonce []byte
|
||||
nc net.Conn
|
||||
ncs string
|
||||
out outbound
|
||||
@@ -246,6 +247,8 @@ type clientOpts struct {
|
||||
Verbose bool `json:"verbose"`
|
||||
Pedantic bool `json:"pedantic"`
|
||||
TLSRequired bool `json:"tls_required"`
|
||||
Nkey string `json:"nkey"`
|
||||
Sig string `json:"sig"`
|
||||
Authorization string `json:"auth_token"`
|
||||
Username string `json:"user"`
|
||||
Password string `json:"pass"`
|
||||
|
||||
39
server/nkey.go
Normal file
39
server/nkey.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// 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 server
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
// Raw length of the nonce challenge
|
||||
const (
|
||||
nonceRawLen = 16
|
||||
nonceLen = 22 // base64.RawURLEncoding.EncodedLen(nonceRawLen)
|
||||
)
|
||||
|
||||
// nonceRequired tells us if we should send a nonce.
|
||||
// Assumes server lock is held
|
||||
func (s *Server) nonceRequired() bool {
|
||||
return len(s.opts.Nkeys) > 0
|
||||
}
|
||||
|
||||
// Generate a nonce for INFO challenge.
|
||||
// Assumes server lock is held
|
||||
func (s *Server) generateNonce(n []byte) {
|
||||
var raw [nonceRawLen]byte
|
||||
data := raw[:]
|
||||
s.prand.Read(data)
|
||||
base64.RawURLEncoding.Encode(n, data)
|
||||
}
|
||||
266
server/nkey_test.go
Normal file
266
server/nkey_test.go
Normal file
@@ -0,0 +1,266 @@
|
||||
// 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 server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
crand "crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
mrand "math/rand"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nkeys"
|
||||
)
|
||||
|
||||
// Nonce has to be a string since we used different encoding by default than json.Unmarshal.
|
||||
type nonceInfo struct {
|
||||
Id string `json:"server_id"`
|
||||
CID uint64 `json:"client_id,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
}
|
||||
|
||||
// This is a seed for a user. We can extract public and private keys from this for testing.
|
||||
const seed = "SUAOB32VQGYIOM622XYNXN4Q6GQR5I6DFZPPYZMNI5MMNVAQZDAL3OLH554ISOUTJM4EC6NWS5UHMS4CMONVVRW3VXXEQULMR6MDLPOEUVFPU"
|
||||
|
||||
func nkeyBasicSetup() (*Server, *client, *bufio.Reader, string) {
|
||||
kp, _ := nkeys.FromSeed(seed)
|
||||
pub, _ := kp.PublicKey()
|
||||
opts := defaultServerOptions
|
||||
opts.Nkeys = []*NkeyUser{&NkeyUser{Nkey: pub}}
|
||||
return rawSetup(opts)
|
||||
}
|
||||
|
||||
func mixedSetup() (*Server, *client, *bufio.Reader, string) {
|
||||
kp, _ := nkeys.FromSeed(seed)
|
||||
pub, _ := kp.PublicKey()
|
||||
opts := defaultServerOptions
|
||||
opts.Nkeys = []*NkeyUser{&NkeyUser{Nkey: pub}}
|
||||
opts.Users = []*User{&User{Username: "derek", Password: "foo"}}
|
||||
return rawSetup(opts)
|
||||
}
|
||||
|
||||
func newClientForServer(s *Server) (*client, *bufio.Reader, string) {
|
||||
cli, srv := net.Pipe()
|
||||
cr := bufio.NewReaderSize(cli, maxBufSize)
|
||||
ch := make(chan *client)
|
||||
createClientAsync(ch, s, srv)
|
||||
l, _ := cr.ReadString('\n')
|
||||
// Grab client
|
||||
c := <-ch
|
||||
return c, cr, l
|
||||
}
|
||||
|
||||
func TestServerInfoNonce(t *testing.T) {
|
||||
_, l := setUpClientWithResponse()
|
||||
if !strings.HasPrefix(l, "INFO ") {
|
||||
t.Fatalf("INFO response incorrect: %s\n", l)
|
||||
}
|
||||
// Make sure payload is proper json
|
||||
var info nonceInfo
|
||||
err := json.Unmarshal([]byte(l[5:]), &info)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not parse INFO json: %v\n", err)
|
||||
}
|
||||
if info.Nonce != "" {
|
||||
t.Fatalf("Expected an empty nonce with no nkeys defined")
|
||||
}
|
||||
|
||||
// Now setup server with auth and nkeys to trigger nonce generation
|
||||
s, _, _, l := nkeyBasicSetup()
|
||||
|
||||
if !strings.HasPrefix(l, "INFO ") {
|
||||
t.Fatalf("INFO response incorrect: %s\n", l)
|
||||
}
|
||||
// Make sure payload is proper json
|
||||
err = json.Unmarshal([]byte(l[5:]), &info)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not parse INFO json: %v\n", err)
|
||||
}
|
||||
if info.Nonce == "" {
|
||||
t.Fatalf("Expected a non-empty nonce with nkeys defined")
|
||||
}
|
||||
|
||||
// Make sure new clients get new nonces
|
||||
oldNonce := info.Nonce
|
||||
|
||||
_, _, l = newClientForServer(s)
|
||||
|
||||
err = json.Unmarshal([]byte(l[5:]), &info)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not parse INFO json: %v\n", err)
|
||||
}
|
||||
if info.Nonce == "" {
|
||||
t.Fatalf("Expected a non-empty nonce")
|
||||
}
|
||||
if strings.Compare(oldNonce, info.Nonce) == 0 {
|
||||
t.Fatalf("Expected subsequent nonces to be different\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNkeyClientConnect(t *testing.T) {
|
||||
s, c, cr, _ := nkeyBasicSetup()
|
||||
// Send CONNECT with no signature or nkey, should fail.
|
||||
connectOp := []byte("CONNECT {\"verbose\":true,\"pedantic\":true}\r\n")
|
||||
go c.parse(connectOp)
|
||||
l, _ := cr.ReadString('\n')
|
||||
if !strings.HasPrefix(l, "-ERR ") {
|
||||
t.Fatalf("Expected an error")
|
||||
}
|
||||
|
||||
kp, _ := nkeys.FromSeed(seed)
|
||||
pubKey, _ := kp.PublicKey()
|
||||
|
||||
// Send nkey but no signature
|
||||
c, cr, _ = newClientForServer(s)
|
||||
cs := fmt.Sprintf("CONNECT {\"nkey\":%q, \"verbose\":true,\"pedantic\":true}\r\n", pubKey)
|
||||
connectOp = []byte(cs)
|
||||
go c.parse(connectOp)
|
||||
l, _ = cr.ReadString('\n')
|
||||
if !strings.HasPrefix(l, "-ERR ") {
|
||||
t.Fatalf("Expected an error")
|
||||
}
|
||||
|
||||
// Now improperly sign etc.
|
||||
c, cr, _ = newClientForServer(s)
|
||||
cs = fmt.Sprintf("CONNECT {\"nkey\":%q,\"sig\":%q,\"verbose\":true,\"pedantic\":true}\r\n", pubKey, "bad_sig")
|
||||
go c.parse([]byte(cs))
|
||||
l, _ = cr.ReadString('\n')
|
||||
if !strings.HasPrefix(l, "-ERR ") {
|
||||
t.Fatalf("Expected an error")
|
||||
}
|
||||
|
||||
// Now properly sign the nonce
|
||||
c, cr, l = newClientForServer(s)
|
||||
// Check for Nonce
|
||||
var info nonceInfo
|
||||
err := json.Unmarshal([]byte(l[5:]), &info)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not parse INFO json: %v\n", err)
|
||||
}
|
||||
if info.Nonce == "" {
|
||||
t.Fatalf("Expected a non-empty nonce with nkeys defined")
|
||||
}
|
||||
sigraw, err := kp.Sign([]byte(info.Nonce))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed signing nonce: %v", err)
|
||||
}
|
||||
sig := base64.RawURLEncoding.EncodeToString(sigraw)
|
||||
|
||||
// PING needed to flush the +OK to us.
|
||||
cs = fmt.Sprintf("CONNECT {\"nkey\":%q,\"sig\":\"%s\",\"verbose\":true,\"pedantic\":true}\r\nPING\r\n", pubKey, sig)
|
||||
go c.parse([]byte(cs))
|
||||
l, _ = cr.ReadString('\n')
|
||||
if !strings.HasPrefix(l, "+OK") {
|
||||
t.Fatalf("Expected an OK, got: %v", l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMixedClientConnect(t *testing.T) {
|
||||
s, c, cr, _ := mixedSetup()
|
||||
// Normal user/pass
|
||||
// PING needed to flush the +OK to us.
|
||||
go c.parse([]byte("CONNECT {\"user\":\"derek\",\"pass\":\"foo\",\"verbose\":true,\"pedantic\":true}\r\nPING\r\n"))
|
||||
l, _ := cr.ReadString('\n')
|
||||
if !strings.HasPrefix(l, "+OK") {
|
||||
t.Fatalf("Expected an OK, got: %v", l)
|
||||
}
|
||||
|
||||
kp, _ := nkeys.FromSeed(seed)
|
||||
pubKey, _ := kp.PublicKey()
|
||||
|
||||
c, cr, l = newClientForServer(s)
|
||||
// Check for Nonce
|
||||
var info nonceInfo
|
||||
err := json.Unmarshal([]byte(l[5:]), &info)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not parse INFO json: %v\n", err)
|
||||
}
|
||||
if info.Nonce == "" {
|
||||
t.Fatalf("Expected a non-empty nonce with nkeys defined")
|
||||
}
|
||||
sigraw, err := kp.Sign([]byte(info.Nonce))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed signing nonce: %v", err)
|
||||
}
|
||||
sig := base64.RawURLEncoding.EncodeToString(sigraw)
|
||||
|
||||
// PING needed to flush the +OK to us.
|
||||
cs := fmt.Sprintf("CONNECT {\"nkey\":%q,\"sig\":\"%s\",\"verbose\":true,\"pedantic\":true}\r\nPING\r\n", pubKey, sig)
|
||||
go c.parse([]byte(cs))
|
||||
l, _ = cr.ReadString('\n')
|
||||
if !strings.HasPrefix(l, "+OK") {
|
||||
t.Fatalf("Expected an OK, got: %v", l)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCryptoRandGeneration(b *testing.B) {
|
||||
data := make([]byte, 16)
|
||||
for i := 0; i < b.N; i++ {
|
||||
crand.Read(data)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMathRandGeneration(b *testing.B) {
|
||||
data := make([]byte, 16)
|
||||
prng := mrand.New(mrand.NewSource(time.Now().UnixNano()))
|
||||
for i := 0; i < b.N; i++ {
|
||||
prng.Read(data)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNonceGeneration(b *testing.B) {
|
||||
data := make([]byte, nonceRawLen)
|
||||
b64 := make([]byte, nonceLen)
|
||||
prand := mrand.New(mrand.NewSource(time.Now().UnixNano()))
|
||||
for i := 0; i < b.N; i++ {
|
||||
prand.Read(data)
|
||||
base64.RawURLEncoding.Encode(b64, data)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPublicVerify(b *testing.B) {
|
||||
data := make([]byte, nonceRawLen)
|
||||
nonce := make([]byte, nonceLen)
|
||||
mrand.Read(data)
|
||||
base64.RawURLEncoding.Encode(nonce, data)
|
||||
|
||||
user, err := nkeys.CreateUser(nil)
|
||||
if err != nil {
|
||||
b.Fatalf("Error creating User Nkey: %v", err)
|
||||
}
|
||||
sig, err := user.Sign(nonce)
|
||||
if err != nil {
|
||||
b.Fatalf("Error sigining nonce: %v", err)
|
||||
}
|
||||
pk, err := user.PublicKey()
|
||||
if err != nil {
|
||||
b.Fatalf("Could not extract public key from user: %v", err)
|
||||
}
|
||||
pub, err := nkeys.FromPublicKey(pk)
|
||||
if err != nil {
|
||||
b.Fatalf("Could not create public key pair from public key string: %v", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := pub.Verify(nonce, sig); err != nil {
|
||||
b.Fatalf("Error verifying nonce: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/gnatsd/conf"
|
||||
"github.com/nats-io/nkeys"
|
||||
)
|
||||
|
||||
// ClusterOpts are options for clusters.
|
||||
@@ -59,6 +60,7 @@ type Options struct {
|
||||
Logtime bool `json:"-"`
|
||||
MaxConn int `json:"max_connections"`
|
||||
MaxSubs int `json:"max_subscriptions,omitempty"`
|
||||
Nkeys []*NkeyUser `json:"-"`
|
||||
Users []*User `json:"-"`
|
||||
Username string `json:"-"`
|
||||
Password string `json:"-"`
|
||||
@@ -110,6 +112,13 @@ func (o *Options) Clone() *Options {
|
||||
clone.Users[i] = user.clone()
|
||||
}
|
||||
}
|
||||
if o.Nkeys != nil {
|
||||
clone.Nkeys = make([]*NkeyUser, len(o.Nkeys))
|
||||
for i, nkey := range o.Nkeys {
|
||||
clone.Nkeys[i] = nkey.clone()
|
||||
}
|
||||
}
|
||||
|
||||
if o.Routes != nil {
|
||||
clone.Routes = make([]*url.URL, len(o.Routes))
|
||||
for i, route := range o.Routes {
|
||||
@@ -133,7 +142,8 @@ type authorization struct {
|
||||
user string
|
||||
pass string
|
||||
token string
|
||||
// Multiple Users
|
||||
// Multiple Nkeys/Users
|
||||
nkeys []*NkeyUser
|
||||
users []*User
|
||||
timeout float64
|
||||
defaultPermissions *Permissions
|
||||
@@ -255,6 +265,14 @@ func (o *Options) ProcessConfigFile(configFile string) error {
|
||||
}
|
||||
o.Users = auth.users
|
||||
}
|
||||
// Check for nkeys
|
||||
if auth.nkeys != nil {
|
||||
if o.Users != nil {
|
||||
return fmt.Errorf("Can not have users when nkeys are also defined.")
|
||||
}
|
||||
o.Nkeys = auth.nkeys
|
||||
}
|
||||
|
||||
case "http":
|
||||
hp, err := parseListen(v)
|
||||
if err != nil {
|
||||
@@ -460,7 +478,12 @@ func parseAuthorization(am map[string]interface{}) (*authorization, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auth.users = users
|
||||
switch users.(type) {
|
||||
case []*User:
|
||||
auth.users = users.([]*User)
|
||||
case []*NkeyUser:
|
||||
auth.nkeys = users.([]*NkeyUser)
|
||||
}
|
||||
case "default_permission", "default_permissions", "permissions":
|
||||
pm, ok := mv.(map[string]interface{})
|
||||
if !ok {
|
||||
@@ -487,22 +510,28 @@ func parseAuthorization(am map[string]interface{}) (*authorization, error) {
|
||||
}
|
||||
|
||||
// Helper function to parse multiple users array with optional permissions.
|
||||
func parseUsers(mv interface{}) ([]*User, error) {
|
||||
func parseUsers(mv interface{}) (interface{}, error) {
|
||||
// Make sure we have an array
|
||||
uv, ok := mv.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Expected users field to be an array, got %v", mv)
|
||||
}
|
||||
users := []*User{}
|
||||
var users []*User
|
||||
var keys []*NkeyUser
|
||||
for _, u := range uv {
|
||||
// Check its a map/struct
|
||||
um, ok := u.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Expected user entry to be a map/struct, got %v", u)
|
||||
}
|
||||
var perms *Permissions
|
||||
var err error
|
||||
user := &User{}
|
||||
nkey := &NkeyUser{}
|
||||
for k, v := range um {
|
||||
switch strings.ToLower(k) {
|
||||
case "nkey":
|
||||
nkey.Nkey = v.(string)
|
||||
case "user", "username":
|
||||
user.Username = v.(string)
|
||||
case "pass", "password":
|
||||
@@ -512,18 +541,41 @@ func parseUsers(mv interface{}) ([]*User, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Expected user permissions to be a map/struct, got %+v", v)
|
||||
}
|
||||
permissions, err := parseUserPermissions(pm)
|
||||
perms, err = parseUserPermissions(pm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Permissions = permissions
|
||||
}
|
||||
}
|
||||
// Check to make sure we have at least username and password
|
||||
if user.Username == "" || user.Password == "" {
|
||||
return nil, fmt.Errorf("User entry requires a user and a password")
|
||||
// Place perms if we have them.
|
||||
if perms != nil {
|
||||
// nkey takes precedent.
|
||||
if nkey.Nkey != "" {
|
||||
nkey.Permissions = perms
|
||||
} else {
|
||||
user.Permissions = perms
|
||||
}
|
||||
}
|
||||
users = append(users, user)
|
||||
|
||||
// Check to make sure we have at least username and password if defined.
|
||||
if nkey.Nkey == "" && (user.Username == "" || user.Password == "") {
|
||||
return nil, fmt.Errorf("User entry requires a user and a password")
|
||||
} else if nkey.Nkey != "" {
|
||||
// Make sure the nkey is legit.
|
||||
if !nkeys.IsValidPublicUserKey(nkey.Nkey) {
|
||||
return nil, fmt.Errorf("Not a valid public nkey for a user")
|
||||
}
|
||||
// If we have user or password defined here that is an error.
|
||||
if user.Username != "" || user.Password != "" {
|
||||
return nil, fmt.Errorf("Nkey users do not take usernames or passwords")
|
||||
}
|
||||
keys = append(keys, nkey)
|
||||
} else {
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
return keys, nil
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -761,6 +762,111 @@ func TestNewStyleAuthorizationConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test new nkey users
|
||||
func TestNkeyUsersConfig(t *testing.T) {
|
||||
confFileName := "nkeys.conf"
|
||||
defer os.Remove(confFileName)
|
||||
content := `
|
||||
authorization {
|
||||
users = [
|
||||
{nkey: "UDKTV7HZVYJFJN64LLMYQBUR6MTNNYCDC3LAZH4VHURW3GZLL3FULBXV"}
|
||||
{nkey: "UA3C5TBZYK5GJQJRWPMU6NFY5JNAEVQB2V2TUZFZDHFJFUYVKTTUOFKZ"}
|
||||
]
|
||||
}`
|
||||
if err := ioutil.WriteFile(confFileName, []byte(content), 0666); err != nil {
|
||||
t.Fatalf("Error writing config file: %v", err)
|
||||
}
|
||||
opts, err := ProcessConfigFile(confFileName)
|
||||
if err != nil {
|
||||
t.Fatalf("Received an error reading config file: %v", err)
|
||||
}
|
||||
lu := len(opts.Nkeys)
|
||||
if lu != 2 {
|
||||
t.Fatalf("Expected 2 nkey users, got %d", lu)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNkeyUsersWithPermsConfig(t *testing.T) {
|
||||
confFileName := "nkeys.conf"
|
||||
defer os.Remove(confFileName)
|
||||
content := `
|
||||
authorization {
|
||||
users = [
|
||||
{nkey: "UDKTV7HZVYJFJN64LLMYQBUR6MTNNYCDC3LAZH4VHURW3GZLL3FULBXV",
|
||||
permissions = {
|
||||
publish = "$SYSTEM.>"
|
||||
subscribe = { deny = ["foo", "bar", "baz"] }
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
if err := ioutil.WriteFile(confFileName, []byte(content), 0666); err != nil {
|
||||
t.Fatalf("Error writing config file: %v", err)
|
||||
}
|
||||
opts, err := ProcessConfigFile(confFileName)
|
||||
if err != nil {
|
||||
t.Fatalf("Received an error reading config file: %v", err)
|
||||
}
|
||||
lu := len(opts.Nkeys)
|
||||
if lu != 1 {
|
||||
t.Fatalf("Expected 1 nkey user, got %d", lu)
|
||||
}
|
||||
nk := opts.Nkeys[0]
|
||||
if nk.Permissions == nil {
|
||||
fmt.Printf("nk is %+v\n", nk)
|
||||
t.Fatal("Expected to have permissions")
|
||||
}
|
||||
if nk.Permissions.Publish == nil {
|
||||
t.Fatal("Expected to have publish permissions")
|
||||
}
|
||||
if nk.Permissions.Publish.Allow[0] != "$SYSTEM.>" {
|
||||
t.Fatalf("Expected publish to allow \"$SYSTEM.>\", but got %v\n", nk.Permissions.Publish.Allow[0])
|
||||
}
|
||||
if nk.Permissions.Subscribe == nil {
|
||||
t.Fatal("Expected to have subscribe permissions")
|
||||
}
|
||||
if nk.Permissions.Subscribe.Allow != nil {
|
||||
t.Fatal("Expected to have no subscribe allow permissions")
|
||||
}
|
||||
deny := nk.Permissions.Subscribe.Deny
|
||||
if deny == nil || len(deny) != 3 ||
|
||||
deny[0] != "foo" || deny[1] != "bar" || deny[2] != "baz" {
|
||||
t.Fatalf("Expected to have subscribe deny permissions, got %v", deny)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadNkeyConfig(t *testing.T) {
|
||||
confFileName := "nkeys_bad.conf"
|
||||
defer os.Remove(confFileName)
|
||||
content := `
|
||||
authorization {
|
||||
users = [ {nkey: "Ufoo"}]
|
||||
}`
|
||||
if err := ioutil.WriteFile(confFileName, []byte(content), 0666); err != nil {
|
||||
t.Fatalf("Error writing config file: %v", err)
|
||||
}
|
||||
if _, err := ProcessConfigFile(confFileName); err == nil {
|
||||
t.Fatalf("Expected an error from nkey entry with password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNkeyWithPassConfig(t *testing.T) {
|
||||
confFileName := "nkeys_pass.conf"
|
||||
defer os.Remove(confFileName)
|
||||
content := `
|
||||
authorization {
|
||||
users = [
|
||||
{nkey: "UDKTV7HZVYJFJN64LLMYQBUR6MTNNYCDC3LAZH4VHURW3GZLL3FULBXV", pass: "foo"}
|
||||
]
|
||||
}`
|
||||
if err := ioutil.WriteFile(confFileName, []byte(content), 0666); err != nil {
|
||||
t.Fatalf("Error writing config file: %v", err)
|
||||
}
|
||||
if _, err := ProcessConfigFile(confFileName); err == nil {
|
||||
t.Fatalf("Expected an error from bad nkey entry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenWithUserPass(t *testing.T) {
|
||||
confFileName := "test.conf"
|
||||
defer os.Remove(confFileName)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -40,24 +41,25 @@ import (
|
||||
// Info is the information sent to clients to help them understand information
|
||||
// about this server.
|
||||
type Info struct {
|
||||
ID string `json:"server_id"`
|
||||
Version string `json:"version"`
|
||||
Proto int `json:"proto"`
|
||||
GitCommit string `json:"git_commit,omitempty"`
|
||||
GoVersion string `json:"go"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
AuthRequired bool `json:"auth_required,omitempty"`
|
||||
TLSRequired bool `json:"tls_required,omitempty"`
|
||||
TLSVerify bool `json:"tls_verify,omitempty"`
|
||||
MaxPayload int `json:"max_payload"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
CID uint64 `json:"client_id,omitempty"`
|
||||
ID string `json:"server_id"`
|
||||
Version string `json:"version"`
|
||||
Proto int `json:"proto"`
|
||||
GitCommit string `json:"git_commit,omitempty"`
|
||||
GoVersion string `json:"go"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
AuthRequired bool `json:"auth_required,omitempty"`
|
||||
TLSRequired bool `json:"tls_required,omitempty"`
|
||||
TLSVerify bool `json:"tls_verify,omitempty"`
|
||||
MaxPayload int `json:"max_payload"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
CID uint64 `json:"client_id,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
ClientConnectURLs []string `json:"connect_urls,omitempty"` // Contains URLs a client can connect to.
|
||||
|
||||
// Route Specific
|
||||
ClientConnectURLs []string `json:"connect_urls,omitempty"` // Contains URLs a client can connect to.
|
||||
Import *SubjectPermission `json:"import,omitempty"`
|
||||
Export *SubjectPermission `json:"export,omitempty"`
|
||||
Import *SubjectPermission `json:"import,omitempty"`
|
||||
Export *SubjectPermission `json:"export,omitempty"`
|
||||
}
|
||||
|
||||
// Server is our main struct.
|
||||
@@ -65,6 +67,7 @@ type Server struct {
|
||||
gcid uint64
|
||||
stats
|
||||
mu sync.Mutex
|
||||
prand *rand.Rand
|
||||
info Info
|
||||
sl *Sublist
|
||||
configFile string
|
||||
@@ -77,6 +80,7 @@ type Server struct {
|
||||
routes map[uint64]*client
|
||||
remotes map[string]*client
|
||||
users map[string]*User
|
||||
nkeys map[string]*NkeyUser
|
||||
totalClients uint64
|
||||
closed *closedRingBuffer
|
||||
done chan bool
|
||||
@@ -165,6 +169,7 @@ func New(opts *Options) *Server {
|
||||
s := &Server{
|
||||
configFile: opts.ConfigFile,
|
||||
info: info,
|
||||
prand: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
sl: NewSublist(),
|
||||
opts: opts,
|
||||
done: make(chan bool, 1),
|
||||
@@ -749,6 +754,13 @@ func (s *Server) copyInfo() Info {
|
||||
info.ClientConnectURLs = make([]string, len(s.info.ClientConnectURLs))
|
||||
copy(info.ClientConnectURLs, s.info.ClientConnectURLs)
|
||||
}
|
||||
if s.nonceRequired() {
|
||||
// Nonce handling
|
||||
var raw [nonceLen]byte
|
||||
nonce := raw[:]
|
||||
s.generateNonce(nonce)
|
||||
info.Nonce = string(nonce)
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
@@ -765,6 +777,7 @@ func (s *Server) createClient(conn net.Conn) *client {
|
||||
// Grab JSON info string
|
||||
s.mu.Lock()
|
||||
info := s.copyInfo()
|
||||
c.nonce = []byte(info.Nonce)
|
||||
s.totalClients++
|
||||
s.mu.Unlock()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user