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>
This commit is contained in:
Joshua Watt 2023-11-03 08:26:32 -06:00 committed by Richard Purdie
parent 1af725b2ec
commit 8cfb94c06c
4 changed files with 97 additions and 5 deletions

View File

@ -166,6 +166,7 @@ def main():
parser.add_argument('--log', default='WARNING', help='Set logging level') parser.add_argument('--log', default='WARNING', help='Set logging level')
parser.add_argument('--login', '-l', metavar="USERNAME", help="Authenticate as USERNAME") 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('--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") parser.add_argument('--no-netrc', '-n', action="store_false", dest="netrc", help="Do not use .netrc")
subparsers = parser.add_subparsers() subparsers = parser.add_subparsers()
@ -251,6 +252,8 @@ def main():
if func: if func:
try: try:
with hashserv.create_client(args.address, login, password) as client: with hashserv.create_client(args.address, login, password) as client:
if args.become:
client.become_user(args.become)
return func(args, client) return func(args, client)
except bb.asyncrpc.InvokeError as e: except bb.asyncrpc.InvokeError as e:
print(f"ERROR: {e}") print(f"ERROR: {e}")

View File

@ -18,10 +18,11 @@ class AsyncClient(bb.asyncrpc.AsyncClient):
MODE_GET_STREAM = 1 MODE_GET_STREAM = 1
def __init__(self, username=None, password=None): 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.mode = self.MODE_NORMAL
self.username = username self.username = username
self.password = password self.password = password
self.saved_become_user = None
async def setup_connection(self): async def setup_connection(self):
await super().setup_connection() await super().setup_connection()
@ -29,8 +30,13 @@ class AsyncClient(bb.asyncrpc.AsyncClient):
self.mode = self.MODE_NORMAL self.mode = self.MODE_NORMAL
await self._set_mode(cur_mode) await self._set_mode(cur_mode)
if self.username: if self.username:
# Save off become user temporarily because auth() resets it
become = self.saved_become_user
await self.auth(self.username, self.password) await self.auth(self.username, self.password)
if become:
await self.become_user(become)
async def send_stream(self, msg): async def send_stream(self, msg):
async def proc(): async def proc():
await self.socket.send(msg) 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): async def get_outhash(self, method, outhash, taskhash, with_unihash=True):
await self._set_mode(self.MODE_NORMAL) await self._set_mode(self.MODE_NORMAL)
return await self.invoke( 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): async def get_stats(self):
@ -120,6 +133,7 @@ class AsyncClient(bb.asyncrpc.AsyncClient):
result = await self.invoke({"auth": {"username": username, "token": token}}) result = await self.invoke({"auth": {"username": username, "token": token}})
self.username = username self.username = username
self.password = token self.password = token
self.saved_become_user = None
return result return result
async def refresh_token(self, username=None): async def refresh_token(self, username=None):
@ -128,13 +142,19 @@ class AsyncClient(bb.asyncrpc.AsyncClient):
if username: if username:
m["username"] = username m["username"] = username
result = await self.invoke({"refresh-token": m}) 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"] self.password = result["token"]
return result return result
async def set_user_perms(self, username, permissions): async def set_user_perms(self, username, permissions):
await self._set_mode(self.MODE_NORMAL) 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): async def get_user(self, username=None):
await self._set_mode(self.MODE_NORMAL) await self._set_mode(self.MODE_NORMAL)
@ -149,12 +169,23 @@ class AsyncClient(bb.asyncrpc.AsyncClient):
async def new_user(self, username, permissions): async def new_user(self, username, permissions):
await self._set_mode(self.MODE_NORMAL) 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): async def delete_user(self, username):
await self._set_mode(self.MODE_NORMAL) await self._set_mode(self.MODE_NORMAL)
return await self.invoke({"delete-user": {"username": username}}) 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): class Client(bb.asyncrpc.Client):
def __init__(self, username=None, password=None): def __init__(self, username=None, password=None):
@ -182,6 +213,7 @@ class Client(bb.asyncrpc.Client):
"get_all_users", "get_all_users",
"new_user", "new_user",
"delete_user", "delete_user",
"become_user",
) )
def _get_async_client(self): def _get_async_client(self):

View File

@ -255,6 +255,7 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
"auth": self.handle_auth, "auth": self.handle_auth,
"get-user": self.handle_get_user, "get-user": self.handle_get_user,
"get-all-users": self.handle_get_all_users, "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} 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): class Server(bb.asyncrpc.AsyncServer):
def __init__( def __init__(

View File

@ -728,6 +728,45 @@ class HashEquivalenceCommonTests(object):
self.assertEqual(user["username"], "test-user") self.assertEqual(user["username"], "test-user")
self.assertEqual(user["permissions"], permissions) 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): class TestHashEquivalenceUnixServer(HashEquivalenceTestSetup, HashEquivalenceCommonTests, unittest.TestCase):
def get_server_addr(self, server_idx): def get_server_addr(self, server_idx):