Skip to content

Commit

Permalink
Add matching of label placeholders
Browse files Browse the repository at this point in the history
  • Loading branch information
Carson Anderson committed Sep 6, 2017
1 parent f5bf6ae commit aea8fdc
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 9 deletions.
53 changes: 49 additions & 4 deletions auth_server/authz/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/cesanta/docker_auth/auth_server/authn"
"github.com/cesanta/glog"
"github.com/schwarmco/go-cartesian-product"
)

type ACL []ACLEntry
Expand Down Expand Up @@ -153,6 +154,42 @@ func matchString(pp *string, s string, vars []string) bool {
return err == nil && matched
}

func matchStringWithLabelPermutations(pp *string, s string, vars []string, labelMap *map[string][]string) bool {
var matched bool
// First try basic matching
matched = matchString(pp, s, vars)
// If basic matching fails then try with label permuations
if !matched {
// Take the labelMap and build the structure required for the cartesian library
var labelSets [][]interface{}
for placeholder, labels := range *labelMap {
// Don't bother generating perumations for placeholders not in match string
// Since the label permuations are a cartesian product this can have
// a huge impact on performance
if strings.Contains(*pp, placeholder) {
var labelSet []interface{}
for _, label := range labels {
labelSet = append(labelSet, []string{placeholder, label})
}
labelSets = append(labelSets, labelSet)
}
}
if len(labelSets) > 0 {
for permuation := range cartesian.Iter(labelSets...) {
var labelVars []string
for _, val := range permuation {
labelVars = append(labelVars, val.([]string)...)
}
matched = matchString(pp, s, append(vars, labelVars...))
if matched {
break
}
}
}
}
return matched
}

