Skip to content

Commit

Permalink
Merge pull request #725 from nats-io/deny
Browse files Browse the repository at this point in the history
Deny/Allow Permissions
  • Loading branch information
derekcollison authored Aug 24, 2018
2 parents 3ad63c7 + 7b9bab2 commit 976e754
Show file tree
Hide file tree
Showing 12 changed files with 402 additions and 50 deletions.
38 changes: 30 additions & 8 deletions server/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,43 @@ func (u *User) clone() *User {
return clone
}

// SubjectPermission is an individual allow and deny struct for publish
// and subscribe authorizations.
type SubjectPermission struct {
Allow []string `json:"allow"`
Deny []string `json:"deny"`
}

// Permissions are the allowed subjects on a per
// publish or subscribe basis.
type Permissions struct {
Publish []string `json:"publish"`
Subscribe []string `json:"subscribe"`
Publish *SubjectPermission `json:"publish"`
Subscribe *SubjectPermission `json:"subscribe"`
}

// RoutePermissions are similar to user permissions
// but describe what a server can import/export from and to
// another server.
type RoutePermissions struct {
Import []string `json:"import"`
Export []string `json:"export"`
Import *SubjectPermission `json:"import"`
Export *SubjectPermission `json:"export"`
}

// clone will clone an individual subject permission.
func (p *SubjectPermission) clone() *SubjectPermission {
if p == nil {
return nil
}
clone := &SubjectPermission{}
if p.Allow != nil {
clone.Allow = make([]string, len(p.Allow))
copy(clone.Allow, p.Allow)
}
if p.Deny != nil {
clone.Deny = make([]string, len(p.Deny))
copy(clone.Deny, p.Deny)
}
return clone
}

// clone performs a deep copy of the Permissions struct, returning a new clone
Expand All @@ -79,12 +103,10 @@ func (p *Permissions) clone() *Permissions {
}
clone := &Permissions{}
if p.Publish != nil {
clone.Publish = make([]string, len(p.Publish))
copy(clone.Publish, p.Publish)
clone.Publish = p.Publish.clone()
}
if p.Subscribe != nil {
clone.Subscribe = make([]string, len(p.Subscribe))
copy(clone.Subscribe, p.Subscribe)
clone.Subscribe = p.Subscribe.clone()
}
return clone
}
Expand Down
10 changes: 7 additions & 3 deletions server/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,12 @@ func TestUserClone(t *testing.T) {
Username: "foo",
Password: "bar",
Permissions: &Permissions{
Publish: []string{"foo"},
Subscribe: []string{"bar"},
Publish: &SubjectPermission{
Allow: []string{"foo"},
},
Subscribe: &SubjectPermission{
Allow: []string{"bar"},
},
},
}

Expand All @@ -54,7 +58,7 @@ func TestUserClone(t *testing.T) {
user, clone)
}

clone.Permissions.Subscribe = []string{"baz"}
clone.Permissions.Subscribe.Allow = []string{"baz"}
if reflect.DeepEqual(user, clone) {
t.Fatal("Expected Users to be different")
}
Expand Down
76 changes: 62 additions & 14 deletions server/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,13 @@ type outbound struct {
lft time.Duration // Last flush time.
}

type perm struct {
allow *Sublist
deny *Sublist
}
type permissions struct {
sub *Sublist
pub *Sublist
sub perm
pub perm
pcache map[string]bool
}

