diff options
| author | Joshua Watt <JPEWhacker@gmail.com> | 2023-11-03 08:26:32 -0600 |
|---|---|---|
| committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2023-11-09 17:33:03 +0000 |
| commit | 8cfb94c06cdfe3e6f0ec1ce0154951108bc3df94 (patch) | |
| tree | 046a6d0d98b0b1bfb1467b2d3e4bbc29b181eb9b /bitbake | |
| parent | 1af725b2eca63fa113cedb6d77eb5c5f1de6e2f0 (diff) | |
| download | poky-8cfb94c06cdfe3e6f0ec1ce0154951108bc3df94.tar.gz | |
bitbake: hashserv: Add become-user API
Adds API that allows a user admin to impersonate another user in the
system. This makes it easier to write external services that have
external authentication, since they can use a common user account to
access the server, then impersonate the logged in user.
(Bitbake rev: 71e2f5b52b686f34df364ae1f2fc058f45cd5e18)
Signed-off-by: Joshua Watt <JPEWhacker@gmail.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake')
| -rwxr-xr-x | bitbake/bin/bitbake-hashclient | 3 | ||||
| -rw-r--r-- | bitbake/lib/hashserv/client.py | 42 | ||||
| -rw-r--r-- | bitbake/lib/hashserv/server.py | 18 | ||||
| -rw-r--r-- | bitbake/lib/hashserv/tests.py | 39 |
4 files changed, 97 insertions, 5 deletions
diff --git a/bitbake/bin/bitbake-hashclient b/bitbake/bin/bitbake-hashclient index 328c15cdec..cfbc197e52 100755 --- a/bitbake/bin/bitbake-hashclient +++ b/bitbake/bin/bitbake-hashclient | |||
| @@ -166,6 +166,7 @@ def main(): | |||
| 166 | parser.add_argument('--log', default='WARNING', help='Set logging level') | 166 | parser.add_argument('--log', default='WARNING', help='Set logging level') |
| 167 | parser.add_argument('--login', '-l', metavar="USERNAME", help="Authenticate as USERNAME") | 167 | parser.add_argument('--login', '-l', metavar="USERNAME", help="Authenticate as USERNAME") |
| 168 | parser.add_argument('--password', '-p', metavar="TOKEN", help="Authenticate using token TOKEN") | 168 | parser.add_argument('--password', '-p', metavar="TOKEN", help="Authenticate using token TOKEN") |
| 169 | parser.add_argument('--become', '-b', metavar="USERNAME", help="Impersonate user USERNAME (if allowed) when performing actions") | ||
| 169 | parser.add_argument('--no-netrc', '-n', action="store_false", dest="netrc", help="Do not use .netrc") | 170 | parser.add_argument('--no-netrc', '-n', action="store_false", dest="netrc", help="Do not use .netrc") |
| 170 | 171 | ||
| 171 | subparsers = parser.add_subparsers() | 172 | subparsers = parser.add_subparsers() |
| @@ -251,6 +252,8 @@ def main(): | |||
| 251 | if func: | 252 | if func: |
| 252 | try: | 253 | try: |
| 253 | with hashserv.create_client(args.address, login, password) as client: | 254 | with hashserv.create_client(args.address, login, password) as client: |
| 255 | if args.become: | ||
| 256 | client.become_user(args.become) | ||
| 254 | return func(args, client) | 257 | return func(args, client) |
| 255 | except bb.asyncrpc.InvokeError as e: | 258 | except bb.asyncrpc.InvokeError as e: |
| 256 | print(f"ERROR: {e}") | 259 | print(f"ERROR: {e}") |
diff --git a/bitbake/lib/hashserv/client.py b/bitbake/lib/hashserv/client.py index 82400fe5aa..4457f8e50d 100644 --- a/bitbake/lib/hashserv/client.py +++ b/bitbake/lib/hashserv/client.py | |||
| @@ -18,10 +18,11 @@ class AsyncClient(bb.asyncrpc.AsyncClient): | |||
| 18 | MODE_GET_STREAM = 1 | 18 | MODE_GET_STREAM = 1 |
| 19 | 19 | ||
| 20 | def __init__(self, username=None, password=None): | 20 | def __init__(self, username=None, password=None): |
| 21 | super().__init__('OEHASHEQUIV', '1.1', logger) | 21 | super().__init__("OEHASHEQUIV", "1.1", logger) |
| 22 | self.mode = self.MODE_NORMAL | 22 | self.mode = self.MODE_NORMAL |
| 23 | self.username = username | 23 | self.username = username |
| 24 | self.password = password | 24 | self.password = password |
| 25 | self.saved_become_user = None | ||
| 25 | 26 | ||
| 26 | async def setup_connection(self): | 27 | async def setup_connection(self): |
| 27 | await super().setup_connection() | 28 | await super().setup_connection() |
| @@ -29,8 +30,13 @@ class AsyncClient(bb.asyncrpc.AsyncClient): | |||
| 29 | self.mode = self.MODE_NORMAL | 30 | self.mode = self.MODE_NORMAL |
| 30 | await self._set_mode(cur_mode) | 31 | await self._set_mode(cur_mode) |
| 31 | if self.username: | 32 | if self.username: |
| 33 | # Save off become user temporarily because auth() resets it | ||
| 34 | become = self.saved_become_user | ||
| 32 | await self.auth(self.username, self.password) | 35 | await self.auth(self.username, self.password) |
| 33 | 36 | ||
| 37 | if become: | ||
| 38 | await self.become_user(become) | ||
| 39 | |||
| 34 | async def send_stream(self, msg): | 40 | async def send_stream(self, msg): |
| 35 | async def proc(): | 41 | async def proc(): |
| 36 | await self.socket.send(msg) | 42 | await self.socket.send(msg) |
| @@ -92,7 +98,14 @@ class AsyncClient(bb.asyncrpc.AsyncClient): | |||
| 92 | async def get_outhash(self, method, outhash, taskhash, with_unihash=True): | 98 | async def get_outhash(self, method, outhash, taskhash, with_unihash=True): |
| 93 | await self._set_mode(self.MODE_NORMAL) | 99 | await self._set_mode(self.MODE_NORMAL) |
| 94 | return await self.invoke( | 100 | return await self.invoke( |
| 95 | {"get-outhash": {"outhash": outhash, "taskhash": taskhash, "method": method, "with_unihash": with_unihash}} | 101 | { |
| 102 | "get-outhash": { | ||
| 103 | "outhash": outhash, | ||
| 104 | "taskhash": taskhash, | ||
| 105 | "method": method, | ||
| 106 | "with_unihash": with_unihash, | ||
| 107 | } | ||
| 108 | } | ||
| 96 | ) | 109 | ) |
| 97 | 110 | ||
| 98 | async def get_stats(self): | 111 | async def get_stats(self): |
| @@ -120,6 +133,7 @@ class AsyncClient(bb.asyncrpc.AsyncClient): | |||
| 120 | result = await self.invoke({"auth": {"username": username, "token": token}}) | 133 | result = await self.invoke({"auth": {"username": username, "token": token}}) |
| 121 | self.username = username | 134 | self.username = username |
| 122 | self.password = token | 135 | self.password = token |
| 136 | self.saved_become_user = None | ||
| 123 | return result | 137 | return result |
| 124 | 138 | ||
| 125 | async def refresh_token(self, username=None): | 139 | async def refresh_token(self, username=None): |
| @@ -128,13 +142,19 @@ class AsyncClient(bb.asyncrpc.AsyncClient): | |||
| 128 | if username: | 142 | if username: |
| 129 | m["username"] = username | 143 | m["username"] = username |
| 130 | result = await self.invoke({"refresh-token": m}) | 144 | result = await self.invoke({"refresh-token": m}) |
| 131 | if self.username and result["username"] == self.username: | 145 | if ( |
| 146 | self.username | ||
| 147 | and not self.saved_become_user | ||
| 148 | and result["username"] == self.username | ||
| 149 | ): | ||
| 132 | self.password = result["token"] | 150 | self.password = result["token"] |
| 133 | return result | 151 | return result |
| 134 | 152 | ||
| 135 | async def set_user_perms(self, username, permissions): | 153 | async def set_user_perms(self, username, permissions): |
| 136 | await self._set_mode(self.MODE_NORMAL) | 154 | await self._set_mode(self.MODE_NORMAL) |
| 137 | return await self.invoke({"set-user-perms": {"username": username, "permissions": permissions}}) | 155 | return await self.invoke( |
| 156 | {"set-user-perms": {"username": username, "permissions": permissions}} | ||
| 157 | ) | ||
| 138 | 158 | ||
| 139 | async def get_user(self, username=None): | 159 | async def get_user(self, username=None): |
| 140 | await self._set_mode(self.MODE_NORMAL) | 160 | await self._set_mode(self.MODE_NORMAL) |
| @@ -149,12 +169,23 @@ class AsyncClient(bb.asyncrpc.AsyncClient): | |||
| 149 | 169 | ||
| 150 | async def new_user(self, username, permissions): | 170 | async def new_user(self, username, permissions): |
| 151 | await self._set_mode(self.MODE_NORMAL) | 171 | await self._set_mode(self.MODE_NORMAL) |
| 152 | return await self.invoke({"new-user": {"username": username, "permissions": permissions}}) | 172 | return await self.invoke( |
| 173 | {"new-user": {"username": username, "permissions": permissions}} | ||
| 174 | ) | ||
| 153 | 175 | ||
| 154 | async def delete_user(self, username): | 176 | async def delete_user(self, username): |
| 155 | await self._set_mode(self.MODE_NORMAL) | 177 | await self._set_mode(self.MODE_NORMAL) |
| 156 | return await self.invoke({"delete-user": {"username": username}}) | 178 | return await self.invoke({"delete-user": {"username": username}}) |
| 157 | 179 | ||
| 180 | async def become_user(self, username): | ||
| 181 | await self._set_mode(self.MODE_NORMAL) | ||
| 182 | result = await self.invoke({"become-user": {"username": username}}) | ||
| 183 | if username == self.username: | ||
| 184 | self.saved_become_user = None | ||
| 185 | else: | ||
| 186 | self.saved_become_user = username | ||
| 187 | return result | ||
| 188 | |||
| 158 | 189 | ||
| 159 | class Client(bb.asyncrpc.Client): | 190 | class Client(bb.asyncrpc.Client): |
| 160 | def __init__(self, username=None, password=None): | 191 | def __init__(self, username=None, password=None): |
| @@ -182,6 +213,7 @@ class Client(bb.asyncrpc.Client): | |||
| 182 | "get_all_users", | 213 | "get_all_users", |
| 183 | "new_user", | 214 | "new_user", |
| 184 | "delete_user", | 215 | "delete_user", |
| 216 | "become_user", | ||
| 185 | ) | 217 | ) |
| 186 | 218 | ||
| 187 | def _get_async_client(self): | 219 | def _get_async_client(self): |
diff --git a/bitbake/lib/hashserv/server.py b/bitbake/lib/hashserv/server.py index f5baa6be78..ca419a1abf 100644 --- a/bitbake/lib/hashserv/server.py +++ b/bitbake/lib/hashserv/server.py | |||
| @@ -255,6 +255,7 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection): | |||
| 255 | "auth": self.handle_auth, | 255 | "auth": self.handle_auth, |
| 256 | "get-user": self.handle_get_user, | 256 | "get-user": self.handle_get_user, |
| 257 | "get-all-users": self.handle_get_all_users, | 257 | "get-all-users": self.handle_get_all_users, |
| 258 | "become-user": self.handle_become_user, | ||
| 258 | } | 259 | } |
| 259 | ) | 260 | ) |
| 260 | 261 | ||
| @@ -707,6 +708,23 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection): | |||
| 707 | 708 | ||
| 708 | return {"username": username} | 709 | return {"username": username} |
| 709 | 710 | ||
| 711 | @permissions(USER_ADMIN_PERM, allow_anon=False) | ||
| 712 | async def handle_become_user(self, request): | ||
| 713 | username = str(request["username"]) | ||
| 714 | |||
| 715 | user = await self.db.lookup_user(username) | ||
| 716 | if user is None: | ||
| 717 | raise bb.asyncrpc.InvokeError(f"User {username} doesn't exist") | ||
| 718 | |||
| 719 | self.user = user | ||
| 720 | |||
| 721 | self.logger.info("Became user %s", username) | ||
| 722 | |||
| 723 | return { | ||
| 724 | "username": self.user.username, | ||
| 725 | "permissions": self.return_perms(self.user.permissions), | ||
| 726 | } | ||
| 727 | |||
| 710 | 728 | ||
| 711 | class Server(bb.asyncrpc.AsyncServer): | 729 | class Server(bb.asyncrpc.AsyncServer): |
| 712 | def __init__( | 730 | def __init__( |
diff --git a/bitbake/lib/hashserv/tests.py b/bitbake/lib/hashserv/tests.py index f92f37c459..311b7b7772 100644 --- a/bitbake/lib/hashserv/tests.py +++ b/bitbake/lib/hashserv/tests.py | |||
| @@ -728,6 +728,45 @@ class HashEquivalenceCommonTests(object): | |||
| 728 | self.assertEqual(user["username"], "test-user") | 728 | self.assertEqual(user["username"], "test-user") |
| 729 | self.assertEqual(user["permissions"], permissions) | 729 | self.assertEqual(user["permissions"], permissions) |
| 730 | 730 | ||
| 731 | def test_auth_become_user(self): | ||
| 732 | admin_client = self.start_auth_server() | ||
| 733 | |||
| 734 | user = admin_client.new_user("test-user", ["@read", "@report"]) | ||
| 735 | user_info = user.copy() | ||
| 736 | del user_info["token"] | ||
| 737 | |||
| 738 | with self.auth_perms() as client, self.assertRaises(InvokeError): | ||
| 739 | client.become_user(user["username"]) | ||
| 740 | |||
| 741 | with self.auth_perms("@user-admin") as client: | ||
| 742 | become = client.become_user(user["username"]) | ||
| 743 | self.assertEqual(become, user_info) | ||
| 744 | |||
| 745 | info = client.get_user() | ||
| 746 | self.assertEqual(info, user_info) | ||
| 747 | |||
| 748 | # Verify become user is preserved across disconnect | ||
| 749 | client.disconnect() | ||
| 750 | |||
| 751 | info = client.get_user() | ||
| 752 | self.assertEqual(info, user_info) | ||
| 753 | |||
| 754 | # test-user doesn't have become_user permissions, so this should | ||
| 755 | # not work | ||
| 756 | with self.assertRaises(InvokeError): | ||
| 757 | client.become_user(user["username"]) | ||
| 758 | |||
| 759 | # No self-service of become | ||
| 760 | with self.auth_client(user) as client, self.assertRaises(InvokeError): | ||
| 761 | client.become_user(user["username"]) | ||
| 762 | |||
| 763 | # Give test user permissions to become | ||
| 764 | admin_client.set_user_perms(user["username"], ["@user-admin"]) | ||
| 765 | |||
| 766 | # It's possible to become yourself (effectively a noop) | ||
| 767 | with self.auth_perms("@user-admin") as client: | ||
| 768 | become = client.become_user(client.username) | ||
| 769 | |||
| 731 | 770 | ||
| 732 | class TestHashEquivalenceUnixServer(HashEquivalenceTestSetup, HashEquivalenceCommonTests, unittest.TestCase): | 771 | class TestHashEquivalenceUnixServer(HashEquivalenceTestSetup, HashEquivalenceCommonTests, unittest.TestCase): |
| 733 | def get_server_addr(self, server_idx): | 772 | def get_server_addr(self, server_idx): |
