aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/system/acl
diff options
context:
space:
mode:
Diffstat (limited to 'internal/system/acl')
-rw-r--r--internal/system/acl/acl.go33
-rw-r--r--internal/system/acl/acl_test.go276
-rw-r--r--internal/system/acl/libacl-helper.c90
-rw-r--r--internal/system/acl/libacl-helper.go40
-rw-r--r--internal/system/acl/libacl-helper.h4
-rw-r--r--internal/system/acl/libacl-helper_test.go60
-rw-r--r--internal/system/acl/perms.go18
-rw-r--r--internal/system/acl/perms_test.go30
8 files changed, 551 insertions, 0 deletions
diff --git a/internal/system/acl/acl.go b/internal/system/acl/acl.go
new file mode 100644
index 00000000..366ad533
--- /dev/null
+++ b/internal/system/acl/acl.go
@@ -0,0 +1,33 @@
+// Package acl implements simple ACL manipulation via libacl.
+package acl
+
+/*
+#cgo linux pkg-config: --static libacl
+
+#include "libacl-helper.h"
+*/
+import "C"
+
+type Perm C.acl_perm_t
+
+const (
+ Read Perm = C.ACL_READ
+ Write Perm = C.ACL_WRITE
+ Execute Perm = C.ACL_EXECUTE
+)
+
+// Update replaces ACL_USER entry with qualifier uid.
+func Update(name string, uid int, perms ...Perm) error {
+ var p *Perm
+ if len(perms) > 0 {
+ p = &perms[0]
+ }
+
+ r, err := C.hakurei_acl_update_file_by_uid(
+ C.CString(name),
+ C.uid_t(uid),
+ (*C.acl_perm_t)(p),
+ C.size_t(len(perms)),
+ )
+ return newAclPathError(name, int(r), err)
+}
diff --git a/internal/system/acl/acl_test.go b/internal/system/acl/acl_test.go
new file mode 100644
index 00000000..b0dd58d7
--- /dev/null
+++ b/internal/system/acl/acl_test.go
@@ -0,0 +1,276 @@
+package acl_test
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path"
+ "reflect"
+ "strconv"
+ "testing"
+
+ "hakurei.app/internal/system/acl"
+)
+
+const testFileName = "acl.test"
+
+var (
+ uid = os.Geteuid()
+ cred = int32(os.Geteuid())
+)
+
+func TestUpdate(t *testing.T) {
+ if os.Getenv("GO_TEST_SKIP_ACL") == "1" {
+ t.Log("acl test skipped")
+ t.SkipNow()
+ }
+
+ testFilePath := path.Join(t.TempDir(), testFileName)
+
+ if f, err := os.Create(testFilePath); err != nil {
+ t.Fatalf("Create: error = %v", err)
+ } else {
+ if err = f.Close(); err != nil {
+ t.Fatalf("Close: error = %v", err)
+ }
+ }
+ defer func() {
+ if err := os.Remove(testFilePath); err != nil {
+ t.Fatalf("Remove: error = %v", err)
+ }
+ }()
+
+ cur := getfacl(t, testFilePath)
+
+ t.Run("default entry count", func(t *testing.T) {
+ if len(cur) != 3 {
+ t.Fatalf("unexpected test file acl length %d", len(cur))
+ }
+ })
+
+ t.Run("default clear mask", func(t *testing.T) {
+ if err := acl.Update(testFilePath, uid); err != nil {
+ t.Fatalf("Update: error = %v", err)
+ }
+ if cur = getfacl(t, testFilePath); len(cur) != 4 {
+ t.Fatalf("Update: %v", cur)
+ }
+ })
+
+ t.Run("default clear consistency", func(t *testing.T) {
+ if err := acl.Update(testFilePath, uid); err != nil {
+ t.Fatalf("Update: error = %v", err)
+ }
+ if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) {
+ t.Fatalf("Update: %v, want %v", val, cur)
+ }
+ })
+
+ testUpdate(t, testFilePath, "r--", cur, fAclPermRead, acl.Read)
+ testUpdate(t, testFilePath, "-w-", cur, fAclPermWrite, acl.Write)
+ testUpdate(t, testFilePath, "--x", cur, fAclPermExecute, acl.Execute)
+ testUpdate(t, testFilePath, "-wx", cur, fAclPermWrite|fAclPermExecute, acl.Write, acl.Execute)
+ testUpdate(t, testFilePath, "r-x", cur, fAclPermRead|fAclPermExecute, acl.Read, acl.Execute)
+ testUpdate(t, testFilePath, "rw-", cur, fAclPermRead|fAclPermWrite, acl.Read, acl.Write)
+ testUpdate(t, testFilePath, "rwx", cur, fAclPermRead|fAclPermWrite|fAclPermExecute, acl.Read, acl.Write, acl.Execute)
+}
+
+func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val fAclPerm, perms ...acl.Perm) {
+ t.Run(name, func(t *testing.T) {
+ t.Cleanup(func() {
+ if err := acl.Update(testFilePath, uid); err != nil {
+ t.Fatalf("Update: error = %v", err)
+ }
+ if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) {
+ t.Fatalf("Update: %v, want %v", v, cur)
+ }
+ })
+
+ if err := acl.Update(testFilePath, uid, perms...); err != nil {
+ t.Fatalf("Update: error = %v", err)
+ }
+ r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred)
+ if r == nil {
+ t.Fatalf("Update did not add an ACL entry")
+ }
+ if !r.equals(fAclTypeUser, cred, val) {
+ t.Fatalf("Update(%s) = %s", name, r)
+ }
+ })
+}
+
+type (
+ getFAclInvocation struct {
+ cmd *exec.Cmd
+ val []*getFAclResp
+ pe []error
+ }
+
+ getFAclResp struct {
+ typ fAclType
+ cred int32
+ val fAclPerm
+
+ raw []byte
+ }
+
+ fAclPerm uintptr
+ fAclType uint8
+)
+
+const fAclBufSize = 16
+
+const (
+ fAclPermRead fAclPerm = 1 << iota
+ fAclPermWrite
+ fAclPermExecute
+)
+
+const (
+ fAclTypeUser fAclType = iota
+ fAclTypeGroup
+ fAclTypeMask
+ fAclTypeOther
+)
+
+func (c *getFAclInvocation) run(name string) error {
+ if c.cmd != nil {
+ panic("attempted to run twice")
+ }
+
+ c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name)
+
+ scanErr := make(chan error, 1)
+ if p, err := c.cmd.StdoutPipe(); err != nil {
+ return err
+ } else {
+ go c.parse(p, scanErr)
+ }
+
+ if err := c.cmd.Start(); err != nil {
+ return err
+ }
+
+ return errors.Join(<-scanErr, c.cmd.Wait())
+}
+
+func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
+ c.val = make([]*getFAclResp, 0, 4+fAclBufSize)
+
+ s := bufio.NewScanner(pipe)
+ for s.Scan() {
+ fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3)
+ if len(fields) != 3 {
+ continue
+ }
+
+ resp := getFAclResp{}
+
+ switch string(fields[0]) {
+ case "user":
+ resp.typ = fAclTypeUser
+ case "group":
+ resp.typ = fAclTypeGroup
+ case "mask":
+ resp.typ = fAclTypeMask
+ case "other":
+ resp.typ = fAclTypeOther
+ default:
+ c.pe = append(c.pe, fmt.Errorf("unknown type %s", string(fields[0])))
+ continue
+ }
+
+ if len(fields[1]) == 0 {
+ resp.cred = -1
+ } else {
+ if cred, err := strconv.Atoi(string(fields[1])); err != nil {
+ c.pe = append(c.pe, err)
+ continue
+ } else {
+ resp.cred = int32(cred)
+ if resp.cred < 0 {
+ c.pe = append(c.pe, fmt.Errorf("credential %d out of range", resp.cred))
+ continue
+ }
+ }
+ }
+
+ if len(fields[2]) != 3 {
+ c.pe = append(c.pe, fmt.Errorf("invalid perm length %d", len(fields[2])))
+ continue
+ } else {
+ switch fields[2][0] {
+ case 'r':
+ resp.val |= fAclPermRead
+ case '-':
+ default:
+ c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0]))
+ continue
+ }
+ switch fields[2][1] {
+ case 'w':
+ resp.val |= fAclPermWrite
+ case '-':
+ default:
+ c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1]))
+ continue
+ }
+ switch fields[2][2] {
+ case 'x':
+ resp.val |= fAclPermExecute
+ case '-':
+ default:
+ c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2]))
+ continue
+ }
+ }
+
+ resp.raw = make([]byte, len(s.Bytes()))
+ copy(resp.raw, s.Bytes())
+ c.val = append(c.val, &resp)
+ }
+ scanErr <- s.Err()
+}
+
+func (r *getFAclResp) String() string {
+ if r.raw != nil && len(r.raw) > 0 {
+ return string(r.raw)
+ }
+
+ return "(user-initialised resp value)"
+}
+
+func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool {
+ return r.typ == typ && r.cred == cred && r.val == val
+}
+
+func getfacl(t *testing.T, name string) []*getFAclResp {
+ c := new(getFAclInvocation)
+ if err := c.run(name); err != nil {
+ t.Fatalf("getfacl: error = %v", err)
+ }
+ if len(c.pe) != 0 {
+ t.Errorf("errors encountered parsing getfacl output\n%s", errors.Join(c.pe...).Error())
+ }
+ return c.val
+}
+
+func respByCred(v []*getFAclResp, typ fAclType, cred int32) *getFAclResp {
+ j := -1
+ for i, r := range v {
+ if r.typ == typ && r.cred == cred {
+ if j != -1 {
+ panic("invalid acl")
+ }
+ j = i
+ }
+ }
+ if j == -1 {
+ return nil
+ }
+ return v[j]
+}
diff --git a/internal/system/acl/libacl-helper.c b/internal/system/acl/libacl-helper.c
new file mode 100644
index 00000000..d1df8167
--- /dev/null
+++ b/internal/system/acl/libacl-helper.c
@@ -0,0 +1,90 @@
+#include "libacl-helper.h"
+#include <acl/libacl.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <sys/acl.h>
+
+int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
+ acl_perm_t *perms, size_t plen) {
+ int ret;
+ bool v;
+ int i;
+ acl_t acl;
+ acl_entry_t entry;
+ acl_tag_t tag_type;
+ void *qualifier_p;
+ acl_permset_t permset;
+
+ ret = -1; /* acl_get_file */
+ acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
+ if (acl == NULL)
+ goto out;
+
+ /* prune entries by uid */
+ for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
+ i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
+ ret = -2; /* acl_get_tag_type */
+ if (acl_get_tag_type(entry, &tag_type) != 0)
+ goto out;
+ if (tag_type != ACL_USER)
+ continue;
+
+ ret = -3; /* acl_get_qualifier */
+ qualifier_p = acl_get_qualifier(entry);
+ if (qualifier_p == NULL)
+ goto out;
+ v = *(uid_t *)qualifier_p == uid;
+ acl_free(qualifier_p);
+
+ if (!v)
+ continue;
+
+ ret = -4; /* acl_delete_entry */
+ if (acl_delete_entry(acl, entry) != 0)
+ goto out;
+ }
+
+ if (plen == 0)
+ goto set;
+
+ ret = -5; /* acl_create_entry */
+ if (acl_create_entry(&acl, &entry) != 0)
+ goto out;
+
+ ret = -6; /* acl_get_permset */
+ if (acl_get_permset(entry, &permset) != 0)
+ goto out;
+
+ ret = -7; /* acl_add_perm */
+ for (i = 0; i < plen; i++) {
+ if (acl_add_perm(permset, perms[i]) != 0)
+ goto out;
+ }
+
+ ret = -8; /* acl_set_tag_type */
+ if (acl_set_tag_type(entry, ACL_USER) != 0)
+ goto out;
+
+ ret = -9; /* acl_set_qualifier */
+ if (acl_set_qualifier(entry, (void *)&uid) != 0)
+ goto out;
+
+set:
+ ret = -10; /* acl_calc_mask */
+ if (acl_calc_mask(&acl) != 0)
+ goto out;
+
+ ret = -11; /* acl_valid */
+ if (acl_valid(acl) != 0)
+ goto out;
+
+ ret = -12; /* acl_set_file */
+ if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
+ ret = 0;
+
+out:
+ free((void *)path_p);
+ if (acl != NULL)
+ acl_free((void *)acl);
+ return ret;
+}
diff --git a/internal/system/acl/libacl-helper.go b/internal/system/acl/libacl-helper.go
new file mode 100644
index 00000000..22575296
--- /dev/null
+++ b/internal/system/acl/libacl-helper.go
@@ -0,0 +1,40 @@
+package acl
+
+import "os"
+
+func newAclPathError(name string, r int, err error) error {
+ pathError := &os.PathError{Path: name, Err: err}
+ switch r {
+ case 0:
+ return nil
+
+ case -1:
+ pathError.Op = "acl_get_file"
+ case -2:
+ pathError.Op = "acl_get_tag_type"
+ case -3:
+ pathError.Op = "acl_get_qualifier"
+ case -4:
+ pathError.Op = "acl_delete_entry"
+ case -5:
+ pathError.Op = "acl_create_entry"
+ case -6:
+ pathError.Op = "acl_get_permset"
+ case -7:
+ pathError.Op = "acl_add_perm"
+ case -8:
+ pathError.Op = "acl_set_tag_type"
+ case -9:
+ pathError.Op = "acl_set_qualifier"
+ case -10:
+ pathError.Op = "acl_calc_mask"
+ case -11:
+ pathError.Op = "acl_valid"
+ case -12:
+ pathError.Op = "acl_set_file"
+
+ default: // unreachable
+ pathError.Op = "setfacl"
+ }
+ return pathError
+}
diff --git a/internal/system/acl/libacl-helper.h b/internal/system/acl/libacl-helper.h
new file mode 100644
index 00000000..b86eb170
--- /dev/null
+++ b/internal/system/acl/libacl-helper.h
@@ -0,0 +1,4 @@
+#include <sys/acl.h>
+
+int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
+ acl_perm_t *perms, size_t plen);
diff --git a/internal/system/acl/libacl-helper_test.go b/internal/system/acl/libacl-helper_test.go
new file mode 100644
index 00000000..13d23a3c
--- /dev/null
+++ b/internal/system/acl/libacl-helper_test.go
@@ -0,0 +1,60 @@
+package acl
+
+import (
+ "os"
+ "reflect"
+ "syscall"
+ "testing"
+
+ "hakurei.app/container"
+)
+
+func TestNewAclPathError(t *testing.T) {
+ testCases := []struct {
+ name string
+ path string
+ r int
+ err error
+ want error
+ }{
+ {"nil", container.Nonexistent, 0, syscall.ENOTRECOVERABLE, nil},
+
+ {"acl_get_file", container.Nonexistent, -1, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_get_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"acl_get_tag_type", container.Nonexistent, -2, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_get_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"acl_get_qualifier", container.Nonexistent, -3, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_get_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"acl_delete_entry", container.Nonexistent, -4, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_delete_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"acl_create_entry", container.Nonexistent, -5, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_create_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"acl_get_permset", container.Nonexistent, -6, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_get_permset", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"acl_add_perm", container.Nonexistent, -7, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_add_perm", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"acl_set_tag_type", container.Nonexistent, -8, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_set_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"acl_set_qualifier", container.Nonexistent, -9, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_set_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"acl_calc_mask", container.Nonexistent, -10, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_calc_mask", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"acl_valid", container.Nonexistent, -11, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_valid", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"acl_set_file", container.Nonexistent, -12, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "acl_set_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+
+ {"acl", container.Nonexistent, -13, syscall.ENOTRECOVERABLE,
+ &os.PathError{Op: "setfacl", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
+ {"invalid", container.Nonexistent, -0xdead, nil,
+ &os.PathError{Op: "setfacl", Path: container.Nonexistent}},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := newAclPathError(tc.path, tc.r, tc.err)
+ if !reflect.DeepEqual(err, tc.want) {
+ t.Errorf("newAclPathError: %v, want %v", err, tc.want)
+ }
+ })
+ }
+}
diff --git a/internal/system/acl/perms.go b/internal/system/acl/perms.go
new file mode 100644
index 00000000..fbc99e82
--- /dev/null
+++ b/internal/system/acl/perms.go
@@ -0,0 +1,18 @@
+package acl
+
+type Perms []Perm
+
+func (ps Perms) String() string {
+ var s = []byte("---")
+ for _, p := range ps {
+ switch p {
+ case Read:
+ s[0] = 'r'
+ case Write:
+ s[1] = 'w'
+ case Execute:
+ s[2] = 'x'
+ }
+ }
+ return string(s)
+}
diff --git a/internal/system/acl/perms_test.go b/internal/system/acl/perms_test.go
new file mode 100644
index 00000000..3d5e3df5
--- /dev/null
+++ b/internal/system/acl/perms_test.go
@@ -0,0 +1,30 @@
+package acl_test
+
+import (
+ "testing"
+
+ "hakurei.app/internal/system/acl"
+)
+
+func TestPerms(t *testing.T) {
+ testCases := []struct {
+ name string
+ perms acl.Perms
+ }{
+ {"---", acl.Perms{}},
+ {"r--", acl.Perms{acl.Read}},
+ {"-w-", acl.Perms{acl.Write}},
+ {"--x", acl.Perms{acl.Execute}},
+ {"rw-", acl.Perms{acl.Read, acl.Read, acl.Write}},
+ {"r-x", acl.Perms{acl.Read, acl.Execute, acl.Execute}},
+ {"-wx", acl.Perms{acl.Write, acl.Write, acl.Execute, acl.Execute}},
+ {"rwx", acl.Perms{acl.Read, acl.Write, acl.Execute}},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ if got := tc.perms.String(); got != tc.name {
+ t.Errorf("String: %q, want %q", got, tc.name)
+ }
+ })
+ }
+}