aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElena ``of Valhalla'' Grandi <valhalla@trueelena.org>2024-01-27 11:26:24 +0100
committerElena ``of Valhalla'' Grandi <valhalla@trueelena.org>2024-01-27 11:26:24 +0100
commit1e974b185d2caff288d73356220e1096910f9b96 (patch)
tree9a43f6a7b0280af7306efcc1477ac1647b86c912
parentb330c1df04135bd79ad18bf81aba02f97995324a (diff)
DS and Graph paths are not limited to existing files
-rw-r--r--rrd/migrations/0002_alter_dashboard_groups_read_and_more.py123
-rw-r--r--rrd/models.py68
-rw-r--r--rrd/tests/test_datasources.py8
-rw-r--r--rrd/tests/test_graphs.py9
4 files changed, 189 insertions, 19 deletions
diff --git a/rrd/migrations/0002_alter_dashboard_groups_read_and_more.py b/rrd/migrations/0002_alter_dashboard_groups_read_and_more.py
new file mode 100644
index 0000000..bc1c863
--- /dev/null
+++ b/rrd/migrations/0002_alter_dashboard_groups_read_and_more.py
@@ -0,0 +1,123 @@
+# Generated by Django 4.2.9 on 2024-01-27 10:24
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("auth", "0012_alter_user_first_name_max_length"),
+ ("rrd", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="dashboard",
+ name="groups_read",
+ field=models.ManyToManyField(
+ related_name="%(class)s_read", to="auth.group"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="dashboard",
+ name="groups_write",
+ field=models.ManyToManyField(
+ related_name="%(class)s_write", to="auth.group"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="dashboard",
+ name="users_read",
+ field=models.ManyToManyField(
+ related_name="%(class)s_read", to=settings.AUTH_USER_MODEL
+ ),
+ ),
+ migrations.AlterField(
+ model_name="dashboard",
+ name="users_write",
+ field=models.ManyToManyField(
+ related_name="%(class)s_write", to=settings.AUTH_USER_MODEL
+ ),
+ ),
+ migrations.AlterField(
+ model_name="datasource",
+ name="groups_read",
+ field=models.ManyToManyField(
+ related_name="%(class)s_read", to="auth.group"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="datasource",
+ name="groups_write",
+ field=models.ManyToManyField(
+ related_name="%(class)s_write", to="auth.group"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="datasource",
+ name="path",
+ field=models.CharField(max_length=512),
+ ),
+ migrations.AlterField(
+ model_name="datasource",
+ name="rrd_config",
+ field=models.TextField(
+ default="\nDS:{ds_name}:GAUGE:600:U:U\nRRA:AVERAGE:0.5:1:2016\nRRA:AVERAGE:0.5:12:720\nRRA:MAX:0.5:288:365\nRRA:MIN:0.5:288:365\n"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="datasource",
+ name="users_read",
+ field=models.ManyToManyField(
+ related_name="%(class)s_read", to=settings.AUTH_USER_MODEL
+ ),
+ ),
+ migrations.AlterField(
+ model_name="datasource",
+ name="users_write",
+ field=models.ManyToManyField(
+ related_name="%(class)s_write", to=settings.AUTH_USER_MODEL
+ ),
+ ),
+ migrations.AlterField(
+ model_name="graph",
+ name="groups_read",
+ field=models.ManyToManyField(
+ related_name="%(class)s_read", to="auth.group"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="graph",
+ name="groups_write",
+ field=models.ManyToManyField(
+ related_name="%(class)s_write", to="auth.group"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="graph",
+ name="path",
+ field=models.CharField(max_length=512),
+ ),
+ migrations.AlterField(
+ model_name="graph",
+ name="rrd_config",
+ field=models.TextField(
+ default="\n--imgformat\n PNG\n--width\n 800\n--height\n 200\n--start\n -86400\n--end\n -1\n--vertical-label\n {ds_names[0]}\n--title\n {title}\n--right-axis\n 1:0\n--alt-autoscale\nDEF:{ds_names[0]}={ds_paths[0]}:{ds_names[0]}:AVERAGE\nVDEF:max={ds_names[0]},MAXIMUM\nVDEF:min={ds_names[0]},MINIMUM\nLINE2:{ds_names[0]}#0000FF\nLINE1:max#FF0000\nLINE1:min#FF0000\n"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="graph",
+ name="users_read",
+ field=models.ManyToManyField(
+ related_name="%(class)s_read", to=settings.AUTH_USER_MODEL
+ ),
+ ),
+ migrations.AlterField(
+ model_name="graph",
+ name="users_write",
+ field=models.ManyToManyField(
+ related_name="%(class)s_write", to=settings.AUTH_USER_MODEL
+ ),
+ ),
+ ]
diff --git a/rrd/models.py b/rrd/models.py
index 3cace0c..2dc629a 100644
--- a/rrd/models.py
+++ b/rrd/models.py
@@ -1,5 +1,6 @@
import logging
import os
+import pathlib
import django.contrib.auth.models as amodels
import rrdtool
@@ -9,6 +10,16 @@ from django.db import models
log = logging.getLogger(__name__)
+def _sanitize_path(path):
+ # changing all separators to a _ should result in an ugly filename
+ # that will not cause issues.
+ # an additional _ is added at the beginning to prevent hidden files
+ for sep in os.sep, os.altsep:
+ if sep:
+ path = path.replace(sep, "_")
+ return "_" + path
+
+
class ModelWithPerms(models.Model):
users_read = models.ManyToManyField(
amodels.User,
@@ -35,9 +46,7 @@ class DataSource(ModelWithPerms):
# an mqtt topic can be as long as 65,535 bytes when UTF-8 encoded,
# which is probably too much for a sensible db
topic = models.CharField(max_length=512)
- path = models.FilePathField(
- path=settings.RRD_DB_PATH.as_posix(),
- recursive=True,
+ path = models.CharField(
max_length=512,
)
rrd_config = models.TextField(
@@ -51,26 +60,36 @@ class DataSource(ModelWithPerms):
return self.topic
@property
+ def rrd_path(self):
+ path = os.path.abspath(os.path.join(
+ settings.RRD_DB_PATH, self.path
+ ))
+ base_path = os.path.abspath(settings.RRD_DB_PATH)
+ if pathlib.Path(base_path) not in pathlib.Path(path).parents:
+ return os.path.abspath(os.path.join(
+ settings.RRD_DB_PATH,
+ _sanitize_path(self.path)
+ ))
+ return path
+
+ @property
+ def ds_name(self):
+ return self.topic.split("/")[-1]
+
+ @property
def lastupdate(self):
try:
- last = rrdtool.lastupdate(os.path.join(
- settings.RRD_DB_PATH, self.path
- ))
+ last = rrdtool.lastupdate(self.rrd_path)
except rrdtool.OperationalError as e:
log.warning("Failure reading from ds: %s", e)
return (None, None)
else:
return last["date"], last["ds"][self.ds_name]
- @property
- def ds_name(self):
- return self.topic.split("/")[-1]
-
def update(self, ts, value):
- rrd_path = os.path.join(settings.RRD_DB_PATH, self.path)
- if not os.path.isfile(rrd_path):
+ if not os.path.isfile(self.rrd_path):
rrdtool.create(
- os.path.join(settings.RRD_DB_PATH, self.path),
+ self.rrd_path,
"--no-overwrite",
self.rrd_config.format(
ds_name=self.ds_name
@@ -78,7 +97,7 @@ class DataSource(ModelWithPerms):
)
try:
rrdtool.update(
- os.path.join(settings.RRD_DB_PATH, self.path),
+ self.rrd_path,
str(ts) + ":" + str(value)
)
except ValueError as e:
@@ -91,9 +110,7 @@ class DataSource(ModelWithPerms):
class Graph(ModelWithPerms):
title = models.CharField(max_length=64)
data_sources = models.ManyToManyField(DataSource)
- path = models.FilePathField(
- path=settings.RRD_GRAPH_PATH.as_posix(),
- recursive=True,
+ path = models.CharField(
max_length=512,
)
rrd_config = models.TextField(
@@ -103,14 +120,27 @@ class Graph(ModelWithPerms):
def __str__(self):
return self.title
+ @property
+ def graph_path(self):
+ path = os.path.abspath(os.path.join(
+ settings.RRD_GRAPH_PATH, self.path
+ ))
+ base_path = os.path.abspath(settings.RRD_GRAPH_PATH)
+ if pathlib.Path(base_path) not in pathlib.Path(path).parents:
+ return os.path.abspath(os.path.join(
+ settings.RRD_GRAPH_PATH,
+ _sanitize_path(self.path)
+ ))
+ return path
+
def update(self):
- graph_path = os.path.join(settings.RRD_GRAPH_PATH, self.path)
+ graph_path = self.graph_path
os.makedirs(os.path.dirname(graph_path), exist_ok=True)
rrd_paths = []
rrd_topics = []
rrd_ds_names = []
for ds in self.data_sources.all():
- rrd_paths.append(os.path.join(settings.RRD_DB_PATH, ds.path))
+ rrd_paths.append(ds.rrd_path)
rrd_topics.append(ds.topic)
rrd_ds_names.append(ds.ds_name)
opts = self.rrd_config.format(
diff --git a/rrd/tests/test_datasources.py b/rrd/tests/test_datasources.py
index 4057b14..030d0ae 100644
--- a/rrd/tests/test_datasources.py
+++ b/rrd/tests/test_datasources.py
@@ -50,3 +50,11 @@ class TestDataSource(TestCase):
last = ds.lastupdate
self.assertEqual(last[1], 10)
self.assertEqual(last[0].year, now.year)
+
+ def test_invalid_path(self):
+ ds = models.DataSource.objects.create(
+ topic="evil",
+ path="../../../evil.rrd"
+ )
+ self.assertTrue(ds.rrd_path.endswith("_.._.._.._evil.rrd"))
+ self.assertIn(settings.RRD_DB_PATH.as_posix(), ds.rrd_path)
diff --git a/rrd/tests/test_graphs.py b/rrd/tests/test_graphs.py
index f2a4948..7a4dd13 100644
--- a/rrd/tests/test_graphs.py
+++ b/rrd/tests/test_graphs.py
@@ -37,3 +37,12 @@ class TestGraphs(TestCase):
ds.update(ts, 10)
stat = os.stat(os.path.join(settings.RRD_GRAPH_PATH, "test/test.png"))
self.assertGreaterEqual(stat.st_mtime, now.timestamp())
+
+ def test_invalid_path(self):
+ graph = models.Graph.objects.create(
+ title="Test Graph",
+ path="../../../etc/evil.png",
+ )
+
+ self.assertTrue(graph.graph_path.endswith("_.._.._.._etc_evil.png"))
+ self.assertIn(settings.RRD_GRAPH_PATH.as_posix(), graph.graph_path)