Repository: spack/spack

Issue:
Installing a package with a new compiler causes directory to be created with the wrong group ownership 
Installing a package with a new compiler causes directory to be created with the wrong group ownership. 

### Steps to reproduce the issue

```
ecptestb@thetalogin1:/lus/theta-fs0/software/spack/spack-dev/opt/spack/cray-cnl8-mic_knl> ll
drwxrwxr-x 6 spack    software 4096 Apr 19 16:47 gcc-8.2.0
ecptestb@thetalogin1:/lus/theta-fs0/software/spack/spack-dev/opt/spack/cray-cnl8-mic_knl> spack install spack-dummy-package %intel@18.0.0.128 
==> mpich@3.1 : has external module in cray-mpich
...
==> Successfully installed spack-dummy-package
  Fetch: 0.25s.  Build: 2.25s.  Total: 2.50s.
[+] /lus/theta-fs0/software/spack/spack-dev/opt/spack/cray-cnl8-mic_knl/intel-18.0.0.128/spack-dummy-package-refactor-yebephikrhqwmc36r2xtrrt7uobwllyx
ecptestb@thetalogin1:/lus/theta-fs0/software/spack/spack-dev/opt/spack/cray-cnl8-mic_knl> ll
drwxrwxr-x 6 spack    software       4096 Apr 19 16:47 gcc-8.2.0
drwxrwxr-x 3 ecptestb **ecp-testbed-01** 4096 Apr 19 20:56 intel-18.0.0.128
ecptestb@thetalogin1:/lus/theta-fs0/software/spack/spack-dev/opt/spack/cray-cnl8-mic_knl> ll intel-18.0.0.128/
total 4
drwxrwsr-x 4 ecptestb software 4096 Apr 19 20:56 spack-dummy-package-refactor-yebephikrhqwmc36r2xtrrt7uobwllyx
```

The package installed and has correct group (software) ownership within the subdirectory, but the parent intel-18.0.0.128 directory is created with the wrong group ownership (user's default group). 

The packages.py file declares the group and permission settings which are handled correctly for the final installation directory, just not the intermediate. 
```
packages:
  all:
    permissions:
      write: group
      group: software
```

### Information on your system

Cray XC-40 theta at ALCF



Discussion:
I'm addressing the same problem with a shared multi-user instance of spack.

I gave a quick look at the code, trying to follow the code.

Starting point is in this snippet, where it checks for the existance of the folder, and in case it triggers the folder creation with `create_install_directory`
https://github.com/spack/spack/blob/d3be42fccaabbdc71c03da0886214b31a29921e5/lib/spack/spack/package.py#L1604-L1618

Then this code snippet follows, where a call to `mkdirp` creates the folder.

https://github.com/spack/spack/blob/d3be42fccaabbdc71c03da0886214b31a29921e5/lib/spack/spack/directory_layout.py#L267-L274

**I think that the problem lies here**: `mkdirp` creates both the destination folder, with the right permissions, and also all the intermediate parent directories that does not exist, without caring about permissions. **So the package folder gets permission set, while all intermediate folders (compiler one, but also architecture) not**.

https://github.com/spack/spack/blob/d3be42fccaabbdc71c03da0886214b31a29921e5/lib/spack/llnl/util/filesystem.py#L417-L438

IMHO `mkdirp` does the right thing and permissions has to be set on the compiler folder by `PackageBase::do_install` or by the `create_install_directory` (that at the moment seems to be used just by packages).

I don't know exactly how deep the package folder can be (e.g. arch_dir/compiler_dir/package_dir, but I think that it is ok to think that permissions set in `packages.yaml` should be set on all folders in the path that are under the prefix `$SPACK_ROOT/opt/spack`

Resolving patch:
diff --git a/lib/spack/llnl/util/filesystem.py b/lib/spack/llnl/util/filesystem.py
--- a/lib/spack/llnl/util/filesystem.py
+++ b/lib/spack/llnl/util/filesystem.py
@@ -423,14 +423,39 @@ def mkdirp(*paths, **kwargs):
     Keyword Aguments:
         mode (permission bits or None, optional): optional permissions to
             set on the created directory -- use OS default if not provided
+        mode_intermediate (permission bits or None, optional):
+            same as mode, but for newly-created intermediate directories
     """
     mode = kwargs.get('mode', None)
+    mode_intermediate = kwargs.get('mode_intermediate', None)
     for path in paths:
         if not os.path.exists(path):
             try:
+                intermediate_folders = []
+                if mode_intermediate is not None:
+                    # detect missing intermediate folders
+                    intermediate_path = os.path.dirname(path)
+
+                    while intermediate_path:
+                        if os.path.exists(intermediate_path):
+                            break
+
+                        intermediate_folders.append(intermediate_path)
+                        intermediate_path = os.path.dirname(intermediate_path)
+
+                # create folders
                 os.makedirs(path)
+
+                # leaf folder permissions
                 if mode is not None:
                     os.chmod(path, mode)
+
+                # for intermediate folders, change mode just for newly created
+                # ones and if mode_intermediate has been specified, otherwise
+                # intermediate folders list is not populated at all and default
+                # OS mode will be used
+                for intermediate_path in reversed(intermediate_folders):
+                    os.chmod(intermediate_path, mode_intermediate)
             except OSError as e:
                 if e.errno != errno.EEXIST or not os.path.isdir(path):
                     raise e
diff --git a/lib/spack/spack/directory_layout.py b/lib/spack/spack/directory_layout.py
--- a/lib/spack/spack/directory_layout.py
+++ b/lib/spack/spack/directory_layout.py
@@ -262,15 +262,26 @@ def create_install_directory(self, spec):
         # Cannot import at top of file
         from spack.package_prefs import get_package_dir_permissions
         from spack.package_prefs import get_package_group
+
+        # Each package folder can have its own specific permissions, while
+        # intermediate folders (arch/compiler) are set with full access to
+        # everyone (0o777) and install_tree root folder is the chokepoint
+        # for restricting global access.
+        # So, whoever has access to the install_tree is allowed to install
+        # packages for same arch/compiler and since no data is stored in
+        # intermediate folders, it does not represent a security threat.
         group = get_package_group(spec)
         perms = get_package_dir_permissions(spec)
-        mkdirp(spec.prefix, mode=perms)
+        perms_intermediate = 0o777
+
+        mkdirp(spec.prefix, mode=perms, mode_intermediate=perms_intermediate)
         if group:
             chgrp(spec.prefix, group)
             # Need to reset the sticky group bit after chgrp
             os.chmod(spec.prefix, perms)
 
         mkdirp(self.metadata_path(spec), mode=perms)
+
         self.write_spec(spec, self.spec_file_path(spec))
 
     def check_installed(self, spec):


Test patch: