diff --git a/server/configs/authorization.conf b/server/configs/authorization.conf new file mode 100644 index 00000000..2cf47a03 --- /dev/null +++ b/server/configs/authorization.conf @@ -0,0 +1,37 @@ +# Copyright 2016 Apcera Inc. All rights reserved. + +listen: 127.0.0.1:4222 + +authorization { + # Our role based permissions. + + # 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.>" + } + + # 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} + ] +} diff --git a/server/opts.go b/server/opts.go index 4952881c..bef89cb8 100644 --- a/server/opts.go +++ b/server/opts.go @@ -5,7 +5,6 @@ package server import ( "crypto/tls" "crypto/x509" - "encoding/json" "fmt" "io/ioutil" "net" @@ -20,26 +19,16 @@ import ( // For multiple accounts/users. type User struct { - Username string `json:"user"` - Password string `json:"password"` - Permissions Authorization `json:"permissions"` - MaxConns int `json:"max_connections"` - MaxSubs int `json:"max_subscriptions"` + Username string `json:"user"` + Password string `json:"password"` + Permissions *Permissions `json:"permissions"` } // Authorization are the allowed subjects on a per // publish or subscribe basis. -type Authorization struct { - pub *Permission `json:"publish"` - sub *Permission `json:"subscribe"` -} - -// Permission is for describing the subjects and rate limits -// that an account connection can publish or subscribe to and -// what limits if any exist for message and/or byte rates. -type Permission struct { - Subjects []string `json:"subjects"` - // FIXME(dlc) figure out rates. +type Permissions struct { + Publish []string `json:"publish"` + Subscribe []string `json:"subscribe"` } // Options block for gnatsd server. @@ -52,7 +41,7 @@ type Options struct { NoSigs bool `json:"-"` Logtime bool `json:"-"` MaxConn int `json:"max_connections"` - Users []User `json:"-"` + Users []*User `json:"-"` Username string `json:"-"` Password string `json:"-"` Authorization string `json:"-"` @@ -89,13 +78,15 @@ type Options struct { TLSConfig *tls.Config `json:"-"` } +// Configuration file quthorization section. type authorization struct { // Singles user string pass string // Multiple Users - users []User - timeout float64 + users []*User + timeout float64 + defaultPermissions *Permissions } // TLSConfigOpts holds the parsed tls config information, @@ -343,17 +334,134 @@ func parseAuthorization(am map[string]interface{}) (*authorization, error) { } auth.timeout = at case "users": - b, _ := json.Marshal(mv) - users := []User{} - if err := json.Unmarshal(b, &users); err != nil { - return nil, fmt.Errorf("Could not parse user array properly, %v", err) + users, err := parseUsers(mv) + if err != nil { + return nil, err } auth.users = users + case "default_permission", "default_permissions": + pm, ok := mv.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("Expected default permissions to be a map/struct, got %+v", mv) + } + permissions, err := parseUserPermissions(pm) + if err != nil { + return nil, err + } + auth.defaultPermissions = permissions } + + // Now check for permission defaults with multiple users, etc. + if auth.users != nil && auth.defaultPermissions != nil { + for _, user := range auth.users { + if user.Permissions == nil { + user.Permissions = auth.defaultPermissions + } + } + } + } return auth, nil } +// Helper function to parse multiple users array with optional permissions. +func parseUsers(mv interface{}) ([]*User, 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{} + 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) + } + user := &User{} + for k, v := range um { + switch strings.ToLower(k) { + case "user", "username": + user.Username = v.(string) + case "pass", "password": + user.Password = v.(string) + case "permission", "permissions", "authroization": + pm, ok := v.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("Expected user permissions to be a map/struct, got %+v", v) + } + permissions, 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") + } + users = append(users, user) + } + return users, nil +} + +// Helper function to parse user/account permissions +func parseUserPermissions(pm map[string]interface{}) (*Permissions, error) { + p := &Permissions{} + for k, v := range pm { + switch strings.ToLower(k) { + case "pub", "publish": + subjects, err := parseSubjects(v) + if err != nil { + return nil, err + } + p.Publish = subjects + case "sub", "subscribe": + subjects, err := parseSubjects(v) + if err != nil { + return nil, err + } + p.Subscribe = subjects + default: + return nil, fmt.Errorf("Unknown field %s parsing permissions", k) + } + } + return p, nil +} + +// Helper function to parse subject singeltons and/or arrays +func parseSubjects(v interface{}) ([]string, error) { + var subjects []string + switch v.(type) { + case string: + subjects = append(subjects, v.(string)) + case []string: + subjects = v.([]string) + case []interface{}: + for _, i := range v.([]interface{}) { + subject, ok := i.(string) + if !ok { + return nil, fmt.Errorf("Subject in permissions array can not be cast to string") + } + subjects = append(subjects, subject) + } + default: + return nil, fmt.Errorf("Expected subject permissions to be a subject, or array of subjects, got %T", v) + } + return checkSubjectArray(subjects) +} + +// Helper function to validate subjects, etc for account permissioning. +func checkSubjectArray(sa []string) ([]string, error) { + for _, s := range sa { + if !IsValidSubject(s) { + return nil, fmt.Errorf("Subject %q is not a valid subject", s) + } + } + return sa, nil +} + // PrintTLSHelpAndDie prints TLS usage and exits. func PrintTLSHelpAndDie() { fmt.Printf("%s\n", tlsUsage) diff --git a/server/opts_test.go b/server/opts_test.go index 43b8326f..f6b5bc46 100644 --- a/server/opts_test.go +++ b/server/opts_test.go @@ -398,10 +398,85 @@ func TestMultipleUsersConfig(t *testing.T) { processOptions(opts) } +// Test highly depends on contents of the config file listed below. Any changes to that file +// may very weel break this test. func TestAuthorizationConfig(t *testing.T) { opts, err := ProcessConfigFile("./configs/authorization.conf") if err != nil { t.Fatalf("Received an error reading config file: %v\n", err) } processOptions(opts) + lu := len(opts.Users) + if lu != 3 { + t.Fatalf("Expected 3 users, got %d\n", lu) + } + // Build a map + mu := make(map[string]*User) + for _, u := range opts.Users { + mu[u.Username] = u + } + alice, ok := mu["alice"] + if !ok { + t.Fatalf("Expected to see user Alice\n") + } + // Check for permissions details + if alice.Permissions == nil { + t.Fatalf("Expected Alice's permissions to be non-nil\n") + } + if alice.Permissions.Publish == nil { + t.Fatalf("Expected Alice's publish permissions to be non-nil\n") + } + if len(alice.Permissions.Publish) != 1 { + t.Fatalf("Expected Alice's publish permissions to have 1 element, got %d\n", + len(alice.Permissions.Publish)) + } + pubPerm := alice.Permissions.Publish[0] + if pubPerm != "*" { + t.Fatalf("Expected Alice's publish permissions to be '*', got %q\n", pubPerm) + } + if alice.Permissions.Subscribe == nil { + t.Fatalf("Expected Alice's subscribe permissions to be non-nil\n") + } + if len(alice.Permissions.Subscribe) != 1 { + t.Fatalf("Expected Alice's subscribe permissions to have 1 element, got %d\n", + len(alice.Permissions.Subscribe)) + } + subPerm := alice.Permissions.Subscribe[0] + if subPerm != ">" { + t.Fatalf("Expected Alice's subscribe permissions to be '>', got %q\n", subPerm) + } + + bob, ok := mu["bob"] + if !ok { + t.Fatalf("Expected to see user Bob\n") + } + if bob.Permissions == nil { + t.Fatalf("Expected Bob's permissions to be non-nil\n") + } + + susan, ok := mu["susan"] + if !ok { + t.Fatalf("Expected to see user Susan\n") + } + if susan.Permissions == nil { + t.Fatalf("Expected Susan's permissions to be non-nil\n") + } + // Check susan closely since she inherited the default permissions. + if susan.Permissions == nil { + t.Fatalf("Expected Susan's permissions to be non-nil\n") + } + if susan.Permissions.Publish != nil { + t.Fatalf("Expected Susan's publish permissions to be nil\n") + } + if susan.Permissions.Subscribe == nil { + t.Fatalf("Expected Susan's subscribe permissions to be non-nil\n") + } + if len(susan.Permissions.Subscribe) != 1 { + t.Fatalf("Expected Susan's subscribe permissions to have 1 element, got %d\n", + len(susan.Permissions.Subscribe)) + } + subPerm = susan.Permissions.Subscribe[0] + if subPerm != "PUBLIC.>" { + t.Fatalf("Expected Susan's subscribe permissions to be 'PUBLIC.>', got %q\n", subPerm) + } } diff --git a/server/sublist.go b/server/sublist.go index fd4cc12f..1fdd24b1 100644 --- a/server/sublist.go +++ b/server/sublist.go @@ -565,7 +565,29 @@ func visitLevel(l *level, depth int) int { return maxDepth } -// IsValidLiteralSubject returns true if a subject is valid, false otherwise +// IsValidSubject returns true if a subject is valid, false otherwise +func IsValidSubject(subject string) bool { + if subject == "" { + return false + } + sfwc := false + tokens := strings.Split(string(subject), tsep) + for _, t := range tokens { + if len(t) == 0 || sfwc { + return false + } + if len(t) > 1 { + continue + } + switch t[0] { + case fwc: + sfwc = true + } + } + return true +} + +// IsValidLiteralSubject returns true if a subject is valid and literal (no wildcards), false otherwise func IsValidLiteralSubject(subject string) bool { tokens := strings.Split(string(subject), tsep) for _, t := range tokens {