diff options
| -rw-r--r-- | rrd/migrations/0002_alter_dashboard_groups_read_and_more.py | 123 | ||||
| -rw-r--r-- | rrd/models.py | 68 | ||||
| -rw-r--r-- | rrd/tests/test_datasources.py | 8 | ||||
| -rw-r--r-- | rrd/tests/test_graphs.py | 9 | 
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)  | 
