1111# See the License for the specific language governing permissions and
1212# limitations under the License.
1313"""Verify DN from HTTP header against list of allowed DN's."""
14-
14+ import threading
1515from logging .config import dictConfig
16+ from typing import Callable
1617
1718from flask import Flask , request
1819from pydantic import BaseModel , FilePath
1920from pydantic_settings import BaseSettings
20- from watchdog .events import DirModifiedEvent , FileModifiedEvent , FileSystemEventHandler
21+ from watchdog .events import FileModifiedEvent , FileSystemEvent , FileSystemEventHandler
2122from watchdog .observers import Observer
2223
2324
@@ -29,6 +30,8 @@ class Settings(BaseSettings):
2930
3031 allowed_client_subject_dn_path : FilePath = FilePath ("/config/allowed_client_dn.txt" )
3132 ssl_client_subject_dn_header : str = "ssl-client-subject-dn"
33+ use_watchdog : bool = False
34+ log_level : str = "INFO"
3235
3336
3437class State (BaseModel ):
@@ -59,7 +62,7 @@ def init_app() -> Flask:
5962 }
6063 )
6164 app = Flask (__name__ )
62- app .logger .setLevel ("DEBUG" )
65+ app .logger .setLevel (settings . log_level )
6366
6467 return app
6568
@@ -83,23 +86,66 @@ def validate() -> tuple[str, int]:
8386
8487
8588#
86- # Load DN from file plus watchdog for auto reload .
89+ # File watch based on watchdog.
8790#
8891class FileChangeHandler (FileSystemEventHandler ):
8992 """On filesystem event, call load_allowed_client_dn() when `filepath` is modified."""
9093
91- def __init__ (self , filepath : FilePath ) -> None :
94+ def __init__ (self , filepath : FilePath , callback : Callable [[ FilePath ], None ] ) -> None :
9295 """Set the filepath of the file to watch."""
93- self .filepath = filepath .resolve ()
96+ self .filepath = filepath
97+ self .callback = callback
9498 load_allowed_client_dn (self .filepath )
9599 app .logger .info (f"watch { self .filepath } for changes" )
96100
97- def on_modified (self , event : DirModifiedEvent | FileModifiedEvent ) -> None :
101+ def on_modified (self , event : FileSystemEvent ) -> None :
98102 """Call load_allowed_client_dn() when `filepath` is modified."""
99- if FilePath (str (event .src_path )).resolve () == self .filepath :
100- load_allowed_client_dn (self .filepath )
103+ app .logger .debug (f"on_modified { event } { FilePath (str (event .src_path )).resolve ()} { self .filepath .resolve ()} " )
104+ if FilePath (str (event .src_path )).resolve () == self .filepath .resolve ():
105+ self .callback (self .filepath )
106+
107+
108+ def watchdog_file (filepath : FilePath , callback : Callable [[FilePath ], None ]) -> None :
109+ """Setup watchdog to watch directory that the file resides in and call handler on change."""
110+ observer = Observer ()
111+ observer .schedule (
112+ FileChangeHandler (filepath , callback ),
113+ path = str (filepath .parent ),
114+ recursive = True ,
115+ event_filter = [FileModifiedEvent ],
116+ )
117+ observer .start ()
118+
119+
120+ #
121+ # File watch based on Path.stat().
122+ #
123+ def watch_file (filepath : FilePath , callback : Callable [[FilePath ], None ]) -> None :
124+ """Watch modification time of `filepath` in a thread and call `callback` on change."""
125+
126+ def watch () -> None :
127+ """If modification time of `filepath` changes call `callback`."""
128+ last_modified = 0
129+ app .logger .info (f"watch { filepath } for changes" )
130+ while True :
131+ app .logger .debug (f"check modification time of { filepath } " )
132+ try :
133+ modified = filepath .stat ().st_mtime_ns
134+ except FileNotFoundError as e :
135+ app .logger .error (f"cannot get last modification time of { filepath } : { e !s} " )
136+ else :
137+ if last_modified < modified :
138+ last_modified = modified
139+ callback (filepath )
140+ event .wait (5 )
141+
142+ event = threading .Event ()
143+ threading .Thread (target = watch , daemon = True ).start ()
101144
102145
146+ #
147+ # Load DN from file.
148+ #
103149def load_allowed_client_dn (filepath : FilePath ) -> None :
104150 """Load list of allowed client DN from file."""
105151 try :
@@ -113,11 +159,7 @@ def load_allowed_client_dn(filepath: FilePath) -> None:
113159 app .logger .info (f"load { len (new_allowed_client_subject_dn )} DN from { filepath } " )
114160
115161
116- def watch_file (file_to_watch : FilePath ) -> None :
117- """Setup watchdog to watch directory that the file resides in and call handler on change."""
118- observer = Observer ()
119- observer .schedule (FileChangeHandler (file_to_watch ), path = str (file_to_watch .parent ), recursive = False )
120- observer .start ()
121-
122-
123- watch_file (settings .allowed_client_subject_dn_path )
162+ if settings .use_watchdog :
163+ watchdog_file (settings .allowed_client_subject_dn_path , load_allowed_client_dn )
164+ else :
165+ watch_file (settings .allowed_client_subject_dn_path , load_allowed_client_dn )
0 commit comments