Skip to content

Commit 077146d

Browse files
Birger SchachtBirger Schacht
authored andcommitted
ENH: Rewrite the configuration file handling
In IntelMQ 3 the defaults.conf and the pipeline.conf were dropped, therefore access to those files via the API is not useful anymore. At the same time, the runtime.conf now includes pipeline configuration and is stored as a YAML file. Given those changes, the whole configuration file handling had to be rewritten: There are now 3 API endpoints: * one to read (get) and write (post) the runtime configuration * one to read (get) and write (post) the positions configuration * one to read (get) the harmonization information Everything is still done using JSON objects, the conversion to YAML is done internally. The removal of the save_file method also includes the removal of some sanity checks that ran before the file was saved. Some of those should probably be part of intelmq itself (i.e. checking for allowed characters in bot names).
1 parent 477aa8e commit 077146d

File tree

2 files changed

+40
-78
lines changed

2 files changed

+40
-78
lines changed

intelmq_api/api.py

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import sys
1515
import os
16+
import pathlib
1617
import string
1718
import typing
1819

@@ -23,6 +24,7 @@
2324
import intelmq_api.config
2425
import intelmq_api.session as session
2526

27+
from intelmq.lib import utils # type: ignore
2628

2729
Levels = hug.types.OneOf(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL",
2830
"ALL"])
@@ -73,7 +75,6 @@ def initialize_api(config: intelmq_api.config.Config) -> None:
7375

7476
runner = runctl.RunIntelMQCtl(api_config.intelmq_ctl_cmd)
7577
file_access = files.FileAccess(api_config)
76-
file_access.update_from_runctl(runner.get_paths())
7778

7879
session_file = api_config.session_store
7980
if session_file is not None:
@@ -181,18 +182,6 @@ def config(response, file: str, fetch: bool=False):
181182
return contents
182183

183184

184-
@hug.post("/api/save", parse_body=True,
185-
inputs={"application/x-www-form-urlencoded": hug.input_format.text},
186-
requires=token_authentication, versions=1)
187-
def save(body, file: str):
188-
global file_access
189-
try:
190-
file_access.save_file(file, body)
191-
return "success"
192-
except files.SaveFileException as e:
193-
return str(e)
194-
195-
196185
@hug.post("/api/login", versions=1)
197186
def login(username: str, password: str):
198187
if session_store is not None:
@@ -203,3 +192,41 @@ def login(username: str, password: str):
203192
"username": username,
204193
}
205194
return "Invalid username and/or password"
195+
196+
197+
@hug.get("/api/harmonization", requires=token_authentication, versions=1)
198+
def get_harmonization():
199+
positions = pathlib.Path('/opt/intelmq/etc/harmonization.conf')
200+
paths = runner.get_paths()
201+
if 'CONFIG_DIR' in paths:
202+
positions = pathlib.Path(paths['CONFIG_DIR']) / 'harmonization.conf'
203+
return positions.read_text()
204+
205+
206+
@hug.get("/api/runtime", requires=token_authentication, versions=1)
207+
def get_runtime():
208+
return utils.get_runtime()
209+
210+
211+
@hug.post("/api/runtime", requires=token_authentication, version=1)
212+
def post_runtime(body):
213+
return utils.set_runtime(body)
214+
215+
216+
@hug.get("/api/positions", requires=token_authentication, versions=1)
217+
def get_positions():
218+
positions = pathlib.Path('/opt/intelmq/etc/manager/positions.conf')
219+
paths = runner.get_paths()
220+
if 'CONFIG_DIR' in paths:
221+
positions = pathlib.Path(paths['CONFIG_DIR']) / 'manager/positions.conf'
222+
return positions.read_text()
223+
224+
225+
@hug.post("/api/positions", requires=token_authentication, version=1)
226+
def post_positions(body):
227+
positions = pathlib.Path('/opt/intelmq/etc/manager/positions.conf')
228+
paths = runner.get_paths()
229+
if 'CONFIG_DIR' in paths:
230+
positions = pathlib.Path(paths['CONFIG_DIR']) / 'manager/positions.conf'
231+
positions.write_text(body)
232+
return positions.read_text()

