summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/hashserv/sqlalchemy.py
diff options
context:
space:
mode:
authorJoshua Watt <JPEWhacker@gmail.com>2023-11-03 08:26:31 -0600
committerRichard Purdie <richard.purdie@linuxfoundation.org>2023-11-09 17:33:03 +0000
commit1af725b2eca63fa113cedb6d77eb5c5f1de6e2f0 (patch)
treeadf200a0b03b8ee1f1a56c55e2ec657dcc7ed04a /bitbake/lib/hashserv/sqlalchemy.py
parent6e67b000efb89c4e3121fd907a47dc7042c07bed (diff)
downloadpoky-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/sqlalchemy.py')
-rw-r--r--bitbake/lib/hashserv/sqlalchemy.py111
1 files changed, 105 insertions, 6 deletions
diff --git a/bitbake/lib/hashserv/sqlalchemy.py b/bitbake/lib/hashserv/sqlalchemy.py
index 3216621f9d..bfd8a8446e 100644
--- a/bitbake/lib/hashserv/sqlalchemy.py
+++ b/bitbake/lib/hashserv/sqlalchemy.py
@@ -7,6 +7,7 @@
7 7
8import logging 8import logging
9from datetime import datetime 9from datetime import datetime
10from . import User
10 11
11from sqlalchemy.ext.asyncio import create_async_engine 12from sqlalchemy.ext.asyncio import create_async_engine
12from sqlalchemy.pool import NullPool 13from sqlalchemy.pool import NullPool
@@ -25,13 +26,12 @@ from sqlalchemy import (
25 literal, 26 literal,
26 and_, 27 and_,
27 delete, 28 delete,
29 update,
28) 30)
29import sqlalchemy.engine 31import sqlalchemy.engine
30from sqlalchemy.orm import declarative_base 32from sqlalchemy.orm import declarative_base
31from sqlalchemy.exc import IntegrityError 33from sqlalchemy.exc import IntegrityError
32 34
33logger = logging.getLogger("hashserv.sqlalchemy")
34
35Base = declarative_base() 35Base = declarative_base()
36 36
37 37
@@ -68,9 +68,19 @@ class OuthashesV2(Base):
68 ) 68 )
69 69
70 70
71class Users(Base):
72 __tablename__ = "users"
73 id = Column(Integer, primary_key=True, autoincrement=True)
74 username = Column(Text, nullable=False)
75 token = Column(Text, nullable=False)
76 permissions = Column(Text)
77
78 __table_args__ = (UniqueConstraint("username"),)
79
80
71class DatabaseEngine(object): 81class DatabaseEngine(object):
72 def __init__(self, url, username=None, password=None): 82 def __init__(self, url, username=None, password=None):
73 self.logger = logger 83 self.logger = logging.getLogger("hashserv.sqlalchemy")
74 self.url = sqlalchemy.engine.make_url(url) 84 self.url = sqlalchemy.engine.make_url(url)
75 85
76 if username is not None: 86 if username is not None:
@@ -85,7 +95,7 @@ class DatabaseEngine(object):
85 95
86 async with self.engine.begin() as conn: 96 async with self.engine.begin() as conn:
87 # Create tables 97 # Create tables
88 logger.info("Creating tables...") 98 self.logger.info("Creating tables...")
89 await conn.run_sync(Base.metadata.create_all) 99 await conn.run_sync(Base.metadata.create_all)
90 100
91 def connect(self, logger): 101 def connect(self, logger):
@@ -98,6 +108,15 @@ def map_row(row):
98 return dict(**row._mapping) 108 return dict(**row._mapping)
99 109
100 110
111def map_user(row):
112 if row is None:
113 return None
114 return User(
115 username=row.username,
116 permissions=set(row.permissions.split()),
117 )
118
119
101class Database(object): 120class Database(object):
102 def __init__(self, engine, logger): 121 def __init__(self, engine, logger):
103 self.engine = engine 122 self.engine = engine
@@ -278,7 +297,7 @@ class Database(object):
278 await self.db.execute(statement) 297 await self.db.execute(statement)
279 return True 298 return True
280 except IntegrityError: 299 except IntegrityError:
281 logger.debug( 300 self.logger.debug(
282 "%s, %s, %s already in unihash database", method, taskhash, unihash 301 "%s, %s, %s already in unihash database", method, taskhash, unihash
283 ) 302 )
284 return False 303 return False
@@ -298,7 +317,87 @@ class Database(object):
298 await self.db.execute(statement) 317 await self.db.execute(statement)
299 return True 318 return True
300 except IntegrityError: 319 except IntegrityError:
301 logger.debug( 320 self.logger.debug(
302 "%s, %s already in outhash database", data["method"], data["outhash"] 321 "%s, %s already in outhash database", data["method"], data["outhash"]
303 ) 322 )
304 return False 323 return False
324
325 async def _get_user(self, username):
326 statement = select(
327 Users.username,
328 Users.permissions,
329 Users.token,
330 ).where(
331 Users.username == username,
332 )
333 self.logger.debug("%s", statement)
334 async with self.db.begin():
335 result = await self.db.execute(statement)
336 return result.first()
337
338 async def lookup_user_token(self, username):
339 row = await self._get_user(username)
340 if not row:
341 return None, None
342 return map_user(row), row.token
343
344 async def lookup_user(self, username):
345 return map_user(await self._get_user(username))
346
347 async def set_user_token(self, username, token):
348 statement = (
349 update(Users)
350 .where(
351 Users.username == username,
352 )
353 .values(
354 token=token,
355 )
356 )
357 self.logger.debug("%s", statement)
358 async with self.db.begin():
359 result = await self.db.execute(statement)
360 return result.rowcount != 0
361
362 async def set_user_perms(self, username, permissions):
363 statement = (
364 update(Users)
365 .where(Users.username == username)
366 .values(permissions=" ".join(permissions))
367 )
368 self.logger.debug("%s", statement)
369 async with self.db.begin():
370 result = await self.db.execute(statement)
371 return result.rowcount != 0
372
373 async def get_all_users(self):
374 statement = select(
375 Users.username,
376 Users.permissions,
377 )
378 self.logger.debug("%s", statement)
379 async with self.db.begin():
380 result = await self.db.execute(statement)
381 return [map_user(row) for row in result]
382
383 async def new_user(self, username, permissions, token):
384 statement = insert(Users).values(
385 username=username,
386 permissions=" ".join(permissions),
387 token=token,
388 )
389 self.logger.debug("%s", statement)
390 try:
391 async with self.db.begin():
392 await self.db.execute(statement)
393 return True
394 except IntegrityError as e:
395 self.logger.debug("Cannot create new user %s: %s", username, e)
396 return False
397
398 async def delete_user(self, username):
399 statement = delete(Users).where(Users.username == username)
400 self.logger.debug("%s", statement)
401 async with self.db.begin():
402 result = await self.db.execute(statement)
403 return result.rowcount != 0