Skip to content

Commit 54aae68

Browse files
authored
Merge pull request #195 from carsonoid/labelmatching
Add matching of label placeholders
2 parents 694cf19 + aea8fdc commit 54aae68

File tree

6 files changed

+181
-8
lines changed

6 files changed

+181
-8
lines changed

auth_server/authz/acl.go

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/cesanta/docker_auth/auth_server/authn"
1414
"github.com/cesanta/glog"
15+
"github.com/schwarmco/go-cartesian-product"
1516
)
1617

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

157+
func matchStringWithLabelPermutations(pp *string, s string, vars []string, labelMap *map[string][]string) bool {
158+
var matched bool
159+
// First try basic matching
160+
matched = matchString(pp, s, vars)
161+
// If basic matching fails then try with label permuations
162+
if !matched {
163+
// Take the labelMap and build the structure required for the cartesian library
164+
var labelSets [][]interface{}
165+
for placeholder, labels := range *labelMap {
166+
// Don't bother generating perumations for placeholders not in match string
167+
// Since the label permuations are a cartesian product this can have
168+
// a huge impact on performance
169+
if strings.Contains(*pp, placeholder) {
170+
var labelSet []interface{}
171+
for _, label := range labels {
172+
labelSet = append(labelSet, []string{placeholder, label})
173+
}
174+
labelSets = append(labelSets, labelSet)
175+
}
176+
}
177+
if len(labelSets) > 0 {
178+
for permuation := range cartesian.Iter(labelSets...) {
179+
var labelVars []string
180+
for _, val := range permuation {
181+
labelVars = append(labelVars, val.([]string)...)
182+
}
183+
matched = matchString(pp, s, append(vars, labelVars...))
184+
if matched {
185+
break
186+
}
187+
}
188+
}
189+
}
190+
return matched
191+
}
192+
156193
func matchIP(ipp *string, ip net.IP) bool {
157194
if ipp == nil {
158195
return true
@@ -233,10 +270,18 @@ func (mc *MatchConditions) Matches(ai *AuthRequestInfo) bool {
233270
vars = append(vars, found[0], text[index])
234271
}
235272
}
236-
return matchString(mc.Account, ai.Account, vars) &&
237-
matchString(mc.Type, ai.Type, vars) &&
238-
matchString(mc.Name, ai.Name, vars) &&
239-
matchString(mc.Service, ai.Service, vars) &&
273+
labelMap := make(map[string][]string)
274+
for label, labelValues := range ai.Labels {
275+
var labelSet []string
276+
for _, lv := range labelValues {
277+
labelSet = append(labelSet, regexp.QuoteMeta(lv))
278+
}
279+
labelMap[fmt.Sprintf("${labels:%s}", label)] = labelSet
280+
}
281+
return matchStringWithLabelPermutations(mc.Account, ai.Account, vars, &labelMap) &&
282+
matchStringWithLabelPermutations(mc.Type, ai.Type, vars, &labelMap) &&
283+
matchStringWithLabelPermutations(mc.Name, ai.Name, vars, &labelMap) &&
284+
matchStringWithLabelPermutations(mc.Service, ai.Service, vars, &labelMap) &&
240285
matchIP(mc.IP, ai.IP) &&
241286
matchLabels(mc.Labels, ai.Labels, vars)
242287
}

