mirror of
https://github.com/morgan9e/systemd
synced 2026-04-14 08:25:20 +09:00
When the credential dir is backed by an fs that supports ACLs we must be more careful with adjusting the 'x' bit of the directory, as any chmod() call on the dir will reset the mask entry of the ACL entirely which we don't want. Hence, do a manual set of ACL changes, that only add/drop the 'x' bit but otherwise leave the ACL as it is. This matters if we use tmpfs rather than ramfs to store credentials.
653 lines
18 KiB
C
653 lines
18 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||
|
||
#include <errno.h>
|
||
#include <stdbool.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/types.h>
|
||
|
||
#include "acl-util.h"
|
||
#include "alloc-util.h"
|
||
#include "errno-util.h"
|
||
#include "string-util.h"
|
||
#include "strv.h"
|
||
#include "user-util.h"
|
||
|
||
#if HAVE_ACL
|
||
|
||
int acl_find_uid(acl_t acl, uid_t uid, acl_entry_t *ret_entry) {
|
||
acl_entry_t i;
|
||
int r;
|
||
|
||
assert(acl);
|
||
assert(uid_is_valid(uid));
|
||
assert(ret_entry);
|
||
|
||
for (r = acl_get_entry(acl, ACL_FIRST_ENTRY, &i);
|
||
r > 0;
|
||
r = acl_get_entry(acl, ACL_NEXT_ENTRY, &i)) {
|
||
|
||
acl_tag_t tag;
|
||
uid_t *u;
|
||
bool b;
|
||
|
||
if (acl_get_tag_type(i, &tag) < 0)
|
||
return -errno;
|
||
|
||
if (tag != ACL_USER)
|
||
continue;
|
||
|
||
u = acl_get_qualifier(i);
|
||
if (!u)
|
||
return -errno;
|
||
|
||
b = *u == uid;
|
||
acl_free(u);
|
||
|
||
if (b) {
|
||
*ret_entry = i;
|
||
return 1;
|
||
}
|
||
}
|
||
if (r < 0)
|
||
return -errno;
|
||
|
||
*ret_entry = NULL;
|
||
return 0;
|
||
}
|
||
|
||
int calc_acl_mask_if_needed(acl_t *acl_p) {
|
||
acl_entry_t i;
|
||
int r;
|
||
bool need = false;
|
||
|
||
assert(acl_p);
|
||
|
||
for (r = acl_get_entry(*acl_p, ACL_FIRST_ENTRY, &i);
|
||
r > 0;
|
||
r = acl_get_entry(*acl_p, ACL_NEXT_ENTRY, &i)) {
|
||
acl_tag_t tag;
|
||
|
||
if (acl_get_tag_type(i, &tag) < 0)
|
||
return -errno;
|
||
|
||
if (tag == ACL_MASK)
|
||
return 0;
|
||
|
||
if (IN_SET(tag, ACL_USER, ACL_GROUP))
|
||
need = true;
|
||
}
|
||
if (r < 0)
|
||
return -errno;
|
||
|
||
if (need && acl_calc_mask(acl_p) < 0)
|
||
return -errno;
|
||
|
||
return need;
|
||
}
|
||
|
||
int add_base_acls_if_needed(acl_t *acl_p, const char *path) {
|
||
acl_entry_t i;
|
||
int r;
|
||
bool have_user_obj = false, have_group_obj = false, have_other = false;
|
||
struct stat st;
|
||
_cleanup_(acl_freep) acl_t basic = NULL;
|
||
|
||
assert(acl_p);
|
||
assert(path);
|
||
|
||
for (r = acl_get_entry(*acl_p, ACL_FIRST_ENTRY, &i);
|
||
r > 0;
|
||
r = acl_get_entry(*acl_p, ACL_NEXT_ENTRY, &i)) {
|
||
acl_tag_t tag;
|
||
|
||
if (acl_get_tag_type(i, &tag) < 0)
|
||
return -errno;
|
||
|
||
if (tag == ACL_USER_OBJ)
|
||
have_user_obj = true;
|
||
else if (tag == ACL_GROUP_OBJ)
|
||
have_group_obj = true;
|
||
else if (tag == ACL_OTHER)
|
||
have_other = true;
|
||
if (have_user_obj && have_group_obj && have_other)
|
||
return 0;
|
||
}
|
||
if (r < 0)
|
||
return -errno;
|
||
|
||
r = stat(path, &st);
|
||
if (r < 0)
|
||
return -errno;
|
||
|
||
basic = acl_from_mode(st.st_mode);
|
||
if (!basic)
|
||
return -errno;
|
||
|
||
for (r = acl_get_entry(basic, ACL_FIRST_ENTRY, &i);
|
||
r > 0;
|
||
r = acl_get_entry(basic, ACL_NEXT_ENTRY, &i)) {
|
||
acl_tag_t tag;
|
||
acl_entry_t dst;
|
||
|
||
if (acl_get_tag_type(i, &tag) < 0)
|
||
return -errno;
|
||
|
||
if ((tag == ACL_USER_OBJ && have_user_obj) ||
|
||
(tag == ACL_GROUP_OBJ && have_group_obj) ||
|
||
(tag == ACL_OTHER && have_other))
|
||
continue;
|
||
|
||
r = acl_create_entry(acl_p, &dst);
|
||
if (r < 0)
|
||
return -errno;
|
||
|
||
r = acl_copy_entry(dst, i);
|
||
if (r < 0)
|
||
return -errno;
|
||
}
|
||
if (r < 0)
|
||
return -errno;
|
||
return 0;
|
||
}
|
||
|
||
int acl_search_groups(const char *path, char ***ret_groups) {
|
||
_cleanup_strv_free_ char **g = NULL;
|
||
_cleanup_(acl_freep) acl_t acl = NULL;
|
||
bool ret = false;
|
||
acl_entry_t entry;
|
||
int r;
|
||
|
||
assert(path);
|
||
|
||
acl = acl_get_file(path, ACL_TYPE_DEFAULT);
|
||
if (!acl)
|
||
return -errno;
|
||
|
||
r = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
|
||
for (;;) {
|
||
_cleanup_(acl_free_gid_tpp) gid_t *gid = NULL;
|
||
acl_tag_t tag;
|
||
|
||
if (r < 0)
|
||
return -errno;
|
||
if (r == 0)
|
||
break;
|
||
|
||
if (acl_get_tag_type(entry, &tag) < 0)
|
||
return -errno;
|
||
|
||
if (tag != ACL_GROUP)
|
||
goto next;
|
||
|
||
gid = acl_get_qualifier(entry);
|
||
if (!gid)
|
||
return -errno;
|
||
|
||
if (in_gid(*gid) > 0) {
|
||
if (!ret_groups)
|
||
return true;
|
||
|
||
ret = true;
|
||
}
|
||
|
||
if (ret_groups) {
|
||
char *name;
|
||
|
||
name = gid_to_name(*gid);
|
||
if (!name)
|
||
return -ENOMEM;
|
||
|
||
r = strv_consume(&g, name);
|
||
if (r < 0)
|
||
return r;
|
||
}
|
||
|
||
next:
|
||
r = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry);
|
||
}
|
||
|
||
if (ret_groups)
|
||
*ret_groups = TAKE_PTR(g);
|
||
|
||
return ret;
|
||
}
|
||
|
||
int parse_acl(
|
||
const char *text,
|
||
acl_t *ret_acl_access,
|
||
acl_t *ret_acl_access_exec, /* extra rules to apply to inodes subject to uppercase X handling */
|
||
acl_t *ret_acl_default,
|
||
bool want_mask) {
|
||
|
||
_cleanup_strv_free_ char **a = NULL, **e = NULL, **d = NULL, **split = NULL;
|
||
_cleanup_(acl_freep) acl_t a_acl = NULL, e_acl = NULL, d_acl = NULL;
|
||
int r;
|
||
|
||
assert(text);
|
||
assert(ret_acl_access);
|
||
assert(ret_acl_access_exec);
|
||
assert(ret_acl_default);
|
||
|
||
split = strv_split(text, ",");
|
||
if (!split)
|
||
return -ENOMEM;
|
||
|
||
STRV_FOREACH(entry, split) {
|
||
_cleanup_strv_free_ char **entry_split = NULL;
|
||
_cleanup_free_ char *entry_join = NULL;
|
||
int n;
|
||
|
||
n = strv_split_full(&entry_split, *entry, ":", EXTRACT_DONT_COALESCE_SEPARATORS|EXTRACT_RETAIN_ESCAPE);
|
||
if (n < 0)
|
||
return n;
|
||
|
||
if (n < 3 || n > 4)
|
||
return -EINVAL;
|
||
|
||
string_replace_char(entry_split[n-1], 'X', 'x');
|
||
|
||
if (n == 4) {
|
||
if (!STR_IN_SET(entry_split[0], "default", "d"))
|
||
return -EINVAL;
|
||
|
||
entry_join = strv_join(entry_split + 1, ":");
|
||
if (!entry_join)
|
||
return -ENOMEM;
|
||
|
||
r = strv_consume(&d, TAKE_PTR(entry_join));
|
||
} else { /* n == 3 */
|
||
entry_join = strv_join(entry_split, ":");
|
||
if (!entry_join)
|
||
return -ENOMEM;
|
||
|
||
if (!streq(*entry, entry_join))
|
||
r = strv_consume(&e, TAKE_PTR(entry_join));
|
||
else
|
||
r = strv_consume(&a, TAKE_PTR(entry_join));
|
||
}
|
||
if (r < 0)
|
||
return r;
|
||
}
|
||
|
||
if (!strv_isempty(a)) {
|
||
_cleanup_free_ char *join = NULL;
|
||
|
||
join = strv_join(a, ",");
|
||
if (!join)
|
||
return -ENOMEM;
|
||
|
||
a_acl = acl_from_text(join);
|
||
if (!a_acl)
|
||
return -errno;
|
||
|
||
if (want_mask) {
|
||
r = calc_acl_mask_if_needed(&a_acl);
|
||
if (r < 0)
|
||
return r;
|
||
}
|
||
}
|
||
|
||
if (!strv_isempty(e)) {
|
||
_cleanup_free_ char *join = NULL;
|
||
|
||
join = strv_join(e, ",");
|
||
if (!join)
|
||
return -ENOMEM;
|
||
|
||
e_acl = acl_from_text(join);
|
||
if (!e_acl)
|
||
return -errno;
|
||
|
||
/* The mask must be calculated after deciding whether the execute bit should be set. */
|
||
}
|
||
|
||
if (!strv_isempty(d)) {
|
||
_cleanup_free_ char *join = NULL;
|
||
|
||
join = strv_join(d, ",");
|
||
if (!join)
|
||
return -ENOMEM;
|
||
|
||
d_acl = acl_from_text(join);
|
||
if (!d_acl)
|
||
return -errno;
|
||
|
||
if (want_mask) {
|
||
r = calc_acl_mask_if_needed(&d_acl);
|
||
if (r < 0)
|
||
return r;
|
||
}
|
||
}
|
||
|
||
*ret_acl_access = TAKE_PTR(a_acl);
|
||
*ret_acl_access_exec = TAKE_PTR(e_acl);
|
||
*ret_acl_default = TAKE_PTR(d_acl);
|
||
|
||
return 0;
|
||
}
|
||
|
||
static int acl_entry_equal(acl_entry_t a, acl_entry_t b) {
|
||
acl_tag_t tag_a, tag_b;
|
||
|
||
if (acl_get_tag_type(a, &tag_a) < 0)
|
||
return -errno;
|
||
|
||
if (acl_get_tag_type(b, &tag_b) < 0)
|
||
return -errno;
|
||
|
||
if (tag_a != tag_b)
|
||
return false;
|
||
|
||
switch (tag_a) {
|
||
case ACL_USER_OBJ:
|
||
case ACL_GROUP_OBJ:
|
||
case ACL_MASK:
|
||
case ACL_OTHER:
|
||
/* can have only one of those */
|
||
return true;
|
||
case ACL_USER: {
|
||
_cleanup_(acl_free_uid_tpp) uid_t *uid_a = NULL, *uid_b = NULL;
|
||
|
||
uid_a = acl_get_qualifier(a);
|
||
if (!uid_a)
|
||
return -errno;
|
||
|
||
uid_b = acl_get_qualifier(b);
|
||
if (!uid_b)
|
||
return -errno;
|
||
|
||
return *uid_a == *uid_b;
|
||
}
|
||
case ACL_GROUP: {
|
||
_cleanup_(acl_free_gid_tpp) gid_t *gid_a = NULL, *gid_b = NULL;
|
||
|
||
gid_a = acl_get_qualifier(a);
|
||
if (!gid_a)
|
||
return -errno;
|
||
|
||
gid_b = acl_get_qualifier(b);
|
||
if (!gid_b)
|
||
return -errno;
|
||
|
||
return *gid_a == *gid_b;
|
||
}
|
||
default:
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
static int find_acl_entry(acl_t acl, acl_entry_t entry, acl_entry_t *ret) {
|
||
acl_entry_t i;
|
||
int r;
|
||
|
||
for (r = acl_get_entry(acl, ACL_FIRST_ENTRY, &i);
|
||
r > 0;
|
||
r = acl_get_entry(acl, ACL_NEXT_ENTRY, &i)) {
|
||
|
||
r = acl_entry_equal(i, entry);
|
||
if (r < 0)
|
||
return r;
|
||
if (r > 0) {
|
||
if (ret)
|
||
*ret = i;
|
||
return 0;
|
||
}
|
||
}
|
||
if (r < 0)
|
||
return -errno;
|
||
|
||
return -ENOENT;
|
||
}
|
||
|
||
int acls_for_file(const char *path, acl_type_t type, acl_t acl, acl_t *ret) {
|
||
_cleanup_(acl_freep) acl_t applied = NULL;
|
||
acl_entry_t i;
|
||
int r;
|
||
|
||
assert(path);
|
||
|
||
applied = acl_get_file(path, type);
|
||
if (!applied)
|
||
return -errno;
|
||
|
||
for (r = acl_get_entry(acl, ACL_FIRST_ENTRY, &i);
|
||
r > 0;
|
||
r = acl_get_entry(acl, ACL_NEXT_ENTRY, &i)) {
|
||
|
||
acl_entry_t j;
|
||
|
||
r = find_acl_entry(applied, i, &j);
|
||
if (r == -ENOENT) {
|
||
if (acl_create_entry(&applied, &j) < 0)
|
||
return -errno;
|
||
} else if (r < 0)
|
||
return r;
|
||
|
||
if (acl_copy_entry(j, i) < 0)
|
||
return -errno;
|
||
}
|
||
if (r < 0)
|
||
return -errno;
|
||
|
||
if (ret)
|
||
*ret = TAKE_PTR(applied);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* POSIX says that ACL_{READ,WRITE,EXECUTE} don't have to be bitmasks. But that is a natural thing to do and
|
||
* all extant implementations do it. Let's make sure that we fail verbosely in the (imho unlikely) scenario
|
||
* that we get a new implementation that does not satisfy this. */
|
||
assert_cc(!(ACL_READ & ACL_WRITE));
|
||
assert_cc(!(ACL_WRITE & ACL_EXECUTE));
|
||
assert_cc(!(ACL_EXECUTE & ACL_READ));
|
||
assert_cc((unsigned) ACL_READ == ACL_READ);
|
||
assert_cc((unsigned) ACL_WRITE == ACL_WRITE);
|
||
assert_cc((unsigned) ACL_EXECUTE == ACL_EXECUTE);
|
||
|
||
int fd_add_uid_acl_permission(
|
||
int fd,
|
||
uid_t uid,
|
||
unsigned mask) {
|
||
|
||
_cleanup_(acl_freep) acl_t acl = NULL;
|
||
acl_permset_t permset;
|
||
acl_entry_t entry;
|
||
int r;
|
||
|
||
/* Adds an ACL entry for the specified file to allow the indicated access to the specified
|
||
* user. Operates purely incrementally. */
|
||
|
||
assert(fd >= 0);
|
||
assert(uid_is_valid(uid));
|
||
|
||
acl = acl_get_fd(fd);
|
||
if (!acl)
|
||
return -errno;
|
||
|
||
r = acl_find_uid(acl, uid, &entry);
|
||
if (r <= 0) {
|
||
if (acl_create_entry(&acl, &entry) < 0 ||
|
||
acl_set_tag_type(entry, ACL_USER) < 0 ||
|
||
acl_set_qualifier(entry, &uid) < 0)
|
||
return -errno;
|
||
}
|
||
|
||
if (acl_get_permset(entry, &permset) < 0)
|
||
return -errno;
|
||
|
||
if ((mask & ACL_READ) && acl_add_perm(permset, ACL_READ) < 0)
|
||
return -errno;
|
||
if ((mask & ACL_WRITE) && acl_add_perm(permset, ACL_WRITE) < 0)
|
||
return -errno;
|
||
if ((mask & ACL_EXECUTE) && acl_add_perm(permset, ACL_EXECUTE) < 0)
|
||
return -errno;
|
||
|
||
r = calc_acl_mask_if_needed(&acl);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
if (acl_set_fd(fd, acl) < 0)
|
||
return -errno;
|
||
|
||
return 0;
|
||
}
|
||
|
||
int fd_acl_make_read_only(int fd) {
|
||
_cleanup_(acl_freep) acl_t acl = NULL;
|
||
bool changed = false;
|
||
acl_entry_t i;
|
||
int r;
|
||
|
||
assert(fd >= 0);
|
||
|
||
/* Safely drops all W bits from all relevant ACL entries of the file, without changing entries which
|
||
* are masked by the ACL mask */
|
||
|
||
acl = acl_get_fd(fd);
|
||
if (!acl) {
|
||
|
||
if (!ERRNO_IS_NOT_SUPPORTED(errno))
|
||
return -errno;
|
||
|
||
/* No ACLs? Then just update the regular mode_t */
|
||
return fd_acl_make_read_only_fallback(fd);
|
||
}
|
||
|
||
for (r = acl_get_entry(acl, ACL_FIRST_ENTRY, &i);
|
||
r > 0;
|
||
r = acl_get_entry(acl, ACL_NEXT_ENTRY, &i)) {
|
||
acl_permset_t permset;
|
||
acl_tag_t tag;
|
||
int b;
|
||
|
||
if (acl_get_tag_type(i, &tag) < 0)
|
||
return -errno;
|
||
|
||
/* These three control the x bits overall (as ACL_MASK affects all remaining tags) */
|
||
if (!IN_SET(tag, ACL_USER_OBJ, ACL_MASK, ACL_OTHER))
|
||
continue;
|
||
|
||
if (acl_get_permset(i, &permset) < 0)
|
||
return -errno;
|
||
|
||
b = acl_get_perm(permset, ACL_WRITE);
|
||
if (b < 0)
|
||
return -errno;
|
||
|
||
if (b) {
|
||
if (acl_delete_perm(permset, ACL_WRITE) < 0)
|
||
return -errno;
|
||
|
||
changed = true;
|
||
}
|
||
}
|
||
if (r < 0)
|
||
return -errno;
|
||
|
||
if (!changed)
|
||
return 0;
|
||
|
||
if (acl_set_fd(fd, acl) < 0) {
|
||
if (!ERRNO_IS_NOT_SUPPORTED(errno))
|
||
return -errno;
|
||
|
||
return fd_acl_make_read_only_fallback(fd);
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
|
||
int fd_acl_make_writable(int fd) {
|
||
_cleanup_(acl_freep) acl_t acl = NULL;
|
||
acl_entry_t i;
|
||
int r;
|
||
|
||
/* Safely adds the writable bit to the owner's ACL entry of this inode. (And only the owner's! – This
|
||
* not the obvious inverse of fd_acl_make_read_only() hence!) */
|
||
|
||
acl = acl_get_fd(fd);
|
||
if (!acl) {
|
||
if (!ERRNO_IS_NOT_SUPPORTED(errno))
|
||
return -errno;
|
||
|
||
/* No ACLs? Then just update the regular mode_t */
|
||
return fd_acl_make_writable_fallback(fd);
|
||
}
|
||
|
||
for (r = acl_get_entry(acl, ACL_FIRST_ENTRY, &i);
|
||
r > 0;
|
||
r = acl_get_entry(acl, ACL_NEXT_ENTRY, &i)) {
|
||
acl_permset_t permset;
|
||
acl_tag_t tag;
|
||
int b;
|
||
|
||
if (acl_get_tag_type(i, &tag) < 0)
|
||
return -errno;
|
||
|
||
if (tag != ACL_USER_OBJ)
|
||
continue;
|
||
|
||
if (acl_get_permset(i, &permset) < 0)
|
||
return -errno;
|
||
|
||
b = acl_get_perm(permset, ACL_WRITE);
|
||
if (b < 0)
|
||
return -errno;
|
||
|
||
if (b)
|
||
return 0; /* Already set? Then there's nothing to do. */
|
||
|
||
if (acl_add_perm(permset, ACL_WRITE) < 0)
|
||
return -errno;
|
||
|
||
break;
|
||
}
|
||
if (r < 0)
|
||
return -errno;
|
||
|
||
if (acl_set_fd(fd, acl) < 0) {
|
||
if (!ERRNO_IS_NOT_SUPPORTED(errno))
|
||
return -errno;
|
||
|
||
return fd_acl_make_writable_fallback(fd);
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
#endif
|
||
|
||
int fd_acl_make_read_only_fallback(int fd) {
|
||
struct stat st;
|
||
|
||
assert(fd >= 0);
|
||
|
||
if (fstat(fd, &st) < 0)
|
||
return -errno;
|
||
|
||
if ((st.st_mode & 0222) == 0)
|
||
return 0;
|
||
|
||
if (fchmod(fd, st.st_mode & 0555) < 0)
|
||
return -errno;
|
||
|
||
return 1;
|
||
}
|
||
|
||
int fd_acl_make_writable_fallback(int fd) {
|
||
struct stat st;
|
||
|
||
assert(fd >= 0);
|
||
|
||
if (fstat(fd, &st) < 0)
|
||
return -errno;
|
||
|
||
if ((st.st_mode & 0200) != 0) /* already set */
|
||
return 0;
|
||
|
||
if (fchmod(fd, (st.st_mode & 07777) | 0200) < 0)
|
||
return -errno;
|
||
|
||
return 1;
|
||
}
|