Repository: simphony/simphony-remote

Issue:
Expose demo applications policy options
Developers have the ability to provide a list of "demo" or "default" application names that are available to all users upon login via the `demo_applications` entry in the `remoteappmanager_config.py` configuration file.

However, it would be advantageous to also be able to configure the policy options that are set for these applications, since currently these are not exposed.

I think the most elegant way might be to convert the `demo_applications` list into a dictionary (keyed by app name), allowing users to provide overrides to the default options for each case.

Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/simphony/simphony-remote/pull/612?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony) Report
> Merging [#612](https://codecov.io/gh/simphony/simphony-remote/pull/612?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony) (979bce5) into [master](https://codecov.io/gh/simphony/simphony-remote/commit/510430bdd2b4601557c0465ac2fda87e2271b6f4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony) (510430b) will **increase** coverage by `0.01%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head 979bce5 differs from pull request most recent head bee71bf. Consider uploading reports for the commit bee71bf to get more accurate results

```diff
@@            Coverage Diff             @@
##           master     #612      +/-   ##
==========================================
+ Coverage   95.06%   95.07%   +0.01%     
==========================================
  Files          92       92              
  Lines        4396     4409      +13     
  Branches      428      428              
==========================================
+ Hits         4179     4192      +13     
  Misses        155      155              
  Partials       62       62              
```


| [Impacted Files](https://codecov.io/gh/simphony/simphony-remote/pull/612?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony) | Coverage Δ | |
|---|---|---|
| [remoteappmanager/tests/test\_user.py](https://codecov.io/gh/simphony/simphony-remote/pull/612/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony#diff-cmVtb3RlYXBwbWFuYWdlci90ZXN0cy90ZXN0X3VzZXIucHk=) | `100.00% <ø> (ø)` | |
| [remoteappmanager/base\_application.py](https://codecov.io/gh/simphony/simphony-remote/pull/612/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony#diff-cmVtb3RlYXBwbWFuYWdlci9iYXNlX2FwcGxpY2F0aW9uLnB5) | `88.88% <100.00%> (+0.25%)` | :arrow_up: |
| [remoteappmanager/db/orm.py](https://codecov.io/gh/simphony/simphony-remote/pull/612/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony#diff-cmVtb3RlYXBwbWFuYWdlci9kYi9vcm0ucHk=) | `94.28% <100.00%> (+0.05%)` | :arrow_up: |
| [remoteappmanager/file\_config.py](https://codecov.io/gh/simphony/simphony-remote/pull/612/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony#diff-cmVtb3RlYXBwbWFuYWdlci9maWxlX2NvbmZpZy5weQ==) | `96.77% <100.00%> (ø)` | |
| [remoteappmanager/tests/mocking/dummy.py](https://codecov.io/gh/simphony/simphony-remote/pull/612/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony#diff-cmVtb3RlYXBwbWFuYWdlci90ZXN0cy9tb2NraW5nL2R1bW15LnB5) | `91.86% <100.00%> (+0.09%)` | :arrow_up: |
| [remoteappmanager/tests/test\_application.py](https://codecov.io/gh/simphony/simphony-remote/pull/612/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony#diff-cmVtb3RlYXBwbWFuYWdlci90ZXN0cy90ZXN0X2FwcGxpY2F0aW9uLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/simphony/simphony-remote/pull/612?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/simphony/simphony-remote/pull/612?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony). Last update [510430b...bee71bf](https://codecov.io/gh/simphony/simphony-remote/pull/612?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simphony).


Patch:
diff --git a/jupyterhub/remoteappmanager_config.py b/jupyterhub/remoteappmanager_config.py
index 9ca1032..5b1a034 100644
--- a/jupyterhub/remoteappmanager_config.py
+++ b/jupyterhub/remoteappmanager_config.py
@@ -67,7 +67,7 @@
 # # User accounting
 #
 # auto_user_creation = True
-# demo_applications = ['my-demo-app']
+# demo_applications = {'my-demo-app': {'allow_home': True}}
 #
 # # ----------------
 # # Google Analytics
diff --git a/remoteappmanager/base_application.py b/remoteappmanager/base_application.py
index 8930b82..1ef707b 100644
--- a/remoteappmanager/base_application.py
+++ b/remoteappmanager/base_application.py
@@ -20,6 +20,15 @@ from remoteappmanager.services.hub import Hub
 from remoteappmanager.services.reverse_proxy import ReverseProxy
 
 
+DEFAULT_POLICY_OPTIONS = {
+    "app_license": None,
+    "allow_home": False,
+    "allow_view": True,
+    "volume": None,
+    "allow_startup_data": False
+}
+
+
 class BaseApplication(web.Application, LoggingMixin):
     """Base application provides the common infrastructure
     to our tornado applications.
@@ -255,14 +264,13 @@ class BaseApplication(web.Application, LoggingMixin):
         for application in self.db.list_applications():
             if application.image in self.file_config.demo_applications:
                 self.log.debug(f"Available image: {application.image}")
+                options = DEFAULT_POLICY_OPTIONS.copy()
+                options.update(
+                    self.file_config.demo_applications[application.image])
                 self.db.grant_access(
-                    application.image,
-                    user.name,
-                    '',
-                    False,
-                    True,
-                    None,
-                    True
+                    app_name=application.image,
+                    user_name=user.name,
+                    **options
                 )
 
     def _webapi_resources(self):
diff --git a/remoteappmanager/db/orm.py b/remoteappmanager/db/orm.py
index 7e9e8bc..11a2acd 100644
--- a/remoteappmanager/db/orm.py
+++ b/remoteappmanager/db/orm.py
@@ -315,6 +315,8 @@ class ORMDatabase(ABCDatabase):
         allow_common = False
         source = target = mode = None
 
+        app_license = app_license if app_license else None
+
         if volume is not None:
             allow_common = True
             source, target, mode = parse_volume_string(volume)
@@ -382,6 +384,8 @@ class ORMDatabase(ABCDatabase):
         allow_common = False
         source = target = mode = None
 
+        app_license = app_license if app_license else None
+
         if volume is not None:
             allow_common = True
             source, target, mode = parse_volume_string(volume)
diff --git a/remoteappmanager/file_config.py b/remoteappmanager/file_config.py
index 29e4969..37ebdc1 100644
--- a/remoteappmanager/file_config.py
+++ b/remoteappmanager/file_config.py
@@ -2,7 +2,7 @@ import os
 
 import tornado.options
 from docker import tls
-from traitlets import HasTraits, List, Int, Unicode, Bool, Dict
+from traitlets import HasTraits, Int, Unicode, Bool, Dict
 
 from remoteappmanager import paths
 from remoteappmanager.traitlets import set_traits_from_dict
@@ -71,8 +71,12 @@ class FileConfig(HasTraits):
         help="The google analytics tracking id"
     )
 
-    #: Provide names of any default applications granted to users
-    demo_applications = List()
+    #: Provide details of default applications granted to users,
+    #: where keys are app names and values are dictionaries of
+    #: policy option overrides
+    demo_applications = Dict(
+        help="Details of default applications granted to all users."
+    )
 
     #: Whether or not to automatically create user accounts upon starting
     #: up the application if they do not already exist in the database


Test patch:
diff --git a/remoteappmanager/tests/mocking/dummy.py b/remoteappmanager/tests/mocking/dummy.py
index 70b0e61..666a9f2 100644
--- a/remoteappmanager/tests/mocking/dummy.py
+++ b/remoteappmanager/tests/mocking/dummy.py
@@ -132,6 +132,7 @@ class DummyDB(interfaces.ABCDatabase):
                      allow_home, allow_view, volume, allow_startup_data):
         app = self._get_application_id_by_name(app_name)
         user = self._get_user_id_by_name(user_name)
+        app_license = app_license if app_license else None
 
         source, target, mode = volume.split(':')
         policy = DummyDBApplicationPolicy(
diff --git a/remoteappmanager/tests/test_application.py b/remoteappmanager/tests/test_application.py
index 25d19e7..27cc684 100644
--- a/remoteappmanager/tests/test_application.py
+++ b/remoteappmanager/tests/test_application.py
@@ -2,12 +2,13 @@ import os
 from unittest import mock
 from unittest.mock import patch
 
+from tornado import testing
+from traitlets.traitlets import TraitError
+
 from remoteappmanager.application import Application
 from remoteappmanager.db.tests import test_csv_db
 from remoteappmanager.tests import utils
 from remoteappmanager.tests.temp_mixin import TempMixin
-from tornado import testing
-from traitlets.traitlets import TraitError
 
 
 class DummyDB:
@@ -109,7 +110,9 @@ class TestApplication(TempMixin,
             "remoteappmanager.db.orm.ORMDatabase")
         self.file_config.database_kwargs = {
             "url": "sqlite:///"+sqlite_file_path}
-        self.file_config.demo_applications = ['my-demo-app']
+        self.file_config.demo_applications = {
+            'my-demo-app': {'allow_home': True}
+        }
         self.file_config.auto_user_creation = True
 
         app = Application(self.command_line_config,
@@ -120,7 +123,20 @@ class TestApplication(TempMixin,
         self.assertIsNotNone(app.user.account)
 
         user_apps = app.db.get_accounting_for_user(app.user.account)
-        self.assertEqual('my-demo-app', user_apps[0].application.image)
+        app_account = user_apps[0]
+        self.assertEqual(app_account.application.image, 'my-demo-app')
+        self.assertIsNone(
+            app_account.application_policy.app_license)
+        self.assertTrue(
+            app_account.application_policy.allow_home)
+        self.assertTrue(
+            app_account.application_policy.allow_view)
+        self.assertIsNone(
+            app_account.application_policy.volume_source)
+        self.assertIsNone(
+            app_account.application_policy.volume_target)
+        self.assertFalse(
+            app_account.application_policy.allow_startup_data)
 
     def test_start(self):
         with patch(
diff --git a/remoteappmanager/tests/test_user.py b/remoteappmanager/tests/test_user.py
index 1c075c7..f038604 100644
--- a/remoteappmanager/tests/test_user.py
+++ b/remoteappmanager/tests/test_user.py
@@ -7,8 +7,7 @@ class TestUser(TestCase):
 
     def setUp(self):
         self.user = User(name='test-user',
-                         login_service='Basic',
-                         demo_applications=['some-image'])
+                         login_service='Basic')
 
     def test_init(self):
         self.assertEqual('test-user', self.user.name)