From patchwork Fri Nov 3 14:26:32 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Joshua Watt X-Patchwork-Id: 33583 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 68958C4167B for ; Fri, 3 Nov 2023 14:27:16 +0000 (UTC) Received: from mail-ot1-f47.google.com (mail-ot1-f47.google.com [209.85.210.47]) by mx.groups.io with SMTP id smtpd.web10.53507.1699021636066036396 for ; Fri, 03 Nov 2023 07:27:16 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20230601 header.b=cYnlZ+U9; spf=pass (domain: gmail.com, ip: 209.85.210.47, mailfrom: jpewhacker@gmail.com) Received: by mail-ot1-f47.google.com with SMTP id 46e09a7af769-6ce2ea3a944so1212138a34.1 for ; Fri, 03 Nov 2023 07:27:15 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1699021633; x=1699626433; darn=lists.openembedded.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=iPyxvtrWvHjMi7ljKiJ0wiuGOhdyW5He1ER1022sots=; b=cYnlZ+U9VRUDPB13fS9PSQhAyomJYJIpPwa8OpIIV9FgxGFd2R873tSjkvoSiOgryz lJWxxW2VmLpMlv/2ij+3O3fQqo8CJiAbgCEtGY1f5FDd94i9XykLECSxdf4hkzpghQ8+ ilMiMu8k0uaMaxrJ508J4z/5amyU8QlxRsUj77qD+ui3f4phC37m1invR1mA6YwA7eOg v/SmaYLubqb09MOe9OBNZKM0AASrO5VS4C3KaTntXpHoyNEKuqg8hy0PpMZmHrQGHN9W 1b477VBrUM6Zs6XgtwkNPpFJ4sQQXPXnesG3x4zrxmI4V8UqxWAK1Wge0gHIJ4uhvVsd 9fRA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1699021633; x=1699626433; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=iPyxvtrWvHjMi7ljKiJ0wiuGOhdyW5He1ER1022sots=; b=adPLFVYzrocpGATNRSO30BuiJMdxdvdtP8kmT+BQ91isE5PbImWCXEGkfehjxvCNJZ cgvKjGYcGaZvmw3RPZlhAu4OcEjsM4nSm80HIR65/ooPgyZa0+n7kwJiHb23Y62H608N OWURZq9eurWKdp9C9Equ+3rGmN4UCjZeKyse5Vt/UUhi4AdozV/qEvWoB3Lklpnjpm1R h6cFDNcP5rkcU7yfpGyrpVqCUQs1ppNHfrmbrKNZBv6B9oWx4Uo8CcySqLkurwa9XihL F6mzRsCTN1kIDFh7i7kydMVNU+dxmfo+moUct4OLzcbLtm7gk/KzJw62uwmNW17123gL rzIA== X-Gm-Message-State: AOJu0YyMeHGXcvTHA+wXiXKtqtp+TcGkGDLzzA6vGvJwp/BCbmr5IzyV QQtihYY58PedvhoDtVVCrcEyJPp3qkQ= X-Google-Smtp-Source: AGHT+IG3hVRGTWTPhAF3svx+yUuqYbJtVei/t9wnEQ/N0KtiOJ+ack3iXyvVa2KVbXSxBCTOXH8DtA== X-Received: by 2002:a05:6870:e984:b0:1d5:a58d:1317 with SMTP id r4-20020a056870e98400b001d5a58d1317mr26638553oao.10.1699021633473; Fri, 03 Nov 2023 07:27:13 -0700 (PDT) Received: from localhost.localdomain ([2601:282:4300:19e0::2fe0]) by smtp.gmail.com with ESMTPSA id bb29-20020a056871b21d00b001dcde628a6fsm308272oac.42.2023.11.03.07.27.12 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 03 Nov 2023 07:27:12 -0700 (PDT) From: Joshua Watt X-Google-Original-From: Joshua Watt To: bitbake-devel@lists.openembedded.org Cc: Joshua Watt Subject: [bitbake-devel][PATCH v6 14/22] hashserv: Add become-user API Date: Fri, 3 Nov 2023 08:26:32 -0600 Message-Id: <20231103142640.1936827-15-JPEWhacker@gmail.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20231103142640.1936827-1-JPEWhacker@gmail.com> References: <20231031172138.3577199-1-JPEWhacker@gmail.com> <20231103142640.1936827-1-JPEWhacker@gmail.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from li982-79.members.linode.com [45.33.32.79] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Fri, 03 Nov 2023 14:27:16 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/bitbake-devel/message/15435 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. Signed-off-by: Joshua Watt --- bin/bitbake-hashclient | 3 +++ lib/hashserv/client.py | 42 +++++++++++++++++++++++++++++++++++++----- lib/hashserv/server.py | 18 ++++++++++++++++++ lib/hashserv/tests.py | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/bin/bitbake-hashclient b/bin/bitbake-hashclient index 328c15cd..cfbc197e 100755 --- a/bin/bitbake-hashclient +++ b/bin/bitbake-hashclient @@ -166,6 +166,7 @@ def main(): parser.add_argument('--log', default='WARNING', help='Set logging level') parser.add_argument('--login', '-l', metavar="USERNAME", help="Authenticate as USERNAME") parser.add_argument('--password', '-p', metavar="TOKEN", help="Authenticate using token TOKEN") + parser.add_argument('--become', '-b', metavar="USERNAME", help="Impersonate user USERNAME (if allowed) when performing actions") parser.add_argument('--no-netrc', '-n', action="store_false", dest="netrc", help="Do not use .netrc") subparsers = parser.add_subparsers() @@ -251,6 +252,8 @@ def main(): if func: try: with hashserv.create_client(args.address, login, password) as client: + if args.become: + client.become_user(args.become) return func(args, client) except bb.asyncrpc.InvokeError as e: print(f"ERROR: {e}") diff --git a/lib/hashserv/client.py b/lib/hashserv/client.py index 82400fe5..4457f8e5 100644 --- a/lib/hashserv/client.py +++ b/lib/hashserv/client.py @@ -18,10 +18,11 @@ class AsyncClient(bb.asyncrpc.AsyncClient): MODE_GET_STREAM = 1 def __init__(self, username=None, password=None): - super().__init__('OEHASHEQUIV', '1.1', logger) + super().__init__("OEHASHEQUIV", "1.1", logger) self.mode = self.MODE_NORMAL self.username = username self.password = password + self.saved_become_user = None async def setup_connection(self): await super().setup_connection() @@ -29,8 +30,13 @@ class AsyncClient(bb.asyncrpc.AsyncClient): self.mode = self.MODE_NORMAL await self._set_mode(cur_mode) if self.username: + # Save off become user temporarily because auth() resets it + become = self.saved_become_user await self.auth(self.username, self.password) + if become: + await self.become_user(become) + async def send_stream(self, msg): async def proc(): await self.socket.send(msg) @@ -92,7 +98,14 @@ class AsyncClient(bb.asyncrpc.AsyncClient): async def get_outhash(self, method, outhash, taskhash, with_unihash=True): await self._set_mode(self.MODE_NORMAL) return await self.invoke( - {"get-outhash": {"outhash": outhash, "taskhash": taskhash, "method": method, "with_unihash": with_unihash}} + { + "get-outhash": { + "outhash": outhash, + "taskhash": taskhash, + "method": method, + "with_unihash": with_unihash, + } + } ) async def get_stats(self): @@ -120,6 +133,7 @@ class AsyncClient(bb.asyncrpc.AsyncClient): result = await self.invoke({"auth": {"username": username, "token": token}}) self.username = username self.password = token + self.saved_become_user = None return result async def refresh_token(self, username=None): @@ -128,13 +142,19 @@ class AsyncClient(bb.asyncrpc.AsyncClient): if username: m["username"] = username result = await self.invoke({"refresh-token": m}) - if self.username and result["username"] == self.username: + if ( + self.username + and not self.saved_become_user + and result["username"] == self.username + ): self.password = result["token"] return result async def set_user_perms(self, username, permissions): await self._set_mode(self.MODE_NORMAL) - return await self.invoke({"set-user-perms": {"username": username, "permissions": permissions}}) + return await self.invoke( + {"set-user-perms": {"username": username, "permissions": permissions}} + ) async def get_user(self, username=None): await self._set_mode(self.MODE_NORMAL) @@ -149,12 +169,23 @@ class AsyncClient(bb.asyncrpc.AsyncClient): async def new_user(self, username, permissions): await self._set_mode(self.MODE_NORMAL) - return await self.invoke({"new-user": {"username": username, "permissions": permissions}}) + return await self.invoke( + {"new-user": {"username": username, "permissions": permissions}} + ) async def delete_user(self, username): await self._set_mode(self.MODE_NORMAL) return await self.invoke({"delete-user": {"username": username}}) + async def become_user(self, username): + await self._set_mode(self.MODE_NORMAL) + result = await self.invoke({"become-user": {"username": username}}) + if username == self.username: + self.saved_become_user = None + else: + self.saved_become_user = username + return result + class Client(bb.asyncrpc.Client): def __init__(self, username=None, password=None): @@ -182,6 +213,7 @@ class Client(bb.asyncrpc.Client): "get_all_users", "new_user", "delete_user", + "become_user", ) def _get_async_client(self): diff --git a/lib/hashserv/server.py b/lib/hashserv/server.py index f5baa6be..ca419a1a 100644 --- a/lib/hashserv/server.py +++ b/lib/hashserv/server.py @@ -255,6 +255,7 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection): "auth": self.handle_auth, "get-user": self.handle_get_user, "get-all-users": self.handle_get_all_users, + "become-user": self.handle_become_user, } ) @@ -707,6 +708,23 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection): return {"username": username} + @permissions(USER_ADMIN_PERM, allow_anon=False) + async def handle_become_user(self, request): + username = str(request["username"]) + + user = await self.db.lookup_user(username) + if user is None: + raise bb.asyncrpc.InvokeError(f"User {username} doesn't exist") + + self.user = user + + self.logger.info("Became user %s", username) + + return { + "username": self.user.username, + "permissions": self.return_perms(self.user.permissions), + } + class Server(bb.asyncrpc.AsyncServer): def __init__( diff --git a/lib/hashserv/tests.py b/lib/hashserv/tests.py index f92f37c4..311b7b77 100644 --- a/lib/hashserv/tests.py +++ b/lib/hashserv/tests.py @@ -728,6 +728,45 @@ class HashEquivalenceCommonTests(object): self.assertEqual(user["username"], "test-user") self.assertEqual(user["permissions"], permissions) + def test_auth_become_user(self): + admin_client = self.start_auth_server() + + user = admin_client.new_user("test-user", ["@read", "@report"]) + user_info = user.copy() + del user_info["token"] + + with self.auth_perms() as client, self.assertRaises(InvokeError): + client.become_user(user["username"]) + + with self.auth_perms("@user-admin") as client: + become = client.become_user(user["username"]) + self.assertEqual(become, user_info) + + info = client.get_user() + self.assertEqual(info, user_info) + + # Verify become user is preserved across disconnect + client.disconnect() + + info = client.get_user() + self.assertEqual(info, user_info) + + # test-user doesn't have become_user permissions, so this should + # not work + with self.assertRaises(InvokeError): + client.become_user(user["username"]) + + # No self-service of become + with self.auth_client(user) as client, self.assertRaises(InvokeError): + client.become_user(user["username"]) + + # Give test user permissions to become + admin_client.set_user_perms(user["username"], ["@user-admin"]) + + # It's possible to become yourself (effectively a noop) + with self.auth_perms("@user-admin") as client: + become = client.become_user(client.username) + class TestHashEquivalenceUnixServer(HashEquivalenceTestSetup, HashEquivalenceCommonTests, unittest.TestCase): def get_server_addr(self, server_idx):