summaryrefslogtreecommitdiffstats
path: root/bitbake/lib
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
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')
-rw-r--r--bitbake/lib/hashserv/__init__.py69
-rw-r--r--bitbake/lib/hashserv/client.py62
-rw-r--r--bitbake/lib/hashserv/server.py357
-rw-r--r--bitbake/lib/hashserv/sqlalchemy.py111
-rw-r--r--bitbake/lib/hashserv/sqlite.py105
-rw-r--r--bitbake/lib/hashserv/tests.py276
6 files changed, 935 insertions, 45 deletions
diff --git a/bitbake/lib/hashserv/__init__.py b/bitbake/lib/hashserv/__init__.py
index 9a8ee4e88b..552a33278f 100644
--- a/bitbake/lib/hashserv/__init__.py
+++ b/bitbake/lib/hashserv/__init__.py
@@ -8,6 +8,7 @@ from contextlib import closing
8import re 8import re
9import itertools 9import itertools
10import json 10import json
11from collections import namedtuple
11from urllib.parse import urlparse 12from urllib.parse import urlparse
12 13
13UNIX_PREFIX = "unix://" 14UNIX_PREFIX = "unix://"
@@ -18,6 +19,8 @@ ADDR_TYPE_UNIX = 0
18ADDR_TYPE_TCP = 1 19ADDR_TYPE_TCP = 1
19ADDR_TYPE_WS = 2 20ADDR_TYPE_WS = 2
20 21
22User = namedtuple("User", ("username", "permissions"))
23
21 24
22def parse_address(addr): 25def parse_address(addr):
23 if addr.startswith(UNIX_PREFIX): 26 if addr.startswith(UNIX_PREFIX):
@@ -43,7 +46,10 @@ def create_server(
43 upstream=None, 46 upstream=None,
44 read_only=False, 47 read_only=False,
45 db_username=None, 48 db_username=None,
46 db_password=None 49 db_password=None,
50 anon_perms=None,
51 admin_username=None,
52 admin_password=None,
47): 53):
48 def sqlite_engine(): 54 def sqlite_engine():
49 from .sqlite import DatabaseEngine 55 from .sqlite import DatabaseEngine
@@ -62,7 +68,17 @@ def create_server(
62 else: 68 else:
63 db_engine = sqlite_engine() 69 db_engine = sqlite_engine()
64 70
65 s = server.Server(db_engine, upstream=upstream, read_only=read_only) 71 if anon_perms is None:
72 anon_perms = server.DEFAULT_ANON_PERMS
73
74 s = server.Server(
75 db_engine,
76 upstream=upstream,
77 read_only=read_only,
78 anon_perms=anon_perms,
79 admin_username=admin_username,
80 admin_password=admin_password,
81 )
66 82
67 (typ, a) = parse_address(addr) 83 (typ, a) = parse_address(addr)
68 if typ == ADDR_TYPE_UNIX: 84 if typ == ADDR_TYPE_UNIX:
@@ -76,33 +92,40 @@ def create_server(
76 return s 92 return s
77 93
78 94
79def create_client(addr): 95def create_client(addr, username=None, password=None):
80 from . import client 96 from . import client
81 97
82 c = client.Client() 98 c = client.Client(username, password)
83
84 (typ, a) = parse_address(addr)
85 if typ == ADDR_TYPE_UNIX:
86 c.connect_unix(*a)
87 elif typ == ADDR_TYPE_WS:
88 c.connect_websocket(*a)
89 else:
90 c.connect_tcp(*a)
91 99
92 return c 100 try:
101 (typ, a) = parse_address(addr)
102 if typ == ADDR_TYPE_UNIX:
103 c.connect_unix(*a)
104 elif typ == ADDR_TYPE_WS:
105 c.connect_websocket(*a)
106 else:
107 c.connect_tcp(*a)
108 return c
109 except Exception as e:
110 c.close()
111 raise e
93 112
94 113
95async def create_async_client(addr): 114async def create_async_client(addr, username=None, password=None):
96 from . import client 115 from . import client
97 116
98 c = client.AsyncClient() 117 c = client.AsyncClient(username, password)
99 118
100 (typ, a) = parse_address(addr) 119 try:
101 if typ == ADDR_TYPE_UNIX: 120 (typ, a) = parse_address(addr)
102 await c.connect_unix(*a) 121 if typ == ADDR_TYPE_UNIX:
103 elif typ == ADDR_TYPE_WS: 122 await c.connect_unix(*a)
104 await c.connect_websocket(*a) 123 elif typ == ADDR_TYPE_WS:
105 else: 124 await c.connect_websocket(*a)
106 await c.connect_tcp(*a) 125 else:
126 await c.connect_tcp(*a)
107 127
108 return c 128 return c
129 except Exception as e:
130 await c.close()
131 raise e
diff --git a/bitbake/lib/hashserv/client.py b/bitbake/lib/hashserv/client.py
index 9542d72f6c..82400fe5aa 100644
--- a/bitbake/lib/hashserv/client.py
+++ b/bitbake/lib/hashserv/client.py
@@ -6,6 +6,7 @@
6import logging 6import logging
7import socket 7import socket
8import bb.asyncrpc 8import bb.asyncrpc
9import json
9from . import create_async_client 10from . import create_async_client
10 11
11 12
@@ -16,15 +17,19 @@ class AsyncClient(bb.asyncrpc.AsyncClient):
16 MODE_NORMAL = 0 17 MODE_NORMAL = 0
17 MODE_GET_STREAM = 1 18 MODE_GET_STREAM = 1
18 19
19 def __init__(self): 20 def __init__(self, username=None, password=None):
20 super().__init__('OEHASHEQUIV', '1.1', logger) 21 super().__init__('OEHASHEQUIV', '1.1', logger)
21 self.mode = self.MODE_NORMAL 22 self.mode = self.MODE_NORMAL
23 self.username = username
24 self.password = password
22 25
23 async def setup_connection(self): 26 async def setup_connection(self):
24 await super().setup_connection() 27 await super().setup_connection()
25 cur_mode = self.mode 28 cur_mode = self.mode
26 self.mode = self.MODE_NORMAL 29 self.mode = self.MODE_NORMAL
27 await self._set_mode(cur_mode) 30 await self._set_mode(cur_mode)
31 if self.username:
32 await self.auth(self.username, self.password)
28 33
29 async def send_stream(self, msg): 34 async def send_stream(self, msg):
30 async def proc(): 35 async def proc():
@@ -41,6 +46,7 @@ class AsyncClient(bb.asyncrpc.AsyncClient):
41 if new_mode == self.MODE_NORMAL and self.mode == self.MODE_GET_STREAM: 46 if new_mode == self.MODE_NORMAL and self.mode == self.MODE_GET_STREAM:
42 r = await self._send_wrapper(stream_to_normal) 47 r = await self._send_wrapper(stream_to_normal)
43 if r != "ok": 48 if r != "ok":
49 self.check_invoke_error(r)
44 raise ConnectionError("Unable to transition to normal mode: Bad response from server %r" % r) 50 raise ConnectionError("Unable to transition to normal mode: Bad response from server %r" % r)
45 elif new_mode == self.MODE_GET_STREAM and self.mode == self.MODE_NORMAL: 51 elif new_mode == self.MODE_GET_STREAM and self.mode == self.MODE_NORMAL:
46 r = await self.invoke({"get-stream": None}) 52 r = await self.invoke({"get-stream": None})
@@ -109,9 +115,52 @@ class AsyncClient(bb.asyncrpc.AsyncClient):
109 await self._set_mode(self.MODE_NORMAL) 115 await self._set_mode(self.MODE_NORMAL)
110 return await self.invoke({"clean-unused": {"max_age_seconds": max_age}}) 116 return await self.invoke({"clean-unused": {"max_age_seconds": max_age}})
111 117
118 async def auth(self, username, token):
119 await self._set_mode(self.MODE_NORMAL)
120 result = await self.invoke({"auth": {"username": username, "token": token}})
121 self.username = username
122 self.password = token
123 return result
124
125 async def refresh_token(self, username=None):
126 await self._set_mode(self.MODE_NORMAL)
127 m = {}
128 if username:
129 m["username"] = username
130 result = await self.invoke({"refresh-token": m})
131 if self.username and result["username"] == self.username:
132 self.password = result["token"]
133 return result
134
135 async def set_user_perms(self, username, permissions):
136 await self._set_mode(self.MODE_NORMAL)
137 return await self.invoke({"set-user-perms": {"username": username, "permissions": permissions}})
138
139 async def get_user(self, username=None):
140 await self._set_mode(self.MODE_NORMAL)
141 m = {}
142 if username:
143 m["username"] = username
144 return await self.invoke({"get-user": m})
145
146 async def get_all_users(self):
147 await self._set_mode(self.MODE_NORMAL)
148 return (await self.invoke({"get-all-users": {}}))["users"]
149
150 async def new_user(self, username, permissions):
151 await self._set_mode(self.MODE_NORMAL)
152 return await self.invoke({"new-user": {"username": username, "permissions": permissions}})
153
154 async def delete_user(self, username):
155 await self._set_mode(self.MODE_NORMAL)
156 return await self.invoke({"delete-user": {"username": username}})
157
112 158
113class Client(bb.asyncrpc.Client): 159class Client(bb.asyncrpc.Client):
114 def __init__(self): 160 def __init__(self, username=None, password=None):
161 self.username = username
162 self.password = password
163
115 super().__init__() 164 super().__init__()
116 self._add_methods( 165 self._add_methods(
117 "connect_tcp", 166 "connect_tcp",
@@ -126,7 +175,14 @@ class Client(bb.asyncrpc.Client):
126 "backfill_wait", 175 "backfill_wait",
127 "remove", 176 "remove",
128 "clean_unused", 177 "clean_unused",
178 "auth",
179 "refresh_token",
180 "set_user_perms",
181 "get_user",
182 "get_all_users",
183 "new_user",
184 "delete_user",
129 ) 185 )
130 186
131 def _get_async_client(self): 187 def _get_async_client(self):
132 return AsyncClient() 188 return AsyncClient(self.username, self.password)
diff --git a/bitbake/lib/hashserv/server.py b/bitbake/lib/hashserv/server.py
index c691df7618..f5baa6be78 100644
--- a/bitbake/lib/hashserv/server.py
+++ b/bitbake/lib/hashserv/server.py
@@ -8,13 +8,48 @@ import asyncio
8import logging 8import logging
9import math 9import math
10import time 10import time
11import os
12import base64
13import hashlib
11from . import create_async_client 14from . import create_async_client
12import bb.asyncrpc 15import bb.asyncrpc
13 16
14
15logger = logging.getLogger("hashserv.server") 17logger = logging.getLogger("hashserv.server")
16 18
17 19
20# This permission only exists to match nothing
21NONE_PERM = "@none"
22
23READ_PERM = "@read"
24REPORT_PERM = "@report"
25DB_ADMIN_PERM = "@db-admin"
26USER_ADMIN_PERM = "@user-admin"
27ALL_PERM = "@all"
28
29ALL_PERMISSIONS = {
30 READ_PERM,
31 REPORT_PERM,
32 DB_ADMIN_PERM,
33 USER_ADMIN_PERM,
34 ALL_PERM,
35}
36
37DEFAULT_ANON_PERMS = (
38 READ_PERM,
39 REPORT_PERM,
40 DB_ADMIN_PERM,
41)
42
43TOKEN_ALGORITHM = "sha256"
44
45# 48 bytes of random data will result in 64 characters when base64
46# encoded. This number also ensures that the base64 encoding won't have any
47# trailing '=' characters.
48TOKEN_SIZE = 48
49
50SALT_SIZE = 8
51
52
18class Measurement(object): 53class Measurement(object):
19 def __init__(self, sample): 54 def __init__(self, sample):
20 self.sample = sample 55 self.sample = sample
@@ -108,6 +143,85 @@ class Stats(object):
108 } 143 }
109 144
110 145
146token_refresh_semaphore = asyncio.Lock()
147
148
149async def new_token():
150 # Prevent malicious users from using this API to deduce the entropy
151 # pool on the server and thus be able to guess a token. *All* token
152 # refresh requests lock the same global semaphore and then sleep for a
153 # short time. The effectively rate limits the total number of requests
154 # than can be made across all clients to 10/second, which should be enough
155 # since you have to be an authenticated users to make the request in the
156 # first place
157 async with token_refresh_semaphore:
158 await asyncio.sleep(0.1)
159 raw = os.getrandom(TOKEN_SIZE, os.GRND_NONBLOCK)
160
161 return base64.b64encode(raw, b"._").decode("utf-8")
162
163
164def new_salt():
165 return os.getrandom(SALT_SIZE, os.GRND_NONBLOCK).hex()
166
167
168def hash_token(algo, salt, token):
169 h = hashlib.new(algo)
170 h.update(salt.encode("utf-8"))
171 h.update(token.encode("utf-8"))
172 return ":".join([algo, salt, h.hexdigest()])
173
174
175def permissions(*permissions, allow_anon=True, allow_self_service=False):
176 """
177 Function decorator that can be used to decorate an RPC function call and
178 check that the current users permissions match the require permissions.
179
180 If allow_anon is True, the user will also be allowed to make the RPC call
181 if the anonymous user permissions match the permissions.
182
183 If allow_self_service is True, and the "username" property in the request
184 is the currently logged in user, or not specified, the user will also be
185 allowed to make the request. This allows users to access normal privileged
186 API, as long as they are only modifying their own user properties (e.g.
187 users can be allowed to reset their own token without @user-admin
188 permissions, but not the token for any other user.
189 """
190
191 def wrapper(func):
192 async def wrap(self, request):
193 if allow_self_service and self.user is not None:
194 username = request.get("username", self.user.username)
195 if username == self.user.username:
196 request["username"] = self.user.username
197 return await func(self, request)
198
199 if not self.user_has_permissions(*permissions, allow_anon=allow_anon):
200 if not self.user:
201 username = "Anonymous user"
202 user_perms = self.anon_perms
203 else:
204 username = self.user.username
205 user_perms = self.user.permissions
206
207 self.logger.info(
208 "User %s with permissions %r denied from calling %s. Missing permissions(s) %r",
209 username,
210 ", ".join(user_perms),
211 func.__name__,
212 ", ".join(permissions),
213 )
214 raise bb.asyncrpc.InvokeError(
215 f"{username} is not allowed to access permissions(s) {', '.join(permissions)}"
216 )
217
218 return await func(self, request)
219
220 return wrap
221
222 return wrapper
223
224
111class ServerClient(bb.asyncrpc.AsyncServerConnection): 225class ServerClient(bb.asyncrpc.AsyncServerConnection):
112 def __init__( 226 def __init__(
113 self, 227 self,
@@ -117,6 +231,7 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
117 backfill_queue, 231 backfill_queue,
118 upstream, 232 upstream,
119 read_only, 233 read_only,
234 anon_perms,
120 ): 235 ):
121 super().__init__(socket, "OEHASHEQUIV", logger) 236 super().__init__(socket, "OEHASHEQUIV", logger)
122 self.db_engine = db_engine 237 self.db_engine = db_engine
@@ -125,6 +240,8 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
125 self.backfill_queue = backfill_queue 240 self.backfill_queue = backfill_queue
126 self.upstream = upstream 241 self.upstream = upstream
127 self.read_only = read_only 242 self.read_only = read_only
243 self.user = None
244 self.anon_perms = anon_perms
128 245
129 self.handlers.update( 246 self.handlers.update(
130 { 247 {
@@ -135,6 +252,9 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
135 # Not always read-only, but internally checks if the server is 252 # Not always read-only, but internally checks if the server is
136 # read-only 253 # read-only
137 "report": self.handle_report, 254 "report": self.handle_report,
255 "auth": self.handle_auth,
256 "get-user": self.handle_get_user,
257 "get-all-users": self.handle_get_all_users,
138 } 258 }
139 ) 259 )
140 260
@@ -146,9 +266,36 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
146 "backfill-wait": self.handle_backfill_wait, 266 "backfill-wait": self.handle_backfill_wait,
147 "remove": self.handle_remove, 267 "remove": self.handle_remove,
148 "clean-unused": self.handle_clean_unused, 268 "clean-unused": self.handle_clean_unused,
269 "refresh-token": self.handle_refresh_token,
270 "set-user-perms": self.handle_set_perms,
271 "new-user": self.handle_new_user,
272 "delete-user": self.handle_delete_user,
149 } 273 }
150 ) 274 )
151 275
276 def raise_no_user_error(self, username):
277 raise bb.asyncrpc.InvokeError(f"No user named '{username}' exists")
278
279 def user_has_permissions(self, *permissions, allow_anon=True):
280 permissions = set(permissions)
281 if allow_anon:
282 if ALL_PERM in self.anon_perms:
283 return True
284
285 if not permissions - self.anon_perms:
286 return True
287
288 if self.user is None:
289 return False
290
291 if ALL_PERM in self.user.permissions:
292 return True
293
294 if not permissions - self.user.permissions:
295 return True
296
297 return False
298
152 def validate_proto_version(self): 299 def validate_proto_version(self):
153 return self.proto_version > (1, 0) and self.proto_version <= (1, 1) 300 return self.proto_version > (1, 0) and self.proto_version <= (1, 1)
154 301
@@ -178,6 +325,7 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
178 325
179 raise bb.asyncrpc.ClientError("Unrecognized command %r" % msg) 326 raise bb.asyncrpc.ClientError("Unrecognized command %r" % msg)
180 327
328 @permissions(READ_PERM)
181 async def handle_get(self, request): 329 async def handle_get(self, request):
182 method = request["method"] 330 method = request["method"]
183 taskhash = request["taskhash"] 331 taskhash = request["taskhash"]
@@ -206,6 +354,7 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
206 354
207 return d 355 return d
208 356
357 @permissions(READ_PERM)
209 async def handle_get_outhash(self, request): 358 async def handle_get_outhash(self, request):
210 method = request["method"] 359 method = request["method"]
211 outhash = request["outhash"] 360 outhash = request["outhash"]
@@ -236,6 +385,7 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
236 await self.db.insert_unihash(data["method"], data["taskhash"], data["unihash"]) 385 await self.db.insert_unihash(data["method"], data["taskhash"], data["unihash"])
237 await self.db.insert_outhash(data) 386 await self.db.insert_outhash(data)
238 387
388 @permissions(READ_PERM)
239 async def handle_get_stream(self, request): 389 async def handle_get_stream(self, request):
240 await self.socket.send_message("ok") 390 await self.socket.send_message("ok")
241 391
@@ -304,8 +454,11 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
304 "unihash": unihash, 454 "unihash": unihash,
305 } 455 }
306 456
457 # Since this can be called either read only or to report, the check to
458 # report is made inside the function
459 @permissions(READ_PERM)
307 async def handle_report(self, data): 460 async def handle_report(self, data):
308 if self.read_only: 461 if self.read_only or not self.user_has_permissions(REPORT_PERM):
309 return await self.report_readonly(data) 462 return await self.report_readonly(data)
310 463
311 outhash_data = { 464 outhash_data = {
@@ -358,6 +511,7 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
358 "unihash": unihash, 511 "unihash": unihash,
359 } 512 }
360 513
514 @permissions(READ_PERM, REPORT_PERM)
361 async def handle_equivreport(self, data): 515 async def handle_equivreport(self, data):
362 await self.db.insert_unihash(data["method"], data["taskhash"], data["unihash"]) 516 await self.db.insert_unihash(data["method"], data["taskhash"], data["unihash"])
363 517
@@ -375,11 +529,13 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
375 529
376 return {k: row[k] for k in ("taskhash", "method", "unihash")} 530 return {k: row[k] for k in ("taskhash", "method", "unihash")}
377 531
532 @permissions(READ_PERM)
378 async def handle_get_stats(self, request): 533 async def handle_get_stats(self, request):
379 return { 534 return {
380 "requests": self.request_stats.todict(), 535 "requests": self.request_stats.todict(),
381 } 536 }
382 537
538 @permissions(DB_ADMIN_PERM)
383 async def handle_reset_stats(self, request): 539 async def handle_reset_stats(self, request):
384 d = { 540 d = {
385 "requests": self.request_stats.todict(), 541 "requests": self.request_stats.todict(),
@@ -388,6 +544,7 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
388 self.request_stats.reset() 544 self.request_stats.reset()
389 return d 545 return d
390 546
547 @permissions(READ_PERM)
391 async def handle_backfill_wait(self, request): 548 async def handle_backfill_wait(self, request):
392 d = { 549 d = {
393 "tasks": self.backfill_queue.qsize(), 550 "tasks": self.backfill_queue.qsize(),
@@ -395,6 +552,7 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
395 await self.backfill_queue.join() 552 await self.backfill_queue.join()
396 return d 553 return d
397 554
555 @permissions(DB_ADMIN_PERM)
398 async def handle_remove(self, request): 556 async def handle_remove(self, request):
399 condition = request["where"] 557 condition = request["where"]
400 if not isinstance(condition, dict): 558 if not isinstance(condition, dict):
@@ -402,19 +560,178 @@ class ServerClient(bb.asyncrpc.AsyncServerConnection):
402 560
403 return {"count": await self.db.remove(condition)} 561 return {"count": await self.db.remove(condition)}
404 562
563 @permissions(DB_ADMIN_PERM)
405 async def handle_clean_unused(self, request): 564 async def handle_clean_unused(self, request):
406 max_age = request["max_age_seconds"] 565 max_age = request["max_age_seconds"]
407 oldest = datetime.now() - timedelta(seconds=-max_age) 566 oldest = datetime.now() - timedelta(seconds=-max_age)
408 return {"count": await self.db.clean_unused(oldest)} 567 return {"count": await self.db.clean_unused(oldest)}
409 568
569 # The authentication API is always allowed
570 async def handle_auth(self, request):
571 username = str(request["username"])
572 token = str(request["token"])
573
574 async def fail_auth():
575 nonlocal username
576 # Rate limit bad login attempts
577 await asyncio.sleep(1)
578 raise bb.asyncrpc.InvokeError(f"Unable to authenticate as {username}")
579
580 user, db_token = await self.db.lookup_user_token(username)
581
582 if not user or not db_token:
583 await fail_auth()
584
585 try:
586 algo, salt, _ = db_token.split(":")
587 except ValueError:
588 await fail_auth()
589
590 if hash_token(algo, salt, token) != db_token:
591 await fail_auth()
592
593 self.user = user
594
595 self.logger.info("Authenticated as %s", username)
596
597 return {
598 "result": True,
599 "username": self.user.username,
600 "permissions": sorted(list(self.user.permissions)),
601 }
602
603 @permissions(USER_ADMIN_PERM, allow_self_service=True, allow_anon=False)
604 async def handle_refresh_token(self, request):
605 username = str(request["username"])
606
607 token = await new_token()
608
609 updated = await self.db.set_user_token(
610 username,
611 hash_token(TOKEN_ALGORITHM, new_salt(), token),
612 )
613 if not updated:
614 self.raise_no_user_error(username)
615
616 return {"username": username, "token": token}
617
618 def get_perm_arg(self, arg):
619 if not isinstance(arg, list):
620 raise bb.asyncrpc.InvokeError("Unexpected type for permissions")
621
622 arg = set(arg)
623 try:
624 arg.remove(NONE_PERM)
625 except KeyError:
626 pass
627
628 unknown_perms = arg - ALL_PERMISSIONS
629 if unknown_perms:
630 raise bb.asyncrpc.InvokeError(
631 "Unknown permissions %s" % ", ".join(sorted(list(unknown_perms)))
632 )
633
634 return sorted(list(arg))
635
636 def return_perms(self, permissions):
637 if ALL_PERM in permissions:
638 return sorted(list(ALL_PERMISSIONS))
639 return sorted(list(permissions))
640
641 @permissions(USER_ADMIN_PERM, allow_anon=False)
642 async def handle_set_perms(self, request):
643 username = str(request["username"])
644 permissions = self.get_perm_arg(request["permissions"])
645
646 if not await self.db.set_user_perms(username, permissions):
647 self.raise_no_user_error(username)
648
649 return {
650 "username": username,
651 "permissions": self.return_perms(permissions),
652 }
653
654 @permissions(USER_ADMIN_PERM, allow_self_service=True, allow_anon=False)
655 async def handle_get_user(self, request):
656 username = str(request["username"])
657
658 user = await self.db.lookup_user(username)
659 if user is None:
660 return None
661
662 return {
663 "username": user.username,
664 "permissions": self.return_perms(user.permissions),
665 }
666
667 @permissions(USER_ADMIN_PERM, allow_anon=False)
668 async def handle_get_all_users(self, request):
669 users = await self.db.get_all_users()
670 return {
671 "users": [
672 {
673 "username": u.username,
674 "permissions": self.return_perms(u.permissions),
675 }
676 for u in users
677 ]
678 }
679
680 @permissions(USER_ADMIN_PERM, allow_anon=False)
681 async def handle_new_user(self, request):
682 username = str(request["username"])
683 permissions = self.get_perm_arg(request["permissions"])
684
685 token = await new_token()
686
687 inserted = await self.db.new_user(
688 username,
689 permissions,
690 hash_token(TOKEN_ALGORITHM, new_salt(), token),
691 )
692 if not inserted:
693 raise bb.asyncrpc.InvokeError(f"Cannot create new user '{username}'")
694
695 return {
696 "username": username,
697 "permissions": self.return_perms(permissions),
698 "token": token,
699 }
700
701 @permissions(USER_ADMIN_PERM, allow_anon=False)
702 async def handle_delete_user(self, request):
703 username = str(request["username"])
704
705 if not await self.db.delete_user(username):
706 self.raise_no_user_error(username)
707
708 return {"username": username}
709
410 710
411class Server(bb.asyncrpc.AsyncServer): 711class Server(bb.asyncrpc.AsyncServer):
412 def __init__(self, db_engine, upstream=None, read_only=False): 712 def __init__(
713 self,
714 db_engine,
715 upstream=None,
716 read_only=False,
717 anon_perms=DEFAULT_ANON_PERMS,
718 admin_username=None,
719 admin_password=None,
720 ):
413 if upstream and read_only: 721 if upstream and read_only:
414 raise bb.asyncrpc.ServerError( 722 raise bb.asyncrpc.ServerError(
415 "Read-only hashserv cannot pull from an upstream server" 723 "Read-only hashserv cannot pull from an upstream server"
416 ) 724 )
417 725
726 disallowed_perms = set(anon_perms) - set(
727 [NONE_PERM, READ_PERM, REPORT_PERM, DB_ADMIN_PERM]
728 )
729
730 if disallowed_perms:
731 raise bb.asyncrpc.ServerError(
732 f"Permission(s) {' '.join(disallowed_perms)} are not allowed for anonymous users"
733 )
734
418 super().__init__(logger) 735 super().__init__(logger)
419 736
420 self.request_stats = Stats() 737 self.request_stats = Stats()
@@ -422,6 +739,13 @@ class Server(bb.asyncrpc.AsyncServer):
422 self.upstream = upstream 739 self.upstream = upstream
423 self.read_only = read_only 740 self.read_only = read_only
424 self.backfill_queue = None 741 self.backfill_queue = None
742 self.anon_perms = set(anon_perms)
743 self.admin_username = admin_username
744 self.admin_password = admin_password
745
746 self.logger.info(
747 "Anonymous user permissions are: %s", ", ".join(self.anon_perms)
748 )
425 749
426 def accept_client(self, socket): 750 def accept_client(self, socket):
427 return ServerClient( 751 return ServerClient(
@@ -431,12 +755,34 @@ class Server(bb.asyncrpc.AsyncServer):
431 self.backfill_queue, 755 self.backfill_queue,
432 self.upstream, 756 self.upstream,
433 self.read_only, 757 self.read_only,
758 self.anon_perms,
434 ) 759 )
435 760
761 async def create_admin_user(self):
762 admin_permissions = (ALL_PERM,)
763 async with self.db_engine.connect(self.logger) as db:
764 added = await db.new_user(
765 self.admin_username,
766 admin_permissions,
767 hash_token(TOKEN_ALGORITHM, new_salt(), self.admin_password),
768 )
769 if added:
770 self.logger.info("Created admin user '%s'", self.admin_username)
771 else:
772 await db.set_user_perms(
773 self.admin_username,
774 admin_permissions,
775 )
776 await db.set_user_token(
777 self.admin_username,
778 hash_token(TOKEN_ALGORITHM, new_salt(), self.admin_password),
779 )
780 self.logger.info("Admin user '%s' updated", self.admin_username)
781
436 async def backfill_worker_task(self): 782 async def backfill_worker_task(self):
437 async with await create_async_client( 783 async with await create_async_client(
438 self.upstream 784 self.upstream
439 ) as client, self.db_engine.connect(logger) as db: 785 ) as client, self.db_engine.connect(self.logger) as db:
440 while True: 786 while True:
441 item = await self.backfill_queue.get() 787 item = await self.backfill_queue.get()
442 if item is None: 788 if item is None:
@@ -457,6 +803,9 @@ class Server(bb.asyncrpc.AsyncServer):
457 803
458 self.loop.run_until_complete(self.db_engine.create()) 804 self.loop.run_until_complete(self.db_engine.create())
459 805
806 if self.admin_username:
807 self.loop.run_until_complete(self.create_admin_user())
808
460 return tasks 809 return tasks
461 810
462 async def stop(self): 811 async def stop(self):
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
diff --git a/bitbake/lib/hashserv/sqlite.py b/bitbake/lib/hashserv/sqlite.py
index 6809c53706..414ee8ffb8 100644
--- a/bitbake/lib/hashserv/sqlite.py
+++ b/bitbake/lib/hashserv/sqlite.py
@@ -7,6 +7,7 @@
7import sqlite3 7import sqlite3
8import logging 8import logging
9from contextlib import closing 9from contextlib import closing
10from . import User
10 11
11logger = logging.getLogger("hashserv.sqlite") 12logger = logging.getLogger("hashserv.sqlite")
12 13
@@ -34,6 +35,14 @@ OUTHASH_TABLE_DEFINITION = (
34 35
35OUTHASH_TABLE_COLUMNS = tuple(name for name, _, _ in OUTHASH_TABLE_DEFINITION) 36OUTHASH_TABLE_COLUMNS = tuple(name for name, _, _ in OUTHASH_TABLE_DEFINITION)
36 37
38USERS_TABLE_DEFINITION = (
39 ("username", "TEXT NOT NULL", "UNIQUE"),
40 ("token", "TEXT NOT NULL", ""),
41 ("permissions", "TEXT NOT NULL", ""),
42)
43
44USERS_TABLE_COLUMNS = tuple(name for name, _, _ in USERS_TABLE_DEFINITION)
45
37 46
38def _make_table(cursor, name, definition): 47def _make_table(cursor, name, definition):
39 cursor.execute( 48 cursor.execute(
@@ -53,6 +62,15 @@ def _make_table(cursor, name, definition):
53 ) 62 )
54 63
55 64
65def map_user(row):
66 if row is None:
67 return None
68 return User(
69 username=row["username"],
70 permissions=set(row["permissions"].split()),
71 )
72
73
56class DatabaseEngine(object): 74class DatabaseEngine(object):
57 def __init__(self, dbname, sync): 75 def __init__(self, dbname, sync):
58 self.dbname = dbname 76 self.dbname = dbname
@@ -66,6 +84,7 @@ class DatabaseEngine(object):
66 with closing(db.cursor()) as cursor: 84 with closing(db.cursor()) as cursor:
67 _make_table(cursor, "unihashes_v2", UNIHASH_TABLE_DEFINITION) 85 _make_table(cursor, "unihashes_v2", UNIHASH_TABLE_DEFINITION)
68 _make_table(cursor, "outhashes_v2", OUTHASH_TABLE_DEFINITION) 86 _make_table(cursor, "outhashes_v2", OUTHASH_TABLE_DEFINITION)
87 _make_table(cursor, "users", USERS_TABLE_DEFINITION)
69 88
70 cursor.execute("PRAGMA journal_mode = WAL") 89 cursor.execute("PRAGMA journal_mode = WAL")
71 cursor.execute( 90 cursor.execute(
@@ -227,6 +246,7 @@ class Database(object):
227 "oldest": oldest, 246 "oldest": oldest,
228 }, 247 },
229 ) 248 )
249 self.db.commit()
230 return cursor.rowcount 250 return cursor.rowcount
231 251
232 async def insert_unihash(self, method, taskhash, unihash): 252 async def insert_unihash(self, method, taskhash, unihash):
@@ -257,3 +277,88 @@ class Database(object):
257 cursor.execute(query, data) 277 cursor.execute(query, data)
258 self.db.commit() 278 self.db.commit()
259 return cursor.lastrowid != prevrowid 279 return cursor.lastrowid != prevrowid
280
281 def _get_user(self, username):
282 with closing(self.db.cursor()) as cursor:
283 cursor.execute(
284 """
285 SELECT username, permissions, token FROM users WHERE username=:username
286 """,
287 {
288 "username": username,
289 },
290 )
291 return cursor.fetchone()
292
293 async def lookup_user_token(self, username):
294 row = self._get_user(username)
295 if row is None:
296 return None, None
297 return map_user(row), row["token"]
298
299 async def lookup_user(self, username):
300 return map_user(self._get_user(username))
301
302 async def set_user_token(self, username, token):
303 with closing(self.db.cursor()) as cursor:
304 cursor.execute(
305 """
306 UPDATE users SET token=:token WHERE username=:username
307 """,
308 {
309 "username": username,
310 "token": token,
311 },
312 )
313 self.db.commit()
314 return cursor.rowcount != 0
315
316 async def set_user_perms(self, username, permissions):
317 with closing(self.db.cursor()) as cursor:
318 cursor.execute(
319 """
320 UPDATE users SET permissions=:permissions WHERE username=:username
321 """,
322 {
323 "username": username,
324 "permissions": " ".join(permissions),
325 },
326 )
327 self.db.commit()
328 return cursor.rowcount != 0
329
330 async def get_all_users(self):
331 with closing(self.db.cursor()) as cursor:
332 cursor.execute("SELECT username, permissions FROM users")
333 return [map_user(r) for r in cursor.fetchall()]
334
335 async def new_user(self, username, permissions, token):
336 with closing(self.db.cursor()) as cursor:
337 try:
338 cursor.execute(
339 """
340 INSERT INTO users (username, token, permissions) VALUES (:username, :token, :permissions)
341 """,
342 {
343 "username": username,
344 "token": token,
345 "permissions": " ".join(permissions),
346 },
347 )
348 self.db.commit()
349 return True
350 except sqlite3.IntegrityError:
351 return False
352
353 async def delete_user(self, username):
354 with closing(self.db.cursor()) as cursor:
355 cursor.execute(
356 """
357 DELETE FROM users WHERE username=:username
358 """,
359 {
360 "username": username,
361 },
362 )
363 self.db.commit()
364 return cursor.rowcount != 0
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
8from . import create_server, create_client 8from . import create_server, create_client
9from .server import DEFAULT_ANON_PERMS, ALL_PERMISSIONS
10from bb.asyncrpc import InvokeError
9import hashlib 11import hashlib
10import logging 12import logging
11import multiprocessing 13import 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
88class HashEquivalenceCommonTests(object): 107class 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
474class TestHashEquivalenceUnixServer(HashEquivalenceTestSetup, HashEquivalenceCommonTests, unittest.TestCase): 732class 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)