From a86b26744db9c932e4c7ea787d956e8efc9235e3 Mon Sep 17 00:00:00 2001
From: Piotr Gawron <piotr.gawron@uni.lu>
Date: Thu, 26 Nov 2020 11:56:59 +0100
Subject: [PATCH] passwords are hidden in configuration and API

---
 CHANGELOG                                     |  2 +
 smash/web/api_views/configuration.py          |  7 +++-
 .../web/migrations/0182_auto_20201126_1042.py | 42 +++++++++++++++++++
 smash/web/models/configuration_item.py        |  8 +++-
 smash/web/models/constants.py                 |  9 ++++
 .../api_views/test_configuration_item.py      | 14 ++++++-
 6 files changed, 79 insertions(+), 3 deletions(-)
 create mode 100644 smash/web/migrations/0182_auto_20201126_1042.py

diff --git a/CHANGELOG b/CHANGELOG
index 69dc0ae8..542e4dc8 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -11,6 +11,8 @@ smasch (1.0.0~alpha.1-0) unstable; urgency=low
   * small improvement: all configuration options that are not obligatory are 
     moved to configuration panel (#343)
   * small improvement: 2FA can be configured to be required option (#358)
+  * small improvement: redcap API token is not visible in configuration panel
+    (#359)
   
  -- Piotr Gawron <piotr.gawron@uni.lu>  Tue, 10 Nov 2020 14:00:00 +0200
 
diff --git a/smash/web/api_views/configuration.py b/smash/web/api_views/configuration.py
index 12c420dc..d351af34 100644
--- a/smash/web/api_views/configuration.py
+++ b/smash/web/api_views/configuration.py
@@ -1,6 +1,7 @@
 from django.http import JsonResponse
 
 from web.models import ConfigurationItem
+from web.models.constants import VALUE_TYPE_PASSWORD
 
 
 def configuration_items(request):
@@ -18,10 +19,14 @@ def configuration_items(request):
 
     data = []
     for configuration_item in sliced_items:
+        value = configuration_item.value
+        if configuration_item.value_type == VALUE_TYPE_PASSWORD:
+            value = ''
         data.append({
             "id": configuration_item.id,
             "name": configuration_item.name,
-            "value": configuration_item.value
+            "value": value,
+            "value_type": configuration_item.value_type,
         })
     return JsonResponse({
         "draw": draw,
diff --git a/smash/web/migrations/0182_auto_20201126_1042.py b/smash/web/migrations/0182_auto_20201126_1042.py
new file mode 100644
index 00000000..6260bec1
--- /dev/null
+++ b/smash/web/migrations/0182_auto_20201126_1042.py
@@ -0,0 +1,42 @@
+# Generated by Django 2.0.13 on 2020-11-26 10:42
+
+import django.core.files.storage
+from django.db import migrations, models
+
+from web.models.constants import REDCAP_TOKEN_CONFIGURATION_TYPE, VALUE_TYPE_PASSWORD, NEXMO_API_KEY, NEXMO_API_SECRET
+
+
+def configuration_items(apps, item_type):
+    # We can't import the ConfigurationItem model directly as it may be a newer
+    # version than this migration expects. We use the historical version.
+
+    # noinspection PyPep8Naming
+    ConfigurationItem = apps.get_model("web", "ConfigurationItem")
+    items = ConfigurationItem.objects.filter(
+        type__in=[REDCAP_TOKEN_CONFIGURATION_TYPE, NEXMO_API_KEY, NEXMO_API_SECRET]).all()
+    for item in items:
+        item.value_type = VALUE_TYPE_PASSWORD
+        item.save()
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('web', '0181_worker_privacy_notice_accepted'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='configurationitem',
+            name='value_type',
+            field=models.CharField(choices=[('PASSWORD', 'Password'), ('TEXT', 'Text')], default='TEXT', max_length=32,
+                                   verbose_name='Value type'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='referral_letter',
+            field=models.FileField(blank=True, null=True,
+                                   storage=django.core.files.storage.FileSystemStorage(location='~/tmp/upload'),
+                                   upload_to='referral_letters', verbose_name='Referral letter'),
+        ),
+        migrations.RunPython(configuration_items),
+    ]
diff --git a/smash/web/models/configuration_item.py b/smash/web/models/configuration_item.py
index 9879847c..f873981e 100644
--- a/smash/web/models/configuration_item.py
+++ b/smash/web/models/configuration_item.py
@@ -5,7 +5,8 @@ from django.db import models
 
 from web.models.constants import CANCELLED_APPOINTMENT_COLOR_CONFIGURATION_TYPE, \
     NO_SHOW_APPOINTMENT_COLOR_CONFIGURATION_TYPE, KIT_EMAIL_HOUR_CONFIGURATION_TYPE, \
-    KIT_EMAIL_DAY_OF_WEEK_CONFIGURATION_TYPE, KIT_DAILY_EMAIL_TIME_FORMAT_TYPE, KIT_DAILY_EMAIL_DAYS_PERIOD_TYPE
+    KIT_EMAIL_DAY_OF_WEEK_CONFIGURATION_TYPE, KIT_DAILY_EMAIL_TIME_FORMAT_TYPE, KIT_DAILY_EMAIL_DAYS_PERIOD_TYPE, \
+    VALUE_TYPE_CHOICES, VALUE_TYPE_TEXT
 
 
 class ConfigurationItem(models.Model):
@@ -24,6 +25,11 @@ class ConfigurationItem(models.Model):
     value = models.CharField(max_length=1024,
                              verbose_name='Value',
                              )
+    value_type = models.CharField(max_length=32,
+                                  choices=VALUE_TYPE_CHOICES,
+                                  verbose_name='Value type',
+                                  default=VALUE_TYPE_TEXT
+                                  )
 
     def __str__(self):
         return "%s %s" % (self.name, self.value)
diff --git a/smash/web/models/constants.py b/smash/web/models/constants.py
index 34f44a48..664604bb 100644
--- a/smash/web/models/constants.py
+++ b/smash/web/models/constants.py
@@ -16,6 +16,15 @@ BOOL_CHOICES_WITH_NONE = (
     (False, 'No'),
     (None, 'N/A'),
 )
+
+VALUE_TYPE_PASSWORD = 'PASSWORD'
+VALUE_TYPE_TEXT = 'TEXT'
+VALUE_TYPE_CHOICES = (
+    (VALUE_TYPE_PASSWORD, 'Password'),
+    (VALUE_TYPE_TEXT, 'Text'),
+)
+
+
 SUBJECT_TYPE_CHOICES_CONTROL = 'C'
 SUBJECT_TYPE_CHOICES_PATIENT = 'P'
 SUBJECT_TYPE_CHOICES = {
diff --git a/smash/web/tests/api_views/test_configuration_item.py b/smash/web/tests/api_views/test_configuration_item.py
index 0628c171..a928dd01 100644
--- a/smash/web/tests/api_views/test_configuration_item.py
+++ b/smash/web/tests/api_views/test_configuration_item.py
@@ -4,7 +4,7 @@
 from django.urls import reverse
 
 from web.models import ConfigurationItem
-from web.models.constants import CANCELLED_APPOINTMENT_COLOR_CONFIGURATION_TYPE
+from web.models.constants import CANCELLED_APPOINTMENT_COLOR_CONFIGURATION_TYPE, VALUE_TYPE_PASSWORD
 from web.tests import LoggedInTestCase
 from web.tests.functions import create_configuration_item
 
@@ -15,6 +15,18 @@ class TestConfigurationItemApi(LoggedInTestCase):
         response = self.client.get(reverse('web.api.configuration_items'))
         self.assertEqual(response.status_code, 200)
 
+    def test_password_item(self):
+        item = ConfigurationItem.objects.create()
+        item.type = "TEST"
+        item.value = "secret_password"
+        item.name = "yyy"
+        item.value_type = VALUE_TYPE_PASSWORD
+        item.save()
+        response = self.client.get(reverse('web.api.configuration_items'))
+        self.assertFalse(item.value in response.content.decode("utf-8"))
+        self.assertTrue(item.value_type in response.content.decode("utf-8"))
+        self.assertEqual(response.status_code, 200)
+
     def test_configuration_modify_invalid(self):
         response = self.client.get(reverse('web.api.update_configuration_item'))
         self.assertEqual(response.status_code, 200)
-- 
GitLab