Skip to content

Commit 213cd62

Browse files
committed
Add 'access' service to manage user access permissions.
1 parent 5856d1d commit 213cd62

10 files changed

Lines changed: 526 additions & 71 deletions

File tree

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22
__pycache__
33
*.egg-info/
44
*.deb
5-
dist/
5+
dist/
6+
.pybuild/
7+
build/
8+
pirogue-admin/debian/pirogue-admin/
9+
pirogue-admin/debian/.debhelper/

pirogue-admin/debian/changelog

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
pirogue-admin (2.0.11) UNRELEASED; urgency=medium
1+
pirogue-admin (2.0.11) bookworm; urgency=medium
22

3-
* Improved packages info message and status.
4-
* Ugrade pyhton systemd library to pystemd.
3+
* Improved packages info message and status
4+
* Ugrade pyhton systemd library to pystemd
5+
* Adds support of user access token
56

6-
-- Christophe Andral <christophe@andral.fr> Fri, 02 Dec 2025 10:06:55 +0100
7+
-- Christophe Andral <christophe@andral.fr> Thu, 12 Feb 2026 17:24:02 +0100
78

89
pirogue-admin (2.0.10) bookworm; urgency=medium
910

pirogue-admin/debian/control

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Build-Depends:
88
pybuild-plugin-pyproject,
99
python3-all,
1010
python3-grpcio,
11-
python3-pirogue-admin-api (= 2.0.2),
11+
python3-pirogue-admin-api (= 2.0.3),
1212
python3-psutil,
1313
python3-pystemd,
1414
python3-rich,
@@ -24,7 +24,7 @@ Package: pirogue-admin
2424
Architecture: all
2525
Depends:
2626
python3-grpcio,
27-
python3-pirogue-admin-api (= 2.0.2),
27+
python3-pirogue-admin-api (= 2.0.3),
2828
python3-psutil,
2929
python3-pystemd,
3030
python3-requests,

pirogue-admin/pirogue_admin/daemon/__init__.py

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
import argparse
2-
from typing import Callable
3-
42
import grpc
53
import logging
64
import os
75
import pystemd.daemon
6+
import re
87
import secrets
98
import yaml
109

10+
from typing import Callable
11+
1112
from concurrent import futures
1213
from pathlib import Path
1314

15+
from pirogue_admin.package_config import ConfigurationContext
16+
1417
from pirogue_admin_api import (
1518
PIROGUE_ADMIN_AUTH_HEADER, PIROGUE_ADMIN_AUTH_SCHEME,
1619
PIROGUE_ADMIN_TCP_PORT)
17-
from pirogue_admin.package_config import ConfigurationContext
18-
from .servicers_impl import SystemServicerImpl, NetworkServicerImpl, ServicesServicerImpl
20+
21+
from .user_access import UserAccessRegistry
22+
from .servicers_impl import (
23+
SystemServicerImpl,
24+
NetworkServicerImpl,
25+
ServicesServicerImpl,
26+
AccessServicerImpl,
27+
)
1928

2029
WORKING_ROOT_DIR = '/'
2130
ADMIN_CONFIG_DIR = '/usr/share/pirogue-admin'
@@ -26,39 +35,69 @@
2635

2736
class TokenValidationInterceptor(grpc.ServerInterceptor):
2837
_resolve_token = Callable[[], str]
38+
_user_accesses : UserAccessRegistry
2939

30-
def __init__(self, token_resolver: Callable[[], str]):
40+
def __init__(self, token_resolver: Callable[[], str], user_accesses: UserAccessRegistry):
3141
self._resolve_token = token_resolver
42+
self._user_accesses = user_accesses
43+
self._token_expression = re.escape(PIROGUE_ADMIN_AUTH_SCHEME) + r" ([^\s,]+)"
3244

3345
def abort(ignored_request, context):
3446
context.abort(grpc.StatusCode.UNAUTHENTICATED, "Invalid token")
3547

