1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package server
16
17 import (
18 "context"
19 "encoding/base64"
20 "encoding/json"
21 "io"
22 "net/http"
23 "net/http/httptest"
24 "os"
25 "regexp"
26 "strings"
27 "testing"
28 "time"
29
30 "github.com/go-playground/validator/v10"
31 "gitlab.com/flimzy/testy"
32
33 "github.com/go-kivik/kivik/v4"
34 _ "github.com/go-kivik/kivik/v4/x/fsdb"
35 _ "github.com/go-kivik/kivik/v4/x/memorydb"
36 "github.com/go-kivik/kivik/v4/x/server/auth"
37 "github.com/go-kivik/kivik/v4/x/server/config"
38 )
39
40 var v = validator.New(validator.WithRequiredStructEnabled())
41
42 const (
43 userAdmin = "admin"
44 userBob = "bob"
45 userAlice = "alice"
46 userCharlie = "charlie"
47 userDavid = "davic"
48 userErin = "erin"
49 userFrank = "frank"
50 userReplicator = "replicator"
51 userDBUpdates = "db_updates"
52 userDesign = "design"
53 testPassword = "abc123"
54 roleFoo = "foo"
55 roleBar = "bar"
56 roleBaz = "baz"
57 )
58
59 func testUserStore(t *testing.T) *auth.MemoryUserStore {
60 t.Helper()
61 us := auth.NewMemoryUserStore()
62 if err := us.AddUser(userAdmin, testPassword, []string{auth.RoleAdmin}); err != nil {
63 t.Fatal(err)
64 }
65 if err := us.AddUser(userBob, testPassword, []string{auth.RoleReader}); err != nil {
66 t.Fatal(err)
67 }
68 if err := us.AddUser(userAlice, testPassword, []string{auth.RoleWriter}); err != nil {
69 t.Fatal(err)
70 }
71 if err := us.AddUser(userCharlie, testPassword, []string{auth.RoleWriter, roleFoo}); err != nil {
72 t.Fatal(err)
73 }
74 if err := us.AddUser(userDavid, testPassword, []string{auth.RoleWriter, roleBar}); err != nil {
75 t.Fatal(err)
76 }
77 if err := us.AddUser(userErin, testPassword, []string{auth.RoleWriter}); err != nil {
78 t.Fatal(err)
79 }
80 if err := us.AddUser(userFrank, testPassword, []string{auth.RoleWriter, roleBaz}); err != nil {
81 t.Fatal(err)
82 }
83 if err := us.AddUser(userReplicator, testPassword, []string{auth.RoleReplicator}); err != nil {
84 t.Fatal(err)
85 }
86 if err := us.AddUser(userDBUpdates, testPassword, []string{auth.RoleDBUpdates}); err != nil {
87 t.Fatal(err)
88 }
89 if err := us.AddUser(userDesign, testPassword, []string{auth.RoleDesign}); err != nil {
90 t.Fatal(err)
91 }
92 return us
93 }
94
95 func basicAuth(user string) string {
96 return "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+testPassword))
97 }
98
99 type serverTest struct {
100 name string
101 client *kivik.Client
102 driver, dsn string
103 init func(t *testing.T, client *kivik.Client)
104 extraOptions []Option
105 method string
106 path string
107 headers map[string]string
108 authUser string
109 body io.Reader
110 wantStatus int
111 wantBodyRE string
112 wantJSON interface{}
113 check func(t *testing.T, client *kivik.Client)
114
115
116
117 target interface{}
118 }
119
120 type serverTests []serverTest
121
122 func (s serverTests) Run(t *testing.T) {
123 t.Helper()
124 for _, tt := range s {
125 tt := tt
126 t.Run(tt.name, func(t *testing.T) {
127 t.Parallel()
128 driver, dsn := "fs", "testdata/fsdb"
129 if tt.dsn != "" {
130 dsn = tt.dsn
131 }
132 client := tt.client
133 if client == nil {
134 if tt.driver != "" {
135 driver = tt.driver
136 }
137 if driver == "fs" {
138 dsn = testy.CopyTempDir(t, dsn, 0)
139 t.Cleanup(func() {
140 _ = os.RemoveAll(dsn)
141 })
142 }
143 var err error
144 client, err = kivik.New(driver, dsn)
145 if err != nil {
146 t.Fatal(err)
147 }
148 }
149 if tt.init != nil {
150 tt.init(t, client)
151 }
152 us := testUserStore(t)
153 const secret = "foo"
154 opts := append([]Option{
155 WithUserStores(us),
156 WithAuthHandlers(auth.BasicAuth()),
157 WithAuthHandlers(auth.CookieAuth(secret, time.Hour)),
158 }, tt.extraOptions...)
159
160 s := New(client, opts...)
161 body := tt.body
162 if body == nil {
163 body = strings.NewReader("")
164 }
165 req, err := http.NewRequest(tt.method, tt.path, body)
166 if err != nil {
167 t.Fatal(err)
168 }
169 for k, v := range tt.headers {
170 req.Header.Set(k, v)
171 }
172 if tt.authUser != "" {
173 user, err := us.UserCtx(context.Background(), tt.authUser)
174 if err != nil {
175 t.Fatal(err)
176 }
177 req.AddCookie(&http.Cookie{
178 Name: kivik.SessionCookieName,
179 Value: auth.CreateAuthToken(user.Name, user.Salt, secret, time.Now().Unix()),
180 })
181 }
182
183 rec := httptest.NewRecorder()
184 s.ServeHTTP(rec, req)
185
186 res := rec.Result()
187 if res.StatusCode != tt.wantStatus {
188 t.Errorf("Unexpected response status: %d %s", res.StatusCode, http.StatusText(res.StatusCode))
189 }
190 switch {
191 case tt.target != nil:
192 if err := json.NewDecoder(res.Body).Decode(tt.target); err != nil {
193 t.Fatal(err)
194 }
195 if err := v.Struct(tt.target); err != nil {
196 t.Fatalf("response does not match expectations: %s\n%v", err, tt.target)
197 }
198 case tt.wantBodyRE != "":
199 re := regexp.MustCompile(tt.wantBodyRE)
200 body, err := io.ReadAll(res.Body)
201 if err != nil {
202 t.Fatal(err)
203 }
204 if !re.Match(body) {
205 t.Errorf("Unexpected response body:\n%s", body)
206 }
207 default:
208 if d := testy.DiffAsJSON(tt.wantJSON, res.Body); d != nil {
209 t.Error(d)
210 }
211 }
212 if tt.check != nil {
213 tt.check(t, client)
214 }
215 })
216 }
217 }
218
219 func TestServer(t *testing.T) {
220 t.Parallel()
221
222 tests := serverTests{
223 {
224 name: "root",
225 method: http.MethodGet,
226 path: "/",
227 wantStatus: http.StatusOK,
228 wantJSON: map[string]interface{}{
229 "couchdb": "Welcome",
230 "vendor": map[string]interface{}{
231 "name": "Kivik",
232 "version": kivik.Version,
233 },
234 "version": kivik.Version,
235 },
236 },
237 {
238 name: "active tasks",
239 method: http.MethodGet,
240 path: "/_active_tasks",
241 headers: map[string]string{"Authorization": basicAuth(userAdmin)},
242 wantStatus: http.StatusOK,
243 wantJSON: []interface{}{},
244 },
245 {
246 name: "all dbs",
247 method: http.MethodGet,
248 path: "/_all_dbs",
249 headers: map[string]string{"Authorization": basicAuth(userAdmin)},
250 wantStatus: http.StatusOK,
251 wantJSON: []string{"bobsdb", "db1", "db2"},
252 },
253 {
254 name: "all dbs, cookie auth",
255 method: http.MethodGet,
256 path: "/_all_dbs",
257 authUser: userAdmin,
258 wantStatus: http.StatusOK,
259 wantJSON: []string{"bobsdb", "db1", "db2"},
260 },
261 {
262 name: "all dbs, non-admin",
263 method: http.MethodGet,
264 path: "/_all_dbs",
265 headers: map[string]string{"Authorization": basicAuth(userBob)},
266 wantStatus: http.StatusForbidden,
267 wantJSON: map[string]interface{}{
268 "error": "forbidden",
269 "reason": "Admin privileges required",
270 },
271 },
272 {
273 name: "all dbs, descending",
274 method: http.MethodGet,
275 path: "/_all_dbs?descending=true",
276 headers: map[string]string{"Authorization": basicAuth(userAdmin)},
277 wantStatus: http.StatusOK,
278 wantJSON: []string{"db2", "db1", "bobsdb"},
279 },
280 {
281 name: "db info",
282 method: http.MethodGet,
283 path: "/db2",
284 headers: map[string]string{"Authorization": basicAuth(userAdmin)},
285 wantStatus: http.StatusOK,
286 wantJSON: map[string]interface{}{
287 "db_name": "db2",
288 "compact_running": false,
289 "data_size": 0,
290 "disk_size": 0,
291 "doc_count": 0,
292 "doc_del_count": 0,
293 "update_seq": "",
294 },
295 },
296 {
297 name: "db info HEAD",
298 method: http.MethodHead,
299 path: "/db2",
300 headers: map[string]string{"Authorization": basicAuth(userAdmin)},
301 wantStatus: http.StatusOK,
302 },
303 {
304 name: "start session, no content type header",
305 method: http.MethodPost,
306 path: "/_session",
307 body: strings.NewReader(`name=root&password=abc123`),
308 wantStatus: http.StatusUnsupportedMediaType,
309 wantJSON: map[string]interface{}{
310 "error": "bad_content_type",
311 "reason": "Content-Type must be 'application/x-www-form-urlencoded' or 'application/json'",
312 },
313 },
314 {
315 name: "start session, invalid content type",
316 method: http.MethodPost,
317 path: "/_session",
318 body: strings.NewReader(`name=root&password=abc123`),
319 headers: map[string]string{"Content-Type": "application/xml"},
320 wantStatus: http.StatusUnsupportedMediaType,
321 wantJSON: map[string]interface{}{
322 "error": "bad_content_type",
323 "reason": "Content-Type must be 'application/x-www-form-urlencoded' or 'application/json'",
324 },
325 },
326 {
327 name: "start session, no user name",
328 method: http.MethodPost,
329 path: "/_session",
330 body: strings.NewReader(`{}`),
331 headers: map[string]string{"Content-Type": "application/json"},
332 wantStatus: http.StatusBadRequest,
333 wantJSON: map[string]interface{}{
334 "error": "bad_request",
335 "reason": "request body must contain a username",
336 },
337 },
338 {
339 name: "start session, success",
340 method: http.MethodPost,
341 path: "/_session",
342 body: strings.NewReader(`{"name":"admin","password":"abc123"}`),
343 headers: map[string]string{"Content-Type": "application/json"},
344 wantStatus: http.StatusOK,
345 wantJSON: map[string]interface{}{
346 "ok": true,
347 "name": userAdmin,
348 "roles": []string{"_admin"},
349 },
350 },
351 {
352 name: "delete session",
353 method: http.MethodDelete,
354 path: "/_session",
355 authUser: userAdmin,
356 wantStatus: http.StatusOK,
357 wantJSON: map[string]interface{}{
358 "ok": true,
359 },
360 },
361 {
362 name: "_up",
363 method: http.MethodGet,
364 path: "/_up",
365 wantStatus: http.StatusOK,
366 wantJSON: map[string]interface{}{
367 "status": "ok",
368 },
369 },
370 {
371 name: "all config",
372 method: http.MethodGet,
373 path: "/_node/_local/_config",
374 authUser: userAdmin,
375 wantStatus: http.StatusOK,
376 wantJSON: map[string]interface{}{
377 "couchdb": map[string]interface{}{
378 "users_db_suffix": "_users",
379 },
380 },
381 },
382 {
383 name: "all config, non-admin",
384 method: http.MethodGet,
385 path: "/_node/_local/_config",
386 authUser: userBob,
387 wantStatus: http.StatusForbidden,
388 wantJSON: map[string]interface{}{
389 "error": "forbidden",
390 "reason": "Admin privileges required",
391 },
392 },
393 {
394 name: "all config, no such node",
395 method: http.MethodGet,
396 path: "/_node/asdf/_config",
397 authUser: userAdmin,
398 wantStatus: http.StatusNotFound,
399 wantJSON: map[string]interface{}{
400 "error": "not_found",
401 "reason": "no such node: asdf",
402 },
403 },
404 {
405 name: "config section",
406 method: http.MethodGet,
407 path: "/_node/_local/_config/couchdb",
408 authUser: userAdmin,
409 wantStatus: http.StatusOK,
410 wantJSON: map[string]interface{}{
411 "users_db_suffix": "_users",
412 },
413 },
414 {
415 name: "config key",
416 method: http.MethodGet,
417 path: "/_node/_local/_config/couchdb/users_db_suffix",
418 authUser: userAdmin,
419 wantStatus: http.StatusOK,
420 wantJSON: "_users",
421 },
422 {
423 name: "reload config",
424 method: http.MethodPost,
425 path: "/_node/_local/_config/_reload",
426 authUser: userAdmin,
427 wantStatus: http.StatusOK,
428 wantJSON: map[string]bool{"ok": true},
429 },
430 {
431 name: "set new config key",
432 method: http.MethodPut,
433 path: "/_node/_local/_config/foo/bar",
434 body: strings.NewReader(`"oink"`),
435 authUser: userAdmin,
436 wantStatus: http.StatusOK,
437 wantJSON: "",
438 },
439 {
440 name: "set existing config key",
441 method: http.MethodPut,
442 path: "/_node/_local/_config/couchdb/users_db_suffix",
443 body: strings.NewReader(`"oink"`),
444 authUser: userAdmin,
445 wantStatus: http.StatusOK,
446 wantJSON: "_users",
447 },
448 {
449 name: "delete existing config key",
450 method: http.MethodDelete,
451 path: "/_node/_local/_config/couchdb/users_db_suffix",
452 authUser: userAdmin,
453 wantStatus: http.StatusOK,
454 wantJSON: "_users",
455 },
456 {
457 name: "delete non-existent config key",
458 method: http.MethodDelete,
459 path: "/_node/_local/_config/foo/bar",
460 authUser: userAdmin,
461 wantStatus: http.StatusNotFound,
462 wantJSON: map[string]interface{}{
463 "error": "not_found",
464 "reason": "unknown_config_value",
465 },
466 },
467 {
468 name: "set config not supported by config backend",
469 extraOptions: []Option{
470 WithConfig(&readOnlyConfig{
471 Config: config.Default(),
472 }),
473 },
474 method: http.MethodPut,
475 path: "/_node/_local/_config/foo/bar",
476 body: strings.NewReader(`"oink"`),
477 authUser: userAdmin,
478 wantStatus: http.StatusMethodNotAllowed,
479 wantJSON: map[string]interface{}{
480 "error": "method_not_allowed",
481 "reason": "configuration is read-only",
482 },
483 },
484 {
485 name: "delete config not supported by config backend",
486 extraOptions: []Option{
487 WithConfig(&readOnlyConfig{
488 Config: config.Default(),
489 }),
490 },
491 method: http.MethodDelete,
492 path: "/_node/_local/_config/foo/bar",
493 authUser: userAdmin,
494 wantStatus: http.StatusMethodNotAllowed,
495 wantJSON: map[string]interface{}{
496 "error": "method_not_allowed",
497 "reason": "configuration is read-only",
498 },
499 },
500 {
501 name: "too many uuids",
502 extraOptions: []Option{
503 WithConfig(&readOnlyConfig{
504 Config: config.Default(),
505 }),
506 },
507 method: http.MethodGet,
508 path: "/_uuids?count=99999",
509 wantStatus: http.StatusBadRequest,
510 wantJSON: map[string]interface{}{
511 "error": "bad_request",
512 "reason": "count must not exceed 1000",
513 },
514 },
515 {
516 name: "invalid count",
517 extraOptions: []Option{
518 WithConfig(&readOnlyConfig{
519 Config: config.Default(),
520 }),
521 },
522 method: http.MethodGet,
523 path: "/_uuids?count=chicken",
524 wantStatus: http.StatusBadRequest,
525 wantJSON: map[string]interface{}{
526 "error": "bad_request",
527 "reason": "count must be a positive integer",
528 },
529 },
530 {
531 name: "random uuids",
532 extraOptions: []Option{
533 WithConfig(&readOnlyConfig{
534 Config: config.Map(
535 map[string]map[string]string{
536 "uuids": {"algorithm": "random"},
537 },
538 ),
539 }),
540 },
541 method: http.MethodGet,
542 path: "/_uuids",
543 wantStatus: http.StatusOK,
544 target: new(struct {
545 UUIDs []string `json:"uuids" validate:"required,len=1,dive,required,len=32,hexadecimal"`
546 }),
547 },
548 {
549 name: "many random uuids",
550 extraOptions: []Option{
551 WithConfig(&readOnlyConfig{
552 Config: config.Map(
553 map[string]map[string]string{
554 "uuids": {"algorithm": "random"},
555 },
556 ),
557 }),
558 },
559 method: http.MethodGet,
560 path: "/_uuids?count=10",
561 wantStatus: http.StatusOK,
562 target: new(struct {
563 UUIDs []string `json:"uuids" validate:"required,len=10,dive,required,len=32,hexadecimal"`
564 }),
565 },
566 {
567 name: "sequential uuids",
568 extraOptions: []Option{
569 WithConfig(&readOnlyConfig{
570 Config: config.Default(),
571 }),
572 },
573 method: http.MethodGet,
574 path: "/_uuids",
575 wantStatus: http.StatusOK,
576 target: new(struct {
577 UUIDs []string `json:"uuids" validate:"required,len=1,dive,required,len=32,hexadecimal"`
578 }),
579 },
580 {
581 name: "many random uuids",
582 extraOptions: []Option{
583 WithConfig(&readOnlyConfig{
584 Config: config.Default(),
585 }),
586 },
587 method: http.MethodGet,
588 path: "/_uuids?count=10",
589 wantStatus: http.StatusOK,
590 target: new(struct {
591 UUIDs []string `json:"uuids" validate:"required,len=10,dive,required,len=32,hexadecimal"`
592 }),
593 },
594 {
595 name: "one utc random uuid",
596 extraOptions: []Option{
597 WithConfig(&readOnlyConfig{
598 Config: config.Map(
599 map[string]map[string]string{
600 "uuids": {"algorithm": "utc_random"},
601 },
602 ),
603 }),
604 },
605 method: http.MethodGet,
606 path: "/_uuids",
607 wantStatus: http.StatusOK,
608 target: new(struct {
609 UUIDs []string `json:"uuids" validate:"required,len=1,dive,required,len=32,hexadecimal"`
610 }),
611 },
612 {
613 name: "10 utc random uuids",
614 extraOptions: []Option{
615 WithConfig(&readOnlyConfig{
616 Config: config.Map(
617 map[string]map[string]string{
618 "uuids": {"algorithm": "utc_random"},
619 },
620 ),
621 }),
622 },
623 method: http.MethodGet,
624 path: "/_uuids?count=10",
625 wantStatus: http.StatusOK,
626 target: new(struct {
627 UUIDs []string `json:"uuids" validate:"required,len=10,dive,required,len=32,hexadecimal"`
628 }),
629 },
630 {
631 name: "one utc id uuid",
632 extraOptions: []Option{
633 WithConfig(&readOnlyConfig{
634 Config: config.Map(
635 map[string]map[string]string{
636 "uuids": {
637 "algorithm": "utc_id",
638 "utc_id_suffix": "oink",
639 },
640 },
641 ),
642 }),
643 },
644 method: http.MethodGet,
645 path: "/_uuids",
646 wantStatus: http.StatusOK,
647 target: new(struct {
648 UUIDs []string `json:"uuids" validate:"required,len=1,dive,required,len=18,endswith=oink"`
649 }),
650 },
651 {
652 name: "10 utc id uuids",
653 extraOptions: []Option{
654 WithConfig(&readOnlyConfig{
655 Config: config.Map(
656 map[string]map[string]string{
657 "uuids": {
658 "algorithm": "utc_id",
659 "utc_id_suffix": "oink",
660 },
661 },
662 ),
663 }),
664 },
665 method: http.MethodGet,
666 path: "/_uuids?count=10",
667 wantStatus: http.StatusOK,
668 target: new(struct {
669 UUIDs []string `json:"uuids" validate:"required,len=10,dive,required,len=18,endswith=oink"`
670 }),
671 },
672 {
673 name: "create db",
674 method: http.MethodPut,
675 path: "/db3",
676 authUser: userAdmin,
677 wantStatus: http.StatusCreated,
678 wantJSON: map[string]interface{}{
679 "ok": true,
680 },
681 },
682 {
683 name: "delete db, not found",
684 method: http.MethodDelete,
685 path: "/db3",
686 authUser: userAdmin,
687 wantStatus: http.StatusNotFound,
688 wantJSON: map[string]interface{}{
689 "error": "not_found",
690 "reason": "database does not exist",
691 },
692 },
693 {
694 name: "delete db",
695 method: http.MethodDelete,
696 path: "/db2",
697 authUser: userAdmin,
698 wantStatus: http.StatusOK,
699 wantJSON: map[string]interface{}{
700 "ok": true,
701 },
702 },
703 {
704 name: "post document",
705 driver: "memory",
706 init: func(t *testing.T, client *kivik.Client) {
707 if err := client.CreateDB(context.Background(), "db1", nil); err != nil {
708 t.Fatal(err)
709 }
710 },
711 method: http.MethodPost,
712 path: "/db1",
713 body: strings.NewReader(`{"foo":"bar"}`),
714 authUser: userAdmin,
715 wantStatus: http.StatusCreated,
716 target: &struct {
717 ID string `json:"id" validate:"required,uuid"`
718 Rev string `json:"rev" validate:"required,startswith=1-"`
719 OK bool `json:"ok" validate:"required,eq=true"`
720 }{},
721 },
722 {
723 name: "get document",
724 method: http.MethodGet,
725 path: "/db1/foo",
726 authUser: userAdmin,
727 wantStatus: http.StatusOK,
728 wantJSON: map[string]interface{}{
729 "_id": "foo",
730 "_rev": "1-beea34a62a215ab051862d1e5d93162e",
731 "foo": "bar",
732 },
733 },
734 {
735 name: "all dbs stats",
736 method: http.MethodGet,
737 path: "/_dbs_info",
738 authUser: userAdmin,
739 wantStatus: http.StatusOK,
740 wantJSON: []map[string]interface{}{
741 {
742 "compact_running": false,
743 "data_size": 0,
744 "db_name": "bobsdb",
745 "disk_size": 0,
746 "doc_count": 0,
747 "doc_del_count": 0,
748 "update_seq": "",
749 },
750 {
751 "compact_running": false,
752 "data_size": 0,
753 "db_name": "db1",
754 "disk_size": 0,
755 "doc_count": 0,
756 "doc_del_count": 0,
757 "update_seq": "",
758 },
759 {
760 "compact_running": false,
761 "data_size": 0,
762 "db_name": "db2",
763 "disk_size": 0,
764 "doc_count": 0,
765 "doc_del_count": 0,
766 "update_seq": "",
767 },
768 },
769 },
770 {
771 name: "dbs stats",
772 method: http.MethodPost,
773 path: "/_dbs_info",
774 authUser: userAdmin,
775 headers: map[string]string{"Content-Type": "application/json"},
776 body: strings.NewReader(`{"keys":["db1","notfound"]}`),
777 wantStatus: http.StatusOK,
778 wantJSON: []map[string]interface{}{
779 {
780 "compact_running": false,
781 "data_size": 0,
782 "db_name": "db1",
783 "disk_size": 0,
784 "doc_count": 0,
785 "doc_del_count": 0,
786 "update_seq": "",
787 },
788 nil,
789 },
790 },
791 {
792 name: "get security",
793 method: http.MethodGet,
794 path: "/db1/_security",
795 authUser: userAdmin,
796 wantStatus: http.StatusOK,
797 wantJSON: map[string]interface{}{
798 "admins": map[string]interface{}{
799 "names": []string{"superuser"},
800 "roles": []string{"admins"},
801 },
802 "members": map[string]interface{}{
803 "names": []string{"user1", "user2"},
804 "roles": []string{"developers"},
805 },
806 },
807 },
808 func() serverTest {
809 const want = `{"admins":{"names":["superuser"],"roles":["admins"]},"members":{"names":["user1","user2"],"roles":["developers"]}}`
810 return serverTest{
811 name: "put security",
812 method: http.MethodPut,
813 path: "/db2/_security",
814 authUser: userAdmin,
815 headers: map[string]string{"Content-Type": "application/json"},
816 body: strings.NewReader(want),
817 wantStatus: http.StatusOK,
818 wantJSON: map[string]interface{}{
819 "ok": true,
820 },
821 check: func(t *testing.T, client *kivik.Client) {
822 sec, err := client.DB("db2").Security(context.Background())
823 if err != nil {
824 t.Fatal(err)
825 }
826 if d := testy.DiffAsJSON([]byte(want), sec); d != nil {
827 t.Errorf("Unexpected final result: %s", d)
828 }
829 },
830 }
831 }(),
832 {
833 name: "put security, unauthorized",
834 method: http.MethodPut,
835 path: "/db2/_security",
836 headers: map[string]string{"Content-Type": "application/json"},
837 body: strings.NewReader(`{"admins":{"names":["bob"]}}`),
838 wantStatus: http.StatusUnauthorized,
839 wantJSON: map[string]interface{}{
840 "error": "unauthorized",
841 "reason": "User not authenticated",
842 },
843 },
844 {
845 name: "put security, no admin access",
846 method: http.MethodPut,
847 authUser: userBob,
848 path: "/db2/_security",
849 headers: map[string]string{"Content-Type": "application/json"},
850 body: strings.NewReader(`{"admins":{"names":["bob"]}}`),
851 wantStatus: http.StatusForbidden,
852 wantJSON: map[string]interface{}{
853 "error": "forbidden",
854 "reason": "User lacks sufficient privileges",
855 },
856 },
857 {
858 name: "put security, correct admin user",
859 method: http.MethodPut,
860 authUser: userErin,
861 path: "/bobsdb/_security",
862 headers: map[string]string{"Content-Type": "application/json"},
863 body: strings.NewReader(`{"admins":{"names":["bob"]}}`),
864 wantStatus: http.StatusOK,
865 wantJSON: map[string]interface{}{
866 "ok": true,
867 },
868 },
869 {
870 name: "put security, correct admin role",
871 method: http.MethodPut,
872 authUser: userFrank,
873 path: "/bobsdb/_security",
874 headers: map[string]string{"Content-Type": "application/json"},
875 body: strings.NewReader(`{"admins":{"names":["bob"]}}`),
876 wantStatus: http.StatusOK,
877 wantJSON: map[string]interface{}{
878 "ok": true,
879 },
880 },
881 {
882 name: "db info, unauthenticated",
883 method: http.MethodHead,
884 path: "/bobsdb",
885 wantStatus: http.StatusUnauthorized,
886 wantJSON: map[string]interface{}{
887 "error": "unauthorized",
888 "reason": "User not authenticated",
889 },
890 },
891 {
892 name: "db info, authenticated wrong user, wrong role",
893 method: http.MethodHead,
894 authUser: userAlice,
895 path: "/bobsdb",
896 wantStatus: http.StatusForbidden,
897 wantJSON: map[string]interface{}{
898 "error": "forbidden",
899 "reason": "User lacks sufficient privileges",
900 },
901 },
902 {
903 name: "db info, authenticated correct user",
904 method: http.MethodHead,
905 authUser: userBob,
906 path: "/bobsdb",
907 wantStatus: http.StatusOK,
908 },
909 {
910 name: "db info, authenticated wrong role",
911 method: http.MethodHead,
912 authUser: userCharlie,
913 path: "/bobsdb",
914 wantStatus: http.StatusForbidden,
915 wantJSON: map[string]interface{}{
916 "error": "forbidden",
917 "reason": "User lacks sufficient privileges",
918 },
919 },
920 {
921 name: "db info, authenticated correct role",
922 method: http.MethodHead,
923 authUser: userDavid,
924 path: "/bobsdb",
925 wantStatus: http.StatusOK,
926 },
927 {
928 name: "db info, authenticated as admin user",
929 method: http.MethodHead,
930 authUser: userErin,
931 path: "/bobsdb",
932 wantStatus: http.StatusOK,
933 },
934 {
935 name: "db info, authenticated as admin role",
936 method: http.MethodHead,
937 authUser: userFrank,
938 path: "/bobsdb",
939 wantStatus: http.StatusOK,
940 },
941 }
942
943 tests.Run(t)
944 }
945
946 type readOnlyConfig struct {
947 config.Config
948
949 SetKey int
950 Delete int
951 }
952
View as plain text