intelmq_api/files.py

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,6 @@
2121
from intelmq_api.config import Config
2222

2323

24-
BOT_CONFIG_CHARS = set(string.ascii_letters + string.digits + string.punctuation
25-
+ " \n\r\t")
26-
27-
28-
class SaveFileException(Exception):
29-
30-
"""Exception thrown for errors/invalid input in save_file"""
31-
32-
3324
def path_starts_with(path: PurePath, prefix: PurePath) -> bool:
3425
"""Return whether the path starts with prefix.
3526
@@ -51,33 +42,8 @@ class FileAccess:
5142
def __init__(self, config: Config):
5243
self.allowed_path = config.allowed_path
5344

54-
self.writable_files = {
55-
'defaults': Path('/opt/intelmq/etc/defaults.conf'),
56-
'pipeline': Path('/opt/intelmq/etc/pipeline.conf'),
57-
'runtime': Path('/opt/intelmq/etc/runtime.conf'),
58-
'positions': Path('/opt/intelmq/etc/manager/positions.conf'),
59-
} # type: Dict[str, Path]
60-
61-
self.readonly_files = {
62-
'harmonization': Path('/opt/intelmq/etc/harmonization.conf'),
63-
} # type: Dict[str, Path]
64-
65-
def update_from_runctl(self, paths: Dict[str, str]):
66-
self.writable_files["defaults"] = Path(paths["DEFAULTS_CONF_FILE"])
67-
self.writable_files["pipeline"] = Path(paths["PIPELINE_CONF_FILE"])
68-
self.writable_files["runtime"] = Path(paths["RUNTIME_CONF_FILE"])
69-
self.writable_files["positions"] = Path(paths["CONFIG_DIR"]
70-
+ "/manager/positions.conf")
71-
self.readonly_files["harmonization"] = Path(paths["HARMONIZATION_CONF_FILE"])
72-
7345
def file_name_allowed(self, filename: str) -> Optional[Tuple[bool, Path]]:
7446
"""Determine wether the API should allow access to a file."""
75-
predefined = self.writable_files.get(filename)
76-
if predefined is None:
77-
predefined = self.readonly_files.get(filename)
78-
if predefined is not None:
79-
return (True, predefined)
80-
8147
resolved = Path(filename).resolve()
8248
if not path_starts_with(resolved, self.allowed_path):
8349
return None
@@ -114,34 +80,3 @@ def load_file_or_directory(self, unvalidated_filename: str, fetch: bool) \
11480
obj = {"size": stat.st_size, "path": str(path.resolve())}
11581
result["files"][path.name] = obj
11682
return (content_type, result)
117-
118-
def save_file(self, unvalidated_filename: str, contents: str) -> None:
119-
target_path = self.writable_files.get(unvalidated_filename)
120-
if target_path is None:
121-
raise SaveFileException("Invalid filename: {!r}"
122-
.format(unvalidated_filename))
123-
124-
try:
125-
parsed = json.loads(contents)
126-
except json.JSONDecodeError as e:
127-
raise SaveFileException("File contents for {!r} are not JSON: {}"
128-
.format(unvalidated_filename, str(e)))
129-
if not isinstance(parsed, dict):
130-
raise SaveFileException("File must contain a JSON object.")
131-
132-
if unvalidated_filename not in ("defaults", "positions"):
133-
for key in parsed.keys():
134-
if key != "__default__" and re.search("[^A-Za-z0-9.-]", key):
135-
raise SaveFileException("Invalid bot ID: {!r}".format(key))
136-
137-
if not set(contents) < BOT_CONFIG_CHARS:
138-
raise SaveFileException("Config has invalid characters");
139-
140-
141-
old_contents = target_path.read_text()
142-
if contents != old_contents:
143-
try:
144-
target_path.write_text(contents, encoding="utf-8")
145-
except IOError:
146-
# TODO: log details of this error
147-
raise SaveFileException("Could not write file.")

0 commit comments

Comments
 (0)