auth_server/authz/acl_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ func TestMatching(t *testing.T) {
5858
ai1 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz", Service: "notary"}
5959
ai2 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz", Service: "notary",
6060
Labels: map[string][]string{"group": []string{"admins", "VIP"}}}
61+
ai3 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "admins/foo", Service: "notary",
62+
Labels: map[string][]string{"group": []string{"admins", "VIP"}}}
63+
ai4 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "VIP/api", Service: "notary",
64+
Labels: map[string][]string{"group": []string{"admins", "VIP"}, "project": []string{"api", "frontend"}}}
65+
ai5 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "devs/api", Service: "notary",
66+
Labels: map[string][]string{"group": []string{"admins", "VIP"}, "project": []string{"api", "frontend"}}}
6167
cases := []struct {
6268
mc MatchConditions
6369
ai AuthRequestInfo
@@ -99,6 +105,16 @@ func TestMatching(t *testing.T) {
99105
{MatchConditions{Labels: map[string]string{"group": "VIP"}}, ai2, true},
100106
{MatchConditions{Labels: map[string]string{"group": "a*"}}, ai2, true},
101107
{MatchConditions{Labels: map[string]string{"group": "/(admins|VIP)/"}}, ai2, true},
108+
// // Label placeholder matching
109+
{MatchConditions{Name: sp("${labels:group}/*")}, ai1, false}, // no labels
110+
{MatchConditions{Name: sp("${labels:noexist}/*")}, ai2, false}, // wrong labels
111+
{MatchConditions{Name: sp("${labels:group}/*")}, ai3, true}, // match label
112+
{MatchConditions{Name: sp("${labels:noexist}/*")}, ai3, false}, // missing label
113+
{MatchConditions{Name: sp("${labels:group}/${labels:project}")}, ai4, true}, // multiple label match success
114+
{MatchConditions{Name: sp("${labels:group}/${labels:noexist}")}, ai4, false}, // multiple label match fail
115+
{MatchConditions{Name: sp("${labels:group}/${labels:project}")}, ai4, true}, // multiple label match success
116+
{MatchConditions{Name: sp("${labels:group}/${labels:noexist}")}, ai4, false}, // multiple label match fail wrong label
117+
{MatchConditions{Name: sp("${labels:group}/${labels:project}")}, ai5, false}, // multiple label match fail. right label, wrong value
102118
}
103119
for i, c := range cases {
104120
if result := c.mc.Matches(&c.ai); result != c.matches {

auth_server/vendor/vendor.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,12 @@
299299
"revision": "d6c945f9fdbf6cad99e85b0feff591caa268e0db",
300300
"revisionTime": "2015-05-30T21:13:11Z"
301301
},
302+
{
303+
"checksumSHA1": "DVgRSBT6UzmEgC90yJhh6X5A7Yc=",
304+
"path": "github.com/schwarmco/go-cartesian-product",
305+
"revision": "c2c0aca869a6cbf51e017ce148b949d9dee09bc3",
306+
"revisionTime": "2017-01-30T17:09:49Z"
307+
},
302308
{
303309
"checksumSHA1": "GVY3lzvj4xmpKOGgA4/h9GWjQVk=",
304310
"path": "github.com/syndtr/goleveldb/leveldb",

docs/Backend_MongoDB.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,21 @@ which can query ACL and Auth from a MongoDB database.
1010
## Auth backend in MongoDB
1111

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

1515
```json
1616
{
1717
"username" : "admin",
18-
"password" : "$2y$05$B.x046DV3bvuwFgn0I42F.W/SbRU5fUoCbCGtjFl7S33aCUHNBxbq"
18+
"password" : "$2y$05$B.x046DV3bvuwFgn0I42F.W/SbRU5fUoCbCGtjFl7S33aCUHNBxbq",
19+
"labels" : {
20+
"group" : [
21+
"dev"
22+
],
23+
"project": [
24+
"website",
25+
"api"
26+
]
27+
}
1928
}
2029
```
2130

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

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

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

59+
**reference_acl.json**
4860
```json
4961
{"seq": 10, "match" : {"account" : "admin"}, "actions" : ["*"], "comment" : "Admin has full access to everything."}
62+
{"seq": 11, "match" : {"labels": {"group": "admin"}}, "actions" : ["*"], "comment" : "Admin group members have full access to everything"}
5063
{"seq": 20, "match" : {"account" : "test", "name" : "test-*"}, "actions" : ["*"], "comment" : "User \"test\" has full access to test-* images but nothing else. (1)"}
5164
{"seq": 30, "match" : {"account" : "test"}, "actions" : [], "comment" : "User \"test\" has full access to test-* images but nothing else. (2)"}
5265
{"seq": 40, "match" : {"account" : "/.+/"}, "actions" : ["pull"], "comment" : "All logged in users can pull all images."}
5366
{"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"}
54-
{"seq": 60, "match" : {"account" : "", "name" : "hello-world"}, "actions" : ["pull"], "comment" : "Anonymous users can pull \"hello-world\"."}
67+
{"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"}
68+
{"seq": 70, "match" : {"name" : "${labels:project}/*"}, "actions" : ["push", "pull"], "comment" : "Users can pull and push to to namespaces matching projects they are assigned to"}
69+
{"seq": 80, "match" : {"account" : "", "name" : "hello-world"}, "actions" : ["pull"], "comment" : "Anonymous users can pull \"hello-world\"."}
5570
```
5671

5772
**Note** that each document entry must span exactly one line or otherwise the

docs/Labels.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Labels
2+
3+
Labels can be used to reduce the number ACLS needed in large, complex installations.
4+
5+
## Label Placeholders
6+
7+
Label placeholders are available for any label that is assigned to a user.
8+
9+
For example, given a user:
10+
11+
```json
12+
{
13+
"username" : "busy-guy",
14+
"password" : "$2y$05$B.x046DV3bvuwFgn0I42F.W/SbRU5fUoCbCGtjFl7S33aCUHNBxbq",
15+
"labels" : {
16+
"group" : [
17+
"web",
18+
"webdev"
19+
],
20+
"project" : [
21+
"website",
22+
"api"
23+
],
24+
"tier" : [
25+
"frontend",
26+
"backend"
27+
]
28+
}
29+
}
30+
```
31+
32+
The following placeholders could be used in any match field:
33+
34+
* `${labels:group}`
35+
* `${labels:project}`
36+
* `${labels:tier}`
37+
38+
Example acl with label matching:
39+
40+
```json
41+
{
42+
"match": { "name": "${labels:project}/*" },
43+
"actions": [ "push", "pull" ],
44+
"comment": "Users can push to any project they are assigned to"
45+
}
46+
```
47+
48+
Single label matching is efficient and will be tested in the order
49+
they are listed in the user record.
50+
51+
52+
## Using Multiple Labels when matching
53+
54+
It's possible to use multiple labels in a single match. When multiple labels are
55+
used in a single match all possible combinations of the labels are tested
56+
in [no particular order](https://blog.golang.org/go-maps-in-action#TOC_7.).
57+
58+
Example acl with multiple label matching:
59+
60+
```json
61+
{
62+
"match": { "name": "${labels:project}/${labels:group}-${labels:tier}" },
63+
"actions": [ "push", "pull" ],
64+
"comment": "Contrived multiple label match rule"
65+
}
66+
```
67+
68+
When paired with the user given above would result in 8 possible combinations
69+
that would need to be tested.
70+
71+
* `${labels:project} : website`, `${labels:group} : dev`, `${labels:tier} : frontend`
72+
* `${labels:project} : website`, `${labels:group} : dev`, `${labels:tier} : backend`
73+
* `${labels:project} : website`, `${labels:group} : webdev`, `${labels:tier} : frontend`
74+
* `${labels:project} : website`, `${labels:group} : webdev`, `${labels:tier} : backend`
75+
* `${labels:project} : api`, `${labels:group} : dev`, `${labels:tier} : frontend`
76+
* `${labels:project} : api`, `${labels:group} : dev`, `${labels:tier} : backend`
77+
* `${labels:project} : api`, `${labels:group} : webdev`, `${labels:tier} : frontend`
78+
* `${labels:project} : api`, `${labels:group} : webdev`, `${labels:tier} : backend`
79+
80+
This grows rapidly as more placeholders and labels are added. So it's best
81+
to limit multiple label matching when possible.

examples/reference.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ ext_auth:
199199
# * ${service} - the service name, specified by auth.token.service in the registry config.
200200
# * ${type} - the type of the entity, normally "repository".
201201
# * ${name} - the name of the repository (i.e. image), e.g. centos.
202+
# * ${labels:<LABEL>} - tests all values in the list of lables:<LABEL> for the user. Refer to the labels doc for details
202203
acl:
203204
- match: {ip: "127.0.0.0/8"}
204205
actions: ["*"]
@@ -239,6 +240,15 @@ acl:
239240
- match: {labels: {"group": "/trainee|dev/"}}
240241
actions: ["push", "pull"]
241242
comment: "Users assigned to group 'trainee' and 'dev' is able to push and pull"
243+
- match: {name: "${labels:group}-shared/*"}
244+
actions: ["push", "pull"]
245+
comment: "Users can push to the shared namespace of any group they are in"
246+
- match: {name: "${labels:project}/*"}
247+
actions: ["push", "pull"]
248+
comment: "Users can push to any project they are assigned to"
249+
- match: {name: "${labels:project}-{labels:tier}/*"}
250+
actions: ["push", "pull"]
251+
comment: "Users can push to a project-tier/* that they are assigned to"
242252
# Access is denied by default.
243253

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

0 commit comments

Comments
 (0)