diff options
Diffstat (limited to 'system/acl')
| -rw-r--r-- | system/acl/acl.go | 36 | ||||
| -rw-r--r-- | system/acl/acl_getfacl_test.go | 156 | ||||
| -rw-r--r-- | system/acl/acl_test.go | 125 | ||||
| -rw-r--r-- | system/acl/libacl-helper.c | 71 | ||||
| -rw-r--r-- | system/acl/libacl-helper.h | 4 | ||||
| -rw-r--r-- | system/acl/perms.go | 18 |
6 files changed, 410 insertions, 0 deletions
diff --git a/system/acl/acl.go b/system/acl/acl.go new file mode 100644 index 00000000..bc590db2 --- /dev/null +++ b/system/acl/acl.go @@ -0,0 +1,36 @@ +// 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)), + ) + if r == 0 { + return nil + } + return err +} diff --git a/system/acl/acl_getfacl_test.go b/system/acl/acl_getfacl_test.go new file mode 100644 index 00000000..c20fade7 --- /dev/null +++ b/system/acl/acl_getfacl_test.go @@ -0,0 +1,156 @@ +package acl_test + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "os/exec" + "strconv" +) + +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 +} diff --git a/system/acl/acl_test.go b/system/acl/acl_test.go new file mode 100644 index 00000000..bfb355e0 --- /dev/null +++ b/system/acl/acl_test.go @@ -0,0 +1,125 @@ +package acl_test + +import ( + "errors" + "os" + "path" + "reflect" + "testing" + + "git.gensokyo.uk/security/hakurei/system/acl" +) + +const testFileName = "acl.test" + +var ( + uid = os.Geteuid() + cred = int32(os.Geteuid()) +) + +func TestUpdatePerm(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("UpdatePerm: error = %v", err) + } + if cur = getfacl(t, testFilePath); len(cur) != 4 { + t.Fatalf("UpdatePerm: %v", cur) + } + }) + + t.Run("default clear consistency", func(t *testing.T) { + if err := acl.Update(testFilePath, uid); err != nil { + t.Fatalf("UpdatePerm: error = %v", err) + } + if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) { + t.Fatalf("UpdatePerm: %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("UpdatePerm: error = %v", err) + } + if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) { + t.Fatalf("UpdatePerm: %v, want %v", v, cur) + } + }) + + if err := acl.Update(testFilePath, uid, perms...); err != nil { + t.Fatalf("UpdatePerm: error = %v", err) + } + r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred) + if r == nil { + t.Fatalf("UpdatePerm did not add an ACL entry") + } + if !r.equals(fAclTypeUser, cred, val) { + t.Fatalf("UpdatePerm(%s) = %s", name, r) + } + }) +} + +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/system/acl/libacl-helper.c b/system/acl/libacl-helper.c new file mode 100644 index 00000000..905cfa52 --- /dev/null +++ b/system/acl/libacl-helper.c @@ -0,0 +1,71 @@ +#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 = -1; + bool v; + int i; + acl_t acl; + acl_entry_t entry; + acl_tag_t tag_type; + void *qualifier_p; + acl_permset_t permset; + + 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)) { + if (acl_get_tag_type(entry, &tag_type) != 0) + return -1; + if (tag_type != ACL_USER) + continue; + + qualifier_p = acl_get_qualifier(entry); + if (qualifier_p == NULL) + return -1; + v = *(uid_t *)qualifier_p == uid; + acl_free(qualifier_p); + + if (!v) + continue; + + acl_delete_entry(acl, entry); + } + + if (plen == 0) + goto set; + + if (acl_create_entry(&acl, &entry) != 0) + goto out; + if (acl_get_permset(entry, &permset) != 0) + goto out; + for (i = 0; i < plen; i++) { + if (acl_add_perm(permset, perms[i]) != 0) + goto out; + } + if (acl_set_tag_type(entry, ACL_USER) != 0) + goto out; + if (acl_set_qualifier(entry, (void *)&uid) != 0) + goto out; + +set: + if (acl_calc_mask(&acl) != 0) + goto out; + if (acl_valid(acl) != 0) + goto out; + 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/system/acl/libacl-helper.h b/system/acl/libacl-helper.h new file mode 100644 index 00000000..b86eb170 --- /dev/null +++ b/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/system/acl/perms.go b/system/acl/perms.go new file mode 100644 index 00000000..fbc99e82 --- /dev/null +++ b/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) +} |
