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
|
||||
|
||||
// Adapted from https://github.com/maxymania/go-system/blob/master/posix_acl/posix_acl.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
aclUserOwner = 0x0001
|
||||
aclUser = 0x0002
|
||||
aclGroupOwner = 0x0004
|
||||
aclGroup = 0x0008
|
||||
aclMask = 0x0010
|
||||
aclOthers = 0x0020
|
||||
// Permissions
|
||||
aclPermRead = 0x4
|
||||
aclPermWrite = 0x2
|
||||
aclPermExecute = 0x1
|
||||
|
||||
// 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 {
|
||||
Tag uint16
|
||||
Perm uint16
|
||||
ID uint32
|
||||
text := make([]byte, 0, 2*len(acl))
|
||||
|
||||
for ; len(acl) >= 8; acl = acl[8:] {
|
||||
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')
|
||||
}
|
||||
|
||||
type acl struct {
|
||||
Version uint32
|
||||
List []aclElement
|
||||
return string(text), nil
|
||||
}
|
||||
|
||||
type aclElement struct {
|
||||
aclSID
|
||||
Perm uint16
|
||||
func aclPermText(p uint16) []byte {
|
||||
s := []byte("---")
|
||||
if p&aclPermRead != 0 {
|
||||
s[0] = 'r'
|
||||
}
|
||||
|
||||
func (a aclSID) getType() int {
|
||||
return int(a >> 32)
|
||||
if p&aclPermWrite != 0 {
|
||||
s[1] = 'w'
|
||||
}
|
||||
func (a aclSID) getID() uint32 {
|
||||
return uint32(a & 0xffffffff)
|
||||
if p&aclPermExecute != 0 {
|
||||
s[2] = 'x'
|
||||
}
|
||||
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
|
||||
return s
|
||||
}
|
||||
|
|
|
@ -1,114 +1,46 @@
|
|||
package dump
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
func Test_acl_decode(t *testing.T) {
|
||||
type args struct {
|
||||
xattr []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
func TestFormatLinuxACL(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
in, out, err string
|
||||
}{
|
||||
{
|
||||
name: "decode string",
|
||||
args: args{
|
||||
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},
|
||||
},
|
||||
want: "user::rw-\nuser:0:rwx\nuser:65534:rwx\ngroup::rwx\nmask::rwx\nother::r--\n",
|
||||
in: "\x02\x00\x00\x00\x01\x00\x06\x00\xff\xff\xff\xff\x02\x00" +
|
||||
"\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" +
|
||||
"\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",
|
||||
out: "user::rw-\nuser:3:r--\nuser:1001:r--\ngroup::-w-\n" +
|
||||
"group:39:--x\nmask::rwx\nother::r--\n",
|
||||
},
|
||||
{
|
||||
name: "decode group",
|
||||
args: args{
|
||||
xattr: []byte{2, 0, 0, 0, 8, 0, 1, 0, 254, 255, 0, 0},
|
||||
},
|
||||
want: "group:65534:--x\n",
|
||||
in: "\x02\x00\x00\x00\x00\x00\x06\x00\xff\xff\xff\xff\x02\x00" +
|
||||
"\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" +
|
||||
"\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",
|
||||
err: "unknown tag",
|
||||
},
|
||||
{
|
||||
name: "decode fail",
|
||||
args: args{
|
||||
xattr: []byte("abctest"),
|
||||
in: "\x01\x00\x00\x00\x01\x00\x06\x00\xff\xff\xff\xff\x02\x00" +
|
||||
"\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" +
|
||||
"\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",
|
||||
err: "unsupported ACL format version",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "decode empty fail",
|
||||
args: args{
|
||||
xattr: []byte(""),
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
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
|
||||
{in: "\x02\x00", err: "wrong length"},
|
||||
{in: "", err: "wrong length"},
|
||||
} {
|
||||
{
|
||||
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)
|
||||
}
|
||||
})
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
@ -104,21 +104,28 @@ func parseXattrs(xattrs []restic.ExtendedAttribute) map[string]string {
|
|||
tmpMap := make(map[string]string)
|
||||
|
||||
for _, attr := range xattrs {
|
||||
attrString := string(attr.Value)
|
||||
|
||||
if strings.HasPrefix(attr.Name, "system.posix_acl_") {
|
||||
na := acl{}
|
||||
na.decode(attr.Value)
|
||||
|
||||
if na.String() != "" {
|
||||
if strings.Contains(attr.Name, "system.posix_acl_access") {
|
||||
tmpMap["SCHILY.acl.access"] = na.String()
|
||||
} else if strings.Contains(attr.Name, "system.posix_acl_default") {
|
||||
tmpMap["SCHILY.acl.default"] = na.String()
|
||||
// Check for Linux POSIX.1e ACLs.
|
||||
//
|
||||
// TODO support ACLs from other operating systems.
|
||||
// FreeBSD ACLs have names "posix1e.acl_(access|default)",
|
||||
// but their binary format may not match the Linux format.
|
||||
aclKey := ""
|
||||
switch attr.Name {
|
||||
case "system.posix_acl_access":
|
||||
aclKey = "SCHILY.acl.access"
|
||||
case "system.posix_acl_default":
|
||||
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 {
|
||||
tmpMap["SCHILY.xattr."+attr.Name] = attrString
|
||||
tmpMap["SCHILY.xattr."+attr.Name] = string(attr.Value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue