Repository: docker/docker-py

Issue:
Incorrectly checked permissions during tar archive creation
Hey everyone,

I recently encountered the following problem with docker-py version 3.0.0 When creating a Docker container with additional files that have permission unix file permission 0o400 (owner readable only) the files are (from my point of view) incorrectly classified as not accessible.

System information:

```
tim@mbp ~/C/d/docker_archive pip freeze | grep docker && python --version && docker version
docker==3.0.0
docker-pycreds==0.2.1
Python 3.6.4
Client:
 Version:       17.12.0-ce
 API version:   1.35
 Go version:    go1.9.2
 Git commit:    c97c6d6
 Built: Wed Dec 27 20:03:51 2017
 OS/Arch:       darwin/amd64

Server:
 Engine:
  Version:      17.12.0-ce
  API version:  1.35 (minimum version 1.12)
  Go version:   go1.9.2
  Git commit:   c97c6d6
  Built:        Wed Dec 27 20:12:29 2017
  OS/Arch:      linux/amd64
  Experimental: true

tim@mbp ~/C/d/docker_archive system_profiler SPSoftwareDataType
      System Version: macOS 10.13.3 (17D47)
      Kernel Version: Darwin 17.4.0
```

I made a minimal reproducible case like so:

```
import os
from pathlib import Path

from docker import APIClient

CWD = Path(os.getcwd())
TEST_FILE = 'testfile'

open(TEST_FILE, 'a').close()
Path(TEST_FILE).chmod(0o400)

DOCKER_FILE = CWD / 'Dockerfile'

F = open(DOCKER_FILE, 'w')
F.write('FROM alpine:latest')
F.close()

CLIENT = APIClient(base_url='unix://var/run/docker.sock')
CLIENT.build(path=str(CWD), dockerfile=str(DOCKER_FILE))
```

This results in the following error for docker-py 3.0.0 but not for 2.7.0

```
tim@mbp ~/C/d/docker_archive python3 test_dockerpy_tar.py
Traceback (most recent call last):
  File "test_dockerpy_tar.py", line 20, in <module>
    CLIENT.build(path=str(CWD), dockerfile=str(DOCKER_FILE))
  File "/Users/tim/Code/dockerpy_bug/dockerpy_env/lib/python3.6/site-packages/docker/api/build.py", line 149, in build
    path, exclude=exclude, dockerfile=dockerfile, gzip=gzip
  File "/Users/tim/Code/dockerpy_bug/dockerpy_env/lib/python3.6/site-packages/docker/utils/build.py", line 14, in tar
    root=root, fileobj=fileobj, gzip=gzip
  File "/Users/tim/Code/dockerpy_bug/dockerpy_env/lib/python3.6/site-packages/docker/utils/utils.py", line 102, in create_archive
    'Can not access file in context: {}'.format(full_path)
OSError: Can not access file in context: /Users/tim/Code/dockerpy_bug/docker_archive/testfile
tim@mbp ~/C/d/docker_archive ls -l
total 8
-rw-r--r-- 1 tim staff 18 Feb  4 00:24 Dockerfile
-rw-r--r-- 1 tim staff 421 Feb  4 00:24 test_dockerpy_tar.py
-r-------- 1 tim staff 0 Feb  4 00:24 testfile
tim@mbp ~/C/d/docker_archive
```

I suspect the problem was introduced with PR [#1880](https://github.com/docker/docker-py/pull/1880/files) when `not os.access(full_path, os.R_OK)` was replaced with `os.lstat(full_path).st_mode & os.R_OK == 0`. After taking a closer look it seems that the statements are not equivalent. `os.R_OK` is used to describe the access rights of a single user. It is intended to be used with `os.access`. The equivalent for comparing the whole access bit vector given by `os.lstat(...).st_mode` would be `stat.S_IRUSR`.

Please confirm whether this is a bug.


Discussion:
thank you for the report!

Resolving patch:
diff --git a/docker/utils/utils.py b/docker/utils/utils.py
--- a/docker/utils/utils.py
+++ b/docker/utils/utils.py
@@ -97,10 +97,6 @@ def create_archive(root, files=None, fileobj=None, gzip=False):
     for path in files:
         full_path = os.path.join(root, path)
 
-        if os.lstat(full_path).st_mode & os.R_OK == 0:
-            raise IOError(
-                'Can not access file in context: {}'.format(full_path)
-            )
         i = t.gettarinfo(full_path, arcname=path)
         if i is None:
             # This happens when we encounter a socket file. We can safely
@@ -117,7 +113,9 @@ def create_archive(root, files=None, fileobj=None, gzip=False):
                 with open(full_path, 'rb') as f:
                     t.addfile(i, f)
             except IOError:
-                t.addfile(i, None)
+                raise IOError(
+                    'Can not read file in context: {}'.format(full_path)
+                )
         else:
             # Directories, FIFOs, symlinks... don't need to be read.
             t.addfile(i, None)


Test patch:
diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py
--- a/tests/unit/utils_test.py
+++ b/tests/unit/utils_test.py
@@ -933,7 +933,10 @@ def test_tar_with_empty_directory(self):
             tar_data = tarfile.open(fileobj=archive)
             assert sorted(tar_data.getnames()) == ['bar', 'foo']
 
-    @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No chmod on Windows')
+    @pytest.mark.skipif(
+        IS_WINDOWS_PLATFORM or os.geteuid() == 0,
+        reason='root user always has access ; no chmod on Windows'
+    )
     def test_tar_with_inaccessible_file(self):
         base = tempfile.mkdtemp()
         full_path = os.path.join(base, 'foo')
@@ -944,8 +947,9 @@ def test_tar_with_inaccessible_file(self):
         with pytest.raises(IOError) as ei:
             tar(base)
 
-        assert 'Can not access file in context: {}'.format(full_path) in \
+        assert 'Can not read file in context: {}'.format(full_path) in (
             ei.exconly()
+        )
 
     @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='No symlinks on Windows')
     def test_tar_with_file_symlinks(self):