forked from TrueCloudLab/restic
dump: Rewrite Linux ACL handling
The old version was taken from an MPL-licensed library. This is a cleanroom implementation. The code is shorter and it's now explicit that only Linux ACLs are supported.
This commit is contained in:
parent
f96896a9c0
commit
aaf5254e26
3 changed files with 120 additions and 224 deletions
|
@ -1,131 +1,88 @@
|
||||||
package dump
|
package dump
|
||||||
|
|
||||||
// Adapted from https://github.com/maxymania/go-system/blob/master/posix_acl/posix_acl.go
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"errors"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
aclUserOwner = 0x0001
|
// Permissions
|
||||||
aclUser = 0x0002
|
aclPermRead = 0x4
|
||||||
aclGroupOwner = 0x0004
|
aclPermWrite = 0x2
|
||||||
aclGroup = 0x0008
|
aclPermExecute = 0x1
|
||||||
aclMask = 0x0010
|
|
||||||
aclOthers = 0x0020
|
// Tags
|
||||||
|
aclTagUserObj = 0x01 // Owner.
|
||||||
|
aclTagUser = 0x02
|
||||||
|
aclTagGroupObj = 0x04 // Owning group.
|
||||||
|
aclTagGroup = 0x08
|
||||||
|
aclTagMask = 0x10
|
||||||
|
aclTagOther = 0x20
|
||||||
)
|
)
|
||||||
|
|
||||||
type aclSID uint64
|
// formatLinuxACL converts a Linux ACL from its binary format to the POSIX.1e
|
||||||
|
// long text format.
|
||||||
|
//
|
||||||
|
// User and group IDs are printed in decimal, because we may be dumping
|
||||||
|
// a snapshot from a different machine.
|
||||||
|
//
|
||||||
|
// https://man7.org/linux/man-pages/man5/acl.5.html
|
||||||
|
// https://savannah.nongnu.org/projects/acl
|
||||||
|
// https://simson.net/ref/1997/posix_1003.1e-990310.pdf
|
||||||
|
func formatLinuxACL(acl []byte) (string, error) {
|
||||||
|
if len(acl)-4 < 0 || (len(acl)-4)%8 != 0 {
|
||||||
|
return "", errors.New("wrong length")
|
||||||
|
}
|
||||||
|
version := binary.LittleEndian.Uint32(acl)
|
||||||
|
if version != 2 {
|
||||||
|
return "", errors.New("unsupported ACL format version")
|
||||||
|
}
|
||||||
|
acl = acl[4:]
|
||||||
|
|
||||||
type aclElem struct {
|
text := make([]byte, 0, 2*len(acl))
|
||||||
Tag uint16
|
|
||||||
Perm uint16
|
for ; len(acl) >= 8; acl = acl[8:] {
|
||||||
ID uint32
|
tag := binary.LittleEndian.Uint16(acl)
|
||||||
|
perm := binary.LittleEndian.Uint16(acl[2:])
|
||||||
|
id := binary.LittleEndian.Uint32(acl[4:])
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case aclTagUserObj:
|
||||||
|
text = append(text, "user:"...)
|
||||||
|
case aclTagUser:
|
||||||
|
text = append(text, "user:"...)
|
||||||
|
text = strconv.AppendUint(text, uint64(id), 10)
|
||||||
|
case aclTagGroupObj:
|
||||||
|
text = append(text, "group:"...)
|
||||||
|
case aclTagGroup:
|
||||||
|
text = append(text, "group:"...)
|
||||||
|
text = strconv.AppendUint(text, uint64(id), 10)
|
||||||
|
case aclTagMask:
|
||||||
|
text = append(text, "mask:"...)
|
||||||
|
case aclTagOther:
|
||||||
|
text = append(text, "other:"...)
|
||||||
|
default:
|
||||||
|
return "", errors.New("unknown tag")
|
||||||
|
}
|
||||||
|
text = append(text, ':')
|
||||||
|
text = append(text, aclPermText(perm)...)
|
||||||
|
text = append(text, '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(text), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type acl struct {
|
func aclPermText(p uint16) []byte {
|
||||||
Version uint32
|
s := []byte("---")
|
||||||
List []aclElement
|
if p&aclPermRead != 0 {
|
||||||
}
|
s[0] = 'r'
|
||||||
|
}
|
||||||
type aclElement struct {
|
if p&aclPermWrite != 0 {
|
||||||
aclSID
|
s[1] = 'w'
|
||||||
Perm uint16
|
}
|
||||||
}
|
if p&aclPermExecute != 0 {
|
||||||
|
s[2] = 'x'
|
||||||
func (a aclSID) getType() int {
|
}
|
||||||
return int(a >> 32)
|
return s
|
||||||
}
|
|
||||||
func (a aclSID) getID() uint32 {
|
|
||||||
return uint32(a & 0xffffffff)
|
|
||||||
}
|
|
||||||
func (a aclSID) String() string {
|
|
||||||
switch a >> 32 {
|
|
||||||
case aclUserOwner:
|
|
||||||
return "user::"
|
|
||||||
case aclUser:
|
|
||||||
return fmt.Sprintf("user:%v:", a.getID())
|
|
||||||
case aclGroupOwner:
|
|
||||||
return "group::"
|
|
||||||
case aclGroup:
|
|
||||||
return fmt.Sprintf("group:%v:", a.getID())
|
|
||||||
case aclMask:
|
|
||||||
return "mask::"
|
|
||||||
case aclOthers:
|
|
||||||
return "other::"
|
|
||||||
}
|
|
||||||
return "?:"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a aclElement) String() string {
|
|
||||||
str := ""
|
|
||||||
if (a.Perm & 4) != 0 {
|
|
||||||
str += "r"
|
|
||||||
} else {
|
|
||||||
str += "-"
|
|
||||||
}
|
|
||||||
if (a.Perm & 2) != 0 {
|
|
||||||
str += "w"
|
|
||||||
} else {
|
|
||||||
str += "-"
|
|
||||||
}
|
|
||||||
if (a.Perm & 1) != 0 {
|
|
||||||
str += "x"
|
|
||||||
} else {
|
|
||||||
str += "-"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%v%v", a.aclSID, str)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *acl) decode(xattr []byte) {
|
|
||||||
var elem aclElement
|
|
||||||
ae := new(aclElem)
|
|
||||||
nr := bytes.NewReader(xattr)
|
|
||||||
e := binary.Read(nr, binary.LittleEndian, &a.Version)
|
|
||||||
if e != nil {
|
|
||||||
a.Version = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(a.List) > 0 {
|
|
||||||
a.List = a.List[:0]
|
|
||||||
}
|
|
||||||
for binary.Read(nr, binary.LittleEndian, ae) == nil {
|
|
||||||
elem.aclSID = (aclSID(ae.Tag) << 32) | aclSID(ae.ID)
|
|
||||||
elem.Perm = ae.Perm
|
|
||||||
a.List = append(a.List, elem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *acl) encode() []byte {
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
ae := new(aclElem)
|
|
||||||
|
|
||||||
err := binary.Write(buf, binary.LittleEndian, &a.Version)
|
|
||||||
// write to a bytes.Buffer always returns a nil error
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, elem := range a.List {
|
|
||||||
ae.Tag = uint16(elem.getType())
|
|
||||||
ae.Perm = elem.Perm
|
|
||||||
ae.ID = elem.getID()
|
|
||||||
|
|
||||||
err := binary.Write(buf, binary.LittleEndian, ae)
|
|
||||||
// write to a bytes.Buffer always returns a nil error
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *acl) String() string {
|
|
||||||
var finalacl string
|
|
||||||
for _, acl := range a.List {
|
|
||||||
finalacl += acl.String() + "\n"
|
|
||||||
}
|
|
||||||
return finalacl
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,114 +1,46 @@
|
||||||
package dump
|
package dump
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
rtest "github.com/restic/restic/internal/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_acl_decode(t *testing.T) {
|
func TestFormatLinuxACL(t *testing.T) {
|
||||||
type args struct {
|
for _, c := range []struct {
|
||||||
xattr []byte
|
in, out, err string
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "decode string",
|
in: "\x02\x00\x00\x00\x01\x00\x06\x00\xff\xff\xff\xff\x02\x00" +
|
||||||
args: args{
|
"\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" +
|
||||||
xattr: []byte{2, 0, 0, 0, 1, 0, 6, 0, 255, 255, 255, 255, 2, 0, 7, 0, 0, 0, 0, 0, 2, 0, 7, 0, 254, 255, 0, 0, 4, 0, 7, 0, 255, 255, 255, 255, 16, 0, 7, 0, 255, 255, 255, 255, 32, 0, 4, 0, 255, 255, 255, 255},
|
"\x04\x00\x02\x00\xff\xff\xff\xff\b\x00\x01\x00'\x00\x00\x00" +
|
||||||
},
|
"\x10\x00\a\x00\xff\xff\xff\xff \x00\x04\x00\xff\xff\xff\xff",
|
||||||
want: "user::rw-\nuser:0:rwx\nuser:65534:rwx\ngroup::rwx\nmask::rwx\nother::r--\n",
|
out: "user::rw-\nuser:3:r--\nuser:1001:r--\ngroup::-w-\n" +
|
||||||
|
"group:39:--x\nmask::rwx\nother::r--\n",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "decode group",
|
in: "\x02\x00\x00\x00\x00\x00\x06\x00\xff\xff\xff\xff\x02\x00" +
|
||||||
args: args{
|
"\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" +
|
||||||
xattr: []byte{2, 0, 0, 0, 8, 0, 1, 0, 254, 255, 0, 0},
|
"\x04\x00\x06\x00\xff\xff\xff\xff\b\x00\x05\x00'\x00\x00\x00" +
|
||||||
},
|
"\x10\x00\a\x00\xff\xff\xff\xff \x00\x04\x00\xff\xff\xff\xff",
|
||||||
want: "group:65534:--x\n",
|
err: "unknown tag",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "decode fail",
|
in: "\x01\x00\x00\x00\x01\x00\x06\x00\xff\xff\xff\xff\x02\x00" +
|
||||||
args: args{
|
"\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" +
|
||||||
xattr: []byte("abctest"),
|
"\x04\x00\x06\x00\xff\xff\xff\xff\b\x00\x05\x00'\x00\x00\x00" +
|
||||||
},
|
"\x10\x00\a\x00\xff\xff\xff\xff \x00\x04\x00\xff\xff\xff\xff",
|
||||||
want: "",
|
err: "unsupported ACL format version",
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "decode empty fail",
|
|
||||||
args: args{
|
|
||||||
xattr: []byte(""),
|
|
||||||
},
|
|
||||||
want: "",
|
|
||||||
},
|
},
|
||||||
|
{in: "\x02\x00", err: "wrong length"},
|
||||||
|
{in: "", err: "wrong length"},
|
||||||
|
} {
|
||||||
|
out, err := formatLinuxACL([]byte(c.in))
|
||||||
|
if c.err == "" {
|
||||||
|
rtest.Equals(t, c.out, out)
|
||||||
|
} else {
|
||||||
|
rtest.Assert(t, err != nil, "wanted %q but got nil", c.err)
|
||||||
|
rtest.Equals(t, c.err, err.Error())
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
a := &acl{}
|
|
||||||
a.decode(tt.args.xattr)
|
|
||||||
if tt.want != a.String() {
|
|
||||||
t.Errorf("acl.decode() = %v, want: %v", a.String(), tt.want)
|
|
||||||
}
|
|
||||||
a.decode(tt.args.xattr)
|
|
||||||
if tt.want != a.String() {
|
|
||||||
t.Errorf("second acl.decode() = %v, want: %v", a.String(), tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_acl_encode(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
want []byte
|
|
||||||
args []aclElement
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "encode values",
|
|
||||||
want: []byte{2, 0, 0, 0, 1, 0, 6, 0, 255, 255, 255, 255, 2, 0, 7, 0, 0, 0, 0, 0, 2, 0, 7, 0, 254, 255, 0, 0, 4, 0, 7, 0, 255, 255, 255, 255, 16, 0, 7, 0, 255, 255, 255, 255, 32, 0, 4, 0, 255, 255, 255, 255},
|
|
||||||
args: []aclElement{
|
|
||||||
{
|
|
||||||
aclSID: 8589934591,
|
|
||||||
Perm: 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
aclSID: 8589934592,
|
|
||||||
Perm: 7,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
aclSID: 8590000126,
|
|
||||||
Perm: 7,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
aclSID: 21474836479,
|
|
||||||
Perm: 7,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
aclSID: 73014444031,
|
|
||||||
Perm: 7,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
aclSID: 141733920767,
|
|
||||||
Perm: 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "encode fail",
|
|
||||||
want: []byte{2, 0, 0, 0},
|
|
||||||
args: []aclElement{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
a := &acl{
|
|
||||||
Version: 2,
|
|
||||||
List: tt.args,
|
|
||||||
}
|
|
||||||
if got := a.encode(); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("acl.encode() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
)
|
)
|
||||||
|
@ -104,21 +104,28 @@ func parseXattrs(xattrs []restic.ExtendedAttribute) map[string]string {
|
||||||
tmpMap := make(map[string]string)
|
tmpMap := make(map[string]string)
|
||||||
|
|
||||||
for _, attr := range xattrs {
|
for _, attr := range xattrs {
|
||||||
attrString := string(attr.Value)
|
// Check for Linux POSIX.1e ACLs.
|
||||||
|
//
|
||||||
if strings.HasPrefix(attr.Name, "system.posix_acl_") {
|
// TODO support ACLs from other operating systems.
|
||||||
na := acl{}
|
// FreeBSD ACLs have names "posix1e.acl_(access|default)",
|
||||||
na.decode(attr.Value)
|
// but their binary format may not match the Linux format.
|
||||||
|
aclKey := ""
|
||||||
if na.String() != "" {
|
switch attr.Name {
|
||||||
if strings.Contains(attr.Name, "system.posix_acl_access") {
|
case "system.posix_acl_access":
|
||||||
tmpMap["SCHILY.acl.access"] = na.String()
|
aclKey = "SCHILY.acl.access"
|
||||||
} else if strings.Contains(attr.Name, "system.posix_acl_default") {
|
case "system.posix_acl_default":
|
||||||
tmpMap["SCHILY.acl.default"] = na.String()
|
aclKey = "SCHILY.acl.default"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if aclKey != "" {
|
||||||
|
text, err := formatLinuxACL(attr.Value)
|
||||||
|
if err != nil {
|
||||||
|
debug.Log("parsing Linux ACL: %v, skipping", err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
tmpMap[aclKey] = text
|
||||||
} else {
|
} else {
|
||||||
tmpMap["SCHILY.xattr."+attr.Name] = attrString
|
tmpMap["SCHILY.xattr."+attr.Name] = string(attr.Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue