1// Copyright 2015 The etcd Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package v2auth
16
17import (
18	"context"
19	"reflect"
20	"testing"
21	"time"
22
23	"go.etcd.io/etcd/etcdserver"
24	"go.etcd.io/etcd/etcdserver/api/v2error"
25	"go.etcd.io/etcd/etcdserver/api/v2store"
26	"go.etcd.io/etcd/etcdserver/etcdserverpb"
27
28	"go.uber.org/zap"
29)
30
31type fakeDoer struct{}
32
33func (fakeDoer) Do(context.Context, etcdserverpb.Request) (etcdserver.Response, error) {
34	return etcdserver.Response{}, nil
35}
36
37func TestCheckPassword(t *testing.T) {
38	st := NewStore(zap.NewExample(), fakeDoer{}, 5*time.Second)
39	u := User{Password: "$2a$10$I3iddh1D..EIOXXQtsra4u8AjOtgEa2ERxVvYGfXFBJDo1omXwP.q"}
40	matched := st.CheckPassword(u, "foo")
41	if matched {
42		t.Fatalf("expected false, got %v", matched)
43	}
44}
45
46const testTimeout = time.Millisecond
47
48func TestMergeUser(t *testing.T) {
49	tbl := []struct {
50		input  User
51		merge  User
52		expect User
53		iserr  bool
54	}{
55		{
56			User{User: "foo"},
57			User{User: "bar"},
58			User{},
59			true,
60		},
61		{
62			User{User: "foo"},
63			User{User: "foo"},
64			User{User: "foo", Roles: []string{}},
65			false,
66		},
67		{
68			User{User: "foo"},
69			User{User: "foo", Grant: []string{"role1"}},
70			User{User: "foo", Roles: []string{"role1"}},
71			false,
72		},
73		{
74			User{User: "foo", Roles: []string{"role1"}},
75			User{User: "foo", Grant: []string{"role1"}},
76			User{},
77			true,
78		},
79		{
80			User{User: "foo", Roles: []string{"role1"}},
81			User{User: "foo", Revoke: []string{"role2"}},
82			User{},
83			true,
84		},
85		{
86			User{User: "foo", Roles: []string{"role1"}},
87			User{User: "foo", Grant: []string{"role2"}},
88			User{User: "foo", Roles: []string{"role1", "role2"}},
89			false,
90		},
91		{ // empty password will not overwrite the previous password
92			User{User: "foo", Password: "foo", Roles: []string{}},
93			User{User: "foo", Password: ""},
94			User{User: "foo", Password: "foo", Roles: []string{}},
95			false,
96		},
97	}
98
99	for i, tt := range tbl {
100		out, err := tt.input.merge(zap.NewExample(), tt.merge, passwordStore{})
101		if err != nil && !tt.iserr {
102			t.Fatalf("Got unexpected error on item %d", i)
103		}
104		if !tt.iserr {
105			if !reflect.DeepEqual(out, tt.expect) {
106				t.Errorf("Unequal merge expectation on item %d: got: %#v, expect: %#v", i, out, tt.expect)
107			}
108		}
109	}
110}
111
112func TestMergeRole(t *testing.T) {
113	tbl := []struct {
114		input  Role
115		merge  Role
116		expect Role
117		iserr  bool
118	}{
119		{
120			Role{Role: "foo"},
121			Role{Role: "bar"},
122			Role{},
123			true,
124		},
125		{
126			Role{Role: "foo"},
127			Role{Role: "foo", Grant: &Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}},
128			Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}},
129			false,
130		},
131		{
132			Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}},
133			Role{Role: "foo", Revoke: &Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}},
134			Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{}, Write: []string{}}}},
135			false,
136		},
137		{
138			Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/bardir"}}}},
139			Role{Role: "foo", Revoke: &Permissions{KV: RWPermission{Read: []string{"/foodir"}}}},
140			Role{},
141			true,
142		},
143	}
144	for i, tt := range tbl {
145		out, err := tt.input.merge(zap.NewExample(), tt.merge)
146		if err != nil && !tt.iserr {
147			t.Fatalf("Got unexpected error on item %d", i)
148		}
149		if !tt.iserr {
150			if !reflect.DeepEqual(out, tt.expect) {
151				t.Errorf("Unequal merge expectation on item %d: got: %#v, expect: %#v", i, out, tt.expect)
152			}
153		}
154	}
155}
156
157type testDoer struct {
158	get               []etcdserver.Response
159	put               []etcdserver.Response
160	getindex          int
161	putindex          int
162	explicitlyEnabled bool
163}
164
165func (td *testDoer) Do(_ context.Context, req etcdserverpb.Request) (etcdserver.Response, error) {
166	if td.explicitlyEnabled && (req.Path == StorePermsPrefix+"/enabled") {
167		t := "true"
168		return etcdserver.Response{
169			Event: &v2store.Event{
170				Action: v2store.Get,
171				Node: &v2store.NodeExtern{
172					Key:   StorePermsPrefix + "/users/cat",
173					Value: &t,
174				},
175			},
176		}, nil
177	}
178	if (req.Method == "GET" || req.Method == "QGET") && td.get != nil {
179		res := td.get[td.getindex]
180		if res.Event == nil {
181			td.getindex++
182			return etcdserver.Response{}, &v2error.Error{
183				ErrorCode: v2error.EcodeKeyNotFound,
184			}
185		}
186		td.getindex++
187		return res, nil
188	}
189	if req.Method == "PUT" && td.put != nil {
190		res := td.put[td.putindex]
191		if res.Event == nil {
192			td.putindex++
193			return etcdserver.Response{}, &v2error.Error{
194				ErrorCode: v2error.EcodeNodeExist,
195			}
196		}
197		td.putindex++
198		return res, nil
199	}
200	return etcdserver.Response{}, nil
201}
202
203func TestAllUsers(t *testing.T) {
204	d := &testDoer{
205		get: []etcdserver.Response{
206			{
207				Event: &v2store.Event{
208					Action: v2store.Get,
209					Node: &v2store.NodeExtern{
210						Nodes: v2store.NodeExterns([]*v2store.NodeExtern{
211							{
212								Key: StorePermsPrefix + "/users/cat",
213							},
214							{
215								Key: StorePermsPrefix + "/users/dog",
216							},
217						}),
218					},
219				},
220			},
221		},
222	}
223	expected := []string{"cat", "dog"}
224
225	s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false}
226	users, err := s.AllUsers()
227	if err != nil {
228		t.Error("Unexpected error", err)
229	}
230	if !reflect.DeepEqual(users, expected) {
231		t.Error("AllUsers doesn't match given store. Got", users, "expected", expected)
232	}
233}
234
235func TestGetAndDeleteUser(t *testing.T) {
236	data := `{"user": "cat", "roles" : ["animal"]}`
237	d := &testDoer{
238		get: []etcdserver.Response{
239			{
240				Event: &v2store.Event{
241					Action: v2store.Get,
242					Node: &v2store.NodeExtern{
243						Key:   StorePermsPrefix + "/users/cat",
244						Value: &data,
245					},
246				},
247			},
248		},
249		explicitlyEnabled: true,
250	}
251	expected := User{User: "cat", Roles: []string{"animal"}}
252
253	s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false}
254	out, err := s.GetUser("cat")
255	if err != nil {
256		t.Error("Unexpected error", err)
257	}
258	if !reflect.DeepEqual(out, expected) {
259		t.Error("GetUser doesn't match given store. Got", out, "expected", expected)
260	}
261	err = s.DeleteUser("cat")
262	if err != nil {
263		t.Error("Unexpected error", err)
264	}
265}
266
267func TestAllRoles(t *testing.T) {
268	d := &testDoer{
269		get: []etcdserver.Response{
270			{
271				Event: &v2store.Event{
272					Action: v2store.Get,
273					Node: &v2store.NodeExtern{
274						Nodes: v2store.NodeExterns([]*v2store.NodeExtern{
275							{
276								Key: StorePermsPrefix + "/roles/animal",
277							},
278							{
279								Key: StorePermsPrefix + "/roles/human",
280							},
281						}),
282					},
283				},
284			},
285		},
286		explicitlyEnabled: true,
287	}
288	expected := []string{"animal", "human", "root"}
289
290	s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false}
291	out, err := s.AllRoles()
292	if err != nil {
293		t.Error("Unexpected error", err)
294	}
295	if !reflect.DeepEqual(out, expected) {
296		t.Error("AllRoles doesn't match given store. Got", out, "expected", expected)
297	}
298}
299
300func TestGetAndDeleteRole(t *testing.T) {
301	data := `{"role": "animal"}`
302	d := &testDoer{
303		get: []etcdserver.Response{
304			{
305				Event: &v2store.Event{
306					Action: v2store.Get,
307					Node: &v2store.NodeExtern{
308						Key:   StorePermsPrefix + "/roles/animal",
309						Value: &data,
310					},
311				},
312			},
313		},
314		explicitlyEnabled: true,
315	}
316	expected := Role{Role: "animal"}
317
318	s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false}
319	out, err := s.GetRole("animal")
320	if err != nil {
321		t.Error("Unexpected error", err)
322	}
323	if !reflect.DeepEqual(out, expected) {
324		t.Error("GetRole doesn't match given store. Got", out, "expected", expected)
325	}
326	err = s.DeleteRole("animal")
327	if err != nil {
328		t.Error("Unexpected error", err)
329	}
330}
331
332func TestEnsure(t *testing.T) {
333	d := &testDoer{
334		get: []etcdserver.Response{
335			{
336				Event: &v2store.Event{
337					Action: v2store.Set,
338					Node: &v2store.NodeExtern{
339						Key: StorePermsPrefix,
340						Dir: true,
341					},
342				},
343			},
344			{
345				Event: &v2store.Event{
346					Action: v2store.Set,
347					Node: &v2store.NodeExtern{
348						Key: StorePermsPrefix + "/users/",
349						Dir: true,
350					},
351				},
352			},
353			{
354				Event: &v2store.Event{
355					Action: v2store.Set,
356					Node: &v2store.NodeExtern{
357						Key: StorePermsPrefix + "/roles/",
358						Dir: true,
359					},
360				},
361			},
362		},
363	}
364
365	s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false}
366	err := s.ensureAuthDirectories()
367	if err != nil {
368		t.Error("Unexpected error", err)
369	}
370}
371
372type fastPasswordStore struct {
373}
374
375func (fastPasswordStore) CheckPassword(user User, password string) bool {
376	return user.Password == password
377}
378
379func (fastPasswordStore) HashPassword(password string) (string, error) { return password, nil }
380
381func TestCreateAndUpdateUser(t *testing.T) {
382	olduser := `{"user": "cat", "roles" : ["animal"]}`
383	newuser := `{"user": "cat", "roles" : ["animal", "pet"]}`
384	d := &testDoer{
385		get: []etcdserver.Response{
386			{
387				Event: nil,
388			},
389			{
390				Event: &v2store.Event{
391					Action: v2store.Get,
392					Node: &v2store.NodeExtern{
393						Key:   StorePermsPrefix + "/users/cat",
394						Value: &olduser,
395					},
396				},
397			},
398			{
399				Event: &v2store.Event{
400					Action: v2store.Get,
401					Node: &v2store.NodeExtern{
402						Key:   StorePermsPrefix + "/users/cat",
403						Value: &olduser,
404					},
405				},
406			},
407		},
408		put: []etcdserver.Response{
409			{
410				Event: &v2store.Event{
411					Action: v2store.Update,
412					Node: &v2store.NodeExtern{
413						Key:   StorePermsPrefix + "/users/cat",
414						Value: &olduser,
415					},
416				},
417			},
418			{
419				Event: &v2store.Event{
420					Action: v2store.Update,
421					Node: &v2store.NodeExtern{
422						Key:   StorePermsPrefix + "/users/cat",
423						Value: &newuser,
424					},
425				},
426			},
427		},
428		explicitlyEnabled: true,
429	}
430	user := User{User: "cat", Password: "meow", Roles: []string{"animal"}}
431	update := User{User: "cat", Grant: []string{"pet"}}
432	expected := User{User: "cat", Roles: []string{"animal", "pet"}}
433
434	s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true, PasswordStore: fastPasswordStore{}}
435	out, created, err := s.CreateOrUpdateUser(user)
436	if !created {
437		t.Error("Should have created user, instead updated?")
438	}
439	if err != nil {
440		t.Error("Unexpected error", err)
441	}
442	out.Password = "meow"
443	if !reflect.DeepEqual(out, user) {
444		t.Error("UpdateUser doesn't match given update. Got", out, "expected", expected)
445	}
446	out, created, err = s.CreateOrUpdateUser(update)
447	if created {
448		t.Error("Should have updated user, instead created?")
449	}
450	if err != nil {
451		t.Error("Unexpected error", err)
452	}
453	if !reflect.DeepEqual(out, expected) {
454		t.Error("UpdateUser doesn't match given update. Got", out, "expected", expected)
455	}
456}
457
458func TestUpdateRole(t *testing.T) {
459	oldrole := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": []}}}`
460	newrole := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": ["/animal"]}}}`
461	d := &testDoer{
462		get: []etcdserver.Response{
463			{
464				Event: &v2store.Event{
465					Action: v2store.Get,
466					Node: &v2store.NodeExtern{
467						Key:   StorePermsPrefix + "/roles/animal",
468						Value: &oldrole,
469					},
470				},
471			},
472		},
473		put: []etcdserver.Response{
474			{
475				Event: &v2store.Event{
476					Action: v2store.Update,
477					Node: &v2store.NodeExtern{
478						Key:   StorePermsPrefix + "/roles/animal",
479						Value: &newrole,
480					},
481				},
482			},
483		},
484		explicitlyEnabled: true,
485	}
486	update := Role{Role: "animal", Grant: &Permissions{KV: RWPermission{Read: []string{}, Write: []string{"/animal"}}}}
487	expected := Role{Role: "animal", Permissions: Permissions{KV: RWPermission{Read: []string{"/animal"}, Write: []string{"/animal"}}}}
488
489	s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true}
490	out, err := s.UpdateRole(update)
491	if err != nil {
492		t.Error("Unexpected error", err)
493	}
494	if !reflect.DeepEqual(out, expected) {
495		t.Error("UpdateRole doesn't match given update. Got", out, "expected", expected)
496	}
497}
498
499func TestCreateRole(t *testing.T) {
500	role := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": []}}}`
501	d := &testDoer{
502		put: []etcdserver.Response{
503			{
504				Event: &v2store.Event{
505					Action: v2store.Create,
506					Node: &v2store.NodeExtern{
507						Key:   StorePermsPrefix + "/roles/animal",
508						Value: &role,
509					},
510				},
511			},
512			{
513				Event: nil,
514			},
515		},
516		explicitlyEnabled: true,
517	}
518	r := Role{Role: "animal", Permissions: Permissions{KV: RWPermission{Read: []string{"/animal"}, Write: []string{}}}}
519
520	s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true}
521	err := s.CreateRole(Role{Role: "root"})
522	if err == nil {
523		t.Error("Should error creating root role")
524	}
525	err = s.CreateRole(r)
526	if err != nil {
527		t.Error("Unexpected error", err)
528	}
529	err = s.CreateRole(r)
530	if err == nil {
531		t.Error("Creating duplicate role, should error")
532	}
533}
534
535func TestEnableAuth(t *testing.T) {
536	rootUser := `{"user": "root", "password": ""}`
537	guestRole := `{"role": "guest", "permissions" : {"kv": {"read": ["*"], "write": ["*"]}}}`
538	trueval := "true"
539	falseval := "false"
540	d := &testDoer{
541		get: []etcdserver.Response{
542			{
543				Event: &v2store.Event{
544					Action: v2store.Get,
545					Node: &v2store.NodeExtern{
546						Key:   StorePermsPrefix + "/enabled",
547						Value: &falseval,
548					},
549				},
550			},
551			{
552				Event: &v2store.Event{
553					Action: v2store.Get,
554					Node: &v2store.NodeExtern{
555						Key:   StorePermsPrefix + "/user/root",
556						Value: &rootUser,
557					},
558				},
559			},
560			{
561				Event: nil,
562			},
563		},
564		put: []etcdserver.Response{
565			{
566				Event: &v2store.Event{
567					Action: v2store.Create,
568					Node: &v2store.NodeExtern{
569						Key:   StorePermsPrefix + "/roles/guest",
570						Value: &guestRole,
571					},
572				},
573			},
574			{
575				Event: &v2store.Event{
576					Action: v2store.Update,
577					Node: &v2store.NodeExtern{
578						Key:   StorePermsPrefix + "/enabled",
579						Value: &trueval,
580					},
581				},
582			},
583		},
584		explicitlyEnabled: false,
585	}
586	s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true}
587	err := s.EnableAuth()
588	if err != nil {
589		t.Error("Unexpected error", err)
590	}
591}
592
593func TestDisableAuth(t *testing.T) {
594	trueval := "true"
595	falseval := "false"
596	d := &testDoer{
597		get: []etcdserver.Response{
598			{
599				Event: &v2store.Event{
600					Action: v2store.Get,
601					Node: &v2store.NodeExtern{
602						Key:   StorePermsPrefix + "/enabled",
603						Value: &falseval,
604					},
605				},
606			},
607			{
608				Event: &v2store.Event{
609					Action: v2store.Get,
610					Node: &v2store.NodeExtern{
611						Key:   StorePermsPrefix + "/enabled",
612						Value: &trueval,
613					},
614				},
615			},
616		},
617		put: []etcdserver.Response{
618			{
619				Event: &v2store.Event{
620					Action: v2store.Update,
621					Node: &v2store.NodeExtern{
622						Key:   StorePermsPrefix + "/enabled",
623						Value: &falseval,
624					},
625				},
626			},
627		},
628		explicitlyEnabled: false,
629	}
630	s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true}
631	err := s.DisableAuth()
632	if err == nil {
633		t.Error("Expected error; already disabled")
634	}
635
636	err = s.DisableAuth()
637	if err != nil {
638		t.Error("Unexpected error", err)
639	}
640}
641
642func TestSimpleMatch(t *testing.T) {
643	role := Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir/*", "/fookey"}, Write: []string{"/bardir/*", "/barkey"}}}}
644	if !role.HasKeyAccess("/foodir/foo/bar", false) {
645		t.Fatal("role lacks expected access")
646	}
647	if !role.HasKeyAccess("/fookey", false) {
648		t.Fatal("role lacks expected access")
649	}
650	if !role.HasRecursiveAccess("/foodir/*", false) {
651		t.Fatal("role lacks expected access")
652	}
653	if !role.HasRecursiveAccess("/foodir/foo*", false) {
654		t.Fatal("role lacks expected access")
655	}
656	if !role.HasRecursiveAccess("/bardir/*", true) {
657		t.Fatal("role lacks expected access")
658	}
659	if !role.HasKeyAccess("/bardir/bar/foo", true) {
660		t.Fatal("role lacks expected access")
661	}
662	if !role.HasKeyAccess("/barkey", true) {
663		t.Fatal("role lacks expected access")
664	}
665
666	if role.HasKeyAccess("/bardir/bar/foo", false) {
667		t.Fatal("role has unexpected access")
668	}
669	if role.HasKeyAccess("/barkey", false) {
670		t.Fatal("role has unexpected access")
671	}
672	if role.HasKeyAccess("/foodir/foo/bar", true) {
673		t.Fatal("role has unexpected access")
674	}
675	if role.HasKeyAccess("/fookey", true) {
676		t.Fatal("role has unexpected access")
677	}
678}
679