diff --git a/Doc/library/os.rst b/Doc/library/os.rst --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -1224,7 +1224,7 @@ .. function:: access(path, mode, *, dir_fd=None, effective_ids=False, follow_symlinks=True) - Use the real uid/gid to test for access to *path*. Note that most operations + On Unix: Use the real uid/gid to test for access to *path*. Note that most operations will use the effective uid/gid, therefore this routine can be used in a suid/sgid environment to test if the invoking user has the specified access to *path*. *mode* should be :const:`F_OK` to test the existence of *path*, or it @@ -1242,6 +1242,10 @@ or not it is available using :data:`os.supports_effective_ids`. If it is unavailable, using it will raise a :exc:`NotImplementedError`. + On Windows: Use the readonly attribute combined with the AccessCheck API to + test for access to *path*. NB :const:`X_OK` only determines whether the file + *may* be executed, not whether it is in fact an executable program. + Availability: Unix, Windows. .. note:: @@ -1276,6 +1280,8 @@ .. versionchanged:: 3.3 Added the *dir_fd*, *effective_ids*, and *follow_symlinks* parameters. + .. versionchanged:: 3.4 + The Windows version now checks against the NTFS ACLs. .. data:: F_OK R_OK diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -56,13 +56,106 @@ class FileTests(unittest.TestCase): def setUp(self): if os.path.exists(support.TESTFN): - os.unlink(support.TESTFN) + support.unlink(support.TESTFN) tearDown = setUp - def test_access(self): - f = os.open(support.TESTFN, os.O_CREAT|os.O_RDWR) - os.close(f) - self.assertTrue(os.access(support.TESTFN, os.W_OK)) + if sys.platform == 'win32': + def test_access_f(self): + with support.temp_dir() as d: + testfn = os.path.join(d, os.path.basename(support.TESTFN)) + f = os.open(testfn, os.O_CREAT) + os.close(f) + os.system("echo y| cacls %s /D Everyone" % testfn) + self.assert_(os.access(testfn, os.F_OK)) + self.assert_(os.access(bytes(testfn, "utf-8"), os.F_OK)) + os.system("echo y| cacls %s /G Everyone:R" % testfn) + self.assert_(os.access(testfn, os.F_OK)) + self.assert_(os.access(bytes(testfn, "utf-8"), os.F_OK)) + os.system("echo y| cacls %s /G Everyone:W" % testfn) + self.assert_(os.access(testfn, os.F_OK)) + self.assert_(os.access(bytes(testfn, "utf-8"), os.F_OK)) + os.system("echo y| cacls %s /G Everyone:C" % testfn) + self.assert_(os.access(testfn, os.F_OK)) + self.assert_(os.access(bytes(testfn, "utf-8"), os.F_OK)) + support.unlink(testfn) + self.assert_(not os.access(testfn, os.F_OK)) + self.assert_(not os.access(bytes(testfn, "utf-8"), os.F_OK)) + + def test_access_r(self): + with support.temp_dir() as d: + testfn = os.path.join(d, os.path.basename(support.TESTFN)) + f = os.open(testfn, os.O_CREAT) + os.close(f) + os.system("echo y| cacls %s /D Everyone" % testfn) + self.assert_(not os.access(testfn, os.R_OK)) + self.assert_(not os.access(bytes(testfn, "utf-8"), os.R_OK)) + os.system("echo y| cacls %s /G Everyone:R" % testfn) + self.assert_(os.access(testfn, os.R_OK)) + self.assert_(os.access(bytes(testfn, "utf-8"), os.R_OK)) + os.system("echo y| cacls %s /G Everyone:W" % testfn) + self.assert_(not os.access(testfn, os.R_OK)) + self.assert_(not os.access(bytes(testfn, "utf-8"), os.R_OK)) + os.system("echo y| cacls %s /G Everyone:C" % testfn) + self.assert_(os.access(testfn, os.R_OK)) + self.assert_(os.access(bytes(testfn, "utf-8"), os.R_OK)) + support.unlink(testfn) + self.assert_(not os.access(testfn, os.R_OK)) + self.assert_(not os.access(bytes(testfn, "utf-8"), os.R_OK)) + + def test_access_w(self): + with support.temp_dir() as d: + testfn = os.path.join(d, os.path.basename(support.TESTFN)) + f = os.open(testfn, os.O_CREAT) + os.close(f) + os.system("echo y| cacls %s /D Everyone" % testfn) + self.assert_(not os.access(testfn, os.W_OK)) + self.assert_(not os.access(bytes(testfn, "utf-8"), os.W_OK)) + os.system("echo y| cacls %s /G Everyone:R" % testfn) + self.assert_(not os.access(testfn, os.W_OK)) + self.assert_(not os.access(bytes(testfn, "utf-8"), os.W_OK)) + os.system("echo y| cacls %s /G Everyone:W" % testfn) + self.assert_(os.access(testfn, os.W_OK)) + self.assert_(os.access(bytes(testfn, "utf-8"), os.W_OK)) + os.system("attrib +r %s" % testfn) + self.assert_(not os.access(bytes(testfn, "utf-8"), os.W_OK)) + os.system("attrib -r %s" % testfn) + os.system("echo y| cacls %s /G Everyone:C" % testfn) + self.assert_(os.access(testfn, os.W_OK)) + self.assert_(os.access(bytes(testfn, "utf-8"), os.W_OK)) + os.system("attrib +r %s" % testfn) + self.assert_(not os.access(bytes(testfn, "utf-8"), os.W_OK)) + os.system("attrib -r %s" % testfn) + support.unlink(testfn) + self.assert_(not os.access(testfn, os.W_OK)) + self.assert_(not os.access(bytes(testfn, "utf-8"), os.W_OK)) + + def test_access_x(self): + with support.temp_dir() as d: + testfn = os.path.join(d, os.path.basename(support.TESTFN)) + f = os.open(testfn, os.O_CREAT) + os.close(f) + os.system("echo y| cacls %s /D Everyone" % testfn) + self.assert_(not os.access(testfn, os.X_OK)) + self.assert_(not os.access(bytes(testfn, "utf-8"), os.X_OK)) + os.system("echo y| cacls %s /G Everyone:R" % testfn) + self.assert_(os.access(testfn, os.X_OK)) + self.assert_(os.access(bytes(testfn, "utf-8"), os.X_OK)) + os.system("echo y| cacls %s /G Everyone:W" % testfn) + self.assert_(os.access(testfn, os.X_OK)) + self.assert_(os.access(bytes(testfn, "utf-8"), os.X_OK)) + os.system("echo y| cacls %s /G Everyone:C" % testfn) + self.assert_(os.access(testfn, os.X_OK)) + self.assert_(os.access(bytes(testfn, "utf-8"), os.X_OK)) + support.unlink(testfn) + self.assert_(not os.access(testfn, os.X_OK)) + self.assert_(not os.access(bytes(testfn, "utf-8"), os.X_OK)) + + else: + def test_access(self): + f = os.open(support.TESTFN, os.O_CREAT|os.O_RDWR) + os.close(f) + self.assert_(os.access(support.TESTFN, os.W_OK)) + def test_closerange(self): first = os.open(support.TESTFN, os.O_CREAT|os.O_RDWR) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -1063,6 +1063,142 @@ return TRUE; } +#ifndef F_OK +#define F_OK 0 +#endif +#ifndef R_OK +#define R_OK 4 +#endif +#ifndef W_OK +#define W_OK 2 +#endif +#ifndef X_OK +#define X_OK 1 +#endif + +/* Since the intention of this functionality is to indicate + whether a file is accessible to the current process, exceptions + are not cascaded; instead, they're taken to mean that the file + is not accessible. This is less defensible in the case of, eg, + a memory allocation error but simplifies the code slightly. */ +static BOOL +win32_get_file_access(path_t path, int mode) +{ + DWORD attributes; + SECURITY_INFORMATION requested_information = OWNER_SECURITY_INFORMATION | + GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION; + PSECURITY_DESCRIPTOR pSD = NULL; + DWORD dwSize = 0; + HANDLE hToken = INVALID_HANDLE_VALUE; + DWORD access_desired = 0; + + GENERIC_MAPPING mapping; + PRIVILEGE_SET privilege_set; + DWORD privilege_set_size = sizeof(privilege_set); + DWORD access_granted = 0; + BOOL is_access_granted = FALSE; + BOOL can_read_access = FALSE; + BOOL impersonating = FALSE; + + /* To allow a fast path out of the function, access is assumed + not to be granted until the final step when the AccessCheck is + done. This way, we can fail as early as possible by simply dropping + through to the epilogue. */ + is_access_granted = FALSE; + + if (path.wide) { + Py_BEGIN_ALLOW_THREADS + attributes = GetFileAttributesW(path.wide); + Py_END_ALLOW_THREADS + } else { + Py_BEGIN_ALLOW_THREADS + attributes = GetFileAttributesA(path.narrow); + Py_END_ALLOW_THREADS + } + + /* If we can't even read the attributes, no file access + is possible. If we can, and all we're checking is existence, + then we're finished; no need to check security. */ + if (attributes == INVALID_FILE_ATTRIBUTES) + goto epilogue; + if (mode == F_OK) { + is_access_granted = TRUE; + goto epilogue; + } + + /* If write access is requested and the file has the DOS read-only + attribute set, then fail immediately, regardless of permissions. + NB readonly on a directory has a separate meaning + cf http://support.microsoft.com/kb/326549 */ + if ((mode & W_OK) && (attributes & FILE_ATTRIBUTE_READONLY) && + !(attributes & FILE_ATTRIBUTE_DIRECTORY)) + goto epilogue; + + /* Get the file's security descriptor and a copy of the active token. + Failure at any point is taken to mean that access is not possible */ + if (path.wide) { + Py_BEGIN_ALLOW_THREADS + GetFileSecurityW(path.wide, requested_information, + 0, 0, &dwSize); + pSD = (PSECURITY_DESCRIPTOR)PyMem_RawMalloc(dwSize); + can_read_access = GetFileSecurityW(path.wide, requested_information, + pSD, dwSize, &dwSize); + Py_END_ALLOW_THREADS + } else { + Py_BEGIN_ALLOW_THREADS + GetFileSecurityA(path.narrow, requested_information, + 0, 0, &dwSize); + pSD = (PSECURITY_DESCRIPTOR)PyMem_RawMalloc(dwSize); + can_read_access = GetFileSecurityA(path.narrow, requested_information, + pSD, dwSize, &dwSize); + Py_END_ALLOW_THREADS + } + if (!can_read_access) + goto epilogue; + if (!IsValidSecurityDescriptor(pSD)) + goto epilogue; + if (!ImpersonateSelf(SecurityImpersonation)) + goto epilogue; + impersonating = TRUE; + if (!OpenThreadToken(GetCurrentThread (), TOKEN_ALL_ACCESS, + TRUE, &hToken)) + goto epilogue; + + /* Convert generic access bits into file-specific ones + and check whether the access requested is allowed. */ + access_desired = 0; + if (mode & X_OK) + access_desired |= FILE_EXECUTE; + if (mode & R_OK) + access_desired |= FILE_READ_DATA; + if (mode & W_OK) + access_desired |= FILE_WRITE_DATA; + mapping.GenericRead = FILE_READ_DATA; + mapping.GenericWrite = FILE_WRITE_DATA; + mapping.GenericExecute = FILE_EXECUTE; + mapping.GenericAll = FILE_ALL_ACCESS; + MapGenericMask(&access_desired, &mapping); + + if (!AccessCheck( + pSD, hToken, access_desired, &mapping, &privilege_set, + &privilege_set_size, &access_granted, + &is_access_granted)) + is_access_granted = FALSE; + +epilogue: + if (impersonating) + RevertToSelf(); + if (pSD) + PyMem_RawFree(pSD); + if (hToken != INVALID_HANDLE_VALUE) + CloseHandle(hToken); + + if (PyErr_Occurred()) + return FALSE; + else + return is_access_granted; +} + #endif /* MS_WINDOWS */ /* Return a dictionary corresponding to the POSIX environment table */ @@ -2349,9 +2485,7 @@ int follow_symlinks = 1; PyObject *return_value = NULL; -#ifdef MS_WINDOWS - DWORD attr; -#else +#ifndef MS_WINDOWS int result; #endif @@ -2377,26 +2511,8 @@ #endif #ifdef MS_WINDOWS - Py_BEGIN_ALLOW_THREADS - if (path.wide != NULL) - attr = GetFileAttributesW(path.wide); - else - attr = GetFileAttributesA(path.narrow); - Py_END_ALLOW_THREADS - - /* - * Access is possible if - * * we didn't get a -1, and - * * write access wasn't requested, - * * or the file isn't read-only, - * * or it's a directory. - * (Directories cannot be read-only on Windows.) - */ - return_value = PyBool_FromLong( - (attr != 0xFFFFFFFF) && - (!(mode & 2) || - !(attr & FILE_ATTRIBUTE_READONLY) || - (attr & FILE_ATTRIBUTE_DIRECTORY))); + return_value = PyBool_FromLong(win32_get_file_access(path, mode)); + goto exit; #else Py_BEGIN_ALLOW_THREADS @@ -2425,19 +2541,6 @@ return return_value; } -#ifndef F_OK -#define F_OK 0 -#endif -#ifndef R_OK -#define R_OK 4 -#endif -#ifndef W_OK -#define W_OK 2 -#endif -#ifndef X_OK -#define X_OK 1 -#endif - #ifdef HAVE_TTYNAME PyDoc_STRVAR(posix_ttyname__doc__, "ttyname(fd) -> string\n\n\