func matchIP(ipp *string, ip net.IP) bool {
if ipp == nil {
return true
Expand Down Expand Up @@ -233,10 +270,18 @@ func (mc *MatchConditions) Matches(ai *AuthRequestInfo) bool {
vars = append(vars, found[0], text[index])
}
}
return matchString(mc.Account, ai.Account, vars) &&
matchString(mc.Type, ai.Type, vars) &&
matchString(mc.Name, ai.Name, vars) &&
matchString(mc.Service, ai.Service, vars) &&
labelMap := make(map[string][]string)
for label, labelValues := range ai.Labels {
var labelSet []string
for _, lv := range labelValues {
labelSet = append(labelSet, regexp.QuoteMeta(lv))
}
labelMap[fmt.Sprintf("${labels:%s}", label)] = labelSet
}
return matchStringWithLabelPermutations(mc.Account, ai.Account, vars, &labelMap) &&
matchStringWithLabelPermutations(mc.Type, ai.Type, vars, &labelMap) &&
matchStringWithLabelPermutations(mc.Name, ai.Name, vars, &labelMap) &&
matchStringWithLabelPermutations(mc.Service, ai.Service, vars, &labelMap) &&
matchIP(mc.IP, ai.IP) &&
matchLabels(mc.Labels, ai.Labels, vars)
}
Expand Down
16 changes: 16 additions & 0 deletions auth_server/authz/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ func TestMatching(t *testing.T) {
ai1 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz", Service: "notary"}
ai2 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz", Service: "notary",
Labels: map[string][]string{"group": []string{"admins", "VIP"}}}
ai3 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "admins/foo", Service: "notary",
Labels: map[string][]string{"group": []string{"admins", "VIP"}}}
ai4 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "VIP/api", Service: "notary",
Labels: map[string][]string{"group": []string{"admins", "VIP"}, "project": []string{"api", "frontend"}}}
ai5 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "devs/api", Service: "notary",
Labels: map[string][]string{"group": []string{"admins", "VIP"}, "project": []string{"api", "frontend"}}}
cases := []struct {
mc MatchConditions
ai AuthRequestInfo
Expand Down Expand Up @@ -99,6 +105,16 @@ func TestMatching(t *testing.T) {
{MatchConditions{Labels: map[string]string{"group": "VIP"}}, ai2, true},
{MatchConditions{Labels: map[string]string{"group": "a*"}}, ai2, true},
{MatchConditions{Labels: map[string]string{"group": "/(admins|VIP)/"}}, ai2, true},
// // Label placeholder matching
{MatchConditions{Name: sp("${labels:group}/*")}, ai1, false}, // no labels
{MatchConditions{Name: sp("${labels:noexist}/*")}, ai2, false}, // wrong labels
{MatchConditions{Name: sp("${labels:group}/*")}, ai3, true}, // match label
{MatchConditions{Name: sp("${labels:noexist}/*")}, ai3, false}, // missing label
{MatchConditions{Name: sp("${labels:group}/${labels:project}")}, ai4, true}, // multiple label match success
{MatchConditions{Name: sp("${labels:group}/${labels:noexist}")}, ai4, false}, // multiple label match fail
{MatchConditions{Name: sp("${labels:group}/${labels:project}")}, ai4, true}, // multiple label match success
{MatchConditions{Name: sp("${labels:group}/${labels:noexist}")}, ai4, false}, // multiple label match fail wrong label
{MatchConditions{Name: sp("${labels:group}/${labels:project}")}, ai5, false}, // multiple label match fail. right label, wrong value
}
for i, c := range cases {
if result := c.mc.Matches(&c.ai); result != c.matches {
Expand Down
8 changes: 7 additions & 1 deletion auth_server/vendor/vendor.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,12 @@
"revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
"revisionTime": "2015-05-30T21:13:11Z"
},
{
"checksumSHA1": "DVgRSBT6UzmEgC90yJhh6X5A7Yc=",
"path": "github.com/schwarmco/go-cartesian-product",
"revision": "c2c0aca869a6cbf51e017ce148b949d9dee09bc3",
"revisionTime": "2017-01-30T17:09:49Z"
},
{
"checksumSHA1": "GVY3lzvj4xmpKOGgA4/h9GWjQVk=",
"path": "github.com/syndtr/goleveldb/leveldb",
Expand Down Expand Up @@ -397,7 +403,7 @@
"revisionTime": "2017-03-21T17:14:25Z"
},
{
"checksumSHA1": "AK65RmsGNBl0/e11OVrf2mW78gU=",
"checksumSHA1": "1WoWjPiwUEFahi5xz29FRMtd8sA=",
"path": "golang.org/x/sys/unix",
"revision": "493114f68206f85e7e333beccfabc11e98cba8dd",
"revisionTime": "2017-03-31T21:25:38Z"
Expand Down
23 changes: 19 additions & 4 deletions docs/Backend_MongoDB.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,21 @@ which can query ACL and Auth from a MongoDB database.
## Auth backend in MongoDB

Auth entries in mongo are single dictionary containing a username and password entry.
The password entry must contain a BCrypt hash.
The password entry must contain a BCrypt hash. The labels entry is optional.

```json
{
"username" : "admin",
"password" : "$2y$05$B.x046DV3bvuwFgn0I42F.W/SbRU5fUoCbCGtjFl7S33aCUHNBxbq"
"password" : "$2y$05$B.x046DV3bvuwFgn0I42F.W/SbRU5fUoCbCGtjFl7S33aCUHNBxbq",
"labels" : {
"group" : [
"dev"
],
"project": [
"website",
"api"
]
}
}
```

Expand Down Expand Up @@ -43,15 +52,21 @@ guarantee by default, i.e. [Natural Sorting](https://docs.mongodb.org/manual/ref

``seq`` is a required field in all MongoDB ACL documents. Any documents without this key will be excluded. seq uniqeness is also enforced.

**reference_acl.json**
- match: {labels: {"group": "/trainee|dev/"}}
actions: ["push", "pull"]
comment: "Users assigned to group 'trainee' and 'dev' is able to push and pull"

**reference_acl.json**
```json
{"seq": 10, "match" : {"account" : "admin"}, "actions" : ["*"], "comment" : "Admin has full access to everything."}
{"seq": 11, "match" : {"labels": {"group": "admin"}}, "actions" : ["*"], "comment" : "Admin group members have full access to everything"}
{"seq": 20, "match" : {"account" : "test", "name" : "test-*"}, "actions" : ["*"], "comment" : "User \"test\" has full access to test-* images but nothing else. (1)"}
{"seq": 30, "match" : {"account" : "test"}, "actions" : [], "comment" : "User \"test\" has full access to test-* images but nothing else. (2)"}
{"seq": 40, "match" : {"account" : "/.+/"}, "actions" : ["pull"], "comment" : "All logged in users can pull all images."}
{"seq": 50, "match" : {"account" : "/.+/", "name" : "${account}/*"}, "actions" : ["*"], "comment" : "All logged in users can push all images that are in a namespace beginning with their name"}
{"seq": 60, "match" : {"account" : "", "name" : "hello-world"}, "actions" : ["pull"], "comment" : "Anonymous users can pull \"hello-world\"."}
{"seq": 60, "match" : {"name" : "${labels:group}-shared/*"}, "actions" : ["push", "pull"], "comment" : "Users can pull and push to the shared namespace of any group they are in"}
{"seq": 70, "match" : {"name" : "${labels:project}/*"}, "actions" : ["push", "pull"], "comment" : "Users can pull and push to to namespaces matching projects they are assigned to"}
{"seq": 80, "match" : {"account" : "", "name" : "hello-world"}, "actions" : ["pull"], "comment" : "Anonymous users can pull \"hello-world\"."}
```

**Note** that each document entry must span exactly one line or otherwise the
Expand Down
81 changes: 81 additions & 0 deletions docs/Labels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Labels

Labels can be used to reduce the number ACLS needed in large, complex installations.

## Label Placeholders

Label placeholders are available for any label that is assigned to a user.

For example, given a user:

```json
{
"username" : "busy-guy",
"password" : "$2y$05$B.x046DV3bvuwFgn0I42F.W/SbRU5fUoCbCGtjFl7S33aCUHNBxbq",
"labels" : {
"group" : [
"web",
"webdev"
],
"project" : [
"website",
"api"
],
"tier" : [
"frontend",
"backend"
]
}
}
```

The following placeholders could be used in any match field:

* `${labels:group}`
* `${labels:project}`
* `${labels:tier}`

Example acl with label matching:

```json
{
"match": { "name": "${labels:project}/*" },
"actions": [ "push", "pull" ],
"comment": "Users can push to any project they are assigned to"
}
```

Single label matching is efficient and will be tested in the order
they are listed in the user record.


## Using Multiple Labels when matching

It's possible to use multiple labels in a single match. When multiple labels are
used in a single match all possible combinations of the labels are tested
in [no particular order](https://blog.golang.org/go-maps-in-action#TOC_7.).

Example acl with multiple label matching:

```json
{
"match": { "name": "${labels:project}/${labels:group}-${labels:tier}" },
"actions": [ "push", "pull" ],
"comment": "Contrived multiple label match rule"
}
```

When paired with the user given above would result in 8 possible combinations
that would need to be tested.

* `${labels:project} : website`, `${labels:group} : dev`, `${labels:tier} : frontend`
* `${labels:project} : website`, `${labels:group} : dev`, `${labels:tier} : backend`
* `${labels:project} : website`, `${labels:group} : webdev`, `${labels:tier} : frontend`
* `${labels:project} : website`, `${labels:group} : webdev`, `${labels:tier} : backend`
* `${labels:project} : api`, `${labels:group} : dev`, `${labels:tier} : frontend`
* `${labels:project} : api`, `${labels:group} : dev`, `${labels:tier} : backend`
* `${labels:project} : api`, `${labels:group} : webdev`, `${labels:tier} : frontend`
* `${labels:project} : api`, `${labels:group} : webdev`, `${labels:tier} : backend`

This grows rapidly as more placeholders and labels are added. So it's best
to limit multiple label matching when possible.
10 changes: 10 additions & 0 deletions examples/reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ ext_auth:
# * ${service} - the service name, specified by auth.token.service in the registry config.
# * ${type} - the type of the entity, normally "repository".
# * ${name} - the name of the repository (i.e. image), e.g. centos.
# * ${labels:<LABEL>} - tests all values in the list of lables:<LABEL> for the user. Refer to the labels doc for details
acl:
- match: {ip: "127.0.0.0/8"}
actions: ["*"]
Expand Down Expand Up @@ -235,6 +236,15 @@ acl:
- match: {labels: {"group": "/trainee|dev/"}}
actions: ["push", "pull"]
comment: "Users assigned to group 'trainee' and 'dev' is able to push and pull"
- match: {name: "${labels:group}-shared/*"}
actions: ["push", "pull"]
comment: "Users can push to the shared namespace of any group they are in"
- match: {name: "${labels:project}/*"}
actions: ["push", "pull"]
comment: "Users can push to any project they are assigned to"
- match: {name: "${labels:project}-{labels:tier}/*"}
actions: ["push", "pull"]
comment: "Users can push to a project-tier/* that they are assigned to"
# Access is denied by default.

# (optional) Define to query ACL from a MongoDB server.
Expand Down

0 comments on commit aea8fdc

Please sign in to comment.