diff options
author | Joshua Watt <JPEWhacker@gmail.com> | 2023-11-03 08:26:31 -0600 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2023-11-09 17:33:03 +0000 |
commit | 1af725b2eca63fa113cedb6d77eb5c5f1de6e2f0 (patch) | |
tree | adf200a0b03b8ee1f1a56c55e2ec657dcc7ed04a /bitbake/lib/hashserv/tests.py | |
parent | 6e67b000efb89c4e3121fd907a47dc7042c07bed (diff) | |
download | poky-1af725b2eca63fa113cedb6d77eb5c5f1de6e2f0.tar.gz |
bitbake: hashserv: Add user permissions
Adds support for the hashserver to have per-user permissions. User
management is done via a new "auth" RPC API where a client can
authenticate itself with the server using a randomly generated token.
The user can then be given permissions to read, report, manage the
database, or manage other users.
In addition to explicit user logins, the server supports anonymous users
which is what all users start as before they make the "auth" RPC call.
Anonymous users can be assigned a set of permissions by the server,
making it unnecessary for users to authenticate to use the server. The
set of Anonymous permissions defines the default behavior of the server,
for example if set to "@read", Anonymous users are unable to report
equivalent hashes with authenticating. Similarly, setting the Anonymous
permissions to "@none" would require authentication for users to perform
any action.
User creation and management is entirely manual (although
bitbake-hashclient is very useful as a front end). There are many
different mechanisms that could be implemented to allow user
self-registration (e.g. OAuth, LDAP, etc.), and implementing these is
outside the scope of the server. Instead, it is recommended to
implement a registration service that validates users against the
necessary service, then adds them as a user in the hash equivalence
server.
(Bitbake rev: 69e5417413ee2414fffaa7dd38057573bac56e35)
Signed-off-by: Joshua Watt <JPEWhacker@gmail.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake/lib/hashserv/tests.py')
-rw-r--r-- | bitbake/lib/hashserv/tests.py | 276 |
1 files changed, 267 insertions, 9 deletions
diff --git a/bitbake/lib/hashserv/tests.py b/bitbake/lib/hashserv/tests.py index e9a361dc4b..f92f37c459 100644 --- a/bitbake/lib/hashserv/tests.py +++ b/bitbake/lib/hashserv/tests.py | |||
@@ -6,6 +6,8 @@ | |||
6 | # | 6 | # |
7 | 7 | ||
8 | from . import create_server, create_client | 8 | from . import create_server, create_client |
9 | from .server import DEFAULT_ANON_PERMS, ALL_PERMISSIONS | ||
10 | from bb.asyncrpc import InvokeError | ||
9 | import hashlib | 11 | import hashlib |
10 | import logging | 12 | import logging |
11 | import multiprocessing | 13 | import multiprocessing |
@@ -29,8 +31,9 @@ class HashEquivalenceTestSetup(object): | |||
29 | METHOD = 'TestMethod' | 31 | METHOD = 'TestMethod' |
30 | 32 | ||
31 | server_index = 0 | 33 | server_index = 0 |
34 | client_index = 0 | ||
32 | 35 | ||
33 | def start_server(self, dbpath=None, upstream=None, read_only=False, prefunc=server_prefunc): | 36 | def start_server(self, dbpath=None, upstream=None, read_only=False, prefunc=server_prefunc, anon_perms=DEFAULT_ANON_PERMS, admin_username=None, admin_password=None): |
34 | self.server_index += 1 | 37 | self.server_index += 1 |
35 | if dbpath is None: | 38 | if dbpath is None: |
36 | dbpath = self.make_dbpath() | 39 | dbpath = self.make_dbpath() |
@@ -45,7 +48,10 @@ class HashEquivalenceTestSetup(object): | |||
45 | server = create_server(self.get_server_addr(self.server_index), | 48 | server = create_server(self.get_server_addr(self.server_index), |
46 | dbpath, | 49 | dbpath, |
47 | upstream=upstream, | 50 | upstream=upstream, |
48 | read_only=read_only) | 51 | read_only=read_only, |
52 | anon_perms=anon_perms, | ||
53 | admin_username=admin_username, | ||
54 | admin_password=admin_password) | ||
49 | server.dbpath = dbpath | 55 | server.dbpath = dbpath |
50 | 56 | ||
51 | server.serve_as_process(prefunc=prefunc, args=(self.server_index,)) | 57 | server.serve_as_process(prefunc=prefunc, args=(self.server_index,)) |
@@ -56,18 +62,31 @@ class HashEquivalenceTestSetup(object): | |||
56 | def make_dbpath(self): | 62 | def make_dbpath(self): |
57 | return os.path.join(self.temp_dir.name, "db%d.sqlite" % self.server_index) | 63 | return os.path.join(self.temp_dir.name, "db%d.sqlite" % self.server_index) |
58 | 64 | ||
59 | def start_client(self, server_address): | 65 | def start_client(self, server_address, username=None, password=None): |
60 | def cleanup_client(client): | 66 | def cleanup_client(client): |
61 | client.close() | 67 | client.close() |
62 | 68 | ||
63 | client = create_client(server_address) | 69 | client = create_client(server_address, username=username, password=password) |
64 | self.addCleanup(cleanup_client, client) | 70 | self.addCleanup(cleanup_client, client) |
65 | 71 | ||
66 | return client | 72 | return client |
67 | 73 | ||
68 | def start_test_server(self): | 74 | def start_test_server(self): |
69 | server = self.start_server() | 75 | self.server = self.start_server() |
70 | return server.address | 76 | return self.server.address |
77 | |||
78 | def start_auth_server(self): | ||
79 | self.auth_server = self.start_server(self.server.dbpath, anon_perms=[], admin_username="admin", admin_password="password") | ||
80 | self.admin_client = self.start_client(self.auth_server.address, username="admin", password="password") | ||
81 | return self.admin_client | ||
82 | |||
83 | def auth_client(self, user): | ||
84 | return self.start_client(self.auth_server.address, user["username"], user["token"]) | ||
85 | |||
86 | def auth_perms(self, *permissions): | ||
87 | self.client_index += 1 | ||
88 | user = self.admin_client.new_user(f"user-{self.client_index}", permissions) | ||
89 | return self.auth_client(user) | ||
71 | 90 | ||
72 | def setUp(self): | 91 | def setUp(self): |
73 | if sys.version_info < (3, 5, 0): | 92 | if sys.version_info < (3, 5, 0): |
@@ -86,18 +105,21 @@ class HashEquivalenceTestSetup(object): | |||
86 | 105 | ||
87 | 106 | ||
88 | class HashEquivalenceCommonTests(object): | 107 | class HashEquivalenceCommonTests(object): |
89 | def test_create_hash(self): | 108 | def create_test_hash(self, client): |
90 | # Simple test that hashes can be created | 109 | # Simple test that hashes can be created |
91 | taskhash = '35788efcb8dfb0a02659d81cf2bfd695fb30faf9' | 110 | taskhash = '35788efcb8dfb0a02659d81cf2bfd695fb30faf9' |
92 | outhash = '2765d4a5884be49b28601445c2760c5f21e7e5c0ee2b7e3fce98fd7e5970796f' | 111 | outhash = '2765d4a5884be49b28601445c2760c5f21e7e5c0ee2b7e3fce98fd7e5970796f' |
93 | unihash = 'f46d3fbb439bd9b921095da657a4de906510d2cd' | 112 | unihash = 'f46d3fbb439bd9b921095da657a4de906510d2cd' |
94 | 113 | ||
95 | self.assertClientGetHash(self.client, taskhash, None) | 114 | self.assertClientGetHash(client, taskhash, None) |
96 | 115 | ||
97 | result = self.client.report_unihash(taskhash, self.METHOD, outhash, unihash) | 116 | result = client.report_unihash(taskhash, self.METHOD, outhash, unihash) |
98 | self.assertEqual(result['unihash'], unihash, 'Server returned bad unihash') | 117 | self.assertEqual(result['unihash'], unihash, 'Server returned bad unihash') |
99 | return taskhash, outhash, unihash | 118 | return taskhash, outhash, unihash |
100 | 119 | ||
120 | def test_create_hash(self): | ||
121 | return self.create_test_hash(self.client) | ||
122 | |||
101 | def test_create_equivalent(self): | 123 | def test_create_equivalent(self): |
102 | # Tests that a second reported task with the same outhash will be | 124 | # Tests that a second reported task with the same outhash will be |
103 | # assigned the same unihash | 125 | # assigned the same unihash |
@@ -471,6 +493,242 @@ class HashEquivalenceCommonTests(object): | |||
471 | # shares a taskhash with Task 2 | 493 | # shares a taskhash with Task 2 |
472 | self.assertClientGetHash(self.client, taskhash2, unihash2) | 494 | self.assertClientGetHash(self.client, taskhash2, unihash2) |
473 | 495 | ||
496 | def test_auth_read_perms(self): | ||
497 | admin_client = self.start_auth_server() | ||
498 | |||
499 | # Create hashes with non-authenticated server | ||
500 | taskhash, outhash, unihash = self.test_create_hash() | ||
501 | |||
502 | # Validate hash can be retrieved using authenticated client | ||
503 | with self.auth_perms("@read") as client: | ||
504 | self.assertClientGetHash(client, taskhash, unihash) | ||
505 | |||
506 | with self.auth_perms() as client, self.assertRaises(InvokeError): | ||
507 | self.assertClientGetHash(client, taskhash, unihash) | ||
508 | |||
509 | def test_auth_report_perms(self): | ||
510 | admin_client = self.start_auth_server() | ||
511 | |||
512 | # Without read permission, the user is completely denied | ||
513 | with self.auth_perms() as client, self.assertRaises(InvokeError): | ||
514 | self.create_test_hash(client) | ||
515 | |||
516 | # Read permission allows the call to succeed, but it doesn't record | ||
517 | # anythin in the database | ||
518 | with self.auth_perms("@read") as client: | ||
519 | taskhash, outhash, unihash = self.create_test_hash(client) | ||
520 | self.assertClientGetHash(client, taskhash, None) | ||
521 | |||
522 | # Report permission alone is insufficient | ||
523 | with self.auth_perms("@report") as client, self.assertRaises(InvokeError): | ||
524 | self.create_test_hash(client) | ||
525 | |||
526 | # Read and report permission actually modify the database | ||
527 | with self.auth_perms("@read", "@report") as client: | ||
528 | taskhash, outhash, unihash = self.create_test_hash(client) | ||
529 | self.assertClientGetHash(client, taskhash, unihash) | ||
530 | |||
531 | def test_auth_no_token_refresh_from_anon_user(self): | ||
532 | self.start_auth_server() | ||
533 | |||
534 | with self.start_client(self.auth_server.address) as client, self.assertRaises(InvokeError): | ||
535 | client.refresh_token() | ||
536 | |||
537 | def assertUserCanAuth(self, user): | ||
538 | with self.start_client(self.auth_server.address) as client: | ||
539 | client.auth(user["username"], user["token"]) | ||
540 | |||
541 | def assertUserCannotAuth(self, user): | ||
542 | with self.start_client(self.auth_server.address) as client, self.assertRaises(InvokeError): | ||
543 | client.auth(user["username"], user["token"]) | ||
544 | |||
545 | def test_auth_self_token_refresh(self): | ||
546 | admin_client = self.start_auth_server() | ||
547 | |||
548 | # Create a new user with no permissions | ||
549 | user = admin_client.new_user("test-user", []) | ||
550 | |||
551 | with self.auth_client(user) as client: | ||
552 | new_user = client.refresh_token() | ||
553 | |||
554 | self.assertEqual(user["username"], new_user["username"]) | ||
555 | self.assertNotEqual(user["token"], new_user["token"]) | ||
556 | self.assertUserCanAuth(new_user) | ||
557 | self.assertUserCannotAuth(user) | ||
558 | |||
559 | # Explicitly specifying with your own username is fine also | ||
560 | with self.auth_client(new_user) as client: | ||
561 | new_user2 = client.refresh_token(user["username"]) | ||
562 | |||
563 | self.assertEqual(user["username"], new_user2["username"]) | ||
564 | self.assertNotEqual(user["token"], new_user2["token"]) | ||
565 | self.assertUserCanAuth(new_user2) | ||
566 | self.assertUserCannotAuth(new_user) | ||
567 | self.assertUserCannotAuth(user) | ||
568 | |||
569 | def test_auth_token_refresh(self): | ||
570 | admin_client = self.start_auth_server() | ||
571 | |||
572 | user = admin_client.new_user("test-user", []) | ||
573 | |||
574 | with self.auth_perms() as client, self.assertRaises(InvokeError): | ||
575 | client.refresh_token(user["username"]) | ||
576 | |||
577 | with self.auth_perms("@user-admin") as client: | ||
578 | new_user = client.refresh_token(user["username"]) | ||
579 | |||
580 | self.assertEqual(user["username"], new_user["username"]) | ||
581 | self.assertNotEqual(user["token"], new_user["token"]) | ||
582 | self.assertUserCanAuth(new_user) | ||
583 | self.assertUserCannotAuth(user) | ||
584 | |||
585 | def test_auth_self_get_user(self): | ||
586 | admin_client = self.start_auth_server() | ||
587 | |||
588 | user = admin_client.new_user("test-user", []) | ||
589 | user_info = user.copy() | ||
590 | del user_info["token"] | ||
591 | |||
592 | with self.auth_client(user) as client: | ||
593 | info = client.get_user() | ||
594 | self.assertEqual(info, user_info) | ||
595 | |||
596 | # Explicitly asking for your own username is fine also | ||
597 | info = client.get_user(user["username"]) | ||
598 | self.assertEqual(info, user_info) | ||
599 | |||
600 | def test_auth_get_user(self): | ||
601 | admin_client = self.start_auth_server() | ||
602 | |||
603 | user = admin_client.new_user("test-user", []) | ||
604 | user_info = user.copy() | ||
605 | del user_info["token"] | ||
606 | |||
607 | with self.auth_perms() as client, self.assertRaises(InvokeError): | ||
608 | client.get_user(user["username"]) | ||
609 | |||
610 | with self.auth_perms("@user-admin") as client: | ||
611 | info = client.get_user(user["username"]) | ||
612 | self.assertEqual(info, user_info) | ||
613 | |||
614 | info = client.get_user("nonexist-user") | ||
615 | self.assertIsNone(info) | ||
616 | |||
617 | def test_auth_reconnect(self): | ||
618 | admin_client = self.start_auth_server() | ||
619 | |||
620 | user = admin_client.new_user("test-user", []) | ||
621 | user_info = user.copy() | ||
622 | del user_info["token"] | ||
623 | |||
624 | with self.auth_client(user) as client: | ||
625 | info = client.get_user() | ||
626 | self.assertEqual(info, user_info) | ||
627 | |||
628 | client.disconnect() | ||
629 | |||
630 | info = client.get_user() | ||
631 | self.assertEqual(info, user_info) | ||
632 | |||
633 | def test_auth_delete_user(self): | ||
634 | admin_client = self.start_auth_server() | ||
635 | |||
636 | user = admin_client.new_user("test-user", []) | ||
637 | |||
638 | # No self service | ||
639 | with self.auth_client(user) as client, self.assertRaises(InvokeError): | ||
640 | client.delete_user(user["username"]) | ||
641 | |||
642 | with self.auth_perms() as client, self.assertRaises(InvokeError): | ||
643 | client.delete_user(user["username"]) | ||
644 | |||
645 | with self.auth_perms("@user-admin") as client: | ||
646 | client.delete_user(user["username"]) | ||
647 | |||
648 | # User doesn't exist, so even though the permission is correct, it's an | ||
649 | # error | ||
650 | with self.auth_perms("@user-admin") as client, self.assertRaises(InvokeError): | ||
651 | client.delete_user(user["username"]) | ||
652 | |||
653 | def assertUserPerms(self, user, permissions): | ||
654 | with self.auth_client(user) as client: | ||
655 | info = client.get_user() | ||
656 | self.assertEqual(info, { | ||
657 | "username": user["username"], | ||
658 | "permissions": permissions, | ||
659 | }) | ||
660 | |||
661 | def test_auth_set_user_perms(self): | ||
662 | admin_client = self.start_auth_server() | ||
663 | |||
664 | user = admin_client.new_user("test-user", []) | ||
665 | |||
666 | self.assertUserPerms(user, []) | ||
667 | |||
668 | # No self service to change permissions | ||
669 | with self.auth_client(user) as client, self.assertRaises(InvokeError): | ||
670 | client.set_user_perms(user["username"], ["@all"]) | ||
671 | self.assertUserPerms(user, []) | ||
672 | |||
673 | with self.auth_perms() as client, self.assertRaises(InvokeError): | ||
674 | client.set_user_perms(user["username"], ["@all"]) | ||
675 | self.assertUserPerms(user, []) | ||
676 | |||
677 | with self.auth_perms("@user-admin") as client: | ||
678 | client.set_user_perms(user["username"], ["@all"]) | ||
679 | self.assertUserPerms(user, sorted(list(ALL_PERMISSIONS))) | ||
680 | |||
681 | # Bad permissions | ||
682 | with self.auth_perms("@user-admin") as client, self.assertRaises(InvokeError): | ||
683 | client.set_user_perms(user["username"], ["@this-is-not-a-permission"]) | ||
684 | self.assertUserPerms(user, sorted(list(ALL_PERMISSIONS))) | ||
685 | |||
686 | def test_auth_get_all_users(self): | ||
687 | admin_client = self.start_auth_server() | ||
688 | |||
689 | user = admin_client.new_user("test-user", []) | ||
690 | |||
691 | with self.auth_client(user) as client, self.assertRaises(InvokeError): | ||
692 | client.get_all_users() | ||
693 | |||
694 | # Give the test user the correct permission | ||
695 | admin_client.set_user_perms(user["username"], ["@user-admin"]) | ||
696 | |||
697 | with self.auth_client(user) as client: | ||
698 | all_users = client.get_all_users() | ||
699 | |||
700 | # Convert to a dictionary for easier comparison | ||
701 | all_users = {u["username"]: u for u in all_users} | ||
702 | |||
703 | self.assertEqual(all_users, | ||
704 | { | ||
705 | "admin": { | ||
706 | "username": "admin", | ||
707 | "permissions": sorted(list(ALL_PERMISSIONS)), | ||
708 | }, | ||
709 | "test-user": { | ||
710 | "username": "test-user", | ||
711 | "permissions": ["@user-admin"], | ||
712 | } | ||
713 | } | ||
714 | ) | ||
715 | |||
716 | def test_auth_new_user(self): | ||
717 | self.start_auth_server() | ||
718 | |||
719 | permissions = ["@read", "@report", "@db-admin", "@user-admin"] | ||
720 | permissions.sort() | ||
721 | |||
722 | with self.auth_perms() as client, self.assertRaises(InvokeError): | ||
723 | client.new_user("test-user", permissions) | ||
724 | |||
725 | with self.auth_perms("@user-admin") as client: | ||
726 | user = client.new_user("test-user", permissions) | ||
727 | self.assertIn("token", user) | ||
728 | self.assertEqual(user["username"], "test-user") | ||
729 | self.assertEqual(user["permissions"], permissions) | ||
730 | |||
731 | |||
474 | class TestHashEquivalenceUnixServer(HashEquivalenceTestSetup, HashEquivalenceCommonTests, unittest.TestCase): | 732 | class TestHashEquivalenceUnixServer(HashEquivalenceTestSetup, HashEquivalenceCommonTests, unittest.TestCase): |
475 | def get_server_addr(self, server_idx): | 733 | def get_server_addr(self, server_idx): |
476 | return "unix://" + os.path.join(self.temp_dir.name, 'sock%d' % server_idx) | 734 | return "unix://" + os.path.join(self.temp_dir.name, 'sock%d' % server_idx) |