48+
def unauthorized(ignored_request, context):
49+
context.abort(grpc.StatusCode.PERMISSION_DENIED, "Permission denied")
50+
3651
self._abort_handler = grpc.unary_unary_rpc_method_handler(abort)
52+
self._unauthorized_handler = grpc.unary_unary_rpc_method_handler(unauthorized)
3753

3854
def intercept_service(self, continuation, handler_call_details):
39-
token = self._resolve_token()
40-
expected_metadata = (PIROGUE_ADMIN_AUTH_HEADER, "%s %s" % (PIROGUE_ADMIN_AUTH_SCHEME, token))
41-
if expected_metadata in handler_call_details.invocation_metadata:
55+
admin_token = self._resolve_token()
56+
expected_admin_metadata = (PIROGUE_ADMIN_AUTH_HEADER, "%s %s" % (PIROGUE_ADMIN_AUTH_SCHEME, admin_token))
57+
target_method = handler_call_details.method
58+
59+
if expected_admin_metadata in handler_call_details.invocation_metadata:
60+
# If 'admin' token, continue regardless of the service/method called
61+
logger.debug("Calling %s as administrator", target_method)
4262
return continuation(handler_call_details)
43-
else:
63+
64+
# Extract toekn for user access check
65+
metadata = dict(handler_call_details.invocation_metadata)
66+
if PIROGUE_ADMIN_AUTH_HEADER not in metadata:
4467
return self._abort_handler
68+
authoization = metadata.get(PIROGUE_ADMIN_AUTH_HEADER)
69+
auth_match = re.search(self._token_expression, authoization)
70+
if not auth_match:
71+
return self._abort_handler
72+
auth_token = auth_match.group(1)
73+
74+
#
75+
if self._user_accesses.has_access(target_method, auth_token):
76+
# Check if
77+
logger.debug("Calling %s with auth token %s", target_method, auth_token)
78+
return continuation(handler_call_details)
79+
else:
80+
return self._unauthorized_handler
4581

4682

4783
class PiRogueAdminDaemon:
4884
_base_context: ConfigurationContext
4985
_port: int
5086
_token: str
87+
_user_accesses: UserAccessRegistry
5188

5289
def __init__(self, ctx: ConfigurationContext):
5390
self._base_context = ctx
5491

5592
self._load_or_create_configuration()
5693

94+
self._user_accesses = UserAccessRegistry(self._base_context)
95+
5796
self.server = grpc.server(
5897
# Ensures PiRogue administration tasks are done one at a time
5998
futures.ThreadPoolExecutor(max_workers=1), maximum_concurrent_rpcs=1,
6099
# Intercepts each call to authenticate against authorization Token
61-
interceptors=(TokenValidationInterceptor(self.get_current_token),),
100+
interceptors=(TokenValidationInterceptor(self.get_current_token,self._user_accesses),),
62101
# Avoids reusable port. We prefer to warn the daemon caller instead.
63102
options=(('grpc.so_reuseport', 0),)
64103
)
@@ -77,6 +116,15 @@ def __init__(self, ctx: ConfigurationContext):
77116
services_servicer_impl = ServicesServicerImpl(self._base_context)
78117
services_servicer_impl.register_to_server(self.server)
79118

119+
access_server_impl = AccessServicerImpl(
120+
self._base_context,
121+
get_port_func=self.get_current_port,
122+
get_token_func=self.get_current_token,
123+
reset_token_func=self.reset_token,
124+
user_accesses=self._user_accesses,
125+
)
126+
access_server_impl.register_to_server(self.server)
127+
80128
port = self.server.add_insecure_port(f"ip6-localhost:{self._port}")
81129
logger.info("Listening on ip6-localhost:%d", port)
82130
port = self.server.add_insecure_port(f"localhost:{self._port}")
@@ -180,7 +228,7 @@ def serve():
180228

181229
if args.reset_token:
182230
pirogue_admin_daemon.reset_token()
183-
print('PiRogue admin token reset done.')
231+
logger.info('Admin token reset done')
184232
return
185233

186234
# Start serving now

0 commit comments

Comments
 (0)