Add -t pedantic config check to the server

Signed-off-by: Waldemar Quevedo <wally@synadia.com>
This commit is contained in:
Waldemar Quevedo
2018-09-05 16:20:30 -07:00
parent 8e3ed6c35d
commit df2364af26
9 changed files with 901 additions and 76 deletions

View File

@@ -51,13 +51,16 @@ type parser struct {
// The config file path, empty by default.
fp string
// pedantic reports error when configuration is not correct.
pedantic bool
}
// Parse will return a map of keys to interface{}, although concrete types
// underly them. The values supported are string, bool, int64, float64, DateTime.
// Arrays and nested Maps are also supported.
func Parse(data string) (map[string]interface{}, error) {
p, err := parse(data, "")
p, err := parse(data, "", false)
if err != nil {
return nil, err
}
@@ -71,20 +74,58 @@ func ParseFile(fp string) (map[string]interface{}, error) {
return nil, fmt.Errorf("error opening config file: %v", err)
}
p, err := parse(string(data), filepath.Dir(fp))
p, err := parse(string(data), fp, false)
if err != nil {
return nil, err
}
return p.mapping, nil
}
func parse(data, fp string) (p *parser, err error) {
func ParseFileWithChecks(fp string) (map[string]interface{}, error) {
data, err := ioutil.ReadFile(fp)
if err != nil {
return nil, fmt.Errorf("error opening config file: %v", err)
}
p, err := parse(string(data), fp, true)
if err != nil {
return nil, err
}
return p.mapping, nil
}
type token struct {
item item
value interface{}
usedVariable bool
sourceFile string
}
func (t *token) Value() interface{} {
return t.value
}
func (t *token) Line() int {
return t.item.line
}
func (t *token) IsUsedVariable() bool {
return t.usedVariable
}
func (t *token) SourceFile() string {
return t.sourceFile
}
func parse(data, fp string, pedantic bool) (p *parser, err error) {
p = &parser{
mapping: make(map[string]interface{}),
lx: lex(data),
ctxs: make([]interface{}, 0, 4),
keys: make([]string, 0, 4),
fp: fp,
mapping: make(map[string]interface{}),
lx: lex(data),
ctxs: make([]interface{}, 0, 4),
keys: make([]string, 0, 4),
fp: filepath.Dir(fp),
pedantic: pedantic,
}
p.pushContext(p.mapping)
@@ -93,7 +134,7 @@ func parse(data, fp string) (p *parser, err error) {
if it.typ == itemEOF {
break
}
if err := p.processItem(it); err != nil {
if err := p.processItem(it, fp); err != nil {
return nil, err
}
}
@@ -135,7 +176,15 @@ func (p *parser) popKey() string {
return last
}
func (p *parser) processItem(it item) error {
func (p *parser) processItem(it item, fp string) error {
setValue := func(it item, v interface{}) {
if p.pedantic {
p.setValue(&token{it, v, false, fp})
} else {
p.setValue(v)
}
}
switch it.typ {
case itemError:
return fmt.Errorf("Parse error on line %d: '%s'", it.line, it.val)
@@ -145,9 +194,10 @@ func (p *parser) processItem(it item) error {
newCtx := make(map[string]interface{})
p.pushContext(newCtx)
case itemMapEnd:
p.setValue(p.popContext())
setValue(it, p.popContext())
case itemString:
p.setValue(it.val) // FIXME(dlc) sanitize string?
// FIXME(dlc) sanitize string?
setValue(it, it.val)
case itemInteger:
lastDigit := 0
for _, r := range it.val {
@@ -167,21 +217,22 @@ func (p *parser) processItem(it item) error {
}
// Process a suffix
suffix := strings.ToLower(strings.TrimSpace(it.val[lastDigit:]))
switch suffix {
case "":
p.setValue(num)
setValue(it, num)
case "k":
p.setValue(num * 1000)
setValue(it, num*1000)
case "kb":
p.setValue(num * 1024)
setValue(it, num*1024)
case "m":
p.setValue(num * 1000 * 1000)
setValue(it, num*1000*1000)
case "mb":
p.setValue(num * 1024 * 1024)
setValue(it, num*1024*1024)
case "g":
p.setValue(num * 1000 * 1000 * 1000)
setValue(it, num*1000*1000*1000)
case "gb":
p.setValue(num * 1024 * 1024 * 1024)
setValue(it, num*1024*1024*1024)
}
case itemFloat:
num, err := strconv.ParseFloat(it.val, 64)
@@ -192,39 +243,55 @@ func (p *parser) processItem(it item) error {
}
return fmt.Errorf("Expected float, but got '%s'.", it.val)
}
p.setValue(num)
setValue(it, num)
case itemBool:
switch strings.ToLower(it.val) {
case "true", "yes", "on":
p.setValue(true)
setValue(it, true)
case "false", "no", "off":
p.setValue(false)
setValue(it, false)
default:
return fmt.Errorf("Expected boolean value, but got '%s'.", it.val)
}
case itemDatetime:
dt, err := time.Parse("2006-01-02T15:04:05Z", it.val)
if err != nil {
return fmt.Errorf(
"Expected Zulu formatted DateTime, but got '%s'.", it.val)
}
p.setValue(dt)
setValue(it, dt)
case itemArrayStart:
var array = make([]interface{}, 0)
p.pushContext(array)
case itemArrayEnd:
array := p.ctx
p.popContext()
p.setValue(array)
setValue(it, array)
case itemVariable:
if value, ok := p.lookupVariable(it.val); ok {
p.setValue(value)
switch tk := value.(type) {
case *token:
// Mark that the variable was used.
tk.usedVariable = true
p.setValue(tk)
default:
p.setValue(value)
}
} else {
return fmt.Errorf("Variable reference for '%s' on line %d can not be found.",
it.val, it.line)
}
case itemInclude:
m, err := ParseFile(filepath.Join(p.fp, it.val))
var (
m map[string]interface{}
err error
)
if p.pedantic {
m, err = ParseFileWithChecks(filepath.Join(p.fp, it.val))
} else {
m, err = ParseFile(filepath.Join(p.fp, it.val))
}
if err != nil {
return fmt.Errorf("Error parsing include file '%s', %v.", it.val, err)
}

View File

@@ -33,6 +33,7 @@ Server Options:
-c, --config <file> Configuration file
-sl,--signal <signal>[=<pid>] Send signal to gnatsd process (stop, quit, reopen, reload)
--client_advertise <string> Client URL to advertise to other servers
-t Test configuration and exit
Logging Options:
-l, --log <file> File to redirect log output
@@ -87,6 +88,9 @@ func main() {
server.PrintTLSHelpAndDie)
if err != nil {
server.PrintAndDie(err.Error())
} else if opts.CheckConfig {
fmt.Fprintf(os.Stderr, "configuration file %s test is successful\n", opts.ConfigFile)
os.Exit(0)
}
// Create the server with appropriate options.

519
server/config_check_test.go Normal file
View File

@@ -0,0 +1,519 @@
// 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 (
"errors"
"fmt"
"os"
"testing"
)
func TestConfigCheck(t *testing.T) {
tests := []struct {
// name is the name of the test.
name string
// config is content of the configuration file.
config string
// defaultErr is the error we get pedantic checks are not enabled.
defaultErr error
// pedanticErr is the error we get when pedantic checks are enabled.
pedanticErr error
// errorLine is the location of the error.
errorLine int
}{
{
name: "when unknown field is used at top level",
config: `
monitor = "127.0.0.1:4442"
`,
defaultErr: nil,
pedanticErr: errors.New(`unknown field "monitor"`),
errorLine: 2,
},
{
name: "when default permissions are used at top level",
config: `
"default_permissions" {
publish = ["_SANDBOX.>"]
subscribe = ["_SANDBOX.>"]
}
`,
defaultErr: nil,
pedanticErr: errors.New(`unknown field "default_permissions"`),
// NOTE: line number is '5' because it is where the map definition ends.
errorLine: 5,
},
{
name: "when authorization config is empty",
config: `
authorization = {
}
`,
defaultErr: nil,
pedanticErr: nil,
},
{
name: "when authorization config has unknown fields",
config: `
authorization = {
foo = "bar"
}
`,
defaultErr: nil,
pedanticErr: errors.New(`unknown field "foo"`),
errorLine: 3,
},
{
name: "when authorization config has unknown fields",
config: `
port = 4222
authorization = {
user = "hello"
foo = "bar"
password = "world"
}
`,
defaultErr: nil,
pedanticErr: errors.New(`unknown field "foo"`),
errorLine: 6,
},
{
name: "when user authorization config has unknown fields",
config: `
authorization = {
users = [
{
user = "foo"
pass = "bar"
token = "quux"
}
]
}
`,
defaultErr: nil,
pedanticErr: errors.New(`unknown field "token"`),
errorLine: 7,
},
{
name: "when user authorization permissions config has unknown fields",
config: `
authorization {
permissions {
subscribe = {}
inboxes = {}
publish = {}
}
}
`,
defaultErr: errors.New(`Unknown field inboxes parsing permissions`),
pedanticErr: errors.New(`unknown field "inboxes"`),
errorLine: 5,
},
{
name: "when user authorization permissions config has unknown fields within allow or deny",
config: `
authorization {
permissions {
subscribe = {
allow = ["hello", "world"]
deny = ["foo", "bar"]
denied = "_INBOX.>"
}
publish = {}
}
}
`,
defaultErr: errors.New(`Unknown field name "denied" parsing subject permissions, only 'allow' or 'deny' are permitted`),
pedanticErr: errors.New(`unknown field "denied"`),
errorLine: 7,
},
{
name: "when user authorization permissions config has unknown fields within allow or deny",
config: `
authorization {
permissions {
publish = {
allow = ["hello", "world"]
deny = ["foo", "bar"]
allowed = "_INBOX.>"
}
subscribe = {}
}
}
`,
defaultErr: errors.New(`Unknown field name "allowed" parsing subject permissions, only 'allow' or 'deny' are permitted`),
pedanticErr: errors.New(`unknown field "allowed"`),
errorLine: 7,
},
{
name: "when user authorization permissions config has unknown fields using arrays",
config: `
authorization {
default_permissions {
subscribe = ["a"]
publish = ["b"]
inboxes = ["c"]
}
users = [
{
user = "foo"
pass = "bar"
}
]
}
`,
defaultErr: errors.New(`Unknown field inboxes parsing permissions`),
pedanticErr: errors.New(`unknown field "inboxes"`),
errorLine: 7,
},
{
name: "when user authorization permissions config has unknown fields using strings",
config: `
authorization {
default_permissions {
subscribe = "a"
requests = "b"
publish = "c"
}
users = [
{
user = "foo"
pass = "bar"
}
]
}
`,
defaultErr: errors.New(`Unknown field requests parsing permissions`),
pedanticErr: errors.New(`unknown field "requests"`),
errorLine: 6,
},
{
name: "when user authorization permissions config is empty",
config: `
authorization = {
users = [
{
user = "foo", pass = "bar", permissions = {
}
}
]
}
`,
defaultErr: nil,
pedanticErr: nil,
},
{
name: "when unknown permissions are included in config",
config: `
authorization = {
users = [
{
user = "foo", pass = "bar", permissions {
inboxes = true
}
}
]
}
`,
defaultErr: errors.New(`Unknown field inboxes parsing permissions`),
pedanticErr: errors.New(`unknown field "inboxes"`),
errorLine: 6,
},
{
name: "when clustering config is empty",
config: `
cluster = {
}
`,
defaultErr: nil,
pedanticErr: nil,
},
{
name: "when unknown option is in clustering config",
config: `
# NATS Server Configuration
port = 4222
cluster = {
port = 6222
foo = "bar"
authorization {
user = "hello"
pass = "world"
}
}
`,
defaultErr: nil,
pedanticErr: errors.New(`unknown field "foo"`),
errorLine: 9,
},
{
name: "when unknown option is in clustering authorization config",
config: `
cluster = {
authorization {
foo = "bar"
}
}
`,
defaultErr: nil,
pedanticErr: errors.New(`unknown field "foo"`),
errorLine: 4,
},
{
name: "when unknown option is in clustering authorization permissions config",
config: `
cluster = {
authorization {
user = "foo"
pass = "bar"
permissions = {
hello = "world"
}
}
}
`,
defaultErr: errors.New(`Unknown field hello parsing permissions`),
pedanticErr: errors.New(`unknown field "hello"`),
errorLine: 7,
},
{
name: "when unknown option is in tls config",
config: `
tls = {
hello = "world"
}
`,
defaultErr: errors.New(`error parsing tls config, unknown field ["hello"]`),
pedanticErr: errors.New(`unknown field "hello"`),
errorLine: 3,
},
{
name: "when unknown option is in cluster tls config",
config: `
cluster {
tls = {
foo = "bar"
}
}
`,
// Backwards compatibility: also report error by default even if pedantic checks disabled.
defaultErr: errors.New(`error parsing tls config, unknown field ["foo"]`),
pedanticErr: errors.New(`unknown field "foo"`),
errorLine: 4,
},
{
name: "when using cipher suites in the TLS config",
config: `
tls = {
cipher_suites: [
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
]
preferences = []
}
`,
defaultErr: errors.New(`error parsing tls config, unknown field ["preferences"]`),
pedanticErr: errors.New(`unknown field "preferences"`),
errorLine: 7,
},
{
name: "when using curve preferences in the TLS config",
config: `
tls = {
curve_preferences: [
"CurveP256",
"CurveP384",
"CurveP521"
]
suites = []
}
`,
defaultErr: errors.New(`error parsing tls config, unknown field ["suites"]`),
pedanticErr: errors.New(`unknown field "suites"`),
errorLine: 8,
},
{
name: "when unknown option is in cluster config with defined routes",
config: `
cluster {
port = 6222
routes = [
nats://127.0.0.1:6222
]
peers = []
}
`,
defaultErr: nil,
pedanticErr: errors.New(`unknown field "peers"`),
errorLine: 7,
},
{
name: "when used as variable in authorization block it should not be considered as unknown field",
config: `
# listen: 127.0.0.1:-1
listen: 127.0.0.1:4222
authorization {
# Superuser can do anything.
super_user = {
publish = ">"
subscribe = ">"
}
# Can do requests on foo or bar, and subscribe to anything
# that is a response to an _INBOX.
#
# Notice that authorization filters can be singletons or arrays.
req_pub_user = {
publish = ["req.foo", "req.bar"]
subscribe = "_INBOX.>"
}
# Setup a default user that can subscribe to anything, but has
# no publish capabilities.
default_user = {
subscribe = "PUBLIC.>"
}
unused = "hello"
# Default permissions if none presented. e.g. susan below.
default_permissions: $default_user
# Users listed with persmissions.
users = [
{user: alice, password: foo, permissions: $super_user}
{user: bob, password: bar, permissions: $req_pub_user}
{user: susan, password: baz}
]
}
`,
defaultErr: nil,
pedanticErr: errors.New(`unknown field "unused"`),
errorLine: 27,
},
{
name: "when used as variable in top level config it should not be considered as unknown field",
config: `
monitoring_port = 8222
http_port = $monitoring_port
port = 4222
`,
defaultErr: nil,
pedanticErr: nil,
},
{
name: "when used as variable in cluster config it should not be considered as unknown field",
config: `
cluster {
clustering_port = 6222
port = $clustering_port
}
`,
defaultErr: nil,
pedanticErr: nil,
},
}
checkConfig := func(config string, pedantic bool) error {
opts := &Options{
CheckConfig: pedantic,
}
return opts.ProcessConfigFile(config)
}
checkErr := func(t *testing.T, err, expectedErr error) {
t.Helper()
switch {
case err == nil && expectedErr == nil:
// OK
case err != nil && expectedErr == nil:
t.Errorf("Unexpected error after processing config: %s", err)
case err == nil && expectedErr != nil:
t.Errorf("Expected %q error after processing invalid config but got nothing", expectedErr)
}
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
conf := createConfFile(t, []byte(test.config))
defer os.Remove(conf)
t.Run("with pedantic check enabled", func(t *testing.T) {
err := checkConfig(conf, true)
expectedErr := test.pedanticErr
if err != nil && expectedErr != nil {
msg := fmt.Sprintf("%s in %s:%d", expectedErr.Error(), conf, test.errorLine)
if err.Error() != msg {
t.Errorf("Expected %q, got %q", msg, err.Error())
}
}
checkErr(t, err, test.pedanticErr)
})
t.Run("with pedantic check disabled", func(t *testing.T) {
err := checkConfig(conf, false)
expectedErr := test.defaultErr
if err != nil && expectedErr != nil && err.Error() != expectedErr.Error() {
t.Errorf("Expected %q, got %q", expectedErr.Error(), err.Error())
}
checkErr(t, err, test.defaultErr)
})
})
}
}
func TestConfigCheckIncludes(t *testing.T) {
// Check happy path first using pedantic mode.
opts := &Options{
CheckConfig: true,
}
err := opts.ProcessConfigFile("./configs/include_conf_check_a.conf")
if err != nil {
t.Errorf("Unexpected error processing include files with configuration check enabled: %s", err)
}
opts = &Options{
CheckConfig: true,
}
err = opts.ProcessConfigFile("./configs/include_bad_conf_check_a.conf")
if err == nil {
t.Errorf("Expected error processing include files with configuration check enabled: %s", err)
}
expectedErr := errors.New(`unknown field "monitoring_port" in configs/include_bad_conf_check_b.conf:2`)
if err != nil && expectedErr != nil && err.Error() != expectedErr.Error() {
t.Errorf("Expected %q, got %q", expectedErr.Error(), err.Error())
}
}

View File

@@ -0,0 +1,6 @@
port = 4222
include "include_bad_conf_check_b.conf"
# http_port = $monitoring_port

View File

@@ -0,0 +1,4 @@
monitoring_port = 8222
include "include_conf_check_c.conf"

View File

@@ -0,0 +1,6 @@
port = 4222
include "include_conf_check_b.conf"
http_port = $monitoring_port

View File

@@ -0,0 +1,3 @@
monitoring_port = 8222
include "include_conf_check_c.conf"

View File

@@ -0,0 +1,5 @@
authorization {
user = "foo"
pass = "bar"
}

View File

@@ -96,6 +96,9 @@ type Options struct {
CustomClientAuthentication Authentication `json:"-"`
CustomRouterAuthentication Authentication `json:"-"`
// CheckConfig enables pedantic configuration file syntax checks.
CheckConfig bool `json:"-"`
}
// Clone performs a deep copy of the Options struct, returning a new clone
@@ -186,6 +189,27 @@ e.g.
Available cipher suites include:
`
type token interface {
Value() interface{}
Line() int
IsUsedVariable() bool
SourceFile() string
}
type unknownConfigFieldErr struct {
field string
token token
configFile string
}
func (e *unknownConfigFieldErr) Error() string {
msg := fmt.Sprintf("unknown field %q", e.field)
if e.token != nil {
return msg + fmt.Sprintf(" in %s:%d", e.configFile, e.token.Line())
}
return msg
}
// ProcessConfigFile processes a configuration file.
// FIXME(dlc): Hacky
func ProcessConfigFile(configFile string) (*Options, error) {
@@ -196,6 +220,18 @@ func ProcessConfigFile(configFile string) (*Options, error) {
return opts, nil
}
// unwrapValue can be used to get the token and value from an item
// to be able to report the line number in case of an incorrect
// configuration.
func unwrapValue(v interface{}) (token, interface{}) {
switch tk := v.(type) {
case token:
return tk, tk.Value()
default:
return nil, v
}
}
// ProcessConfigFile updates the Options structure with options
// present in the given configuration file.
// This version is convenient if one wants to set some default
@@ -216,12 +252,24 @@ func (o *Options) ProcessConfigFile(configFile string) error {
return nil
}
m, err := conf.ParseFile(configFile)
var (
m map[string]interface{}
tk token
pedantic bool = o.CheckConfig
err error
)
if pedantic {
m, err = conf.ParseFileWithChecks(configFile)
} else {
m, err = conf.ParseFile(configFile)
}
if err != nil {
return err
}
for k, v := range m {
// When pedantic checks are enabled then need to unwrap
// to get the value along with reported error line.
tk, v = unwrapValue(v)
switch strings.ToLower(k) {
case "listen":
hp, err := parseListen(v)
@@ -243,8 +291,12 @@ func (o *Options) ProcessConfigFile(configFile string) error {
case "logtime":
o.Logtime = v.(bool)
case "authorization":
am := v.(map[string]interface{})
auth, err := parseAuthorization(am)
var auth *authorization
if pedantic {
auth, err = parseAuthorization(tk, o)
} else {
auth, err = parseAuthorization(v, o)
}
if err != nil {
return err
}
@@ -288,8 +340,13 @@ func (o *Options) ProcessConfigFile(configFile string) error {
case "https_port":
o.HTTPSPort = int(v.(int64))
case "cluster":
cm := v.(map[string]interface{})
if err := parseCluster(cm, o); err != nil {
var err error
if pedantic {
err = parseCluster(tk, o)
} else {
err = parseCluster(v, o)
}
if err != nil {
return err
}
case "logfile", "log_file":
@@ -319,8 +376,15 @@ func (o *Options) ProcessConfigFile(configFile string) error {
case "ping_max":
o.MaxPingsOut = int(v.(int64))
case "tls":
tlsm := v.(map[string]interface{})
tc, err := parseTLS(tlsm)
var (
tc *TLSConfigOpts
err error
)
if pedantic {
tc, err = parseTLS(tk, o)
} else {
tc, err = parseTLS(v, o)
}
if err != nil {
return err
}
@@ -342,6 +406,14 @@ func (o *Options) ProcessConfigFile(configFile string) error {
o.WriteDeadline = time.Duration(v.(int64)) * time.Second
fmt.Printf("WARNING: write_deadline should be converted to a duration\n")
}
default:
if pedantic && tk != nil && !tk.IsUsedVariable() {
return &unknownConfigFieldErr{
field: k,
token: tk,
configFile: tk.SourceFile(),
}
}
}
}
return nil
@@ -375,8 +447,17 @@ func parseListen(v interface{}) (*hostPort, error) {
}
// parseCluster will parse the cluster config.
func parseCluster(cm map[string]interface{}, opts *Options) error {
func parseCluster(v interface{}, opts *Options) error {
var (
cm map[string]interface{}
tk token
pedantic bool = opts.CheckConfig
)
_, v = unwrapValue(v)
cm = v.(map[string]interface{})
for mk, mv := range cm {
// Again, unwrap token value if line check is required.
tk, mv = unwrapValue(mv)
switch strings.ToLower(mk) {
case "listen":
hp, err := parseListen(mv)
@@ -390,8 +471,15 @@ func parseCluster(cm map[string]interface{}, opts *Options) error {
case "host", "net":
opts.Cluster.Host = mv.(string)
case "authorization":
am := mv.(map[string]interface{})
auth, err := parseAuthorization(am)
var (
auth *authorization
err error
)
if pedantic {
auth, err = parseAuthorization(tk, opts)
} else {
auth, err = parseAuthorization(mv, opts)
}
if err != nil {
return err
}
@@ -409,6 +497,7 @@ func parseCluster(cm map[string]interface{}, opts *Options) error {
ra := mv.([]interface{})
opts.Routes = make([]*url.URL, 0, len(ra))
for _, r := range ra {
_, r = unwrapValue(r)
routeURL := r.(string)
url, err := url.Parse(routeURL)
if err != nil {
@@ -417,8 +506,15 @@ func parseCluster(cm map[string]interface{}, opts *Options) error {
opts.Routes = append(opts.Routes, url)
}
case "tls":
tlsm := mv.(map[string]interface{})
tc, err := parseTLS(tlsm)
var (
tc *TLSConfigOpts
err error
)
if pedantic {
tc, err = parseTLS(tk, opts)
} else {
tc, err = parseTLS(mv, opts)
}
if err != nil {
return err
}
@@ -442,12 +538,20 @@ func parseCluster(cm map[string]interface{}, opts *Options) error {
if !ok {
return fmt.Errorf("Expected permissions to be a map/struct, got %+v", mv)
}
perms, err := parseUserPermissions(pm)
perms, err := parseUserPermissions(pm, opts)
if err != nil {
return err
}
// This will possibly override permissions that were define in auth block
setClusterPermissions(&opts.Cluster, perms)
default:
if pedantic && tk != nil && !tk.IsUsedVariable() {
return &unknownConfigFieldErr{
field: mk,
token: tk,
configFile: tk.SourceFile(),
}
}
}
}
return nil
@@ -468,9 +572,19 @@ func setClusterPermissions(opts *ClusterOpts, perms *Permissions) {
}
// Helper function to parse Authorization configs.
func parseAuthorization(am map[string]interface{}) (*authorization, error) {
auth := &authorization{}
func parseAuthorization(v interface{}, opts *Options) (*authorization, error) {
var (
am map[string]interface{}
tk token
pedantic bool = opts.CheckConfig
auth *authorization = &authorization{}
)
// Unwrap value first if pedantic config check enabled.
_, v = unwrapValue(v)
am = v.(map[string]interface{})
for mk, mv := range am {
tk, mv = unwrapValue(mv)
switch strings.ToLower(mk) {
case "user", "username":
auth.user = mv.(string)
@@ -488,22 +602,43 @@ func parseAuthorization(am map[string]interface{}) (*authorization, error) {
}
auth.timeout = at
case "users":
nkeys, users, err := parseUsers(mv)
var (
users []*User
err error
nkeys []*NkeyUser
)
if pedantic {
nkeys, users, err = parseUsers(tk, opts)
} else {
nkeys, users, err = parseUsers(mv, opts)
}
if err != nil {
return nil, err
}
auth.users = users
auth.nkeys = nkeys
case "default_permission", "default_permissions", "permissions":
pm, ok := mv.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("Expected default permissions to be a map/struct, got %+v", mv)
var (
permissions *Permissions
err error
)
if pedantic {
permissions, err = parseUserPermissions(tk, opts)
} else {
permissions, err = parseUserPermissions(mv, opts)
}
permissions, err := parseUserPermissions(pm)
if err != nil {
return nil, err
}
auth.defaultPermissions = permissions
default:
if pedantic && tk != nil && !tk.IsUsedVariable() {
return nil, &unknownConfigFieldErr{
field: mk,
token: tk,
configFile: tk.SourceFile(),
}
}
}
// Now check for permission defaults with multiple users, etc.
@@ -520,25 +655,39 @@ func parseAuthorization(am map[string]interface{}) (*authorization, error) {
}
// Helper function to parse multiple users array with optional permissions.
func parseUsers(mv interface{}) ([]*NkeyUser, []*User, error) {
func parseUsers(mv interface{}, opts *Options) ([]*NkeyUser, []*User, error) {
var (
tk token
pedantic bool = opts.CheckConfig
users []*User = []*User{}
keys []*NkeyUser
)
_, mv = unwrapValue(mv)
// Make sure we have an array
uv, ok := mv.([]interface{})
if !ok {
return nil, nil, fmt.Errorf("Expected users field to be an array, got %v", mv)
}
var users []*User
var keys []*NkeyUser
for _, u := range uv {
_, u = unwrapValue(u)
// Check its a map/struct
um, ok := u.(map[string]interface{})
if !ok {
return nil, nil, fmt.Errorf("Expected user entry to be a map/struct, got %v", u)
}
var perms *Permissions
var err error
user := &User{}
nkey := &NkeyUser{}
var (
user *User = &User{}
nkey *NkeyUser = &NkeyUser{}
perms *Permissions
err error
)
for k, v := range um {
// Also needs to unwrap first
tk, v = unwrapValue(v)
switch strings.ToLower(k) {
case "nkey":
nkey.Nkey = v.(string)
@@ -547,14 +696,22 @@ func parseUsers(mv interface{}) ([]*NkeyUser, []*User, error) {
case "pass", "password":
user.Password = v.(string)
case "permission", "permissions", "authorization":
pm, ok := v.(map[string]interface{})
if !ok {
return nil, nil, fmt.Errorf("Expected user permissions to be a map/struct, got %+v", v)
if pedantic {
perms, err = parseUserPermissions(tk, opts)
} else {
perms, err = parseUserPermissions(v, opts)
}
perms, err = parseUserPermissions(pm)
if err != nil {
return nil, nil, err
}
default:
if pedantic && tk != nil && !tk.IsUsedVariable() {
return nil, nil, &unknownConfigFieldErr{
field: k,
token: tk,
configFile: tk.SourceFile(),
}
}
}
}
// Place perms if we have them.
@@ -588,38 +745,58 @@ func parseUsers(mv interface{}) ([]*NkeyUser, []*User, error) {
}
// Helper function to parse user/account permissions
func parseUserPermissions(pm map[string]interface{}) (*Permissions, error) {
p := &Permissions{}
func parseUserPermissions(mv interface{}, opts *Options) (*Permissions, error) {
var (
tk token
pedantic bool = opts.CheckConfig
p *Permissions = &Permissions{}
)
_, mv = unwrapValue(mv)
pm, ok := mv.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("Expected permissions to be a map/struct, got %+v", mv)
}
for k, v := range pm {
tk, v = unwrapValue(v)
switch strings.ToLower(k) {
// For routes:
// Import is Publish
// Export is Subscribe
case "pub", "publish", "import":
perms, err := parseVariablePermissions(v)
perms, err := parseVariablePermissions(v, opts)
if err != nil {
return nil, err
}
p.Publish = perms
case "sub", "subscribe", "export":
perms, err := parseVariablePermissions(v)
perms, err := parseVariablePermissions(v, opts)
if err != nil {
return nil, err
}
p.Subscribe = perms
default:
if pedantic && tk != nil && !tk.IsUsedVariable() {
return nil, &unknownConfigFieldErr{
field: k,
token: tk,
configFile: tk.SourceFile(),
}
}
return nil, fmt.Errorf("Unknown field %s parsing permissions", k)
}
}
return p, nil
}
// Tope level parser for authorization configurations.
func parseVariablePermissions(v interface{}) (*SubjectPermission, error) {
switch v.(type) {
// Top level parser for authorization configurations.
func parseVariablePermissions(v interface{}, opts *Options) (*SubjectPermission, error) {
switch vv := v.(type) {
case map[string]interface{}:
// New style with allow and/or deny properties.
return parseSubjectPermission(v.(map[string]interface{}))
return parseSubjectPermission(vv, opts)
default:
// Old style
return parseOldPermissionStyle(v)
@@ -629,13 +806,15 @@ func parseVariablePermissions(v interface{}) (*SubjectPermission, error) {
// Helper function to parse subject singeltons and/or arrays
func parseSubjects(v interface{}) ([]string, error) {
var subjects []string
switch v.(type) {
switch vv := v.(type) {
case string:
subjects = append(subjects, v.(string))
subjects = append(subjects, vv)
case []string:
subjects = v.([]string)
subjects = vv
case []interface{}:
for _, i := range v.([]interface{}) {
for _, i := range vv {
_, i = unwrapValue(i)
subject, ok := i.(string)
if !ok {
return nil, fmt.Errorf("Subject in permissions array cannot be cast to string")
@@ -661,14 +840,15 @@ func parseOldPermissionStyle(v interface{}) (*SubjectPermission, error) {
}
// Helper function to parse new style authorization into a SubjectPermission with Allow and Deny.
func parseSubjectPermission(m map[string]interface{}) (*SubjectPermission, error) {
func parseSubjectPermission(v interface{}, opts *Options) (*SubjectPermission, error) {
m := v.(map[string]interface{})
if len(m) == 0 {
return nil, nil
}
p := &SubjectPermission{}
pedantic := opts.CheckConfig
for k, v := range m {
tk, v := unwrapValue(v)
switch strings.ToLower(k) {
case "allow":
subjects, err := parseSubjects(v)
@@ -683,6 +863,13 @@ func parseSubjectPermission(m map[string]interface{}) (*SubjectPermission, error
}
p.Deny = subjects
default:
if pedantic && tk != nil && !tk.IsUsedVariable() {
return nil, &unknownConfigFieldErr{
field: k,
token: tk,
configFile: tk.SourceFile(),
}
}
return nil, fmt.Errorf("Unknown field name %q parsing subject permissions, only 'allow' or 'deny' are permitted", k)
}
}
@@ -713,7 +900,6 @@ func PrintTLSHelpAndDie() {
}
func parseCipher(cipherName string) (uint16, error) {
cipher, exists := cipherMap[cipherName]
if !exists {
return 0, fmt.Errorf("Unrecognized cipher %s", cipherName)
@@ -731,9 +917,17 @@ func parseCurvePreferences(curveName string) (tls.CurveID, error) {
}
// Helper function to parse TLS configs.
func parseTLS(tlsm map[string]interface{}) (*TLSConfigOpts, error) {
tc := TLSConfigOpts{}
func parseTLS(v interface{}, opts *Options) (*TLSConfigOpts, error) {
var (
tlsm map[string]interface{}
tk token
tc TLSConfigOpts = TLSConfigOpts{}
pedantic bool = opts.CheckConfig
)
_, v = unwrapValue(v)
tlsm = v.(map[string]interface{})
for mk, mv := range tlsm {
tk, mv = unwrapValue(mv)
switch strings.ToLower(mk) {
case "cert_file":
certFile, ok := mv.(string)
@@ -766,6 +960,7 @@ func parseTLS(tlsm map[string]interface{}) (*TLSConfigOpts, error) {
}
tc.Ciphers = make([]uint16, 0, len(ra))
for _, r := range ra {
_, r = unwrapValue(r)
cipher, err := parseCipher(r.(string))
if err != nil {
return nil, err
@@ -779,6 +974,7 @@ func parseTLS(tlsm map[string]interface{}) (*TLSConfigOpts, error) {
}
tc.CurvePreferences = make([]tls.CurveID, 0, len(ra))
for _, r := range ra {
_, r = unwrapValue(r)
cps, err := parseCurvePreferences(r.(string))
if err != nil {
return nil, err
@@ -795,6 +991,14 @@ func parseTLS(tlsm map[string]interface{}) (*TLSConfigOpts, error) {
}
tc.Timeout = at
default:
if pedantic && tk != nil && !tk.IsUsedVariable() {
return nil, &unknownConfigFieldErr{
field: mk,
token: tk,
configFile: tk.SourceFile(),
}
}
return nil, fmt.Errorf("error parsing tls config, unknown field [%q]", mk)
}
}
@@ -1134,6 +1338,7 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp,
fs.IntVar(&opts.HTTPSPort, "https_port", 0, "HTTPS Port for /varz, /connz endpoints.")
fs.StringVar(&configFile, "c", "", "Configuration file.")
fs.StringVar(&configFile, "config", "", "Configuration file.")
fs.BoolVar(&opts.CheckConfig, "t", false, "Check configuration and exit.")
fs.StringVar(&signal, "sl", "", "Send signal to gnatsd process (stop, quit, reopen, reload)")
fs.StringVar(&signal, "signal", "", "Send signal to gnatsd process (stop, quit, reopen, reload)")
fs.StringVar(&opts.PidFile, "P", "", "File to store process pid.")
@@ -1216,12 +1421,18 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp,
// This will update the options with values from the config file.
if err := opts.ProcessConfigFile(configFile); err != nil {
return nil, err
} else if opts.CheckConfig {
// Report configuration file syntax test was successful and exit.
return opts, nil
}
// Call this again to override config file options with options from command line.
// Note: We don't need to check error here since if there was an error, it would
// have been caught the first time this function was called (after setting up the
// flags).
fs.Parse(args)
} else if opts.CheckConfig {
return nil, fmt.Errorf("must specify [-c, --config] option to check configuration file syntax")
}
// Special handling of some flags