]> arthur.barton.de Git - bup.git/commitdiff
metadata: port ACL support to C
authorJohannes Berg <johannes@sipsolutions.net>
Sat, 30 May 2020 21:55:46 +0000 (23:55 +0200)
committerRob Browning <rlb@defaultvalue.org>
Sun, 5 Jul 2020 16:16:22 +0000 (11:16 -0500)
Use some own C code instead of the posix1e python bindings, as those
don't have correct 'bytes' support (at least right now), which means
that we cannot use them with arbitrary file, user and group names.
Our own wrappers just use 'bytes' throughout.

Signed-off-by: Johannes Berg <johannes@sipsolutions.net>
[rlb@defaultvalue.org: adjust to rely on pkg-config]
Reviewed-by: Rob Browning <rlb@defaultvalue.org>
Signed-off-by: Rob Browning <rlb@defaultvalue.org>
Tested-by: Rob Browning <rlb@defaultvalue.org>
Makefile
README.md
config/config.vars.in
config/configure
dev/prep-for-debianish-build
dev/prep-for-freebsd-build
lib/bup/_helpers.c
lib/bup/csetup.py
lib/bup/metadata.py
lib/bup/t/tmetadata.py

index 4ca1a031c7c2a3daf362849a782e7f364e5a7ce7..ca22c0557e0b080a82d11da80790e69338d927e5 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -62,6 +62,10 @@ readline_cflags += $(addprefix -DBUP_RL_EXPECTED_XOPEN_SOURCE=,$(readline_xopen)
 
 CFLAGS += $(readline_cflags)
 LDFLAGS += $(shell pkg-config readline --libs)
+ifeq ($(BUP_HAVE_LIBACL),yes)
+  CFLAGS += $(shell pkg-config libacl --cflags)
+  LDFLAGS += $(shell pkg-config libacl --libs)
+endif
 
 config/bin/python: config/config.vars
 
index 05fc56f2c1043bac1a6c79dcd66f8b59e2a66673..65c8e89bed1fd30dc1a8e4ac98482595ce184d2d 100644 (file)
--- a/README.md
+++ b/README.md
@@ -148,8 +148,8 @@ From source
 
     ```sh
     apt-get install python2.7-dev python-fuse
-    apt-get install python-pyxattr python-pylibacl
-    apt-get install linux-libc-dev
+    apt-get install python-pyxattr
+    apt-get install linux-libc-dev libacl1-dev
     apt-get install acl attr
     apt-get install libreadline-dev # optional (bup ftp)
     apt-get install python-tornado # optional (bup web)
@@ -160,15 +160,15 @@ From source
 
     ```sh
     yum groupinstall "Development Tools"
-    yum install python python-devel
-    yum install fuse-python pyxattr pylibacl
+    yum install python python-devel libacl-devel
+    yum install fuse-python pyxattr
     yum install perl-Time-HiRes
     yum install readline-devel # optional (bup ftp)
     yum install python-tornado # optional (bup web)
     ```
 
    In addition to the default CentOS repositories, you may need to add
-   RPMForge (for fuse-python) and EPEL (for pyxattr and pylibacl).
+   RPMForge (for fuse-python) and EPEL (for pyxattr).
 
    On Cygwin, install python, make, rsync, and gcc4.
 
index 3f3e2edb1e05793488acbc63def3493dfafd0356..9dba67eba2b482c93a9a355a9fcbd9ae54409a62 100644 (file)
@@ -1,3 +1,4 @@
+BUP_HAVE_LIBACL=@BUP_HAVE_LIBACL@
 CONFIGURE_FILES=@CONFIGURE_FILES@
 GENERATED_FILES=@GENERATED_FILES@
 bup_make=@bup_make@
index 6dacf8be34d61bc41f4a6a5509c717558cef6521..fe4d85e1eedda0b32871da5ba316e21a8572f031 100755 (executable)
@@ -184,6 +184,31 @@ AC_CHECK_FIELD stat st_ctimensec sys/types.h sys/stat.h unistd.h
 
 AC_CHECK_FIELD tm tm_gmtoff time.h
 
+
+TLOGN "checking for libacl"
+if pkg-config libacl; then
+    AC_DEFINE BUP_HAVE_READLINE 1
+    TLOG ' (yes)'
+    LIBS=-lacl
+    AC_CHECK_HEADERS sys/acl.h
+    AC_CHECK_HEADERS acl/libacl.h
+    AC_CHECK_FUNCS acl_get_file
+    AC_CHECK_FUNCS acl_from_text
+    AC_CHECK_FUNCS acl_set_file
+    # Note: These are linux specific, but we need them (for now?)
+    AC_CHECK_FUNCS acl_extended_file
+    AC_CHECK_FUNCS acl_to_any_text
+    LIBS=
+    if test "$ac_defined_HAVE_ACL_EXTENDED_FILE"; then
+        AC_SUB BUP_HAVE_LIBACL yes
+    else
+        AC_SUB BUP_HAVE_LIBACL no
+    fi
+else
+    TLOG ' (no)'
+fi
+
+
 AC_OUTPUT config.vars
 
 if test -e config.var; then rm -r config.var; fi
index 73fa2357f1c969abc59aece7e6e29a014059b7e6..f441d5b6e23c6deeb166c7a2c0cc154890e18120 100755 (executable)
@@ -12,7 +12,7 @@ apt-get update
 
 common_debs='gcc make linux-libc-dev git rsync eatmydata acl attr par2'
 common_debs="$common_debs duplicity rdiff-backup rsnapshot dosfstools kmod"
-common_debs="$common_debs libreadline-dev"
+common_debs="$common_debs libreadline-dev libacl1-dev"
 
 pyver="${1:-python2}"
 xattr="${2:-pyxattr}"
@@ -24,13 +24,13 @@ case "$pyver" in
         apt-get install -y \
                 $common_debs \
                 python2.7-dev python-fuse \
-                python-"$xattr" python-pylibacl python-tornado
+                python-"$xattr" python-tornado
         ;;
     python3)
         apt-get install -y \
                 $common_debs \
                 python3.7-dev python3-distutils python3-fuse \
-                python3-"$xattr" python3-pylibacl python3-tornado
+                python3-"$xattr" python3-tornado
         ;;
     *)
         usage 1>&2
index 7a8901c7bd30229fe9eae3ae1f00e91b1ca77e90..e56425984ae0070f2d2ae87f3c5e1d8c978e09bd 100755 (executable)
@@ -7,5 +7,5 @@ export ASSUME_ALWAYS_YES=yes
 pkg update
 pkg install \
     gmake git bash rsync curl par2cmdline \
-    python2 python py27-pylibacl py27-tornado readline \
+    python2 python py27-tornado readline \
     duplicity rdiff-backup rsnapshot
index d954f1d3266766d353ca9694aa6865e8225164e5..b39db0ad9477e759cdd6d256505864b39e809753 100644 (file)
@@ -2095,6 +2095,137 @@ bup_readline(PyObject *self, PyObject *args)
 
 #endif // defined BUP_HAVE_READLINE
 
+#if defined(HAVE_SYS_ACL_H) && \
+    defined(HAVE_ACL_LIBACL_H) && \
+    defined(HAVE_ACL_EXTENDED_FILE) && \
+    defined(HAVE_ACL_GET_FILE) && \
+    defined(HAVE_ACL_TO_ANY_TEXT) && \
+    defined(HAVE_ACL_FROM_TEXT) && \
+    defined(HAVE_ACL_SET_FILE)
+#define ACL_SUPPORT 1
+#include <sys/acl.h>
+#include <acl/libacl.h>
+
+// Returns
+//   0 for success
+//  -1 for errors, with python exception set
+//  -2 for ignored errors (not supported)
+static int bup_read_acl_to_text(const char *name, acl_type_t type,
+                                char **txt, char **num)
+{
+    acl_t acl;
+
+    acl = acl_get_file(name, type);
+    if (!acl) {
+        if (errno == EOPNOTSUPP || errno == ENOSYS)
+            return -2;
+        PyErr_SetFromErrno(PyExc_IOError);
+        return -1;
+    }
+
+    *num = NULL;
+    *txt = acl_to_any_text(acl, "", '\n', TEXT_ABBREVIATE);
+    if (*txt)
+        *num = acl_to_any_text(acl, "", '\n', TEXT_ABBREVIATE | TEXT_NUMERIC_IDS);
+
+    if (*txt && *num)
+        return 0;
+
+    if (errno == ENOMEM)
+        PyErr_NoMemory();
+    else
+        PyErr_SetFromErrno(PyExc_IOError);
+
+    if (*txt)
+        acl_free((acl_t)*txt);
+    if (*num)
+        acl_free((acl_t)*num);
+
+    return -1;
+}
+
+static PyObject *bup_read_acl(PyObject *self, PyObject *args)
+{
+    char *name;
+    int isdir, rv;
+    PyObject *ret = NULL;
+    char *acl_txt = NULL, *acl_num = NULL;
+
+    if (!PyArg_ParseTuple(args, cstr_argf "i", &name, &isdir))
+       return NULL;
+
+    if (!acl_extended_file(name))
+        Py_RETURN_NONE;
+
+    rv = bup_read_acl_to_text(name, ACL_TYPE_ACCESS, &acl_txt, &acl_num);
+    if (rv)
+        goto out;
+
+    if (isdir) {
+        char *def_txt = NULL, *def_num = NULL;
+
+        rv = bup_read_acl_to_text(name, ACL_TYPE_DEFAULT, &def_txt, &def_num);
+        if (rv)
+            goto out;
+
+        ret = Py_BuildValue("[" cstr_argf cstr_argf cstr_argf cstr_argf "]",
+                            acl_txt, acl_num, def_txt, def_num);
+
+        if (def_txt)
+            acl_free((acl_t)def_txt);
+        if (def_num)
+            acl_free((acl_t)def_num);
+    } else {
+        ret = Py_BuildValue("[" cstr_argf cstr_argf "]",
+                            acl_txt, acl_num);
+    }
+
+out:
+    if (acl_txt)
+        acl_free((acl_t)acl_txt);
+    if (acl_num)
+        acl_free((acl_t)acl_num);
+    if (rv == -2)
+        Py_RETURN_NONE;
+    return ret;
+}
+
+static int bup_apply_acl_string(const char *name, const char *s)
+{
+    acl_t acl = acl_from_text(s);
+    int ret = 0;
+
+    if (!acl) {
+        PyErr_SetFromErrno(PyExc_IOError);
+        return -1;
+    }
+
+    if (acl_set_file(name, ACL_TYPE_ACCESS, acl)) {
+        PyErr_SetFromErrno(PyExc_IOError);
+        ret = -1;
+    }
+
+    acl_free(acl);
+
+    return ret;
+}
+
+static PyObject *bup_apply_acl(PyObject *self, PyObject *args)
+{
+    char *name, *acl, *def = NULL;
+
+    if (!PyArg_ParseTuple(args, cstr_argf cstr_argf "|" cstr_argf, &name, &acl, &def))
+       return NULL;
+
+    if (bup_apply_acl_string(name, acl))
+        return NULL;
+
+    if (def && bup_apply_acl_string(name, def))
+        return NULL;
+
+    Py_RETURN_NONE;
+}
+#endif
 
 static PyMethodDef helper_methods[] = {
     { "write_sparsely", bup_write_sparsely, METH_VARARGS,
@@ -2201,6 +2332,16 @@ static PyMethodDef helper_methods[] = {
     { "readline", bup_readline, METH_VARARGS,
       "Call readline(prompt)." },
 #endif // defined BUP_HAVE_READLINE
+#ifdef ACL_SUPPORT
+    { "read_acl", bup_read_acl, METH_VARARGS,
+      "read_acl(name, isdir)\n\n"
+      "Read ACLs for the given file/dirname and return the correctly encoded"
+      " list [txt, num, def_tx, def_num] (the def_* being empty bytestrings"
+      " unless the second argument 'isdir' is True)." },
+    { "apply_acl", bup_apply_acl, METH_VARARGS,
+      "apply_acl(name, acl, def=None)\n\n"
+      "Given a file/dirname (bytes) and the ACLs to restore, do that." },
+#endif /* HAVE_ACLS */
     { NULL, NULL, 0, NULL },  // sentinel
 };
 
index c5b6bb136ca3a2cdfb0a4fff40e800cc26bf6f70..9dbb4a7dccb7963ad3804b3eb0863182c5caf950 100644 (file)
@@ -3,7 +3,7 @@ from __future__ import absolute_import, print_function
 
 import shlex, sys
 from distutils.core import setup, Extension
-from os import environ
+import os
 
 if len(sys.argv) != 4:
     print('Usage: csetup.py CFLAGS LDFLAGS', file=sys.stderr)
index da79dc24b7bad57bca1decdf25c08f5f2128c385..eeec559e76ba37f453f7c43c13c58acf1f4f9e07 100644 (file)
@@ -40,14 +40,14 @@ if sys.platform.startswith('linux'):
             log('Warning: python-xattr module is too old; '
                 'upgrade or install python-pyxattr instead.\n')
 
-posix1e = None
-if not (sys.platform.startswith('cygwin') \
-        or sys.platform.startswith('darwin') \
-        or sys.platform.startswith('netbsd')):
-    try:
-        import posix1e
-    except ImportError:
-        log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
+try:
+    from bup._helpers import read_acl, apply_acl
+except ImportError:
+    if not (sys.platform.startswith('cygwin') or
+            sys.platform.startswith('darwin') or
+            sys.platform.startswith('netbsd')):
+        log('Warning: POSIX ACL support missing; recompile with libacl1-dev/libacl-devel.\n')
+    read_acl = apply_acl = None
 
 try:
     from bup._helpers import get_linux_file_attr, set_linux_file_attr
@@ -523,31 +523,11 @@ class Metadata:
     # The numeric/text distinction only matters when reading/restoring
     # a stored record.
     def _add_posix1e_acl(self, path, st):
-        if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
+        if not read_acl:
             return
         if not stat.S_ISLNK(st.st_mode):
-            acls = None
-            def_acls = None
-            try:
-                if posix1e.has_extended(path):
-                    acl = posix1e.ACL(file=path)
-                    acls = [acl, acl] # txt and num are the same
-                    if stat.S_ISDIR(st.st_mode):
-                        def_acl = posix1e.ACL(filedef=(path if py_maj < 3
-                                                       else path.decode('iso-8859-1')))
-                        def_acls = [def_acl, def_acl]
-            except EnvironmentError as e:
-                if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
-                    raise
-            if acls:
-                txt_flags = posix1e.TEXT_ABBREVIATE
-                num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
-                acl_rep = [acls[0].to_any_text('', b'\n', txt_flags),
-                           acls[1].to_any_text('', b'\n', num_flags)]
-                if def_acls:
-                    acl_rep.append(def_acls[0].to_any_text('', b'\n', txt_flags))
-                    acl_rep.append(def_acls[1].to_any_text('', b'\n', num_flags))
-                self.posix1e_acl = acl_rep
+            isdir = 1 if stat.S_ISDIR(st.st_mode) else 0
+            self.posix1e_acl = read_acl(path, isdir)
 
     def _same_posix1e_acl(self, other):
         """Return true or false to indicate similarity in the hardlink sense."""
@@ -570,42 +550,31 @@ class Metadata:
         self.posix1e_acl = acl_rep
 
     def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
-        def apply_acl(acl_rep, kind):
-            try:
-                acl = posix1e.ACL(text=acl_rep.decode('ascii'))
-            except IOError as e:
-                if e.errno == 0:
-                    # pylibacl appears to return an IOError with errno
-                    # set to 0 if a group referred to by the ACL rep
-                    # doesn't exist on the current system.
-                    raise ApplyError("POSIX1e ACL: can't create %r for %r"
-                                     % (acl_rep, path_msg(path)))
-                else:
-                    raise
-            try:
-                acl.applyto(path, kind)
-            except IOError as e:
-                if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
-                    raise ApplyError('POSIX1e ACL applyto: %s' % e)
-                else:
-                    raise
+        if not self.posix1e_acl:
+            return
 
-        if not posix1e:
-            if self.posix1e_acl:
-                add_error("%s: can't restore ACLs; posix1e support missing.\n"
-                          % path_msg(path))
+        if not apply_acl:
+            add_error("%s: can't restore ACLs; posix1e support missing.\n"
+                      % path_msg(path))
             return
-        if self.posix1e_acl:
+
+        try:
             acls = self.posix1e_acl
+            offs = 1 if restore_numeric_ids else 0
             if len(acls) > 2:
-                if restore_numeric_ids:
-                    apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
-                else:
-                    apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
-            if restore_numeric_ids:
-                apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
+                apply_acl(path, acls[offs], acls[offs + 2])
             else:
-                apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
+                apply_acl(path, acls[offs])
+        except IOError as e:
+            if e.errno == errno.EINVAL:
+                # libacl returns with errno set to EINVAL if a user
+                # (or group) doesn't exist
+                raise ApplyError("POSIX1e ACL: can't create %r for %r"
+                                 % (acls, path_msg(path)))
+            elif e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
+                raise ApplyError('POSIX1e ACL applyto: %s' % e)
+            else:
+                raise
 
 
     ## Linux attributes (lsattr(1), chattr(1))
index 3b77af1a9b111b34595b2f983dccd13d2fe117c3..e14e0107c1c09fe7ec721a717ac1b178d20f4567 100644 (file)
@@ -270,8 +270,8 @@ def test_restore_over_existing_target():
             WVEXCEPT(Exception, dir_m.create_path, path, create_symlinks=True)
 
 
-from bup.metadata import posix1e
-if not posix1e:
+from bup.metadata import read_acl
+if not read_acl:
     @wvtest
     def POSIX1E_ACL_SUPPORT_IS_MISSING():
         pass