Files
nats-server/server/auth_test.go
R.I.Pienaar 3bb473c67d adds the notion of a connection deadline to User
This will be used mainly by CustomClientAuthentication implementations
to indicate that the user connection should be disconnected at some
point in future - like when a certificate or token expires

Signed-off-by: R.I.Pienaar <rip@devco.net>
2022-10-27 12:57:30 +02:00

358 lines
9.6 KiB
Go

// Copyright 2012-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 (
"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", ""), 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()
if nc.IsConnected() {
t.Fatalf("Expected to be disconnected")
}
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)
}