Expand Down Expand Up @@ -322,22 +326,39 @@ func (c *client) RegisterUser(user *User) {
// Initializes client.perms structure.
// Lock is held on entry.
func (c *client) setPermissions(perms *Permissions) {
// Pre-allocate all to simplify checks later.
c.perms = &permissions{}
c.perms.sub = NewSublist()
c.perms.pub = NewSublist()
c.perms.pcache = make(map[string]bool)

// Loop over publish permissions
for _, pubSubject := range perms.Publish {
if len(perms.Publish.Allow) > 0 {
c.perms.pub.allow = NewSublist()
}
for _, pubSubject := range perms.Publish.Allow {
sub := &subscription{subject: []byte(pubSubject)}
c.perms.pub.allow.Insert(sub)
}
if len(perms.Publish.Deny) > 0 {
c.perms.pub.deny = NewSublist()
}
for _, pubSubject := range perms.Publish.Deny {
sub := &subscription{subject: []byte(pubSubject)}
c.perms.pub.Insert(sub)
c.perms.pub.deny.Insert(sub)
}

// Loop over subscribe permissions
for _, subSubject := range perms.Subscribe {
if len(perms.Subscribe.Allow) > 0 {
c.perms.sub.allow = NewSublist()
}
for _, subSubject := range perms.Subscribe.Allow {
sub := &subscription{subject: []byte(subSubject)}
c.perms.sub.Insert(sub)
c.perms.sub.allow.Insert(sub)
}
if len(perms.Subscribe.Deny) > 0 {
c.perms.sub.deny = NewSublist()
}
for _, subSubject := range perms.Subscribe.Deny {
sub := &subscription{subject: []byte(subSubject)}
c.perms.sub.deny.Insert(sub)
}
}

Expand Down Expand Up @@ -1184,11 +1205,24 @@ func (c *client) processSub(argo []byte) (err error) {

// canSubscribe determines if the client is authorized to subscribe to the
// given subject. Assumes caller is holding lock.
func (c *client) canSubscribe(sub []byte) bool {
func (c *client) canSubscribe(subject []byte) bool {
if c.perms == nil {
return true
}
return len(c.perms.sub.Match(string(sub)).psubs) > 0

allowed := true

// Check allow list. If no allow list that means all are allowed. Deny can overrule.
if c.perms.sub.allow != nil {
r := c.perms.sub.allow.Match(string(subject))
allowed = len(r.psubs) != 0
}
// If we have a deny list and we think we are allowed, check that as well.
if allowed && c.perms.sub.deny != nil {
r := c.perms.sub.deny.Match(string(subject))
allowed = len(r.psubs) == 0
}
return allowed
}

// Low level unsubscribe for a given client.
Expand Down Expand Up @@ -1400,10 +1434,24 @@ func (c *client) pubAllowed(subject []byte) bool {
if ok {
return allowed
}
// Cache miss
r := c.perms.pub.Match(string(subject))
allowed = len(r.psubs) != 0

// Cache miss, check allow then deny as needed.
if c.perms.pub.allow != nil {
r := c.perms.pub.allow.Match(string(subject))
allowed = len(r.psubs) != 0
} else {
// No entries means all are allowed. Deny will overrule as needed.
allowed = true
}
// If we have a deny list and are currently allowed, check that as well.
if allowed && c.perms.pub.deny != nil {
r := c.perms.pub.deny.Match(string(subject))
allowed = len(r.psubs) == 0
}

// Update our cache here.
c.perms.pcache[string(subject)] = allowed

// Prune if needed.
if len(c.perms.pcache) > maxPermCacheSize {
c.prunePubPermsCache()
Expand Down
33 changes: 33 additions & 0 deletions server/configs/new_style_authorization.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
listen: 127.0.0.1:4222

authorization {
# Our new style role based permissions.
# These support both allow and deny.

# If allow is empty it means all or ">"
# If deny is empty it means none, or empty list.

normal_user = {
# Can send to foo, bar or baz only.
publish = {
allow = ["foo", "bar", "baz"]
}
# Can subscribe to everything but $SYSTEM prefixed subjects.
subscribe = {
deny = "$SYSTEM.>"
}
}

admin_user = {
publish = "$SYSTEM.>"
subscribe = {
deny = ["foo", "bar", "baz"]
}
}

# Users listed with persmissions.
users = [
{user: alice, password: foo, permissions: $normal_user}
{user: bob, password: special, permissions: $admin_user}
]
}
79 changes: 69 additions & 10 deletions server/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,14 @@ func parseCluster(cm map[string]interface{}, opts *Options) error {
// The parsing sets Import into Publish and Export into Subscribe, convert
// accordingly.
opts.Cluster.Permissions = &RoutePermissions{
Import: auth.defaultPermissions.Publish,
Export: auth.defaultPermissions.Subscribe,
Import: &SubjectPermission{
Allow: auth.defaultPermissions.Publish.Allow,
Deny: auth.defaultPermissions.Publish.Deny,
},
Export: &SubjectPermission{
Allow: auth.defaultPermissions.Subscribe.Allow,
Deny: auth.defaultPermissions.Subscribe.Deny,
},
}
}
case "routes":
Expand Down Expand Up @@ -538,24 +544,36 @@ func parseUserPermissions(pm map[string]interface{}) (*Permissions, error) {
// Import is Publish
// Export is Subscribe
case "pub", "publish", "import":
subjects, err := parseSubjects(v)
perms, err := parseVariablePermissions(v)
if err != nil {
return nil, err
}
p.Publish = subjects
p.Publish = perms
case "sub", "subscribe", "export":
subjects, err := parseSubjects(v)
perms, err := parseVariablePermissions(v)
if err != nil {
return nil, err
}
p.Subscribe = subjects
p.Subscribe = perms
default:
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) {
case map[string]interface{}:
// New style with allow and/or deny properties.
return parseSubjectPermission(v.(map[string]interface{}))
default:
// Old style
return parseOldPermissionStyle(v)
}
}

// Helper function to parse subject singeltons and/or arrays
func parseSubjects(v interface{}) ([]string, error) {
var subjects []string
Expand All @@ -575,17 +593,58 @@ func parseSubjects(v interface{}) ([]string, error) {
default:
return nil, fmt.Errorf("Expected subject permissions to be a subject, or array of subjects, got %T", v)
}
return checkSubjectArray(subjects)
if err := checkSubjectArray(subjects); err != nil {
return nil, err
}
return subjects, nil
}

// Helper function to parse old style authorization configs.
func parseOldPermissionStyle(v interface{}) (*SubjectPermission, error) {
subjects, err := parseSubjects(v)
if err != nil {
return nil, err
}
return &SubjectPermission{Allow: subjects}, nil
}

// Helper function to parse new style authorization into a SubjectPermission with Allow and Deny.
func parseSubjectPermission(m map[string]interface{}) (*SubjectPermission, error) {
if len(m) == 0 {
return nil, nil
}

p := &SubjectPermission{}

for k, v := range m {
switch strings.ToLower(k) {
case "allow":
subjects, err := parseSubjects(v)
if err != nil {
return nil, err
}
p.Allow = subjects
case "deny":
subjects, err := parseSubjects(v)
if err != nil {
return nil, err
}
p.Deny = subjects
default:
return nil, fmt.Errorf("Unknown field name %q parsing subject permissions, only 'allow' or 'deny' are permitted", k)
}
}
return p, nil
}

// Helper function to validate subjects, etc for account permissioning.
func checkSubjectArray(sa []string) ([]string, error) {
func checkSubjectArray(sa []string) error {
for _, s := range sa {
if !IsValidSubject(s) {
return nil, fmt.Errorf("Subject %q is not a valid subject", s)
return fmt.Errorf("Subject %q is not a valid subject", s)
}
}
return sa, nil
return nil
}

// PrintTLSHelpAndDie prints TLS usage and exits.
Expand Down
Loading

0 comments on commit 976e754

Please sign in to comment.