Repository: scoutapp/scout_apm_python

Issue:
set /tmp/scout_apm_core directory perms from config
Right now, directory permissions are set with:

https://github.com/scoutapp/scout_apm_python/blob/cab4f2218cfada275a45c355269e61a8945fa581/src/scout_apm/core/core_agent_manager.py#L144

Which obviously results in `700` perms on `/tmp/scout_apm_core/*`.

In my setup, this ends up requiring `root` to clear these files.  If I could set the perms to `770` from a configuration option, it would make it a lot easier to clear these files out.


Discussion:
@rsyring - Can you describe how your app is configured to start? Ideally scout would be launched only when a python app has been launched as the final, unprivileged user.
When we have an application running in production, the app runs under a user account that has very limited privileges and can not write to application source files.  We also then have a user that can deploy new source code changes, restart the app, etc.  But that user is also limited, it's not a root account.

As an example, for project "foo" we would have two users and a shared group:

* `foo`: the OS user devs SSH in with and the user our deployment scripts work with
* `foo-app`: the OS user that the application runs under (including scout's APM code/binary)
* both users above are a part of the `foo` OS group

So, if the application creates files under `/tmp/scout_apm_core` that are owned by `foo-app` with `700` perms, our `foo` user can't manage those files.  That means I need a devops person who has root access to clear out Scout APM binary files, which means I can't automate that workflow.

If I could configure scout to use `770` perms instead, then my `foo` user can clear out those files because it's in the same OS group `foo`.

HTH clarify what my problem is.
@rsyring - my hesitation to making it configurable is accidental incorrect permissions due to a misconfiguration or any potential bug in determining the mode from the config.

I'm wondering if this workflow would be an option for you: We provide a command-line interface `core-agent-manager` when the `scout_apm` package is installed. As part of your deploy process, you can call `core-agent-manager download` and then `chmod -R 770 /tmp/scout_apm_core` (and/or chown, etc) to explicitly configure the permissions. Thoughts?
@dlanderson Thanks for your thoughts on this issue.

I'd leave the default permissions as you have them, so unless an app developer makes an explicit decision to override the perms, it should be the same level of security as what you have currently IMO.  If they do make that decision, aren't the security implications on them?  Presumably, we are talking about app developers who would make this change, so they have a lot of power already in the application.

Regarding misconfiguration, in your config example, use:

```python
# If setting, use octal notation, this gets fed directly to os.mkdir's `mode` parameter.
# app.config['SCOUT_TMP_FILE_PERMS'] = 0o700  # default value shown
```
And it's on the app dev if they manage to mess that up, right?

> I'm wondering if this workflow would be an option for you: We provide a command-line interface core-agent-manager when the scout_apm package is installed. As part of your deploy process, you can call core-agent-manager download and then chmod -R 770 /tmp/scout_apm_core (and/or chown, etc) to explicitly configure the permissions. Thoughts?

I can actually do this.  But I'd have to do it on every deploy, which doesn't seem ideal.  Would that potentially upgrade the binary even without the python package version changing?

I'm happy to have a work around but I'm not sure I share your concern about the config option.  Having a config value for files written by an application is a very common paradigm used by lots of libraries/tools.  E.g.: https://docs.djangoproject.com/en/2.0/ref/settings/#file-upload-permissions
@dlanderson I also want to second what @rsyring Is advocating for here. It's a little frustrating that this tool out of the box does not "just work". Even with `SCOUT_SOCKET_PATH` the non-configurable default permissions seems like a bit of a glaring omission.

Is this issue open for PR's?
Thanks @rsyring and @avelis for your comments.We'll be making the file permissions configurable in an upcoming release.

Do either of you happen to know how the actual python application are being executed as `root`? Usually a web server will start as `root` in order to bind privileged ports, then execute the actual application as a non-privileged user. Just trying to figure out the execution flow in your cases.
@dlanderson thanks for addressing this issue.  To be clear, my applications do not execute as root.  Please see [my comment above](https://github.com/scoutapp/scout_apm_python/issues/72#issuecomment-405689753) for clarity on how our app users are setup.  If you have questions about that setup, just let me know.

Resolving patch:
diff --git a/src/scout_apm/core/config.py b/src/scout_apm/core/config.py
--- a/src/scout_apm/core/config.py
+++ b/src/scout_apm/core/config.py
@@ -6,6 +6,7 @@
 
 from scout_apm.core.git_revision import GitRevision
 from scout_apm.core.platform_detection import PlatformDetection
+from scout_apm.core.util import octal
 
 logger = logging.getLogger(__name__)
 
@@ -58,6 +59,7 @@ def known_keys(self):
             "core_agent_dir",
             "core_agent_download",
             "core_agent_launch",
+            "core_agent_permissions",
             "core_agent_version",
             "disabled_instruments",
             "download_url",
@@ -73,6 +75,17 @@ def known_keys(self):
             "socket_path",
         ]
 
+    def core_agent_permissions(self):
+        try:
+            return octal(self.value("core_agent_permissions"))
+        except ValueError as e:
+            logger.error(
+                "Invalid core_agent_permissions value: %s." " Using default: %s",
+                repr(e),
+                0o700,
+            )
+            return 0o700
+
     @classmethod
     def set(cls, **kwargs):
         """
@@ -195,6 +208,7 @@ def __init__(self):
             "core_agent_dir": "/tmp/scout_apm_core",
             "core_agent_download": True,
             "core_agent_launch": True,
+            "core_agent_permissions": 700,
             "core_agent_version": "v1.1.8",  # can be an exact tag name, or 'latest'
             "disabled_instruments": [],
             "download_url": "https://s3-us-west-1.amazonaws.com/scout-public-downloads/apm_core_agent/release",  # noqa: E501
diff --git a/src/scout_apm/core/core_agent_manager.py b/src/scout_apm/core/core_agent_manager.py
--- a/src/scout_apm/core/core_agent_manager.py
+++ b/src/scout_apm/core/core_agent_manager.py
@@ -146,7 +146,9 @@ def download(self):
 
     def create_core_agent_dir(self):
         try:
-            os.makedirs(self.destination, 0o700)
+            os.makedirs(
+                self.destination, AgentContext.instance.config.core_agent_permissions()
+            )
         except OSError:
             pass
 
diff --git a/src/scout_apm/core/util.py b/src/scout_apm/core/util.py
new file mode 100644
--- /dev/null
+++ b/src/scout_apm/core/util.py
@@ -0,0 +1,8 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+
+# Takes an integer or a string containing an integer and returns
+# the octal value.
+# Raises a ValueError if the value cannot be converted to octal.
+def octal(value):
+    return int("{}".format(value), 8)


Test patch:
diff --git a/tests/unit/core/test_config.py b/tests/unit/core/test_config.py
--- a/tests/unit/core/test_config.py
+++ b/tests/unit/core/test_config.py
@@ -76,6 +76,29 @@ def test_log_config():
         ScoutConfig.reset_all()
 
 
+def test_core_agent_permissions_default():
+    config = ScoutConfig()
+    assert 0o700 == config.core_agent_permissions()
+
+
+def test_core_agent_permissions_custom():
+    ScoutConfig.set(core_agent_permissions=770)
+    config = ScoutConfig()
+    try:
+        assert 0o770 == config.core_agent_permissions()
+    finally:
+        ScoutConfig.reset_all()
+
+
+def test_core_agent_permissions_invalid_uses_default():
+    ScoutConfig.set(core_agent_permissions="THIS IS INVALID")
+    config = ScoutConfig()
+    try:
+        assert 0o700 == config.core_agent_permissions()
+    finally:
+        ScoutConfig.reset_all()
+
+
 def test_null_config_name():
     # For coverage... this is never called elsewhere.
     ScoutConfigNull().name()
diff --git a/tests/unit/core/test_util.py b/tests/unit/core/test_util.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/core/test_util.py
@@ -0,0 +1,18 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from scout_apm.core.util import octal
+
+
+def test_octal_with_valid_integer():
+    assert 0o700 == octal(700)
+
+
+def test_octal_with_valid_string():
+    assert 0o700 == octal("700")
+
+
+def test_octal_raises_valueerror_on_invalid_value():
+    try:
+        octal("THIS IS INVALID")
+    except ValueError:
+        pass