11import argparse
2- from typing import Callable
3-
42import grpc
53import logging
64import os
75import pystemd .daemon
6+ import re
87import secrets
98import yaml
109
10+ from typing import Callable
11+
1112from concurrent import futures
1213from pathlib import Path
1314
15+ from pirogue_admin .package_config import ConfigurationContext
16+
1417from 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
2029WORKING_ROOT_DIR = '/'
2130ADMIN_CONFIG_DIR = '/usr/share/pirogue-admin'
2635
2736class 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
4783class 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