mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 11:48:43 -07:00
This adds the ability to augment or override the NATS auth system. A server will send a signed request to $SYS.REQ.USER.AUTH on the specified account. The request will contain client information, all client options sent to the server, and optionally TLS information and client certificates. The external auth service will respond with an empty message if not authorized, or a signed User JWT that the user will bind to. The response can change the account the client will be bound to. Signed-off-by: Derek Collison <derek@nats.io>
365 lines
9.7 KiB
Go
365 lines
9.7 KiB
Go
// Copyright 2012-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 server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/jwt/v2"
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
func TestUserCloneNilPermissions(t *testing.T) {
|
|
user := &User{
|
|
Username: "foo",
|
|
Password: "bar",
|
|
}
|
|
|
|
clone := user.clone()
|
|
|
|
if !reflect.DeepEqual(user, clone) {
|
|
t.Fatalf("Cloned Users are incorrect.\nexpected: %+v\ngot: %+v",
|
|
user, clone)
|
|
}
|
|
|
|
clone.Password = "baz"
|
|
if reflect.DeepEqual(user, clone) {
|
|
t.Fatal("Expected Users to be different")
|
|
}
|
|
}
|
|
|
|
func TestUserClone(t *testing.T) {
|
|
user := &User{
|
|
Username: "foo",
|
|
Password: "bar",
|
|
Permissions: &Permissions{
|
|
Publish: &SubjectPermission{
|
|
Allow: []string{"foo"},
|
|
},
|
|
Subscribe: &SubjectPermission{
|
|
Allow: []string{"bar"},
|
|
},
|
|
},
|
|
}
|
|
|
|
clone := user.clone()
|
|
|
|
if !reflect.DeepEqual(user, clone) {
|
|
t.Fatalf("Cloned Users are incorrect.\nexpected: %+v\ngot: %+v",
|
|
user, clone)
|
|
}
|
|
|
|
clone.Permissions.Subscribe.Allow = []string{"baz"}
|
|
if reflect.DeepEqual(user, clone) {
|
|
t.Fatal("Expected Users to be different")
|
|
}
|
|
}
|
|
|
|
func TestUserClonePermissionsNoLists(t *testing.T) {
|
|
user := &User{
|
|
Username: "foo",
|
|
Password: "bar",
|
|
Permissions: &Permissions{},
|
|
}
|
|
|
|
clone := user.clone()
|
|
|
|
if clone.Permissions.Publish != nil {
|
|
t.Fatalf("Expected Publish to be nil, got: %v", clone.Permissions.Publish)
|
|
}
|
|
if clone.Permissions.Subscribe != nil {
|
|
t.Fatalf("Expected Subscribe to be nil, got: %v", clone.Permissions.Subscribe)
|
|
}
|
|
}
|
|
|
|
func TestUserCloneNoPermissions(t *testing.T) {
|
|
user := &User{
|
|
Username: "foo",
|
|
Password: "bar",
|
|
}
|
|
|
|
clone := user.clone()
|
|
|
|
if clone.Permissions != nil {
|
|
t.Fatalf("Expected Permissions to be nil, got: %v", clone.Permissions)
|
|
}
|
|
}
|
|
|
|
func TestUserCloneNil(t *testing.T) {
|
|
user := (*User)(nil)
|
|
clone := user.clone()
|
|
if clone != nil {
|
|
t.Fatalf("Expected nil, got: %+v", clone)
|
|
}
|
|
}
|
|
|
|
func TestUserUnknownAllowedConnectionType(t *testing.T) {
|
|
o := DefaultOptions()
|
|
o.Users = []*User{{
|
|
Username: "user",
|
|
Password: "pwd",
|
|
AllowedConnectionTypes: testCreateAllowedConnectionTypes([]string{jwt.ConnectionTypeStandard, "someNewType"}),
|
|
}}
|
|
_, err := NewServer(o)
|
|
if err == nil || !strings.Contains(err.Error(), "connection type") {
|
|
t.Fatalf("Expected error about unknown connection type, got %v", err)
|
|
}
|
|
|
|
o.Users[0].AllowedConnectionTypes = testCreateAllowedConnectionTypes([]string{"websocket"})
|
|
s, err := NewServer(o)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
s.mu.Lock()
|
|
user := s.opts.Users[0]
|
|
s.mu.Unlock()
|
|
for act := range user.AllowedConnectionTypes {
|
|
if act != jwt.ConnectionTypeWebsocket {
|
|
t.Fatalf("Expected map to have been updated with proper case, got %v", act)
|
|
}
|
|
}
|
|
// Same with NKey user now.
|
|
o.Users = nil
|
|
o.Nkeys = []*NkeyUser{{
|
|
Nkey: "somekey",
|
|
AllowedConnectionTypes: testCreateAllowedConnectionTypes([]string{jwt.ConnectionTypeStandard, "someNewType"}),
|
|
}}
|
|
_, err = NewServer(o)
|
|
if err == nil || !strings.Contains(err.Error(), "connection type") {
|
|
t.Fatalf("Expected error about unknown connection type, got %v", err)
|
|
}
|
|
o.Nkeys[0].AllowedConnectionTypes = testCreateAllowedConnectionTypes([]string{"websocket"})
|
|
s, err = NewServer(o)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
s.mu.Lock()
|
|
nkey := s.opts.Nkeys[0]
|
|
s.mu.Unlock()
|
|
for act := range nkey.AllowedConnectionTypes {
|
|
if act != jwt.ConnectionTypeWebsocket {
|
|
t.Fatalf("Expected map to have been updated with proper case, got %v", act)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDNSAltNameMatching(t *testing.T) {
|
|
for idx, test := range []struct {
|
|
altName string
|
|
urls []string
|
|
match bool
|
|
}{
|
|
{"foo", []string{"FOO"}, true},
|
|
{"foo", []string{".."}, false},
|
|
{"foo", []string{"."}, false},
|
|
{"Foo", []string{"foO"}, true},
|
|
{"FOO", []string{"foo"}, true},
|
|
{"foo1", []string{"bar"}, false},
|
|
{"multi", []string{"m", "mu", "mul", "multi"}, true},
|
|
{"multi", []string{"multi", "m", "mu", "mul"}, true},
|
|
{"foo.bar", []string{"foo", "foo.bar.bar", "foo.baz"}, false},
|
|
{"foo.Bar", []string{"foo", "bar.foo", "Foo.Bar"}, true},
|
|
{"foo.*", []string{"foo", "bar.foo", "Foo.Bar"}, false}, // only match left most
|
|
{"f*.bar", []string{"foo", "bar.foo", "Foo.Bar"}, false},
|
|
{"*.bar", []string{"foo.bar"}, true},
|
|
{"*", []string{"baz.bar", "bar", "z.y"}, true},
|
|
{"*", []string{"bar"}, true},
|
|
{"*", []string{"."}, false},
|
|
{"*", []string{""}, true},
|
|
{"*", []string{"*"}, true},
|
|
{"bar.*", []string{"bar.*"}, true},
|
|
{"*.Y-X-red-mgmt.default.svc", []string{"A.Y-X-red-mgmt.default.svc"}, true},
|
|
{"*.Y-X-green-mgmt.default.svc", []string{"A.Y-X-green-mgmt.default.svc"}, true},
|
|
{"*.Y-X-blue-mgmt.default.svc", []string{"A.Y-X-blue-mgmt.default.svc"}, true},
|
|
{"Y-X-red-mgmt", []string{"Y-X-red-mgmt"}, true},
|
|
{"Y-X-red-mgmt", []string{"X-X-red-mgmt"}, false},
|
|
{"Y-X-red-mgmt", []string{"Y-X-green-mgmt"}, false},
|
|
{"Y-X-red-mgmt", []string{"Y"}, false},
|
|
{"Y-X-red-mgmt", []string{"Y-X"}, false},
|
|
{"Y-X-red-mgmt", []string{"Y-X-red"}, false},
|
|
{"Y-X-red-mgmt", []string{"X-red-mgmt"}, false},
|
|
{"Y-X-green-mgmt", []string{"Y-X-green-mgmt"}, true},
|
|
{"Y-X-blue-mgmt", []string{"Y-X-blue-mgmt"}, true},
|
|
{"connect.Y.local", []string{"connect.Y.local"}, true},
|
|
{"connect.Y.local", []string{".Y.local"}, false},
|
|
{"connect.Y.local", []string{"..local"}, false},
|
|
{"gcp.Y.local", []string{"gcp.Y.local"}, true},
|
|
{"uswest1.gcp.Y.local", []string{"uswest1.gcp.Y.local"}, true},
|
|
} {
|
|
urlSet := make([]*url.URL, len(test.urls))
|
|
for i, u := range test.urls {
|
|
var err error
|
|
urlSet[i], err = url.Parse("nats://" + u)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if dnsAltNameMatches(dnsAltNameLabels(test.altName), urlSet) != test.match {
|
|
t.Fatal("Test", idx, "Match miss match, expected:", test.match)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNoAuthUser(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
FOO { users [{user: "foo", password: "pwd1"}] }
|
|
BAR { users [{user: "bar", password: "pwd2"}] }
|
|
}
|
|
no_auth_user: "foo"
|
|
`))
|
|
defer os.Remove(conf)
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
usrInfo string
|
|
ok bool
|
|
account string
|
|
}{
|
|
{"valid user/pwd", "bar:pwd2@", true, "BAR"},
|
|
{"invalid pwd", "bar:wrong@", false, _EMPTY_},
|
|
{"some token", "sometoken@", false, _EMPTY_},
|
|
{"user used without pwd", "bar@", false, _EMPTY_}, // will be treated as a token
|
|
{"user with empty password", "bar:@", false, _EMPTY_},
|
|
{"no user", _EMPTY_, true, "FOO"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
url := fmt.Sprintf("nats://%s127.0.0.1:%d", test.usrInfo, o.Port)
|
|
nc, err := nats.Connect(url)
|
|
if err != nil {
|
|
if test.ok {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
return
|
|
} else if !test.ok {
|
|
nc.Close()
|
|
t.Fatalf("Should have failed, did not")
|
|
}
|
|
var accName string
|
|
s.mu.Lock()
|
|
for _, c := range s.clients {
|
|
c.mu.Lock()
|
|
if c.acc != nil {
|
|
accName = c.acc.Name
|
|
}
|
|
c.mu.Unlock()
|
|
break
|
|
}
|
|
s.mu.Unlock()
|
|
nc.Close()
|
|
checkClientsCount(t, s, 0)
|
|
if accName != test.account {
|
|
t.Fatalf("The account should have been %q, got %q", test.account, accName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserConnectionDeadline(t *testing.T) {
|
|
clientAuth := &DummyAuth{
|
|
t: t,
|
|
register: true,
|
|
deadline: time.Now().Add(50 * time.Millisecond),
|
|
}
|
|
|
|
opts := DefaultOptions()
|
|
opts.CustomClientAuthentication = clientAuth
|
|
|
|
s := RunServer(opts)
|
|
defer s.Shutdown()
|
|
|
|
var dcerr error
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
|
|
nc, err := nats.Connect(
|
|
s.ClientURL(),
|
|
nats.UserInfo("valid", _EMPTY_),
|
|
nats.NoReconnect(),
|
|
nats.ErrorHandler(func(nc *nats.Conn, _ *nats.Subscription, err error) {
|
|
dcerr = err
|
|
cancel()
|
|
}))
|
|
if err != nil {
|
|
t.Fatalf("Expected client to connect, got: %s", err)
|
|
}
|
|
|
|
<-ctx.Done()
|
|
|
|
checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
|
|
if nc.IsConnected() {
|
|
return fmt.Errorf("Expected to be disconnected")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if dcerr == nil || dcerr.Error() != "nats: authentication expired" {
|
|
t.Fatalf("Expected a auth expired error: got: %v", dcerr)
|
|
}
|
|
}
|
|
|
|
func TestNoAuthUserNoConnectProto(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
A { users [{user: "foo", password: "pwd"}] }
|
|
}
|
|
authorization { timeout: 1 }
|
|
no_auth_user: "foo"
|
|
`))
|
|
defer os.Remove(conf)
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
checkClients := func(n int) {
|
|
t.Helper()
|
|
time.Sleep(100 * time.Millisecond)
|
|
if nc := s.NumClients(); nc != n {
|
|
t.Fatalf("Expected %d clients, got %d", n, nc)
|
|
}
|
|
}
|
|
|
|
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", o.Host, o.Port))
|
|
require_NoError(t, err)
|
|
defer conn.Close()
|
|
checkClientsCount(t, s, 1)
|
|
|
|
// With no auth user we should not require a CONNECT.
|
|
// Make sure we are good on not sending CONN first.
|
|
_, err = conn.Write([]byte("PUB foo 2\r\nok\r\n"))
|
|
require_NoError(t, err)
|
|
checkClients(1)
|
|
conn.Close()
|
|
|
|
// Now make sure we still do get timed out though.
|
|
conn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", o.Host, o.Port))
|
|
require_NoError(t, err)
|
|
defer conn.Close()
|
|
checkClientsCount(t, s, 1)
|
|
|
|
time.Sleep(1200 * time.Millisecond)
|
|
checkClientsCount(t, s, 0)
|
|
}
|