From 20e574e236ab49c6e8539c9490a43e5ad17b7f8a Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Wed, 1 Nov 2023 15:34:26 +0400 Subject: [PATCH 01/56] Add a library for ACL manipulation --- flake.nix | 3 +- src/libutil/acl.cc | 391 ++++++++++++++++++++++++ src/libutil/acl.hh | 429 +++++++++++++++++++++++++++ src/libutil/experimental-features.cc | 9 +- src/libutil/experimental-features.hh | 1 + src/libutil/local.mk | 6 + src/libutil/util.cc | 37 ++- src/libutil/util.hh | 11 + 8 files changed, 883 insertions(+), 4 deletions(-) create mode 100644 src/libutil/acl.cc create mode 100644 src/libutil/acl.hh diff --git a/flake.nix b/flake.nix index bdbf541693a..99b8e199501 100644 --- a/flake.nix +++ b/flake.nix @@ -132,7 +132,7 @@ boost lowdown-nix ] - ++ lib.optionals stdenv.isLinux [libseccomp] + ++ lib.optionals stdenv.isLinux [libseccomp acl] ++ lib.optional (stdenv.isLinux || stdenv.isDarwin) libsodium ++ lib.optional stdenv.hostPlatform.isx86_64 libcpuid; @@ -426,6 +426,7 @@ pkgs.perl boost ] + ++ lib.optional stdenv.isLinux acl ++ lib.optional (currentStdenv.isLinux || currentStdenv.isDarwin) libsodium ++ lib.optional currentStdenv.isDarwin darwin.apple_sdk.frameworks.Security; diff --git a/src/libutil/acl.cc b/src/libutil/acl.cc new file mode 100644 index 00000000000..5fbf79615ea --- /dev/null +++ b/src/libutil/acl.cc @@ -0,0 +1,391 @@ +#include "acl.hh" +#include "error.hh" +#include "finally.hh" +#include "util.hh" + +#include +#include + +#if __linux__ +#include +#endif + +#ifdef __APPLE__ +#include +#endif + +/* + * The acl_get_entry function returns a 0 on success on Darwin, but a 1 Linux + */ +#if __APPLE__ +#define GET_ENTRY_SUCCESS 0 +#else +#define GET_ENTRY_SUCCESS 1 +#endif + +namespace nix::ACL +{ + +User::User(std::string name) +{ + if (passwd * pw = getpwnam(name.c_str())) + uid = pw->pw_uid; + else + if (errno == 0 || errno == ENOENT || errno == ESRCH || errno == EBADF || errno == EPERM) + throw Error("user '%s' does not exist", name); + else + throw SysError("unable to get the passwd entry for user '%s'", name); +} + +Group::Group(std::string name) +{ + if (group * gr = getgrnam(name.c_str())) + gid = gr->gr_gid; + else + if (errno == 0 || errno == ENOENT || errno == ESRCH || errno == EBADF || errno == EPERM) + throw Error("group '%s' does not exist", name); + else + throw SysError("unable to get group information for group '%s'", name); +} + +namespace Native +{ +acl_t AccessControlList::_acl_get_fd(int fd) +{ + acl_t acl = acl_get_fd(fd); + if (!acl) throw SysError("getting ACL of a file pointed to by fd %d", fd); + return acl; +} + +acl_t AccessControlList::_acl_get_file(std::filesystem::path path, Type t) +{ +#ifdef __APPLE__ + // On Linux, a file with an empty ACL returns just that, an empty ACL. + // On Darwin, NULL is returned instead with errno set to ENOENT (No such + // file or directory), even though the file/directory does exist. + acl_t acl = acl_get_file(path.c_str(), (acl_type_t) t); + if (!acl && std::filesystem::exists(path)) { + // False error, path does exists, create empty acl + acl = acl_init(0); + } +#else + acl_t acl = acl_get_file(path.c_str(), t); +#endif + if (!acl) throw SysError("getting ACL of an object %s", path); + return acl; +} + +void _acl_get_permset(acl_entry_t entry, acl_permset_t * permset) +{ + if (acl_get_permset(entry, permset) != 0) throw SysError("getting a permission set of an ACL"); +} + +void * _acl_get_qualifier(acl_entry_t entry, const std::string & qualifier_type) +{ + void * qualifier = acl_get_qualifier(entry); + if (!qualifier) throw SysError("getting an ACL %s qualifier", qualifier_type); + return qualifier; +} + +void _acl_free(acl_t acl) +{ + if (acl_free(acl) != 0) throw SysError("freeing memory allocated by an ACL"); +} + +bool _acl_get_perm(acl_permset_t perms, acl_perm_t perm) +{ +#if __APPLE__ + return acl_get_perm_np(perms, perm); +#else + return acl_get_perm(perms, perm); +#endif +} + +AccessControlList::AccessControlList(acl_t acl) +{ + Finally free {[&](){ _acl_free(acl); }}; + + int entry_id = ACL_FIRST_ENTRY; + acl_entry_t entry; + while (acl_get_entry(acl, entry_id, &entry) == GET_ENTRY_SUCCESS) { + entry_id = ACL_NEXT_ENTRY; + acl_tag_t tag; + if (acl_get_tag_type(entry, &tag) != 0) throw SysError("getting ACL tag type"); + // Placed in optional, because creating a default user on MacOS seems dangerous + std::optional entity = {}; + switch (tag) { +#if __APPLE__ + case ACL_UNDEFINED_TAG: + warn("encountered an undefined ACL Tag"); + break; + case ACL_EXTENDED_ALLOW: { + void * guid = _acl_get_qualifier(entry, "guid"); + uid_t ugid; + int idtype; + + if (mbr_uuid_to_id((const unsigned char *) guid, &ugid, &idtype) != 0) { + throw Error("converting a guid_t to a uid/gid"); + } + + switch (idtype) { + case ID_TYPE_UID: + entity = User(ugid); + break; + case ID_TYPE_GID: + entity = Group((gid_t) ugid); + break; + default: + throw Error("unknown ACL qualifier type %d", idtype); + } + acl_free(guid); + break; + } + case ACL_EXTENDED_DENY: + // TODO(ACLs) our model currently does not model DENY ACLs + throw Error("ACLS: TODO"); +#else + case ACL_USER_OBJ: + entity = UserObj {}; + break; + + case ACL_USER: + entity = User {* (uid_t*) _acl_get_qualifier(entry, "uid")}; + break; + + case ACL_GROUP_OBJ: + entity = GroupObj {}; + break; + + case ACL_GROUP: + entity = Group {* (gid_t*) _acl_get_qualifier(entry, "gid")}; + break; + + case ACL_MASK: + entity = Mask {}; + break; + + case ACL_OTHER: + entity = Other {}; + break; +#endif + + default: + throw Error("unknown ACL tag type %d", tag); + } + + std::set p; + acl_permset_t permset; + _acl_get_permset(entry, &permset); +#ifdef __APPLE__ + if (_acl_get_perm(permset, acl_perm_t::ACL_READ_DATA)) p.insert(Permission::Read_Data); + if (_acl_get_perm(permset, acl_perm_t::ACL_LIST_DIRECTORY)) p.insert(Permission::List_Directory); + if (_acl_get_perm(permset, acl_perm_t::ACL_WRITE_DATA)) p.insert(Permission::Write_Data); + if (_acl_get_perm(permset, acl_perm_t::ACL_ADD_FILE)) p.insert(Permission::Add_File); + if (_acl_get_perm(permset, acl_perm_t::ACL_EXECUTE)) p.insert(Permission::Execute); + if (_acl_get_perm(permset, acl_perm_t::ACL_SEARCH)) p.insert(Permission::Search); + if (_acl_get_perm(permset, acl_perm_t::ACL_DELETE)) p.insert(Permission::Delete); + if (_acl_get_perm(permset, acl_perm_t::ACL_APPEND_DATA)) p.insert(Permission::Append_Data); + if (_acl_get_perm(permset, acl_perm_t::ACL_ADD_SUBDIRECTORY)) p.insert(Permission::Add_Subdirectory); + if (_acl_get_perm(permset, acl_perm_t::ACL_DELETE_CHILD)) p.insert(Permission::Delete_Child); + if (_acl_get_perm(permset, acl_perm_t::ACL_READ_ATTRIBUTES)) p.insert(Permission::Read_Attributes); + if (_acl_get_perm(permset, acl_perm_t::ACL_WRITE_ATTRIBUTES)) p.insert(Permission::Write_Attributes); + if (_acl_get_perm(permset, acl_perm_t::ACL_READ_EXTATTRIBUTES)) p.insert(Permission::Read_Extattributes); + if (_acl_get_perm(permset, acl_perm_t::ACL_WRITE_EXTATTRIBUTES)) p.insert(Permission::Write_Extattributes); + if (_acl_get_perm(permset, acl_perm_t::ACL_READ_SECURITY)) p.insert(Permission::Read_Security); + if (_acl_get_perm(permset, acl_perm_t::ACL_WRITE_SECURITY)) p.insert(Permission::Write_Security); + // if (_acl_get_perm(permset, acl_perm_t::ACL_CHANGE_OWNER)) p.insert(Permission::Change_Owner); + // if (_acl_get_perm(permset, acl_perm_t::ACL_SYNCHRONIZE)) p.insert(Permission::Synchronize); +#else + if (_acl_get_perm(permset, ACL_READ)) p.insert(Permission::Read); + if (_acl_get_perm(permset, ACL_WRITE)) p.insert(Permission::Write); + if (_acl_get_perm(permset, ACL_EXECUTE)) p.insert(Permission::Execute); +#endif + if (entity.has_value()) + insert({entity.value(), p}); + else + throw Error("adding the entity of the acl"); + } +} + +void _acl_set_tag_type(acl_entry_t entry, acl_tag_t tag) +{ + if (acl_set_tag_type(entry, tag) != 0) throw SysError("setting an ACL tag type"); +} + +void _acl_set_qualifier(acl_entry_t entry, void* qualifier, const std::string & qualifier_type) +{ + if (acl_set_qualifier(entry, qualifier) != 0) throw SysError("setting an ACL %s qualifier", qualifier_type); +} + +acl_t AccessControlList::to_acl() +{ + acl_t acl = acl_init(size()); + if (!acl) throw SysError("initializing an ACL"); + for (auto [tag, perms] : *this) { + acl_entry_t entry; + if (acl_create_entry(&acl, &entry) != 0) throw SysError("creating an ACL entry"); +#ifdef __APPLE__ + std::visit(overloaded { + [&](User u){ + _acl_set_tag_type(entry, acl_tag_t::ACL_EXTENDED_ALLOW); + uuid_t uu; + if (mbr_uid_to_uuid(u.uid, uu) != 0) { + throw SysError("converting a uid to a uuid"); + } + _acl_set_qualifier(entry, (void*) &uu, "uid"); + }, + [&](Group g){ + _acl_set_tag_type(entry, acl_tag_t::ACL_EXTENDED_ALLOW); + uuid_t uu; + if (mbr_uid_to_uuid(g.gid, uu) != 0) { + throw SysError("converting a gid to a uuid"); + } + _acl_set_qualifier(entry, (void*) &uu, "gid"); + }, + }, tag); +#else + std::visit(overloaded { + [&](UserObj _){ _acl_set_tag_type(entry, ACL_USER_OBJ); }, + [&](User u){ _acl_set_tag_type(entry, ACL_USER); _acl_set_qualifier(entry, (void*) &u.uid, "uid"); }, + [&](GroupObj _){ _acl_set_tag_type(entry, ACL_GROUP_OBJ); }, + [&](Group g){ _acl_set_tag_type(entry, ACL_GROUP); _acl_set_qualifier(entry, (void*) &g.gid, "gid"); }, + [&](Mask _){ _acl_set_tag_type(entry, ACL_MASK); }, + [&](Other _){ _acl_set_tag_type(entry, ACL_OTHER); }, + }, tag); +#endif + acl_permset_t permset; + _acl_get_permset(entry, &permset); + for (auto perm : perms) { +#ifdef __APPLE__ + if (acl_add_perm(permset, (acl_perm_t) perm) != 0) +#else + if (acl_add_perm(permset, perm) != 0) +#endif + throw SysError("adding permissions to an ACL permission set"); + } + } + return acl; +} + +void AccessControlList::set(int fd) +{ + acl_t acl = to_acl(); + Finally free {[&](){ _acl_free(acl); }}; + if (acl_set_fd(fd, acl) != 0) throw SysError("setting ACL on a file pointed to by fd %d", fd); +} + +void AccessControlList::set(std::filesystem::path file, Type t) +{ + acl_t acl = to_acl(); + Finally free {[&](){ _acl_free(acl); }}; + +#ifdef __APPLE__ + if (acl_set_file(file.c_str(), (acl_type_t) t, acl) != 0) +#else + if (acl_set_file(file.c_str(), t, acl) != 0) +#endif + throw SysError("setting ACL of an object %s", file); +} +} + +/* Generic interface */ + +Permissions::Permissions(std::initializer_list perms) +{ + insert(perms); +} +Permissions::Permissions(std::set perms) +{ + insert(perms.begin(), perms.end()); +} + +bool intersects(const std::set & a, const std::set & b) +{ + for (auto & el : a) if (b.contains(el)) return true; + return false; +} +bool matches(const std::set & a, const std::set & b) +{ + for (auto & el : a) if (!b.contains(el)) return false; + return true; +} + +Permissions::HasPermission Permissions::checkPermission(const std::set & reqs) +{ + if (matches(*this, reqs)) return Full; + else if (intersects(*this, reqs)) return Partial; + else return None; +} + +Permissions::HasPermission Permissions::canRead() +{ + return checkPermission(ACL_PERMISSIONS_READ); +} +Permissions::HasPermission Permissions::canWrite() +{ + return checkPermission(ACL_PERMISSIONS_WRITE); +} +Permissions::HasPermission Permissions::canExecute() +{ + return checkPermission(ACL_PERMISSIONS_EXECUTE); +} + +void Permissions::allowRead(bool allow) +{ + std::set perms ACL_PERMISSIONS_READ; + if (allow) + insert(perms.begin(), perms.end()); + else + erase(perms.begin(), perms.end()); +} +void Permissions::allowWrite(bool allow) +{ + std::set perms ACL_PERMISSIONS_WRITE; + if (allow) + insert(perms.begin(), perms.end()); + else + erase(perms.begin(), perms.end()); +} +void Permissions::allowExecute(bool allow) +{ + std::set perms ACL_PERMISSIONS_EXECUTE; + if (allow) + insert(perms.begin(), perms.end()); + else + erase(perms.begin(), perms.end()); +} + +AccessControlList::AccessControlList(std::filesystem::path p) +{ + auto native = Native::AccessControlList(p); + for (auto & [k, v] : native) { + if (auto tag = std::get_if(&k)) + insert({*tag, v}); + else if (auto tag = std::get_if(&k)) + insert({*tag, v}); + } +} + +void AccessControlList::set(std::filesystem::path p) +{ + using namespace Native; + Native::AccessControlList native; + for (auto [k, v] : *this) { + if (auto tag = std::get_if(&k)) + native.insert({*tag, v}); + else if (auto tag = std::get_if(&k)) + native.insert({*tag, v}); + } +#ifndef __APPLE__ + // On Linux, preserve non-extended ACL entries + Native::AccessControlList current(p); + native[UserObj {}] = current[UserObj {}]; + native[GroupObj {}] = current[GroupObj {}]; + native[Other {}] = current[Other {}]; + if (!empty()) + native[Mask {}] = {Permission::Read, Permission::Write, Permission::Execute}; +#endif + native.set(p); +} + +} diff --git a/src/libutil/acl.hh b/src/libutil/acl.hh new file mode 100644 index 00000000000..a63308dfea2 --- /dev/null +++ b/src/libutil/acl.hh @@ -0,0 +1,429 @@ +#pragma once +///@file + +#include +#include +#include +#include +#include +#include +#include +#include "comparator.hh" + + +/** + * A C++ API to POSIX ACLs + */ + +namespace nix { + + struct UserLock; + +namespace ACL { + + +/* +A template for an Access Control List (ACL); this is instantiated to get both +a "native" interface (which depends on the system for which Nix is built) and +a "generic" interface, which uses the "native" interface to provide a cross- +platform, consistent way to interact with ACLs. +*/ +/** + * ACL_USER + */ +struct User +{ + uid_t uid; + + User(uid_t uid) : uid(uid) {}; + User(struct passwd & pw) : uid(pw.pw_uid) {}; + User(std::string name); + User(const UserLock & lock); + + GENERATE_CMP(User, me->uid); +}; + +/** + * ACL_GROUP + */ +struct Group +{ + gid_t gid; + + Group(gid_t gid) : gid(gid) {}; + Group(struct group & gr) : gid(gr.gr_gid) {}; + Group(std::string name); + + GENERATE_CMP(Group, me->gid); +}; + +/** + * An ACL tag; the entity to which the permissions in this particular ACL entry will apply. + * + * For groups, all users which are members of this group (either as their + * primary or secondary group) shall be given the respective permissions. + */ +typedef std::variant Tag; + +namespace Native { +#ifdef __APPLE__ + +/** + * ACL type + */ +enum Type { + /** + * Unlike Linux, Darwin only has ACL_TYPE_EXTENDED, meaning no default value + * can be set. + */ + Extended = acl_type_t::ACL_TYPE_EXTENDED, +}; + +#define ACL_DEFAULT_TYPE Type::Extended + +/** + * Tag of an ACL entry; tags qualify which entity the given access permission set should be applied to. + * + * - @User : Access rights for a user identified by a uuid + * + * - @Group : Access rights for a group identified by a uuid + * + */ +using Tag = Tag; + +/** + * Permission to perform an operation with an object + */ +enum Permission { + Read_Data = acl_perm_t::ACL_READ_DATA, + List_Directory = acl_perm_t::ACL_LIST_DIRECTORY, // Equivalent to Read_Data + Read_Attributes = acl_perm_t::ACL_READ_ATTRIBUTES, + Read_Extattributes = acl_perm_t::ACL_READ_EXTATTRIBUTES, + Read_Security = acl_perm_t::ACL_READ_SECURITY, + + Write_Data = acl_perm_t::ACL_WRITE_DATA, + Add_File = acl_perm_t::ACL_ADD_FILE, // Equivalent to Write_Data + Append_Data = acl_perm_t::ACL_APPEND_DATA, + Add_Subdirectory = acl_perm_t::ACL_ADD_SUBDIRECTORY, // Equivalent to Add_Subdirectory + Delete = acl_perm_t::ACL_DELETE, + Delete_Child = acl_perm_t::ACL_DELETE_CHILD, + Write_Attributes = acl_perm_t::ACL_WRITE_ATTRIBUTES, + Write_Extattributes = acl_perm_t::ACL_WRITE_EXTATTRIBUTES, + Write_Security = acl_perm_t::ACL_WRITE_SECURITY, + + Execute = acl_perm_t::ACL_EXECUTE, + Search = acl_perm_t::ACL_SEARCH, // Equivalent to Execute + + // Mostly unused, see comments on what they are for, we do not need nor include these + // Change_Owner = acl_perm_t::ACL_CHANGE_OWNER, // Backwards compatibility + // Synchronize = acl_perm_t::ACL_SYNCHRONIZE, // Windows interoperability +}; + +// READ permissions (a set of all of these is equivalent to setting the posix read bit) +#define ACL_PERMISSIONS_READ \ + { \ + nix::ACL::Native::Permission::Read_Data, \ + nix::ACL::Native::Permission::List_Directory, \ + nix::ACL::Native::Permission::Read_Attributes, \ + nix::ACL::Native::Permission::Read_Extattributes, \ + nix::ACL::Native::Permission::Read_Security \ + } + +// WRITE permissions (a set of all of these is equivalent to setting the posix write bit) +#define ACL_PERMISSIONS_WRITE \ + { \ + nix::ACL::Native::Permission::Write_Data, \ + nix::ACL::Native::Permission::Add_File, \ + nix::ACL::Native::Permission::Append_Data, \ + nix::ACL::Native::Permission::Add_Subdirectory, \ + nix::ACL::Native::Permission::Delete, \ + nix::ACL::Native::Permission::Delete_Child, \ + nix::ACL::Native::Permission::Write_Attributes, \ + nix::ACL::Native::Permission::Write_Extattributes, \ + nix::ACL::Native::Permission::Write_Security, \ + } + +// EXECUTE permissions (a set of all of these is equivalent to setting the posix execute bit) +#define ACL_PERMISSIONS_EXECUTE \ + { \ + nix::ACL::Native::Permission::Execute, \ + nix::ACL::Native::Permission::Search, \ + } + +#else + +/** + * The ACL type; + */ +enum Type { + /** + * Access to the object itself + * (ACL_TYPE_ACCESS) + */ + Access = ACL_TYPE_ACCESS, + /** + * Initial ACL assigned to newly created objects within a directory + * + * Note that for sub-directories, this ACL is assigned as both the Access ACL + * and the Default ACL, meaning it is inherited recursively. There doesn't + * appear to be a way to prevent this behaviour. + * + * (ACL_TYPE_DEFAULT) + */ + Default = ACL_TYPE_DEFAULT +}; + +#define ACL_DEFAULT_TYPE Type::Access + +/** + * ACL_USER_OBJ + */ +struct UserObj { GENERATE_CMP(UserObj); }; +/** + * ACL_GROUP_OBJ + */ +struct GroupObj { GENERATE_CMP(GroupObj); }; +/** + * ACL_MASK + */ +struct Mask { GENERATE_CMP(Mask); }; +/** + * ACL_OTHER + */ +struct Other { GENERATE_CMP(Other); }; +/** + * Tag of an ACL entry; tags qualify which entity the given access permission set should be applied to. + * + * - @UserObj (ACL_USER_OBJ) : Access rights for the file owner + * + * - @User (ACL_USER) : Access rights for a user identified by a uid + * + * - @GroupObj (ACL_GROUP_OBJ) : Access rights for the file group + * + * - @Group (ACL_GROUP) : Access rights for a group identified by a uid + * + * - @Mask (ACL_MASK) : Maximum access rights that can be granted to @User, @GroupObj, or @Group + * + * - @Other (ACL_OTHER) : Access rights for processes that don't match any other entry in the ACL + */ +typedef std::variant Tag; + +/** + * Permission to perform an operation with an object + */ +enum Permission { + /** + * (ACL_READ) + */ + Read = ACL_READ, + /** + * (ACL_WRITE) + */ + Write = ACL_WRITE, + /** + * (ACL_EXECUTE) + */ + Execute = ACL_EXECUTE +}; + +#define ACL_PERMISSIONS_READ {nix::ACL::Native::Permission::Read} +#define ACL_PERMISSIONS_WRITE {nix::ACL::Native::Permission::Write} +#define ACL_PERMISSIONS_EXECUTE {nix::ACL::Native::Permission::Execute} + +#endif + +/** + * Access Control List for an object. + * + * The access control list for a filesystem object is a map from @Tag types to + * @Permissions; The @Tag type represents the access control subject (a user + * or group to which access is granted), and @Permissions represents the set of + * permissions which are granted to the subject. + * + * **Linux notes:** + * + * Only one entry of each tag type @UserObj, @GroupObj, @Mask and @Other is + * possible, and only one entry for each @User and @Group with a unique uid/ gid + * is possible. + * + * For an ACL to be valid, it must have at least @UserObj, @GroupObj and `Other` + * entries, and, in case at least one @User or @Group entry is present, a @Mask + * entry (which is optional otherwise); An ACL may have arbitrarily many @User + * and @Group entries. + * + * Refer to `man acl` for an access check algorithm. + * + * **Darwin notes:** + * + * The ACL contains @User and @Group tag types. Internally, they are represented + * by a uuid, and the uid/gid values have to be converted, and as such errors will + * be thrown if no such uid/gid exists. + */ + +class AccessControlList : public std::map> +{ +private: + /** + * Construct the C++ wrapper from a C acl struct; Consumes the C struct (frees memory allocated to it) + */ + AccessControlList(acl_t acl); + /** + * Get the C acl struct from the C++ wrapper; The user is expected to call acl_free on the struct when they are done. + */ + acl_t to_acl(); + // Helper functions that throw instead of returning NULL + static acl_t _acl_get_fd(int fd); + static acl_t _acl_get_file(std::filesystem::path file, Type t); +public: + /** + * Construct an empty ACL. Note that it may not be valid until you add the necessary entries yourself. + */ + AccessControlList() { } + /** + * Read an ACL of a file pointed to by a file descriptor. + * + * Throws a SysError on failure. + */ + AccessControlList(int fd) : AccessControlList(_acl_get_fd(fd)) {} + /** + * Read an ACL from an object at a path. + * + * Throws a SysError on failure. + */ + AccessControlList(std::filesystem::path file, Type t = ACL_DEFAULT_TYPE) : AccessControlList(_acl_get_file(file.c_str(), t)) {}; + /** + * Write ACL to a file pointed to by a file descriptor. + * + * Throws a SysError on failure. + */ + void set(int fd); + /** + * Write ACL to an object pointed to by a path. + * + * Throws a SysError on failure. + */ + void set(std::filesystem::path file, Type t = ACL_DEFAULT_TYPE); +}; + +} + + +/** + * A set of permissions given to a subject. + * + * Note that the set of possible permissions differs by platform; however, + * there are functions provided to translate the per-platform permissions into + * "traditional" POSIX permissions. + */ +; +class Permissions : std::set +{ +public: + Permissions() {} + /** + * Correspondance between this set of permissions and the traditional POSIX permissions + */ + enum HasPermission { + /** + * The subject would NOT be able to perform any operations permitted by traditional permissions + */ + None = 0, + /** + * The subject would be able to perform SOME of the operations permitted by traditional permissions + */ + Partial = 1, + /** + * The subject would be able to perform ANY operation permitted by traditional permissions + */ + Full = 2 + }; + + /** + * Whether the subject would be able to "read" the object: + * + * - Read contents of a file + * - Read extended attributes + * - List objects in a directory + * - Read a symlink's target + * - Read from a device + */ + HasPermission canRead(); + /** + * Whether the subject would be able to "write to" the object: + * + * - Write contents of a file + * - Write extended attributes + * - Create or delete objects in a directory + * - Change a symlink's target + * - Write to a device + */ + HasPermission canWrite(); + /** + * Whether the subject would be able to "execute" the object + * + * - Execute a file + * - Change current directory to a directory + * - Access objects in a directory + * - Create objects in a directory + * - Execute a symlink's target + */ + HasPermission canExecute(); + + /** + * Add (or remove, depending on @allow parameter) the permissions necessary to "read" the object (see @canRead) + */ + void allowRead(bool allow); + /** + * Add (or remove, depending on @allow parameter) the permissions necessary to "write to" the object (see @canWrite) + */ + void allowWrite(bool allow); + /** + * Add (or remove, depending on @allow parameter) the permissions necessary to "execute" the object (see @canExecute) + */ + void allowExecute(bool allow); + + friend class AccessControlList; + +private: + /** + * Check this set of permissions against some requirements (used for canRead, canWrite and canExecute) + */ + HasPermission checkPermission(const std::set & requirements); + + Permissions(std::initializer_list p); + Permissions(std::set p); + +}; + +/** + * A generic Access Control List; this is the "lowest common denominator" + * between the Darwin and Linux ACL interfaces. + * + * It allows you to manipulate the Read, Write and Execute permissions, + * analogous to the traditional Unix permissions (see @Permissions class), of + * individual @User 's and @Group 's to the given filesystem object. + */ + +class AccessControlList : public std::map +{ +public: + /** + * Construct an empty ACL; as in, an ACL in which no users or groups have permissions to access the object. + */ + AccessControlList() { } + /** + * Read an ACL from an object at a path. + *FA + * Throws a SysError on failure. + */ + AccessControlList(std::filesystem::path file); + /** + * Write ACL to an object pointed to by a path. + * + * Throws a SysError on failure. + */ + void set(std::filesystem::path file); +}; + +} +} \ No newline at end of file diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index 7c4112d32fd..03d8f1ef011 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -12,7 +12,7 @@ struct ExperimentalFeatureDetails std::string_view description; }; -constexpr std::array xpFeatureDetails = {{ +constexpr std::array xpFeatureDetails = {{ { .tag = Xp::CaDerivations, .name = "ca-derivations", @@ -228,6 +228,13 @@ constexpr std::array xpFeatureDetails = {{ Allow the use of the `read-only` parameter in [local store](@docroot@/command-ref/new-cli/nix3-help-stores.md#local-store) URIs. )", }, + { + .tag = Xp::ACLs, + .name = "acls", + .description = R"( + Allow protection of store paths with the use of [POSIX ACLs](https://man7.org/linux/man-pages/man5/acl.5.html). + )", + }, }}; static_assert( diff --git a/src/libutil/experimental-features.hh b/src/libutil/experimental-features.hh index faf2e939805..8ba09ab321a 100644 --- a/src/libutil/experimental-features.hh +++ b/src/libutil/experimental-features.hh @@ -32,6 +32,7 @@ enum struct ExperimentalFeature DynamicDerivations, ParseTomlTimestamps, ReadOnlyLocalStore, + ACLs, }; /** diff --git a/src/libutil/local.mk b/src/libutil/local.mk index f880c0fc562..69cfcdfe96b 100644 --- a/src/libutil/local.mk +++ b/src/libutil/local.mk @@ -6,8 +6,14 @@ libutil_DIR := $(d) libutil_SOURCES := $(wildcard $(d)/*.cc) +libutil_CXXFLAGS := -g + libutil_LDFLAGS += -pthread $(OPENSSL_LIBS) $(LIBBROTLI_LIBS) $(LIBARCHIVE_LIBS) $(BOOST_LDFLAGS) -lboost_context +ifdef HOST_LINUX + libutil_LDFLAGS += -lacl +endif + ifeq ($(HAVE_LIBCPUID), 1) libutil_LDFLAGS += -lcpuid endif diff --git a/src/libutil/util.cc b/src/libutil/util.cc index 26f9dc8a85e..bcbcd5ed1d0 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -41,6 +41,8 @@ #include #endif +#include "execinfo.h" + extern char * * environ __attribute__((weak)); @@ -548,15 +550,46 @@ void deletePath(const Path & path, uint64_t & bytesFreed) } -std::string getUserName() +std::string getUserName(uid_t uid) { - auto pw = getpwuid(geteuid()); + auto pw = getpwuid(uid); std::string name = pw ? pw->pw_name : getEnv("USER").value_or(""); if (name.empty()) throw Error("cannot figure out user name"); return name; } +std::string getUserName() +{ + return getUserName(getuid()); +} + +std::vector getUserGroups(uid_t uid) { + struct passwd * pw = getpwuid(uid); + int ngroups = 0; + getgrouplist(pw->pw_name, pw->pw_gid, NULL, &ngroups); + gid_t _groups[ngroups]; +// Apple takes ints instead of gids for the second and third arguments +#if __APPLE__ + getgrouplist(pw->pw_name, (int) pw->pw_gid, (int *) _groups, &ngroups); +#else + getgrouplist(pw->pw_name, pw->pw_gid, _groups, &ngroups); +#endif + std::vector groups; + for (auto group : _groups) groups.push_back(group); + return groups; +} + +std::vector getUserGroupNames(uid_t uid) { + auto groups = getUserGroups(uid); + std::vector groupsWithNames; + for (auto group : groups) { + struct group * g = getgrgid(group); + groupsWithNames.push_back(g->gr_name); + } + return groupsWithNames; +} + Path getHomeOf(uid_t userId) { std::vector buf(16384); diff --git a/src/libutil/util.hh b/src/libutil/util.hh index b302d6f4544..c71cf0864b3 100644 --- a/src/libutil/util.hh +++ b/src/libutil/util.hh @@ -197,6 +197,17 @@ void deletePath(const Path & path); void deletePath(const Path & path, uint64_t & bytesFreed); std::string getUserName(); +std::string getUserName(uid_t uid); + +/** + * Get the groups to which the user belongs + */ +std::vector getUserGroups(uid_t uid); + +/** + * Get the names of groups to which the user belongs + */ +std::vector getUserGroupNames(uid_t uid); /** * @return the given user's home directory from /etc/passwd. From 337a1272d53be4aadfe5c7917376a1ef79131f23 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Wed, 1 Nov 2023 18:58:51 +0400 Subject: [PATCH 02/56] Add a granular store interface --- src/libstore/access-status.hh | 18 +++++ src/libstore/granular-access-store.hh | 111 ++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 src/libstore/access-status.hh create mode 100644 src/libstore/granular-access-store.hh diff --git a/src/libstore/access-status.hh b/src/libstore/access-status.hh new file mode 100644 index 00000000000..7edbae3929e --- /dev/null +++ b/src/libstore/access-status.hh @@ -0,0 +1,18 @@ +#pragma once +///@file + + +#include +#include +#include "comparator.hh" + +namespace nix { +template +struct AccessStatusFor { + bool isProtected = false; + std::set entities; + + GENERATE_CMP(AccessStatusFor, me->isProtected, me->entities); +}; +} + diff --git a/src/libstore/granular-access-store.hh b/src/libstore/granular-access-store.hh new file mode 100644 index 00000000000..51dcacc6e4b --- /dev/null +++ b/src/libstore/granular-access-store.hh @@ -0,0 +1,111 @@ +#pragma once +///@file + +#include "config.hh" +#include "derivations.hh" +#include "store-api.hh" +#include "acl.hh" +#include "access-status.hh" + +namespace nix { + +struct StoreObjectDerivationOutput +{ + StorePath drvPath; + std::string output; + + StoreObjectDerivationOutput(DerivedPath::Built p) : drvPath(p.drvPath) + { + if (auto names = std::get_if(&p.outputs)) + if (names->size() == 1) { + output = *names->begin(); + return; + } + throw Error("StoreObjectDerivationOutput requires a DerivedPathBuilt with just one named output"); + } + StoreObjectDerivationOutput(StorePath drvPath, std::string output = "out") : drvPath(drvPath), output(output) { }; + + GENERATE_CMP(StoreObjectDerivationOutput, me->drvPath, me->output); +}; + +struct StoreObjectDerivationLog +{ + StorePath drvPath; + + GENERATE_CMP(StoreObjectDerivationLog, me->drvPath); +}; + +typedef std::variant StoreObject; + + +template +struct GranularAccessStore : public virtual Store +{ + inline static std::string operationName = "Granular access"; + + /** + * Subject against which the access should be checked + */ + std::optional effectiveUser; + bool trusted = false; + + typedef std::variant AccessControlEntity; + typedef AccessStatusFor AccessStatus; + + + virtual void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status) = 0; + virtual AccessStatus getAccessStatus(const StoreObject & storeObject) = 0; + + virtual std::set getSubjectGroups(AccessControlSubject subject) = 0; + + /** + * Whether any of the given @entities@ can access the path + */ + bool canAccess(const StoreObject & storeObject, const std::set & entities) + { + if (! experimentalFeatureSettings.isEnabled(Xp::ACLs) || trusted) return true; + auto status = getAccessStatus(storeObject); + if (! status.isProtected) return true; + for (auto ent : status.entities) if (entities.contains(ent)) return true; + return false; + } + /** + * Whether a subject can access the store path + */ + bool canAccess(const StoreObject & storeObject, AccessControlSubject subject) + { + std::set entities; + auto groups = getSubjectGroups(subject); + for (auto group : groups) entities.insert(group); + entities.insert(subject); + return canAccess(storeObject, entities); + } + + /** + * Whether the effective subject can access the store path + */ + bool canAccess(const StoreObject & storeObject) { + if (!experimentalFeatureSettings.isEnabled(Xp::ACLs) || trusted) return true; + if (effectiveUser) + return canAccess(storeObject, *effectiveUser); + else + return ! getAccessStatus(storeObject).isProtected; + } + + void addAllowedEntities(const StoreObject & storeObject, const std::set & entities) { + auto status = getAccessStatus(storeObject); + for (auto entity : entities) status.entities.insert(entity); + setAccessStatus(storeObject, status); + } + + void removeAllowedEntities(const StoreObject & storeObject, const std::set & entities) { + auto status = getAccessStatus(storeObject); + for (auto entity : entities) status.entities.erase(entity); + setAccessStatus(storeObject, status); + } +}; + +using LocalGranularAccessStore = GranularAccessStore; + + +} From 5024921c8b6432589669442eecdef50a1043c6bf Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Wed, 1 Nov 2023 19:00:11 +0400 Subject: [PATCH 03/56] Implement granular access store --- src/libstore/build/derivation-goal.cc | 52 ++- src/libstore/build/derivation-goal.hh | 6 +- src/libstore/build/local-derivation-goal.cc | 137 +++++-- src/libstore/daemon.cc | 109 +++++- src/libstore/daemon.hh | 3 +- src/libstore/globals.cc | 1 - src/libstore/globals.hh | 7 + src/libstore/local-store.cc | 402 ++++++++++++++++++-- src/libstore/local-store.hh | 27 +- src/libstore/local.mk | 4 + src/libstore/lock.cc | 20 +- src/libstore/lock.hh | 11 +- src/libstore/misc.cc | 19 +- src/libstore/nar-info.cc | 29 ++ src/libstore/path-info.cc | 14 + src/libstore/path-info.hh | 5 +- src/libstore/realisation.hh | 2 +- src/libstore/remote-store.cc | 33 ++ src/libstore/remote-store.hh | 9 +- src/libstore/store-api.cc | 10 + src/libstore/store-api.hh | 10 +- src/libstore/worker-protocol-impl.hh | 101 +++++ src/libstore/worker-protocol.cc | 58 +++ src/libstore/worker-protocol.hh | 37 +- src/libutil/archive.cc | 14 +- src/libutil/archive.hh | 4 +- src/nix/daemon.cc | 67 ++-- 27 files changed, 1027 insertions(+), 164 deletions(-) diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 5e37f7ecbd1..d9bf8610bf2 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -1,4 +1,6 @@ #include "derivation-goal.hh" +#include "config.hh" +#include "granular-access-store.hh" #include "hook-instance.hh" #include "worker.hh" #include "builtins.hh" @@ -679,13 +681,20 @@ void DerivationGoal::tryToBuild() return; } - /* If any of the outputs already exist but are not valid, delete - them. */ + /* If any of the outputs already exist but are not valid, delete them. If + any of the outputs are inacessible, set the build mode to `Check` so that + if outputs match, access is granted */ for (auto & [_, status] : initialOutputs) { - if (!status.known || status.known->isValid()) continue; - auto storePath = status.known->path; - debug("removing invalid path '%s'", worker.store.printStorePath(status.known->path)); - deletePath(worker.store.Store::toRealPath(storePath)); + if (status.known) { + if (status.known->status == PathStatus::Corrupt || status.known->status == PathStatus::Absent) { + auto storePath = status.known->path; + debug("removing invalid path '%s'", worker.store.printStorePath(status.known->path)); + deletePath(worker.store.Store::toRealPath(storePath)); + } else if (status.known->status == PathStatus::Inaccessible) { + logger->cout("don't have access to path %s; checking outputs", worker.store.printStorePath(status.known->path)); + buildMode = bmCheck; + } + } } /* Don't do a remote build if the derivation has the attribute @@ -1216,7 +1225,11 @@ Path DerivationGoal::openLogFile() Path logFileName = fmt("%s/%s%s", dir, baseName.substr(2), settings.compressLog ? ".bz2" : ""); - fdLogFile = open(logFileName.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0666); + bool logFileExisted = std::filesystem::exists(logFileName); + + auto mode = experimentalFeatureSettings.isEnabled(Xp::ACLs) ? 0660 : 0666; + + fdLogFile = open(logFileName.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, mode); if (!fdLogFile) throw SysError("creating log file '%1%'", logFileName); logFileSink = std::make_shared(fdLogFile.get()); @@ -1226,6 +1239,13 @@ Path DerivationGoal::openLogFile() else logSink = logFileSink; + if (experimentalFeatureSettings.isEnabled(Xp::ACLs) && !logFileExisted) + if (auto localStore = dynamic_cast(&worker.store)) { + auto storeObject = StoreObjectDerivationLog {drvPath}; + auto status = localStore->futurePermissions.contains(storeObject) ? localStore->futurePermissions.at(storeObject) : LocalGranularAccessStore::AccessStatus {}; + localStore->setCurrentAccessStatus(logFileName, status); + } + return logFileName; } @@ -1372,21 +1392,31 @@ std::pair DerivationGoal::checkPathValidity() wantedOutputsLeft.erase(i.first); if (i.second) { auto outputPath = *i.second; + bool canAccess = true; + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) + if (auto aclStore = dynamic_cast(&worker.store)) + canAccess = aclStore->canAccess(outputPath); info.known = { .path = outputPath, .status = !worker.store.isValidPath(outputPath) ? PathStatus::Absent - : !checkHash || worker.pathContentsGood(outputPath) - ? PathStatus::Valid - : PathStatus::Corrupt, + : checkHash && !worker.pathContentsGood(outputPath) + ? PathStatus::Corrupt + : !canAccess + ? PathStatus::Inaccessible + : PathStatus::Valid, }; } auto drvOutput = DrvOutput{info.outputHash, i.first}; if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { if (auto real = worker.store.queryRealisation(drvOutput)) { + bool canAccess = true; + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) + if (auto aclStore = dynamic_cast(&worker.store)) + canAccess = aclStore->canAccess(real->outPath); info.known = { .path = real->outPath, - .status = PathStatus::Valid, + .status = canAccess ? PathStatus::Valid : PathStatus::Inaccessible, }; } else if (info.known && info.known->isValid()) { // We know the output because it's a static output of the diff --git a/src/libstore/build/derivation-goal.hh b/src/libstore/build/derivation-goal.hh index ee8f06f2550..f396eef92bc 100644 --- a/src/libstore/build/derivation-goal.hh +++ b/src/libstore/build/derivation-goal.hh @@ -18,19 +18,20 @@ typedef enum {rpAccept, rpDecline, rpPostpone} HookReply; /** * Unless we are repairing, we don't both to test validity and just assume it, - * so the choices are `Absent` or `Valid`. + * so the choices are `Absent`, `Inaccessible` or `Valid`. */ enum struct PathStatus { Corrupt, Absent, Valid, + Inaccessible, }; struct InitialOutputStatus { StorePath path; PathStatus status; /** - * Valid in the store, and additionally non-corrupt if we are repairing + * Valid in the store, accessible, and additionally non-corrupt if we are repairing */ bool isValid() const { return status == PathStatus::Valid; @@ -40,6 +41,7 @@ struct InitialOutputStatus { */ bool isPresent() const { return status == PathStatus::Corrupt + || status == PathStatus::Inaccessible || status == PathStatus::Valid; } }; diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index ee66ee50011..ce464afce10 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -1,6 +1,10 @@ #include "local-derivation-goal.hh" +#include "acl.hh" +#include "config.hh" #include "gc-store.hh" +#include "granular-access-store.hh" #include "hook-instance.hh" +#include "local-fs-store.hh" #include "worker.hh" #include "builtins.hh" #include "builtins/buildenv.hh" @@ -17,11 +21,13 @@ #include "personality.hh" #include "namespaces.hh" +#include #include #include #include #include +#include #include #include #include @@ -231,6 +237,22 @@ void LocalDerivationGoal::tryLocalBuild() } } + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) + if (auto localStore = dynamic_cast(&worker.store)) { + for (auto path : inputPaths) { + if (localStore->getAccessStatus(path).isProtected) { + if (!localStore->canAccess(path)) + throw AccessDenied( + "%s (uid %d) does not have access to path %s", + getUserName(localStore->effectiveUser->uid), + localStore->effectiveUser->uid, + localStore->printStorePath(path)); + localStore->grantBuildUserAccess(path, ACL::User(getuid())); + localStore->grantBuildUserAccess(path, ACL::User(sandboxUid())); + } + } + } + actLock.reset(); try { @@ -265,17 +287,19 @@ static void chmod_(const Path & path, mode_t mode) directory's parent link ".."). */ static void movePath(const Path & src, const Path & dst) { + debug("Moving %s to %s", src, dst); auto st = lstat(src); bool changePerm = (geteuid() && S_ISDIR(st.st_mode) && !(st.st_mode & S_IWUSR)); + mode_t mode = st.st_mode; + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) + mode &= ~S_IRWXO; - if (changePerm) - chmod_(src, st.st_mode | S_IWUSR); + chmod_(src, mode | (changePerm ? S_IWUSR : 0)); renameFile(src, dst); - if (changePerm) - chmod_(dst, st.st_mode); + chmod_(dst, mode); } @@ -298,6 +322,15 @@ void LocalDerivationGoal::closeReadPipes() void LocalDerivationGoal::cleanupHookFinally() { + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) { + if (auto localStore = dynamic_cast(&worker.store)) { + for (auto path : inputPaths) { + localStore->revokeBuildUserAccess(path, ACL::User(getuid())); + localStore->revokeBuildUserAccess(path, ACL::User(sandboxUid())); + } + } + } + /* Release the build user at the end of this function. We don't do it right away because we don't want another build grabbing this uid and then messing around with our output. */ @@ -358,8 +391,10 @@ bool LocalDerivationGoal::cleanupDecideWhetherDiskFull() if (!status.known) continue; if (buildMode != bmCheck && status.known->isValid()) continue; auto p = worker.store.toRealPath(status.known->path); - if (pathExists(chrootRootDir + p)) + if (pathExists(chrootRootDir + p)) { + deletePath(p); renameFile((chrootRootDir + p), p); + } } return diskFull; @@ -702,13 +737,11 @@ void LocalDerivationGoal::startBuilder() if (buildUser && chown(chrootStoreDir.c_str(), 0, buildUser->getGID()) == -1) throw SysError("cannot change ownership of '%1%'", chrootStoreDir); + // auto & localStore = getLocalStore(); for (auto & i : inputPaths) { auto p = worker.store.printStorePath(i); Path r = worker.store.toRealPath(p); - if (S_ISDIR(lstat(r).st_mode)) - dirsInChroot.insert_or_assign(p, r); - else - linkOrCopy(r, chrootRootDir + p); + dirsInChroot.insert_or_assign(p, r); } /* If we're repairing, checking or rebuilding part of a @@ -797,6 +830,7 @@ void LocalDerivationGoal::startBuilder() /* Run the builder. */ printMsg(lvlChatty, "executing builder '%1%'", drv->builder); printMsg(lvlChatty, "using builder args '%1%'", concatStringsSep(" ", drv->args)); + for (auto & i : drv->env) printMsg(lvlVomit, "setting builder env variable '%1%'='%2%'", i.first, i.second); @@ -1470,9 +1504,15 @@ void LocalDerivationGoal::startDaemon() auto workerThread = std::thread([store, remote{std::move(remote)}]() { FdSource from(remote.get()); FdSink to(remote.get()); + AuthenticatedUser user; + if (store->next->effectiveUser) { + user = {NotTrusted, store->next->effectiveUser->uid}; + } else { + user = {NotTrusted, 0}; + } try { daemon::processConnection(store, from, to, - NotTrusted, daemon::Recursive); + user, daemon::Recursive); debug("terminated daemon connection"); } catch (SysError &) { ignoreException(); @@ -1587,7 +1627,6 @@ void LocalDerivationGoal::chownToBuilder(const Path & path) throw SysError("cannot change ownership of '%1%'", path); } - void setupSeccomp() { #if __linux__ @@ -1804,22 +1843,52 @@ void LocalDerivationGoal::runChild() filesystem that we want in the chroot environment. */ auto doBind = [&](const Path & source, const Path & target, bool optional = false) { - debug("bind mounting '%1%' to '%2%'", source, target); - struct stat st; - if (stat(source.c_str(), &st) == -1) { - if (optional && errno == ENOENT) + auto doMount = [&](const Path & source, const Path & target) { + debug("bind mounting '%1%' to '%2%'", source, target); + struct stat st; + if (stat(source.c_str(), &st) == -1) { + if (optional && errno == ENOENT) + return; + else + throw SysError("getting attributes of path '%1%'", source); + } + + if (S_ISDIR(st.st_mode)) + createDirs(target); + else { + createDirs(dirOf(target)); + writeFile(target, ""); + } + + if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REC, 0) == -1) + throw SysError("bind mount from '%1%' to '%2%' failed", source, target); + }; + + + if (experimentalFeatureSettings.isEnabled(Xp::ACLs) && worker.store.isInStore(source)) { + auto [storePath, subPath] = worker.store.toStorePath(source); + + // TODO(ACL) Add tests to check that ACL information is never leaked + // FIXME probably should use a FUSE fs or something? + ssize_t eaSize = llistxattr(source.c_str(), nullptr, 0); + if (subPath == "" && eaSize > 0) { + // The source store path contains extended attributes + // mounting it as-is would preserve them, which is undesireable. + if (std::filesystem::is_directory(source)) { + createDirs(target); // In case the directory is empty + for (auto dirent : std::filesystem::directory_iterator(std::filesystem::directory_entry(source))) + doMount(dirent.path().c_str(), (target + "/" + baseNameOf(dirent.path().c_str())).c_str()); + } + else { + std::filesystem::copy(source, target); + } + using namespace std::filesystem; + auto p = status(target).permissions(); + permissions(target, (p | ((p & perms::owner_read) != perms::none ? perms::others_read : perms::none) | ((p & perms::owner_exec) != perms::none ? perms::others_exec : perms::none)), perm_options::add); return; - else - throw SysError("getting attributes of path '%1%'", source); - } - if (S_ISDIR(st.st_mode)) - createDirs(target); - else { - createDirs(dirOf(target)); - writeFile(target, ""); + } } - if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REC, 0) == -1) - throw SysError("bind mount from '%1%' to '%2%' failed", source, target); + doMount(source, target); }; for (auto & i : dirsInChroot) { @@ -2017,6 +2086,7 @@ void LocalDerivationGoal::runChild() /* Add all our input paths to the chroot */ for (auto & i : inputPaths) { auto p = worker.store.printStorePath(i); + dirsInChroot[p] = p; } @@ -2370,6 +2440,12 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() auto actualPath = toRealPathChroot(worker.store.printStorePath(*scratchPath)); auto finish = [&](StorePath finalStorePath) { + auto & localStore = getLocalStore(); + + StoreObjectDerivationOutput thisOutput(drvPath, outputName); + if (localStore.futurePermissions.contains(thisOutput)) { + localStore.setFutureAccessStatus(finalStorePath, localStore.futurePermissions[thisOutput]); + } /* Store the final path */ finalOutputs.insert_or_assign(outputName, finalStorePath); /* The rewrite rule will be used in downstream outputs that refer to @@ -2653,12 +2729,15 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() worker.store.printStorePath(drvPath), worker.store.toRealPath(finalDestPath)); } - /* Since we verified the build, it's now ultimately trusted. */ + /* Since we verified the build, it's now ultimately trusted, and we + can grant access to whoever requested the build */ if (!oldInfo.ultimate) { oldInfo.ultimate = true; localStore.signPathInfo(oldInfo); localStore.registerValidPaths({{oldInfo.path, oldInfo}}); } + if (localStore.effectiveUser && !localStore.canAccess(oldInfo.path)) + localStore.addAllowedEntities(oldInfo.path, {*localStore.effectiveUser}); continue; } @@ -2690,6 +2769,12 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() } if (buildMode == bmCheck) { + auto & localStore = getLocalStore(); + StoreObjectDerivationLog log { drvPath }; + /* Since all outputs are known to be matching, give access to the log */ + if (localStore.effectiveUser && !localStore.canAccess(log)) + localStore.addAllowedEntities(log, {*localStore.effectiveUser}); + /* In case of fixed-output derivations, if there are mismatches on `--check` an error must be thrown as this is also a source for non-determinism. */ diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index ad3dee1a27c..903dc968432 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -1,4 +1,7 @@ #include "daemon.hh" +#include "granular-access-store.hh" +#include "local-fs-store.hh" +#include "local-store.hh" #include "monitor-fd.hh" #include "worker-protocol.hh" #include "worker-protocol-impl.hh" @@ -273,7 +276,7 @@ static std::vector readDerivedPaths(Store & store, unsigned int cli } static void performOp(TunnelLogger * logger, ref store, - TrustedFlag trusted, RecursiveFlag recursive, unsigned int clientVersion, + AuthenticatedUser user, RecursiveFlag recursive, unsigned int clientVersion, Source & from, BufferedSink & to, WorkerProto::Op op) { WorkerProto::ReadConn rconn { .from = from }; @@ -482,7 +485,7 @@ static void performOp(TunnelLogger * logger, ref store, case WorkerProto::Op::AddMultipleToStore: { bool repair, dontCheckSigs; from >> repair >> dontCheckSigs; - if (!trusted && dontCheckSigs) + if (!user.trusted && dontCheckSigs) dontCheckSigs = false; logger->startWork(); @@ -509,6 +512,7 @@ static void performOp(TunnelLogger * logger, ref store, case WorkerProto::Op::ExportPath: { auto path = store->parseStorePath(readString(from)); + if (!require(*store).canAccess(path)) throw AccessDenied("Access Denied"); readInt(from); // obsolete logger->startWork(); TunnelSink sink(to); @@ -522,7 +526,7 @@ static void performOp(TunnelLogger * logger, ref store, logger->startWork(); TunnelSource source(from, to); auto paths = store->importPaths(source, - trusted ? NoCheckSigs : CheckSigs); + user.trusted ? NoCheckSigs : CheckSigs); logger->stopWork(); Strings paths2; for (auto & i : paths) paths2.push_back(store->printStorePath(i)); @@ -545,7 +549,7 @@ static void performOp(TunnelLogger * logger, ref store, need not be getting the UID of the other end of a Unix Domain Socket. */ - if (mode == bmRepair && !trusted) + if (mode == bmRepair && !user.trusted) throw Error("repairing is not allowed because you are not in 'trusted-users'"); } logger->startWork(); @@ -564,7 +568,7 @@ static void performOp(TunnelLogger * logger, ref store, clients. FIXME: layer violation; see above. */ - if (mode == bmRepair && !trusted) + if (mode == bmRepair && !user.trusted) throw Error("repairing is not allowed because you are not in 'trusted-users'"); logger->startWork(); @@ -617,15 +621,15 @@ static void performOp(TunnelLogger * logger, ref store, derivations, we throw out the precomputed output paths and just store the hashes, so there aren't two competing sources of truth an attacker could exploit. */ - if (!(drvType.isCA() || trusted)) + if (!(drvType.isCA() || user.trusted)) throw Error("you are not privileged to build input-addressed derivations"); /* Make sure that the non-input-addressed derivations that got this far are in fact content-addressed if we don't trust them. */ - assert(drvType.isCA() || trusted); + assert(drvType.isCA() || user.trusted); /* Recompute the derivation path when we cannot trust the original. */ - if (!trusted) { + if (!user.trusted) { /* Recomputing the derivation path for input-address derivations makes it harder to audit them after the fact, since we need the original not-necessarily-resolved derivation to verify the drv @@ -694,7 +698,7 @@ static void performOp(TunnelLogger * logger, ref store, case WorkerProto::Op::FindRoots: { logger->startWork(); auto & gcStore = require(*store); - Roots roots = gcStore.findRoots(!trusted); + Roots roots = gcStore.findRoots(!user.trusted); logger->stopWork(); size_t size = 0; @@ -765,7 +769,7 @@ static void performOp(TunnelLogger * logger, ref store, // FIXME: use some setting in recursive mode. Will need to use // non-global variables. if (!recursive) - clientSettings.apply(trusted); + clientSettings.apply(user.trusted); logger->stopWork(); break; @@ -852,7 +856,7 @@ static void performOp(TunnelLogger * logger, ref store, bool checkContents, repair; from >> checkContents >> repair; logger->startWork(); - if (repair && !trusted) + if (repair && !user.trusted) throw Error("you are not privileged to repair paths"); bool errors = store->verifyStore(checkContents, (RepairFlag) repair); logger->stopWork(); @@ -891,9 +895,9 @@ static void performOp(TunnelLogger * logger, ref store, info.sigs = readStrings(from); info.ca = ContentAddress::parseOpt(readString(from)); from >> repair >> dontCheckSigs; - if (!trusted && dontCheckSigs) + if (!user.trusted && dontCheckSigs) dontCheckSigs = false; - if (!trusted) + if (!user.trusted) info.ultimate = false; if (GET_PROTOCOL_MINOR(clientVersion) >= 23) { @@ -979,7 +983,7 @@ static void performOp(TunnelLogger * logger, ref store, case WorkerProto::Op::AddBuildLog: { StorePath path{readString(from)}; logger->startWork(); - if (!trusted) + if (!user.trusted) throw Error("you are not privileged to add logs"); auto & logStore = require(*store); { @@ -993,6 +997,71 @@ static void performOp(TunnelLogger * logger, ref store, break; } + case WorkerProto::Op::GetAccessStatus: { + auto object = WorkerProto::Serialise::read(*store, rconn); + logger->startWork(); + auto status = require(*store).getAccessStatus(object); + logger->stopWork(); + WorkerProto::Serialise::write(*store, wconn, status); + break; + } + + case WorkerProto::Op::SetAccessStatus: { + auto object = WorkerProto::Serialise::read(*store, rconn); + auto status = WorkerProto::Serialise::read(*store, rconn); + logger->startWork(); + auto localStore = dynamic_cast(&*store); + auto curStatus = require(*store).getAccessStatus(object); + if (status != curStatus) { + if (user.trusted) { + localStore->setAccessStatus(object, status); + } else { + // TODO document rationale behind this logic + auto [exists, description] = std::visit(overloaded { + [&](StorePath p) { + auto rp = store->toRealPath(p); + return std::pair{pathExists(rp), fmt("path %s", rp)}; + }, + [&](StoreObjectDerivationOutput b) { + auto drv = localStore->readDerivation(b.drvPath); + auto outputHashes = staticOutputHashes(*localStore, drv); + auto drvOutputs = drv.outputsAndOptPaths(*localStore); + bool known = drvOutputs.contains(b.output) && drvOutputs.at(b.output).second; + if (known) { + auto realPath = store->toRealPath(*drvOutputs.at(b.output).second); + bool exists = pathExists(realPath); + return std::pair{exists, fmt("path %s", realPath)}; + } else { + return std::pair{false, fmt("output %s of derivation %s", b.output, store->toRealPath(b.drvPath))}; + } + }, + [&](StoreObjectDerivationLog l) { + auto baseName = l.drvPath.to_string(); + + auto logPath = fmt("%s/%s/%s/%s.bz2", localStore->logDir, localStore->drvsLogDir, baseName.substr(0, 2), baseName.substr(2)); + + return std::pair{pathExists(logPath), fmt("build log of derivation %s", store->toRealPath(l.drvPath))}; + } + }, object); + if (exists && status.isProtected != curStatus.isProtected) + throw AccessDenied("You have to be a trusted user to set a protection status on an existing %s", description); + if (! status.isProtected) + throw AccessDenied("Only trusted users can set allowed entities on an unprotected %s", description); + if (exists && ! std::includes(status.entities.begin(), status.entities.end(), curStatus.entities.begin(), curStatus.entities.end())) + throw AccessDenied("Only trusted users can revoke permissions on %s", description); + + if (! exists || localStore->canAccess(object, user.uid)) + localStore->setAccessStatus(object, status); + else { + localStore->setFutureAccessStatus(object, status); + } + } + } + logger->stopWork(); + to << 1; + break; + } + case WorkerProto::Op::QueryFailedPaths: case WorkerProto::Op::ClearFailedPaths: throw Error("Removed operation %1%", op); @@ -1006,9 +1075,14 @@ void processConnection( ref store, FdSource & from, FdSink & to, - TrustedFlag trusted, + AuthenticatedUser user, RecursiveFlag recursive) { + if (auto aclStore = store.dynamic_pointer_cast()) { + aclStore->effectiveUser = user.uid; + aclStore->trusted = user.trusted || user.uid == 0; + } + auto monitor = !recursive ? std::make_unique(from.fd) : nullptr; /* Exchange the greeting. */ @@ -1048,7 +1122,7 @@ void processConnection( if (GET_PROTOCOL_MINOR(clientVersion) >= 35) { // We and the underlying store both need to trust the client for // it to be trusted. - auto temp = trusted + auto temp = user.trusted ? store->isTrustedClient() : std::optional { NotTrusted }; WorkerProto::WriteConn wconn { .to = to }; @@ -1065,6 +1139,7 @@ void processConnection( /* Process client requests. */ while (true) { + printMsgUsing(prevLogger, lvlDebug, "waiting for op"); WorkerProto::Op op; try { op = (enum WorkerProto::Op) readInt(from); @@ -1081,7 +1156,7 @@ void processConnection( debug("performing daemon worker op: %d", op); try { - performOp(tunnelLogger, store, trusted, recursive, clientVersion, from, to, op); + performOp(tunnelLogger, store, user, recursive, clientVersion, from, to, op); } catch (Error & e) { /* If we're not in a state where we can send replies, then something went wrong processing the input of the diff --git a/src/libstore/daemon.hh b/src/libstore/daemon.hh index 1964c0d997c..b8d5873bc9e 100644 --- a/src/libstore/daemon.hh +++ b/src/libstore/daemon.hh @@ -1,6 +1,7 @@ #pragma once ///@file +#include "globals.hh" #include "serialise.hh" #include "store-api.hh" @@ -12,7 +13,7 @@ void processConnection( ref store, FdSource & from, FdSink & to, - TrustedFlag trusted, + AuthenticatedUser user, RecursiveFlag recursive); } diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc index 5a4cb18240c..73fd1f8da83 100644 --- a/src/libstore/globals.cc +++ b/src/libstore/globals.cc @@ -384,5 +384,4 @@ void initLibStore() { initLibStoreDone = true; } - } diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index ec862502045..d82ad60acc3 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -1036,4 +1036,11 @@ void initLibStore(); */ void assertLibStoreInitialized(); +enum TrustedFlag : bool { NotTrusted = false, Trusted = true }; + +struct AuthenticatedUser { + TrustedFlag trusted; + uid_t uid; +}; + } diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index e69460e6cd3..a9dab7ac0d1 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1,6 +1,10 @@ #include "local-store.hh" +#include "acl.hh" +#include "config.hh" +#include "derived-path.hh" #include "globals.hh" #include "archive.hh" +#include "granular-access-store.hh" #include "pathlocks.hh" #include "worker-protocol.hh" #include "derivations.hh" @@ -11,10 +15,13 @@ #include "finally.hh" #include "compression.hh" +#include #include #include #include +#include +#include #include #include #include @@ -26,6 +33,7 @@ #include #include #include +#include #if __linux__ #include @@ -195,6 +203,10 @@ LocalStore::LocalStore(const Params & params) } else { makeStoreWritable(); } + + effectiveUser = getuid(); + trusted = true; + createDirs(linksDir); Path profilesDir = stateDir + "/profiles"; createDirs(profilesDir); @@ -251,6 +263,15 @@ LocalStore::LocalStore(const Params & params) } } + Path aclDir = stateDir + "/acls"; + createDirs(aclDir); + Path aclBuilderPermissions = stateDir + "/acls/builder-permissions"; + createDirs(aclBuilderPermissions); + + // Clean up build users from ACLs, in case the process was killed during a build + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) + revokeBuildUserAccess(); + /* We can't open a SQLite database if the disk is full. Since this prevents the garbage collector from running when it's most needed, we reserve some dummy space that we can free just @@ -587,15 +608,30 @@ static void canonicaliseTimestampAndPermissions(const Path & path, const struct { if (!S_ISLNK(st.st_mode)) { - /* Mask out all type related bits. */ - mode_t mode = st.st_mode & ~S_IFMT; - - if (mode != 0444 && mode != 0555) { - mode = (st.st_mode & S_IFMT) - | 0444 - | (st.st_mode & S_IXUSR ? 0111 : 0); - if (chmod(path.c_str(), mode) == -1) - throw SysError("changing mode of '%1%' to %2$o", path, mode); + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) { + /* Mask out all type related bits. */ + mode_t mode = st.st_mode & ~S_IFMT; + + if (mode != 0440 && mode != 0550 && mode != 0444 && mode != 0555) { + mode = (st.st_mode & S_IFMT) + | 0444 + | (st.st_mode & S_IXUSR ? 0111 : 0); + if (! (st.st_mode & S_IRWXO)) + mode &= ~S_IRWXO; + if (chmod(path.c_str(), mode) == -1) + throw SysError("changing mode of '%1%' to %2$o", path, mode); + } + } else { + /* Mask out all type related bits. */ + mode_t mode = st.st_mode & ~S_IFMT; + + if (mode != 0444 && mode != 0555) { + mode = (st.st_mode & S_IFMT) + | 0444 + | (st.st_mode & S_IXUSR ? 0111 : 0); + if (chmod(path.c_str(), mode) == -1) + throw SysError("changing mode of '%1%' to %2$o", path, mode); + } } } @@ -799,6 +835,7 @@ void LocalStore::registerDrvOutput(const Realisation & info) .exec(); } }); + /* FIXME(ACL) set ACLs correctly */ } void LocalStore::cacheDrvOutputMapping( @@ -824,6 +861,8 @@ uint64_t LocalStore::addValidPath(State & state, throw Error("cannot add path '%s' to the Nix store because it claims to be content-addressed but isn't", printStorePath(info.path)); + syncPathPermissions(info); + state.stmts->RegisterValidPath.use() (printStorePath(info.path)) (info.narHash.to_string(Base16, true)) @@ -883,6 +922,9 @@ void LocalStore::queryPathInfoUncached(const StorePath & path, std::shared_ptr LocalStore::queryPathInfoInternal(State & state, const StorePath & path) { + if (!canAccess(path)) + throw AccessDenied("Access Denied"); + /* Get the path info. */ auto useQueryPathInfo(state.stmts->QueryPathInfo.use()(printStorePath(path))); @@ -924,6 +966,9 @@ std::shared_ptr LocalStore::queryPathInfoInternal(State & s while (useQueryReferences.next()) info->references.insert(parseStorePath(useQueryReferences.getStr(0))); + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) + info->accessStatus = getAccessStatus(path); + return info; } @@ -991,6 +1036,8 @@ void LocalStore::queryReferrers(State & state, const StorePath & path, StorePath { auto useQueryReferrers(state.stmts->QueryReferrers.use()(printStorePath(path))); + if (!canAccess(path)) throw AccessDenied("Access Denied"); + while (useQueryReferrers.next()) referrers.insert(parseStorePath(useQueryReferrers.getStr(0))); } @@ -1007,6 +1054,7 @@ void LocalStore::queryReferrers(const StorePath & path, StorePathSet & referrers StorePathSet LocalStore::queryValidDerivers(const StorePath & path) { + if (!canAccess(path)) throw AccessDenied("Access Denied"); return retrySQLite([&]() { auto state(_state.lock()); @@ -1105,6 +1153,26 @@ StorePathSet LocalStore::querySubstitutablePaths(const StorePathSet & paths) return res; } +void LocalStore::syncPathPermissions(const ValidPathInfo & info) +{ + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) { + auto realPath = Store::toRealPath(info.path); + if (futurePermissions.contains(info.path)) { + setCurrentAccessStatus(realPath, futurePermissions[info.path]); + /* FIXME: we should erase the permissions to prevent memory leakage; + However, it's not easy to only call this function once, so we end + up resetting the permissions to the default ones */ + // futurePermissions.erase(info.path); + if (info.accessStatus) + addAllowedEntities(info.path, info.accessStatus->entities); + } else if (info.accessStatus) { + setCurrentAccessStatus(realPath, *info.accessStatus); + } else { + // TODO: a mode where all new paths are protected by default + setCurrentAccessStatus(realPath, {false, {}}); + } + } +} void LocalStore::registerValidPath(const ValidPathInfo & info) { @@ -1120,7 +1188,7 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos) registering operation. */ if (settings.syncBeforeRegistering) sync(); - return retrySQLite([&]() { + retrySQLite([&]() { auto state(_state.lock()); SQLiteTxn txn(state->db); @@ -1170,6 +1238,263 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos) }); } +void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::AccessStatus & status) +{ + experimentalFeatureSettings.require(Xp::ACLs); + + using namespace ACL; + + // NOTE: On Darwin, the standard posix permissions are not part of the ACL API. + // As such, we use the standard posix API instead. + // TODO(ACLs): We could consider extending the ACL api to include these + // FS permissions for Darwin. Essentially ading UserObj, GroupObj, and Other manually. + auto perms_bm = std::filesystem::status(path).permissions(); + + // These perms will be used later to substitute the permissions + Permissions perms; + + // Remove other permissions + perms_bm &= ~std::filesystem::perms::others_all; + + // NOTE: We cannot bitshift on the permissions, so we have to copy + // the user permissions manually. + // NOTE: the bitmask is only used if the path is not going to be protected + // NOTE: The Permissions are the permissions that are equivalent to the posix bits + if ((perms_bm & std::filesystem::perms::owner_read) != std::filesystem::perms::none) { + perms_bm |= std::filesystem::perms::others_read; + + perms.allowRead(true); + } + if ((perms_bm & std::filesystem::perms::owner_write) != std::filesystem::perms::none) { + perms_bm |= std::filesystem::perms::others_write; + + perms.allowWrite(true); + } + if ((perms_bm & std::filesystem::perms::owner_exec) != std::filesystem::perms::none) { + perms_bm |= std::filesystem::perms::others_exec; + + perms.allowExecute(true); + } + + if (status.isProtected) { + std::filesystem::permissions( + path, + std::filesystem::perms::others_all, + std::filesystem::perm_options::remove + ); + } else { + std::filesystem::permissions( + path, + perms_bm, + std::filesystem::perm_options::replace + ); + } + + AccessControlList acl; + + for (auto entity : status.entities) { + std::visit(overloaded { + [&](User u){ acl[u] = perms; }, + [&](Group g){ acl[g] = perms; }, + }, entity); + } + + acl.set(path); +} + +void LocalStore::setFutureAccessStatus(const StoreObject & storePathstoreObject, const AccessStatus & status) +{ + futurePermissions[storePathstoreObject] = status; +} + +void LocalStore::setAccessStatus(const StoreObject & storePathstoreObject, const AccessStatus & status) +{ + + std::set users; + std::set groups; + for (auto entity : status.entities) { + std::visit(overloaded { + [&](ACL::User user) { + struct passwd * pw = getpwuid(user.uid); + users.insert(pw->pw_name); + }, + [&](ACL::Group group) { + struct group * gr = getgrgid(group.gid); + groups.insert(gr->gr_name); + } + }, entity); + } + std::visit(overloaded { + [&](StorePath p) { + auto path = Store::toRealPath(p); + if (pathExists(path)) + setCurrentAccessStatus(path, status); + else { + setFutureAccessStatus(p, status); + } + }, + [&](StoreObjectDerivationOutput p) { + auto drv = readDerivation(p.drvPath); + auto outputHashes = staticOutputHashes(*this, drv); + auto drvOutputs = drv.outputsAndOptPaths(*this); + if (drvOutputs.contains(p.output) && drvOutputs.at(p.output).second) { + auto path = Store::toRealPath(*drvOutputs.at(p.output).second); + if (pathExists(path)) { + setCurrentAccessStatus(path, status); + return; + } + } + setFutureAccessStatus(p, status); + }, + [&](StoreObjectDerivationLog l) { + auto baseName = l.drvPath.to_string(); + + auto logPath = fmt("%s/%s/%s/%s.bz2", logDir, drvsLogDir, baseName.substr(0, 2), baseName.substr(2)); + + if (pathExists(logPath)) { + setCurrentAccessStatus(logPath, status); + } else { + setFutureAccessStatus(l, status); + } + } + }, storePathstoreObject); +} + + +LocalStore::AccessStatus LocalStore::getCurrentAccessStatus(const Path & path) +{ + AccessStatus status; + + using namespace ACL; + + AccessControlList acl(path); + + auto perms_bm = std::filesystem::status(path).permissions(); + // Only take others read and exec + perms_bm &= perms_bm & (std::filesystem::perms::others_read | std::filesystem::perms::others_exec); + + // If neither is set, the path isProtected + status.isProtected = perms_bm == std::filesystem::perms::none; + for (auto [tag, perms] : acl) { + // Try to handle unmodelled permissions: if the subject can't read, write or execute the path, they don't really have access + if (perms.canRead() == Permissions::HasPermission::None && perms.canWrite() == Permissions::HasPermission::None && perms.canExecute() == Permissions::HasPermission::None) continue; + if (auto u = std::get_if(&tag)) status.entities.insert(*u); + else if (auto g = std::get_if(&tag)) status.entities.insert(*g); + } + + return status; +} + +LocalStore::AccessStatus LocalStore::getFutureAccessStatus(const StoreObject & storeObject) +{ + return futurePermissions.at(storeObject); +} + +LocalStore::AccessStatus LocalStore::getAccessStatus(const StoreObject & storeObject) +{ + experimentalFeatureSettings.require(Xp::ACLs); + + return std::visit(overloaded { + [&](StorePath p){ + auto path = Store::toRealPath(p); + if (pathExists(path)) + return getCurrentAccessStatus(path); + else if (futurePermissions.contains(p)) + return futurePermissions[p]; + return AccessStatus {}; + }, + [&](StoreObjectDerivationOutput p) { + auto drv = readDerivation(p.drvPath); + auto outputHashes = staticOutputHashes(*this, drv); + auto drvOutputs = drv.outputsAndOptPaths(*this); + if (drvOutputs.contains(p.output) && drvOutputs.at(p.output).second) { + auto path = Store::toRealPath(*drvOutputs.at(p.output).second); + if (pathExists(path)) { + return getCurrentAccessStatus(path); + } + } + else if (futurePermissions.contains(p)) + return futurePermissions[p]; + return AccessStatus {}; + }, + [&](StoreObjectDerivationLog l) { + auto baseName = l.drvPath.to_string(); + + auto logPath = fmt("%s/%s/%s/%s.bz2", logDir, drvsLogDir, baseName.substr(0, 2), baseName.substr(2)); + + if (pathExists(logPath)) + return getCurrentAccessStatus(logPath); + else if (futurePermissions.contains(l)) + return getFutureAccessStatus(l); + return AccessStatus {}; + } + }, storeObject); +} + +void LocalStore::grantBuildUserAccess(const StorePath & storePath, const LocalStore::AccessControlEntity & buildUser) +{ + auto basePath = stateDir + "/acls/builder-permissions/" + storePath.to_string(); + std::visit(overloaded { + [&](ACL::User u) { createDirs(basePath + "/users/" + std::to_string(u.uid)); }, + [&](ACL::Group g) { createDirs(basePath + "/groups/" + std::to_string(g.gid)); }, + }, buildUser); + addAllowedEntities(storePath, {buildUser}); +} + +void LocalStore::revokeBuildUserAccess(const StorePath & storePath, const LocalStore::AccessControlEntity & buildUser) +{ + auto basePath = stateDir + "/acls/builder-permissions/" + storePath.to_string(); + std::visit(overloaded { + [&](ACL::User u) { std::filesystem::remove((basePath + "/users/" + std::to_string(u.uid)).c_str()); }, + [&](ACL::Group g) { std::filesystem::remove((basePath + "/groups/" + std::to_string(g.gid)).c_str()); }, + }, buildUser); + removeAllowedEntities(storePath, {buildUser}); +} + +void LocalStore::revokeBuildUserAccess(const StorePath & storePath) +{ + auto basePath = stateDir + "/acls/builder-permissions/" + storePath.to_string(); + for (auto entry : std::filesystem::directory_iterator(basePath)) { + if (entry.is_directory()) { + for (auto entity : std::filesystem::directory_iterator(entry.path())) { + if (entity.path().filename() == "users") { + auto entity_ = ACL::User (std::stoi(entity.path().filename())); + revokeBuildUserAccess(storePath, entity_); + } + else if (entity.path().filename() == "groups") { + auto entity_ = ACL::Group (std::stoi(entity.path().filename())); + revokeBuildUserAccess(storePath, entity_); + } + else { + std::filesystem::remove(entity.path()); + } + } + } + std::filesystem::remove(entry.path()); + } +} + +void LocalStore::revokeBuildUserAccess() +{ + for (auto storePath : std::filesystem::directory_iterator(stateDir + "/acls/builder-permissions")) { + if (storePath.is_directory()) { + revokeBuildUserAccess(StorePath(storePath.path().filename().c_str())); + } else { + std::filesystem::remove(storePath.path()); + } + } +} + +std::set LocalStore::getSubjectGroups(ACL::User user) +{ + struct passwd * pw = getpwuid(user.uid); + auto groups_vec = getUserGroups(pw->pw_uid); + std::set groups; + for (auto group : groups_vec) { + groups.insert(group); + } + return groups; +} /* Invalidate a path. The caller is responsible for checking that there are no referrers. */ @@ -1214,7 +1539,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, addTempRoot(info.path); - if (repair || !isValidPath(info.path)) { + if (repair || !isValidPath(info.path) || !canAccess(info.path)) { PathLocks outputLock; @@ -1226,20 +1551,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, if (!locksHeld.count(printStorePath(info.path))) outputLock.lockPaths({realPath}); - if (repair || !isValidPath(info.path)) { - - deletePath(realPath); - - /* While restoring the path from the NAR, compute the hash - of the NAR. */ - HashSink hashSink(htSHA256); - - TeeSource wrapperSource { source, hashSink }; - - restorePath(realPath, wrapperSource); - - auto hashResult = hashSink.finish(); - + auto checkInfoValidity = [&](HashResult hashResult){ if (hashResult.first != info.narHash) throw Error("hash mismatch importing path '%s';\n specified: %s\n got: %s", printStorePath(info.path), info.narHash.to_string(Base32, true), hashResult.first.to_string(Base32, true)); @@ -1272,6 +1584,22 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, } } } + }; + + + if (repair || !isValidPath(info.path)) { + + deletePath(realPath); + + /* While restoring the path from the NAR, compute the hash + of the NAR. */ + HashSink hashSink(htSHA256); + + TeeSource wrapperSource { source, hashSink }; + + restorePath(realPath, wrapperSource, experimentalFeatureSettings.isEnabled(Xp::ACLs)); + + checkInfoValidity(hashSink.finish()); autoGC(); @@ -1280,6 +1608,16 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, optimisePath(realPath, repair); // FIXME: combine with hashPath() registerValidPath(info); + } else if (effectiveUser && !canAccess(info.path)) { + auto curInfo = queryPathInfo(info.path); + HashSink hashSink(htSHA256); + source.drainInto(hashSink); + + /* Check that both new and old info matches */ + checkInfoValidity(hashSink.finish()); + checkInfoValidity({curInfo->narHash, curInfo->narSize}); + + addAllowedEntities(info.path, {*effectiveUser}); } outputLock.setDeletion(true); @@ -1302,6 +1640,8 @@ StorePath LocalStore::addToStoreFromDump(Source & source0, std::string_view name path. */ bool inMemory = false; + bool protect = experimentalFeatureSettings.isEnabled(Xp::ACLs); + std::string dump; /* Fill out buffer, and decide whether we are working strictly in @@ -1339,9 +1679,9 @@ StorePath LocalStore::addToStoreFromDump(Source & source0, std::string_view name tempPath = tempDir + "/x"; if (method == FileIngestionMethod::Recursive) - restorePath(tempPath, bothSource); + restorePath(tempPath, bothSource, protect); else - writeFile(tempPath, bothSource); + writeFile(tempPath, bothSource, protect ? 0660 : 0666); dump.clear(); } @@ -1383,9 +1723,9 @@ StorePath LocalStore::addToStoreFromDump(Source & source0, std::string_view name StringSource dumpSource { dump }; /* Restore from the NAR in memory. */ if (method == FileIngestionMethod::Recursive) - restorePath(realPath, dumpSource); + restorePath(realPath, dumpSource, protect); else - writeFile(realPath, dumpSource); + writeFile(realPath, dumpSource, protect ? 0660 : 0666); } else { /* Move the temporary path we restored above. */ moveFile(tempPath, realPath); @@ -1446,7 +1786,9 @@ StorePath LocalStore::addTextToStore( autoGC(); - writeFile(realPath, s); + mode_t mode = experimentalFeatureSettings.isEnabled(Xp::ACLs) ? 0440 : 0444; + + writeFile(realPath, s, mode); canonicalisePathMetaData(realPath, {}); diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh index 8a3b0b43fc1..ea20d2b57a4 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -68,7 +68,7 @@ struct LocalStoreConfig : virtual LocalFSStoreConfig std::string doc() override; }; -class LocalStore : public virtual LocalStoreConfig, public virtual LocalFSStore, public virtual GcStore +class LocalStore : public virtual LocalStoreConfig, public virtual LocalFSStore, public virtual GcStore, public virtual LocalGranularAccessStore { private: @@ -113,6 +113,16 @@ private: Sync _state; + /** + * Map of paths, which, when added to the store need permissions to be set. + */ + std::map futurePermissions; + + /** + * Sync path permissions from futurePermissions to a real path in store + */ + void syncPathPermissions(const ValidPathInfo & info); + public: const Path dbDir; @@ -282,8 +292,21 @@ public: void queryRealisationUncached(const DrvOutput&, Callback> callback) noexcept override; - std::optional getVersion() override; + void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; + void setFutureAccessStatus(const StoreObject & storeObject, const AccessStatus & status); + void setCurrentAccessStatus(const Path & path, const AccessStatus & status); + AccessStatus getAccessStatus(const StoreObject & storeObject) override; + AccessStatus getFutureAccessStatus(const StoreObject & storeObject); + AccessStatus getCurrentAccessStatus(const Path & path); + + void grantBuildUserAccess(const StorePath & path, const AccessControlEntity & entity); + void revokeBuildUserAccess(const StorePath & path, const AccessControlEntity & entity); + void revokeBuildUserAccess(const StorePath & path); + void revokeBuildUserAccess(); + std::set getSubjectGroups(ACL::User user) override; + + std::optional getVersion() override; private: /** diff --git a/src/libstore/local.mk b/src/libstore/local.mk index 0be0bf31058..438af83259c 100644 --- a/src/libstore/local.mk +++ b/src/libstore/local.mk @@ -27,6 +27,10 @@ ifeq ($(HAVE_SECCOMP), 1) libstore_LDFLAGS += $(LIBSECCOMP_LIBS) endif +ifdef HOST_LINUX + libstore_LDFLAGS += -lacl +endif + libstore_CXXFLAGS += \ -I src/libutil -I src/libstore -I src/libstore/build \ -DNIX_PREFIX=\"$(prefix)\" \ diff --git a/src/libstore/lock.cc b/src/libstore/lock.cc index 7202a64b3c5..83b46d1058b 100644 --- a/src/libstore/lock.cc +++ b/src/libstore/lock.cc @@ -14,11 +14,11 @@ struct SimpleUserLock : UserLock gid_t gid; std::vector supplementaryGIDs; - uid_t getUID() override { assert(uid); return uid; } - uid_t getUIDCount() override { return 1; } - gid_t getGID() override { assert(gid); return gid; } + uid_t getUID() const override { assert(uid); return uid; } + uid_t getUIDCount() const override { return 1; } + gid_t getGID() const override { assert(gid); return gid; } - std::vector getSupplementaryGIDs() override { return supplementaryGIDs; } + std::vector getSupplementaryGIDs() const override { return supplementaryGIDs; } static std::unique_ptr acquire() { @@ -115,13 +115,13 @@ struct AutoUserLock : UserLock gid_t firstGid = 0; uid_t nrIds = 1; - uid_t getUID() override { assert(firstUid); return firstUid; } + uid_t getUID() const override { assert(firstUid); return firstUid; } - gid_t getUIDCount() override { return nrIds; } + gid_t getUIDCount() const override { return nrIds; } - gid_t getGID() override { assert(firstGid); return firstGid; } + gid_t getGID() const override { assert(firstGid); return firstGid; } - std::vector getSupplementaryGIDs() override { return {}; } + std::vector getSupplementaryGIDs() const override { return {}; } static std::unique_ptr acquire(uid_t nrIds, bool useUserNamespace) { @@ -178,6 +178,10 @@ struct AutoUserLock : UserLock } }; +ACL::User::User(const UserLock & lock) { + uid = lock.getUID(); +} + std::unique_ptr acquireUserLock(uid_t nrIds, bool useUserNamespace) { if (settings.autoAllocateUids) diff --git a/src/libstore/lock.hh b/src/libstore/lock.hh index 1c268e1fbd5..621922656e7 100644 --- a/src/libstore/lock.hh +++ b/src/libstore/lock.hh @@ -2,6 +2,7 @@ ///@file #include "types.hh" +#include "acl.hh" #include @@ -16,7 +17,7 @@ struct UserLock /** * Get the first and last UID. */ - std::pair getUIDRange() + std::pair getUIDRange() const { auto first = getUID(); return {first, first + getUIDCount() - 1}; @@ -25,13 +26,13 @@ struct UserLock /** * Get the first UID. */ - virtual uid_t getUID() = 0; + virtual uid_t getUID() const = 0; - virtual uid_t getUIDCount() = 0; + virtual uid_t getUIDCount() const = 0; - virtual gid_t getGID() = 0; + virtual gid_t getGID() const = 0; - virtual std::vector getSupplementaryGIDs() = 0; + virtual std::vector getSupplementaryGIDs() const = 0; }; /** diff --git a/src/libstore/misc.cc b/src/libstore/misc.cc index 50336c77971..c9caa872287 100644 --- a/src/libstore/misc.cc +++ b/src/libstore/misc.cc @@ -309,10 +309,20 @@ std::map drvOutputReferences( } OutputPathMap resolveDerivedPath(Store & store, const DerivedPath::Built & bfd, Store * evalStore_) +{ + auto [outputs, missing] = resolveDerivedPathAll(store, bfd, evalStore_); + if (!missing.empty()) + throw MissingRealisation(*missing.begin()); + return outputs; +} + +// FIXME refactor with resolveDerivedPath to remove repetition +std::pair> resolveDerivedPathAll(Store & store, const DerivedPath::Built & bfd, Store * evalStore_) { auto & evalStore = evalStore_ ? *evalStore_ : store; OutputPathMap outputs; + std::set missingOutputs; auto drv = evalStore.readDerivation(bfd.drvPath); auto outputHashes = staticOutputHashes(store, drv); auto drvOutputs = drv.outputsAndOptPaths(store); @@ -336,9 +346,10 @@ OutputPathMap resolveDerivedPath(Store & store, const DerivedPath::Built & bfd, if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { DrvOutput outputId { *outputHash, output }; auto realisation = store.queryRealisation(outputId); - if (!realisation) - throw MissingRealisation(outputId); - outputs.insert_or_assign(output, realisation->outPath); + if (realisation) + outputs.insert_or_assign(output, realisation->outPath); + else + missingOutputs.insert(outputId); } else { // If ca-derivations isn't enabled, assume that // the output path is statically known. @@ -348,7 +359,7 @@ OutputPathMap resolveDerivedPath(Store & store, const DerivedPath::Built & bfd, outputs.insert_or_assign(output, *drvOutput->second); } } - return outputs; + return {outputs, missingOutputs}; } } diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc index d17253741a6..0ee7a850b25 100644 --- a/src/libstore/nar-info.cc +++ b/src/libstore/nar-info.cc @@ -1,5 +1,7 @@ +#include "config.hh" #include "globals.hh" #include "nar-info.hh" +#include "path-info.hh" #include "store-api.hh" namespace nix { @@ -78,6 +80,24 @@ NarInfo::NarInfo(const Store & store, const std::string & s, const std::string & if (ca) throw corrupt("extra CA"); // FIXME: allow blank ca or require skipping field? ca = ContentAddress::parseOpt(value); + } else if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) { + if (name == "Protected") { + if (!accessStatus) accessStatus = ValidPathInfo::AccessStatus {}; + if (value == "true") + accessStatus->isProtected = true; + else if (value == "false") + accessStatus->isProtected = false; + else + throw corrupt("invalid Protected value"); + } + else if (name == "AllowedUser") { + if (!accessStatus) accessStatus = ValidPathInfo::AccessStatus {}; + accessStatus->entities.insert(ACL::User{getpwnam(value.c_str())->pw_uid}); + } + else if (name == "AllowedGroup") { + if (!accessStatus) accessStatus = ValidPathInfo::AccessStatus {}; + accessStatus->entities.insert(ACL::Group{getgrnam(value.c_str())->gr_gid}); + } } pos = eol + 1; @@ -122,6 +142,15 @@ std::string NarInfo::to_string(const Store & store) const if (ca) res += "CA: " + renderContentAddress(*ca) + "\n"; + if (experimentalFeatureSettings.isEnabled(Xp::ACLs) && accessStatus) { + res += "Protected: " + std::string(accessStatus->isProtected ? "true" : "false") + "\n"; + for (auto entity : accessStatus->entities) + std::visit(overloaded { + [&](ACL::User u){ res += "AllowedUser: " + std::string(getpwuid(u.uid)->pw_name) + "\n"; }, + [&](ACL::Group g){ res += "AllowedGroup: " + std::string(getgrgid(g.gid)->gr_name) + "\n"; } + }, entity); + } + return res; } diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index 981bbfb1499..dccd6e87d1d 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -141,6 +141,12 @@ ValidPathInfo ValidPathInfo::read(Source & source, const Store & store, unsigned info.sigs = readStrings(source); info.ca = ContentAddress::parseOpt(readString(source)); } + if (format >= 36) { + bool hasAccessStatus; + source >> hasAccessStatus; + if (hasAccessStatus) + info.accessStatus = WorkerProto::Serialise::read(store, WorkerProto::ReadConn {.from = source}); + } return info; } @@ -164,6 +170,14 @@ void ValidPathInfo::write( << sigs << renderContentAddress(ca); } + if (format >= 36) { + if (accessStatus) { + sink << true; + WorkerProto::Serialise::write(store, WorkerProto::WriteConn {.to = sink}, *accessStatus); + } else { + sink << false; + } + } } } diff --git a/src/libstore/path-info.hh b/src/libstore/path-info.hh index 22152362206..da472e4ab36 100644 --- a/src/libstore/path-info.hh +++ b/src/libstore/path-info.hh @@ -5,6 +5,8 @@ #include "path.hh" #include "hash.hh" #include "content-address.hh" +#include "acl.hh" +#include "access-status.hh" #include #include @@ -14,7 +16,6 @@ namespace nix { class Store; - struct SubstitutablePathInfo { std::optional deriver; @@ -117,6 +118,8 @@ struct ValidPathInfo * Verify a single signature. */ bool checkSignature(const Store & store, const PublicKeys & publicKeys, const std::string & sig) const; + using AccessStatus = AccessStatusFor>; + std::optional accessStatus; Strings shortRefs() const; diff --git a/src/libstore/realisation.hh b/src/libstore/realisation.hh index 2a093c1289f..5aa86516271 100644 --- a/src/libstore/realisation.hh +++ b/src/libstore/realisation.hh @@ -142,7 +142,7 @@ struct RealisedPath { class MissingRealisation : public Error { public: - MissingRealisation(DrvOutput & outputId) + MissingRealisation(const DrvOutput & outputId) : Error( "cannot operate on an output of the " "unbuilt derivation '%s'", outputId.to_string()) diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index 1e2104e1faa..eb5d1c4b569 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -1,3 +1,4 @@ +#include "local-fs-store.hh" #include "serialise.hh" #include "util.hh" #include "path-with-outputs.hh" @@ -45,6 +46,7 @@ RemoteStore::RemoteStore(const Params & params) } )) { + effectiveUser = getuid(); } @@ -103,6 +105,7 @@ void RemoteStore::initConnection(Connection & conn) if (GET_PROTOCOL_MINOR(conn.daemonVersion) >= 35) { conn.remoteTrustsUs = WorkerProto::Serialise>::read(*this, conn); + if (conn.remoteTrustsUs && *conn.remoteTrustsUs) trusted = true; } else { // We don't know the answer; protocol to old. conn.remoteTrustsUs = std::nullopt; @@ -951,6 +954,36 @@ void RemoteStore::addBuildLog(const StorePath & drvPath, std::string_view log) readInt(conn->from); } +void RemoteStore::setAccessStatus(const StoreObject & storeObject, const RemoteStore::AccessStatus & status) +{ + auto conn(getConnection()); + conn->to << WorkerProto::Op::SetAccessStatus; + WorkerProto::Serialise::write(*this, *conn, storeObject); + WorkerProto::Serialise::write(*this, *conn, status); + conn.processStderr(); + readInt(conn->from); +} +RemoteStore::AccessStatus RemoteStore::getAccessStatus(const StoreObject & storeObject) +{ + auto conn(getConnection()); + conn->to << WorkerProto::Op::GetAccessStatus; + WorkerProto::Serialise::write(*this, *conn, storeObject); + conn.processStderr(); + auto status = WorkerProto::Serialise::read(*this, *conn); + return status; +} + +std::set RemoteStore::getSubjectGroups(ACL::User user) +{ + struct passwd * pw = getpwuid(user.uid); + auto groups_vec = getUserGroups(pw->pw_uid); + std::set groups; + for (auto group : groups_vec) { + groups.insert(group); + } + return groups; +} + std::optional RemoteStore::getVersion() { diff --git a/src/libstore/remote-store.hh b/src/libstore/remote-store.hh index cb7a71acf1d..3bd5f155d57 100644 --- a/src/libstore/remote-store.hh +++ b/src/libstore/remote-store.hh @@ -4,6 +4,7 @@ #include #include +#include "local-fs-store.hh" #include "store-api.hh" #include "gc-store.hh" #include "log-store.hh" @@ -39,7 +40,8 @@ struct RemoteStoreConfig : virtual StoreConfig class RemoteStore : public virtual RemoteStoreConfig, public virtual Store, public virtual GcStore, - public virtual LogStore + public virtual LogStore, + public virtual LocalGranularAccessStore { public: @@ -170,6 +172,11 @@ public: ref openConnectionWrapper(); + void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; + AccessStatus getAccessStatus(const StoreObject & storeObject) override; + + std::set getSubjectGroups(ACL::User user) override; + protected: virtual ref openConnection() = 0; diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 5bee1af9fd1..ad39f7bbded 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -975,6 +975,16 @@ json Store::pathInfoToJSON(const StorePathSet & storePaths, if (showClosureSize) jsonPath["closureDownloadSize"] = closureSizes.second; } + + if (info->accessStatus) { + jsonPath["protected"] = info->accessStatus->isProtected; + for (auto & entity : info->accessStatus->entities) { + std::visit(overloaded { + [&](ACL::User u) { jsonPath["allowedUsers"].push_back(getpwuid(u.uid)->pw_name); }, + [&](ACL::Group g) { jsonPath["allowedGroups"].push_back(getgrgid(g.gid)->gr_name); }, + }, entity); + } + } } } catch (InvalidPath &) { diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index 14a862eefca..8029f4db1ff 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -15,10 +15,12 @@ #include "path-info.hh" #include "repair-flag.hh" +#include #include #include #include #include +#include #include #include #include @@ -68,6 +70,8 @@ MakeError(BadStorePath, Error); MakeError(InvalidStoreURI, Error); +MakeError(AccessDenied, Error); + struct BasicDerivation; struct Derivation; class FSAccessor; @@ -82,6 +86,7 @@ enum CheckSigsFlag : bool { NoCheckSigs = false, CheckSigs = true }; enum SubstituteFlag : bool { NoSubstitute = false, Substitute = true }; enum AllowInvalidFlag : bool { DisallowInvalid = false, AllowInvalid = true }; + /** * Magic header of exportPath() output (obsolete). */ @@ -89,7 +94,6 @@ const uint32_t exportMagic = 0x4558494e; enum BuildMode { bmNormal, bmRepair, bmCheck }; -enum TrustedFlag : bool { NotTrusted = false, Trusted = true }; struct BuildResult; struct KeyedBuildResult; @@ -920,6 +924,10 @@ void removeTempRoots(); * is unknown. */ OutputPathMap resolveDerivedPath(Store &, const DerivedPath::Built &, Store * evalStore = nullptr); +/** + * Resolve the derived path, splitting it into known and unknown outputs. + */ +std::pair> resolveDerivedPathAll(Store & store, const DerivedPath::Built & bfd, Store * evalStore_ = nullptr); /** diff --git a/src/libstore/worker-protocol-impl.hh b/src/libstore/worker-protocol-impl.hh index d3d2792ff52..7167d6ad2aa 100644 --- a/src/libstore/worker-protocol-impl.hh +++ b/src/libstore/worker-protocol-impl.hh @@ -9,9 +9,25 @@ */ #include "worker-protocol.hh" +#include "granular-access-store.hh" namespace nix { +template +AccessStatusFor WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) { + AccessStatusFor status; + conn.from >> status.isProtected; + status.entities = WorkerProto::Serialise>::read(store, conn); + return status; +} + +template +void WorkerProto::Serialise>::write(const Store & store, WorkerProto::WriteConn conn, const AccessStatusFor & status) +{ + conn.to << status.isProtected; + WorkerProto::Serialise>::write(store, conn, status.entities); +} + template std::vector WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) { @@ -52,6 +68,76 @@ void WorkerProto::Serialise>::write(const Store & store, WorkerProto } } +template +std::variant WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) +{ + size_t index; + conn.from >> index; + switch (index) { + case 0: + return WorkerProto::Serialise::read(store, conn); + break; + case 1: + return WorkerProto::Serialise::read(store, conn); + break; + default: + throw Error("Invalid variant index from remote"); + } +} + +template +void WorkerProto::Serialise>::write(const Store & store, WorkerProto::WriteConn conn, const std::variant & resVariant) +{ + size_t index = resVariant.index(); + conn.to << index; + switch (index) { + case 0: + WorkerProto::Serialise::write(store, conn, std::get<0>(resVariant)); + break; + case 1: + WorkerProto::Serialise::write(store, conn, std::get<1>(resVariant)); + break; + default: + throw Error("Invalid variant index"); + } +} +template +std::variant WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) +{ + size_t index; + conn.from >> index; + switch (index) { + case 0: + return WorkerProto::Serialise::read(store, conn); + case 1: + return WorkerProto::Serialise::read(store, conn); + case 2: + return WorkerProto::Serialise::read(store, conn); + default: + throw Error("Invalid variant index from remote"); + } +} + +template +void WorkerProto::Serialise>::write(const Store & store, WorkerProto::WriteConn conn, const std::variant & resVariant) +{ + size_t index = resVariant.index(); + conn.to << index; + switch (index) { + case 0: + WorkerProto::Serialise::write(store, conn, std::get<0>(resVariant)); + break; + case 1: + WorkerProto::Serialise::write(store, conn, std::get<1>(resVariant)); + break; + case 2: + WorkerProto::Serialise::write(store, conn, std::get<2>(resVariant)); + break; + default: + throw Error("Invalid variant index"); + } +} + template std::map WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) { @@ -75,4 +161,19 @@ void WorkerProto::Serialise>::write(const Store & store, WorkerPr } } +template +std::pair WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) +{ + auto a = WorkerProto::Serialise::read(store, conn); + auto b = WorkerProto::Serialise::read(store, conn); + return {a, b}; +} + +template +void WorkerProto::Serialise>::write(const Store & store, WorkerProto::WriteConn conn, const std::pair & p) +{ + WorkerProto::Serialise::write(store, conn, p.first); + WorkerProto::Serialise::write(store, conn, p.second); +} + } diff --git a/src/libstore/worker-protocol.cc b/src/libstore/worker-protocol.cc index a23130743ee..ea0fa888cfe 100644 --- a/src/libstore/worker-protocol.cc +++ b/src/libstore/worker-protocol.cc @@ -1,3 +1,5 @@ +#include "globals.hh" +#include "granular-access-store.hh" #include "serialise.hh" #include "util.hh" #include "path-with-outputs.hh" @@ -67,6 +69,40 @@ void WorkerProto::Serialise>::write(const Store & sto } } +AuthenticatedUser WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) { + AuthenticatedUser user; + user.trusted = *WorkerProto::Serialise>::read(store, conn); + conn.from >> user.uid; + return user; +} + +void WorkerProto::Serialise::write(const Store & store, WorkerProto::WriteConn conn, const AuthenticatedUser & user) +{ + WorkerProto::Serialise>::write(store, conn, user.trusted); + conn.to << user.uid; +} + +ACL::User WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) { + uid_t uid; + conn.from >> uid; + return uid; +} + +void WorkerProto::Serialise::write(const Store & store, WorkerProto::WriteConn conn, const ACL::User & user) +{ + conn.to << user.uid; +} + +ACL::Group WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) { + gid_t gid; + conn.from >> gid; + return gid; +} + +void WorkerProto::Serialise::write(const Store & store, WorkerProto::WriteConn conn, const ACL::Group & group) +{ + conn.to << group.gid; +} ContentAddress WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) { @@ -116,6 +152,28 @@ void WorkerProto::Serialise::write(const Store & store, WorkerProto:: conn.to << drvOutput.to_string(); } +StoreObjectDerivationOutput WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) +{ + auto drvPath = WorkerProto::Serialise::read(store, conn); + auto output = WorkerProto::Serialise::read(store, conn); + return {drvPath, output}; +} + +void WorkerProto::Serialise::write(const Store & store, WorkerProto::WriteConn conn, const StoreObjectDerivationOutput & drvOutput) +{ + WorkerProto::Serialise::write(store, conn, drvOutput.drvPath); + WorkerProto::Serialise::write(store, conn, drvOutput.output); +} + +StoreObjectDerivationLog WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) +{ + return { WorkerProto::Serialise::read(store, conn) }; +} + +void WorkerProto::Serialise::write(const Store & store, WorkerProto::WriteConn conn, const StoreObjectDerivationLog & drvLog) +{ + WorkerProto::Serialise::write(store, conn, drvLog.drvPath); +} KeyedBuildResult WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) { diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh index ff762c924ab..06fa69becbf 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -2,6 +2,7 @@ ///@file #include "serialise.hh" +#include "acl.hh" namespace nix { @@ -9,7 +10,7 @@ namespace nix { #define WORKER_MAGIC_1 0x6e697863 #define WORKER_MAGIC_2 0x6478696f -#define PROTOCOL_VERSION (1 << 8 | 35) +#define PROTOCOL_VERSION (1 << 8 | 36) #define GET_PROTOCOL_MAJOR(x) ((x) & 0xff00) #define GET_PROTOCOL_MINOR(x) ((x) & 0x00ff) @@ -29,12 +30,16 @@ struct Source; // items being serialised struct DerivedPath; +struct StoreObjectDerivationOutput; +struct StoreObjectDerivationLog; struct DrvOutput; struct Realisation; struct BuildResult; struct KeyedBuildResult; enum TrustedFlag : bool; - +struct AuthenticatedUser; +namespace acl { struct User; struct Group; }; +template struct AccessStatusFor; /** * The "worker protocol", used by unix:// and ssh-ng:// stores. @@ -158,6 +163,8 @@ enum struct WorkerProto::Op : uint64_t AddMultipleToStore = 44, AddBuildLog = 45, BuildPathsWithResults = 46, + GetAccessStatus = 47, + SetAccessStatus = 48, }; /** @@ -206,6 +213,10 @@ MAKE_WORKER_PROTO(ContentAddress); template<> MAKE_WORKER_PROTO(DerivedPath); template<> +MAKE_WORKER_PROTO(StoreObjectDerivationOutput); +template<> +MAKE_WORKER_PROTO(StoreObjectDerivationLog); +template<> MAKE_WORKER_PROTO(Realisation); template<> MAKE_WORKER_PROTO(DrvOutput); @@ -215,17 +226,39 @@ template<> MAKE_WORKER_PROTO(KeyedBuildResult); template<> MAKE_WORKER_PROTO(std::optional); +template<> +MAKE_WORKER_PROTO(AuthenticatedUser); +template<> +MAKE_WORKER_PROTO(ACL::User); +template<> +MAKE_WORKER_PROTO(ACL::Group); +template +MAKE_WORKER_PROTO(AccessStatusFor); template MAKE_WORKER_PROTO(std::vector); template MAKE_WORKER_PROTO(std::set); +template +#define X_ std::variant +MAKE_WORKER_PROTO(X_); +#undef X_ +template +#define X_ std::variant +MAKE_WORKER_PROTO(X_); +#undef X_ + template #define X_ std::map MAKE_WORKER_PROTO(X_); #undef X_ +template +#define X_ std::pair +MAKE_WORKER_PROTO(X_); +#undef X_ + /** * These use the empty string for the null case, relying on the fact * that the underlying types never serialise to the empty string. diff --git a/src/libutil/archive.cc b/src/libutil/archive.cc index 268a798d900..4feb424e5b0 100644 --- a/src/libutil/archive.cc +++ b/src/libutil/archive.cc @@ -306,18 +306,21 @@ struct RestoreSink : ParseSink { Path dstPath; AutoCloseFD fd; + bool protect = false; void createDirectory(const Path & path) override { Path p = dstPath + path; - if (mkdir(p.c_str(), 0777) == -1) + auto mode = (protect && (path == "" || path == "/")) ? 0770 : 0777; + if (mkdir(p.c_str(), mode) == -1) throw SysError("creating directory '%1%'", p); }; void createRegularFile(const Path & path) override { Path p = dstPath + path; - fd = open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666); + auto mode = (protect && (path == "" || path == "/")) ? 0660 : 0666; + fd = open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, mode); if (!fd) throw SysError("creating file '%1%'", p); } @@ -367,10 +370,11 @@ struct RestoreSink : ParseSink }; -void restorePath(const Path & path, Source & source) +void restorePath(const Path & path, Source & source, bool protect) { RestoreSink sink; sink.dstPath = path; + sink.protect = protect; parseDump(sink, source); } @@ -388,12 +392,12 @@ void copyNAR(Source & source, Sink & sink) } -void copyPath(const Path & from, const Path & to) +void copyPath(const Path & from, const Path & to, bool protect) { auto source = sinkToSource([&](Sink & sink) { dumpPath(from, sink); }); - restorePath(to, *source); + restorePath(to, *source, protect); } diff --git a/src/libutil/archive.hh b/src/libutil/archive.hh index 2cf164a417d..bb7e711e980 100644 --- a/src/libutil/archive.hh +++ b/src/libutil/archive.hh @@ -117,14 +117,14 @@ struct RetrieveRegularNARSink : ParseSink void parseDump(ParseSink & sink, Source & source); -void restorePath(const Path & path, Source & source); +void restorePath(const Path & path, Source & source, bool protect = false); /** * Read a NAR from 'source' and write it to 'sink'. */ void copyNAR(Source & source, Sink & sink); -void copyPath(const Path & from, const Path & to); +void copyPath(const Path & from, const Path & to, bool protect = false); inline constexpr std::string_view narVersionMagic1 = "nix-archive-1"; diff --git a/src/nix/daemon.cc b/src/nix/daemon.cc index 1511f9e6e99..06cc2519e7f 100644 --- a/src/nix/daemon.cc +++ b/src/nix/daemon.cc @@ -132,21 +132,6 @@ static void setSigChldAction(bool autoReap) throw SysError("setting SIGCHLD handler"); } -/** - * @return Is the given user a member of this group? - * - * @param user User specified by username. - * - * @param group Group the user might be a member of. - */ -static bool matchUser(std::string_view user, const struct group & gr) -{ - for (char * * mem = gr.gr_mem; *mem; mem++) - if (user == std::string_view(*mem)) return true; - return false; -} - - /** * Does the given user (specified by user name and primary group name) * match the given user/group whitelist? @@ -155,13 +140,11 @@ static bool matchUser(std::string_view user, const struct group & gr) * * If the username is in the set: Yes. * - * If the groupname is in the set: Yes. - * - * If the user is in another group which is in the set: yes. + * If any of the groups the user is in is in the set: Yes. * * Otherwise: No. */ -static bool matchUser(const std::string & user, const std::string & group, const Strings & users) +static bool matchUser(const std::string & user, const std::vector & groups, const Strings & users) { if (find(users.begin(), users.end(), "*") != users.end()) return true; @@ -170,12 +153,8 @@ static bool matchUser(const std::string & user, const std::string & group, const return true; for (auto & i : users) - if (i.substr(0, 1) == "@") { - if (group == i.substr(1)) return true; - struct group * gr = getgrnam(i.c_str() + 1); - if (!gr) continue; - if (matchUser(user, *gr)) return true; - } + if (i.substr(0, 1) == "@") + if (find(groups.begin(), groups.end(), i.substr(1)) != groups.end()) return true; return false; } @@ -250,7 +229,7 @@ static ref openUncachedStore() * * If the potential client is not allowed to talk to us, we throw an `Error`. */ -static std::pair authPeer(const PeerInfo & peer) +static AuthenticatedUser authPeer(const PeerInfo & peer) { TrustedFlag trusted = NotTrusted; @@ -260,16 +239,19 @@ static std::pair authPeer(const PeerInfo & peer) struct group * gr = peer.gidKnown ? getgrgid(peer.gid) : 0; std::string group = gr ? gr->gr_name : std::to_string(peer.gid); + std::vector groups = getUserGroupNames(peer.uid); + groups.push_back(group); + const Strings & trustedUsers = authorizationSettings.trustedUsers; const Strings & allowedUsers = authorizationSettings.allowedUsers; - if (matchUser(user, group, trustedUsers)) + if (matchUser(user, groups, trustedUsers)) trusted = Trusted; - if ((!trusted && !matchUser(user, group, allowedUsers)) || group == settings.buildUsersGroup) + if ((!trusted && !matchUser(user, groups, allowedUsers)) || group == settings.buildUsersGroup) throw Error("user '%1%' is not allowed to connect to the Nix daemon", user); - return { trusted, std::move(user) }; + return { trusted, peer.uid }; } @@ -325,19 +307,14 @@ static void daemonLoop(std::optional forceTrustClientOpt) closeOnExec(remote.get()); PeerInfo peer { .pidKnown = false }; - TrustedFlag trusted; std::string user; - if (forceTrustClientOpt) - trusted = *forceTrustClientOpt; - else { - peer = getPeerInfo(remote.get()); - auto [_trusted, _user] = authPeer(peer); - trusted = _trusted; - user = _user; - }; + peer = getPeerInfo(remote.get()); + AuthenticatedUser _user = authPeer(peer); + if (forceTrustClientOpt) _user.trusted = Trusted; + user = getUserName(_user.uid); - printInfo((std::string) "accepted connection from pid %1%, user %2%" + (trusted ? " (trusted)" : ""), + printInfo((std::string) "accepted connection from pid %1%, user %2%" + (_user.trusted ? " (trusted)" : ""), peer.pidKnown ? std::to_string(peer.pid) : "", peer.uidKnown ? user : ""); @@ -366,7 +343,7 @@ static void daemonLoop(std::optional forceTrustClientOpt) // Handle the connection. FdSource from(remote.get()); FdSink to(remote.get()); - processConnection(openUncachedStore(), from, to, trusted, NotRecursive); + processConnection(openUncachedStore(), from, to, _user, NotRecursive); exit(0); }, options); @@ -429,11 +406,11 @@ static void forwardStdioConnection(RemoteStore & store) { * @param trustClient Whether to trust the client. Forwarded directly to * `processConnection()`. */ -static void processStdioConnection(ref store, TrustedFlag trustClient) +static void processStdioConnection(ref store, AuthenticatedUser user) { FdSource from(STDIN_FILENO); FdSink to(STDOUT_FILENO); - processConnection(store, from, to, trustClient, NotRecursive); + processConnection(store, from, to, user, NotRecursive); } /** @@ -453,11 +430,13 @@ static void runDaemon(bool stdio, std::optional forceTrustClientOpt // force untrusting the client. if (auto remoteStore = store.dynamic_pointer_cast(); remoteStore && (!forceTrustClientOpt || *forceTrustClientOpt != NotTrusted)) forwardStdioConnection(*remoteStore); - else + else { // `Trusted` is passed in the auto (no override case) because we // cannot see who is on the other side of a plain pipe. Limiting // access to those is explicitly not `nix-daemon`'s responsibility. - processStdioConnection(store, forceTrustClientOpt.value_or(Trusted)); + AuthenticatedUser user {forceTrustClientOpt.value_or(Trusted), 0}; + processStdioConnection(store, user); + } } else daemonLoop(forceTrustClientOpt); } From 552e4e529c40535b464315b094b9f7db4e20113b Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Wed, 1 Nov 2023 19:00:50 +0400 Subject: [PATCH 04/56] Add CLI commands to manipulate ACLs --- src/libcmd/installables.cc | 22 ++++++- src/libcmd/installables.hh | 6 +- src/libmain/common-args.hh | 14 ++++ src/nix/add-to-store.cc | 11 +++- src/nix/build.cc | 5 +- src/nix/store-access-grant.cc | 64 +++++++++++++++++++ src/nix/store-access-grant.md | 24 +++++++ src/nix/store-access-info.cc | 103 ++++++++++++++++++++++++++++++ src/nix/store-access-info.md | 17 +++++ src/nix/store-access-protect.cc | 37 +++++++++++ src/nix/store-access-protect.md | 25 ++++++++ src/nix/store-access-revoke.cc | 77 ++++++++++++++++++++++ src/nix/store-access-revoke.md | 24 +++++++ src/nix/store-access-unprotect.cc | 36 +++++++++++ src/nix/store-access-unprotect.md | 23 +++++++ src/nix/store-access.cc | 32 ++++++++++ src/nix/store-access.md | 11 ++++ 17 files changed, 523 insertions(+), 8 deletions(-) create mode 100644 src/nix/store-access-grant.cc create mode 100644 src/nix/store-access-grant.md create mode 100644 src/nix/store-access-info.cc create mode 100644 src/nix/store-access-info.md create mode 100644 src/nix/store-access-protect.cc create mode 100644 src/nix/store-access-protect.md create mode 100644 src/nix/store-access-revoke.cc create mode 100644 src/nix/store-access-revoke.md create mode 100644 src/nix/store-access-unprotect.cc create mode 100644 src/nix/store-access-unprotect.md create mode 100644 src/nix/store-access.cc create mode 100644 src/nix/store-access.md diff --git a/src/libcmd/installables.cc b/src/libcmd/installables.cc index 10b077fb58c..d9a1bef6365 100644 --- a/src/libcmd/installables.cc +++ b/src/libcmd/installables.cc @@ -1,4 +1,5 @@ #include "globals.hh" +#include "granular-access-store.hh" #include "installables.hh" #include "installable-derived-path.hh" #include "installable-attr-path.hh" @@ -19,6 +20,8 @@ #include "url.hh" #include "registry.hh" #include "build-result.hh" +#include "store-cast.hh" +#include "local-store.hh" #include #include @@ -519,10 +522,11 @@ std::vector Installable::build( ref store, Realise mode, const Installables & installables, - BuildMode bMode) + BuildMode bMode, + bool protect) { std::vector res; - for (auto & [_, builtPathWithResult] : build2(evalStore, store, mode, installables, bMode)) + for (auto & [_, builtPathWithResult] : build2(evalStore, store, mode, installables, bMode, protect)) res.push_back(builtPathWithResult); return res; } @@ -532,7 +536,8 @@ std::vector, BuiltPathWithResult>> Installable::build ref store, Realise mode, const Installables & installables, - BuildMode bMode) + BuildMode bMode, + bool protect) { if (mode == Realise::Nothing) settings.readOnlyMode = true; @@ -550,6 +555,17 @@ std::vector, BuiltPathWithResult>> Installable::build for (auto b : i->toDerivedPaths()) { pathsToBuild.push_back(b.path); backmap[b.path].push_back({.info = b.info, .installable = i}); + if (protect) { + LocalStore::AccessStatus status {true, {ACL::User(getuid())}}; + std::visit(overloaded { + [&](DerivedPath::Opaque p){ + require(*store).setAccessStatus(p.path, status); + }, + [&](DerivedPath::Built b){ + require(*store).setAccessStatus(b, status); + } + }, b.path); + } } } diff --git a/src/libcmd/installables.hh b/src/libcmd/installables.hh index 42d6c7c7ce6..87a623b7989 100644 --- a/src/libcmd/installables.hh +++ b/src/libcmd/installables.hh @@ -156,14 +156,16 @@ struct Installable ref store, Realise mode, const Installables & installables, - BuildMode bMode = bmNormal); + BuildMode bMode = bmNormal, + bool protect = false); static std::vector, BuiltPathWithResult>> build2( ref evalStore, ref store, Realise mode, const Installables & installables, - BuildMode bMode = bmNormal); + BuildMode bMode = bmNormal, + bool protect = false); static std::set toStorePaths( ref evalStore, diff --git a/src/libmain/common-args.hh b/src/libmain/common-args.hh index c35406c3bcc..8463a99d281 100644 --- a/src/libmain/common-args.hh +++ b/src/libmain/common-args.hh @@ -35,6 +35,20 @@ struct MixDryRun : virtual Args } }; +struct MixProtect : virtual Args +{ + bool protect = false; + + MixProtect() + { + addFlag({ + .longName = "protect", + .description = "Protect the resulting paths in nix store upon addition.", + .handler = {&protect, true}, + }); + } +}; + struct MixJSON : virtual Args { bool json = false; diff --git a/src/nix/add-to-store.cc b/src/nix/add-to-store.cc index 16e48a39b38..2aa146f82f5 100644 --- a/src/nix/add-to-store.cc +++ b/src/nix/add-to-store.cc @@ -1,11 +1,13 @@ #include "command.hh" #include "common-args.hh" +#include "granular-access-store.hh" #include "store-api.hh" #include "archive.hh" +#include "store-cast.hh" using namespace nix; -struct CmdAddToStore : MixDryRun, StoreCommand +struct CmdAddToStore : MixDryRun, MixProtect, StoreCommand { Path path; std::optional namePart; @@ -56,6 +58,13 @@ struct CmdAddToStore : MixDryRun, StoreCommand info.narSize = sink.s.size(); if (!dryRun) { + if (protect) { + LocalGranularAccessStore::AccessStatus status; + status.isProtected = true; + status.entities = {ACL::User(getuid())}; + info.accessStatus = status; + } + auto source = StringSource(sink.s); store->addToStore(info, source); } diff --git a/src/nix/build.cc b/src/nix/build.cc index ad1842a4ec4..4a5b29c0fe9 100644 --- a/src/nix/build.cc +++ b/src/nix/build.cc @@ -65,7 +65,7 @@ static void createOutLinks(const Path& outLink, const std::vectorcout("%s", builtPathsWithResultToJSON(buildables, store).dump()); diff --git a/src/nix/store-access-grant.cc b/src/nix/store-access-grant.cc new file mode 100644 index 00000000000..a2fb8274c43 --- /dev/null +++ b/src/nix/store-access-grant.cc @@ -0,0 +1,64 @@ +#include "command.hh" +#include "store-api.hh" +#include "local-fs-store.hh" +#include "store-cast.hh" + +using namespace nix; + +struct CmdStoreAccessGrant : StorePathsCommand +{ + std::set users; + std::set groups; + CmdStoreAccessGrant() + { + addFlag({ + .longName = "user", + .shortName = 'u', + .description = "User to whom access should be granted", + .labels = {"user"}, + .handler = {[&](std::string _user){ + users.insert(_user); + }} + }); + addFlag({ + .longName = "group", + .shortName = 'g', + .description = "Group to which access should be granted", + .labels = {"group"}, + .handler = {[&](std::string _group){ + groups.insert(_group); + }} + }); + } + std::string description() override + { + return "grant a user access to store paths"; + } + + std::string doc() override + { + return + #include "store-repair.md" + ; + } + + void run(ref store, StorePaths && storePaths) override + { + if (users.empty() && groups.empty()) { + throw Error("At least one of either --user/-u or --group/-g is required"); + } else { + auto & localStore = require(*store); + for (auto & path : storePaths) { + auto status = localStore.getAccessStatus(path); + if (!status.isProtected) warn("Path '%s' is not protected; all users can access it regardless of permissions", store->printStorePath(path)); + if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); + + for (auto user : users) status.entities.insert(*getpwnam(user.c_str())); + for (auto group : groups) status.entities.insert(*getgrnam(group.c_str())); + localStore.setAccessStatus(path, status); + } + } + } +}; + +static auto rStoreAccessGrant = registerCommand2({"store", "access", "grant"}); diff --git a/src/nix/store-access-grant.md b/src/nix/store-access-grant.md new file mode 100644 index 00000000000..b597b1f94d2 --- /dev/null +++ b/src/nix/store-access-grant.md @@ -0,0 +1,24 @@ +R"( +# Examples + +```console +$ nix store access info /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +The path is protected +The following users have access to the path: + alice +$ nix store access grant /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo --user bob --user carol +$ nix store access info /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +The path is protected +The following users have access to the path: + alice + bob + carol +``` + +# Description + +`nix store access grant` grants users access to store paths. + + + +)" diff --git a/src/nix/store-access-info.cc b/src/nix/store-access-info.cc new file mode 100644 index 00000000000..fe837f6dfc2 --- /dev/null +++ b/src/nix/store-access-info.cc @@ -0,0 +1,103 @@ +#include "ansicolor.hh" +#include "command.hh" +#include "store-api.hh" +#include "local-store.hh" +#include "store-cast.hh" + +using namespace nix; + +struct CmdStoreAccessInfo : StorePathCommand, MixJSON +{ + std::string description() override + { + return "get information about store path access"; + } + + std::string doc() override + { + return + #include "store-access-info.md" + ; + } + + void run(ref store, const StorePath & path) override + { + auto & aclStore = require(*store); + auto status = aclStore.getAccessStatus(path); + bool isValid = aclStore.isValidPath(path); + std::set users; + std::set groups; + for (auto entity : status.entities) { + std::visit(overloaded { + [&](ACL::User user) { + struct passwd * pw = getpwuid(user.uid); + users.insert(pw->pw_name); + }, + [&](ACL::Group group) { + struct group * gr = getgrgid(group.gid); + groups.insert(gr->gr_name); + } + }, entity); + } + if (json) { + nlohmann::json j; + j["exists"] = isValid; + j["protected"] = status.isProtected; + j["users"] = users; + j["groups"] = groups; + logger->cout(j.dump()); + } + else { + std::string be, have, has; + if (isValid) { + be = "is"; + have = "have"; + has = "has"; + } + else { + be = "will be"; + have = "will have"; + has = "will have"; + + logger->cout("The path does not exist yet; the permissions will be applied when it is added to the store.\n"); + } + + if (status.isProtected) + logger->cout("The path " + be + " " ANSI_BOLD ANSI_GREEN "protected" ANSI_NORMAL); + else + logger->cout("The path " + be + " " ANSI_BOLD ANSI_RED "not" ANSI_NORMAL " protected"); + + if (users.empty() && groups.empty()) { + if (status.isProtected) { logger->cout(""); logger->cout("Nobody " + has + " access to the path"); }; + } else { + logger->cout(""); + if (!status.isProtected) { + logger->warn("Despite this path not being protected, some users and groups " + have + " additional access to it."); + logger->cout(""); + } + + if (!users.empty()) { + if (status.isProtected) + logger->cout("The following users " + have + " access to the path:"); + else + logger->cout(ANSI_BOLD "If the path was protected" ANSI_NORMAL ", the following users would have access to it:"); + + for (auto user : users) + logger->cout(ANSI_MAGENTA " %s" ANSI_NORMAL, user); + } + if (! (users.empty() && groups.empty())) logger->cout(""); + if (!groups.empty()) { + if (status.isProtected) + logger->cout("Users in the following groups " + have + " access to the path:"); + else + logger->cout(ANSI_BOLD "If the path was protected" ANSI_NORMAL ", users in the following groups would have access to it:"); + for (auto group : groups) + logger->cout(ANSI_CYAN " %s" ANSI_NORMAL, group); + } + } + } + } + +}; + +static auto rStoreAccessInfo = registerCommand2({"store", "access", "info"}); diff --git a/src/nix/store-access-info.md b/src/nix/store-access-info.md new file mode 100644 index 00000000000..049fad7442d --- /dev/null +++ b/src/nix/store-access-info.md @@ -0,0 +1,17 @@ +R"( +# Examples + +```console +$ nix store access info /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +The path is protected +The following users have access to the path: + alice +$ nix store access info /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo --json +{"protected":true,users:["alice"]} +``` + +# Description + +This command shows information about the access control list of a store path. + +)" diff --git a/src/nix/store-access-protect.cc b/src/nix/store-access-protect.cc new file mode 100644 index 00000000000..4d938518741 --- /dev/null +++ b/src/nix/store-access-protect.cc @@ -0,0 +1,37 @@ +#include "command.hh" +#include "granular-access-store.hh" +#include "local-fs-store.hh" +#include "store-api.hh" +#include "store-cast.hh" + +using namespace nix; + +struct CmdStoreAccessProtect : StorePathsCommand +{ + std::string description() override + { + return "protect store paths"; + } + + std::string doc() override + { + return + #include "store-repair.md" + ; + } + + void run(ref store, StorePaths && storePaths) override + { + auto & localStore = require(*store); + for (auto & path : storePaths) { + auto status = localStore.getAccessStatus(path); + if (!status.entities.empty()) + warn("There are some users or groups who have access to path %s; consider removing them with \n" ANSI_BOLD "nix store access revoke --all-entities %s" ANSI_NORMAL, localStore.printStorePath(path), localStore.printStorePath(path)); + if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); + status.isProtected = true; + localStore.setAccessStatus(path, status); + } + } +}; + +static auto rStoreAccessProtect = registerCommand2({"store", "access", "protect"}); diff --git a/src/nix/store-access-protect.md b/src/nix/store-access-protect.md new file mode 100644 index 00000000000..da4f0892190 --- /dev/null +++ b/src/nix/store-access-protect.md @@ -0,0 +1,25 @@ +R"( +# Examples + +```console +$ nix store access info /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +The path is not protected +$ cat /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +foo +$ nix store access protect /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +$ nix store access info /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +The path is protected +No users have access to the path +$ cat /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +cat: /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo: Permission denied +``` + +# Description + +`nix store access protect` protects a store path from being readable and executable by arbitrary users. + +You can use `nix store access grant` to grant users access to the path, and `nix store access unprotect` to remove the protection entirely. + + + +)" diff --git a/src/nix/store-access-revoke.cc b/src/nix/store-access-revoke.cc new file mode 100644 index 00000000000..61b0a1e3d30 --- /dev/null +++ b/src/nix/store-access-revoke.cc @@ -0,0 +1,77 @@ +#include "command.hh" +#include "store-api.hh" +#include "local-fs-store.hh" +#include "store-cast.hh" + +using namespace nix; + +struct CmdStoreAccessRevoke : StorePathsCommand +{ + std::set users; + std::set groups; + bool all = false; + CmdStoreAccessRevoke() + { + addFlag({ + .longName = "user", + .shortName = 'u', + .description = "User from whom access should be revoked", + .labels = {"user"}, + .handler = {[&](std::string _user){ + users.insert(_user); + }} + }); + addFlag({ + .longName = "group", + .shortName = 'g', + .description = "Group from which access should be revoked", + .labels = {"group"}, + .handler = {[&](std::string _group){ + groups.insert(_group); + }} + }); + addFlag({ + .longName = "all-entities", + .shortName = 'a', + .description = "Revoke access from all entities", + .handler = {&all, true} + }); + } + std::string description() override + { + return "revoke user's access to store paths"; + } + + std::string doc() override + { + return + #include "store-repair.md" + ; + } + + void run(ref store, StorePaths && storePaths) override + { + if (! all && users.empty() && groups.empty()) { + throw Error("At least one of either --all-entities/-a, --user/-u or --group/-g is required"); + } else if (all && ! (users.empty() && groups.empty())) { + warn("--all-entities/-a implies removal of all users and groups from the access control list; ignoring --user/-u and --group/-g"); + } else { + auto & localStore = require(*store); + for (auto & path : storePaths) { + auto status = localStore.getAccessStatus(path); + if (!status.isProtected) warn("Path '%s' is not protected; all users can access it regardless of permissions", store->printStorePath(path)); + if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); + + if (all) { + status.entities = {}; + } else { + for (auto user : users) status.entities.erase(*getpwnam(user.c_str())); + for (auto group : groups) status.entities.erase(*getgrnam(group.c_str())); + } + localStore.setAccessStatus(path, status); + } + } + } +}; + +static auto rStoreAccessRevoke = registerCommand2({"store", "access", "revoke"}); diff --git a/src/nix/store-access-revoke.md b/src/nix/store-access-revoke.md new file mode 100644 index 00000000000..078e0c62541 --- /dev/null +++ b/src/nix/store-access-revoke.md @@ -0,0 +1,24 @@ +R"( +# Examples + +```console +$ nix store access info /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +The path is protected +The following users have access to the path: + alice + bob + carol +$ nix store access revoke /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo --user bob --user carol +$ nix store access info /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +The path is protected +The following users have access to the path: + alice +``` + +# Description + +`nix store access revoke` revokes users access to store paths. + + + +)" diff --git a/src/nix/store-access-unprotect.cc b/src/nix/store-access-unprotect.cc new file mode 100644 index 00000000000..bcdb574a4e8 --- /dev/null +++ b/src/nix/store-access-unprotect.cc @@ -0,0 +1,36 @@ +#include "command.hh" +#include "store-api.hh" +#include "local-fs-store.hh" +#include "store-cast.hh" + +using namespace nix; + +struct CmdStoreAccessUnprotect : StorePathsCommand +{ + std::string description() override + { + return "unprotect store paths"; + } + + std::string doc() override + { + return + #include "store-repair.md" + ; + } + + void run(ref store, StorePaths && storePaths) override + { + auto & localStore = require(*store); + for (auto & path : storePaths) { + auto status = localStore.getAccessStatus(path); + if (!status.entities.empty()) + warn("There are still some users or groups who have access to path %s; consider removing them with \n" ANSI_BOLD "nix store access revoke --all-entities %s" ANSI_NORMAL, localStore.printStorePath(path), localStore.printStorePath(path)); + if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); + status.isProtected = false; + localStore.setAccessStatus(path, status); + } + } +}; + +static auto rStoreAccessUnprotect = registerCommand2({"store", "access", "unprotect"}); diff --git a/src/nix/store-access-unprotect.md b/src/nix/store-access-unprotect.md new file mode 100644 index 00000000000..5f41d79ab8a --- /dev/null +++ b/src/nix/store-access-unprotect.md @@ -0,0 +1,23 @@ +R"( +# Examples + +```console +$ nix store access info /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +The path is protected +No users have access to the path +$ cat /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +cat: /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo: Permission denied +$ nix store access unprotect /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +$ nix store access info /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +The path is not protected +$ cat /nix/store/fzn8agjb9qmikbf97h1a5wlf3iifjgqz-foo +foo +``` + +# Description + +`nix store access unprotect` removes the ACL protection from a store path. + + + +)" diff --git a/src/nix/store-access.cc b/src/nix/store-access.cc new file mode 100644 index 00000000000..34576807d22 --- /dev/null +++ b/src/nix/store-access.cc @@ -0,0 +1,32 @@ +#include "command.hh" + +using namespace nix; + +struct CmdStoreAccess : virtual NixMultiCommand +{ + CmdStoreAccess() : MultiCommand(RegisterCommand::getCommandsFor({"store", "access"})) + { } + + std::string description() override + { + return "manage access to Nix Store paths"; + } + + std::string doc() override + { + return + #include "store-access.md" + ; + } + + Category category() override { return catUtility; } + + void run() override + { + if (!command) + throw UsageError("'nix store access' requires a sub-command."); + command->second->run(); + } +}; + +static auto rCmdStore = registerCommand2({"store", "access"}); diff --git a/src/nix/store-access.md b/src/nix/store-access.md new file mode 100644 index 00000000000..4ebad458db9 --- /dev/null +++ b/src/nix/store-access.md @@ -0,0 +1,11 @@ +R"( +# Description + +`nix store access` provides subcommands that query and manipulate access control lists (ACLs) of store paths. +ACLs allow for granular access to the nix store: paths can be protected from all users (`nix store access protect`), and then necessary users can be granted permission to those paths (`nix store access grant`). + +Under the hood, `nix store access` uses POSIX ACLs. + + + +)" From 61a3cea7902adc7bf6dbe974f056786871c583db Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Wed, 1 Nov 2023 19:01:34 +0400 Subject: [PATCH 05/56] Add __permissions to builtins.derivation and builtins.path --- src/libexpr/eval.cc | 6 ++ src/libexpr/eval.hh | 1 + src/libexpr/primops.cc | 140 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 140 insertions(+), 7 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 706a1902456..69f9481c447 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -525,6 +525,12 @@ EvalState::EvalState( , sArgs(symbols.create("args")) , sContentAddressed(symbols.create("__contentAddressed")) , sImpure(symbols.create("__impure")) + , sDrv(symbols.create("drv")) + , sLog(symbols.create("log")) + , sProtected(symbols.create("protected")) + , sPermissions(symbols.create("__permissions")) + , sUsers(symbols.create("users")) + , sGroups(symbols.create("groups")) , sOutputHash(symbols.create("outputHash")) , sOutputHashAlgo(symbols.create("outputHashAlgo")) , sOutputHashMode(symbols.create("outputHashMode")) diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index e3676c1b773..3aaec5ee167 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -199,6 +199,7 @@ public: sFile, sLine, sColumn, sFunctor, sToString, sRight, sWrong, sStructuredAttrs, sBuilder, sArgs, sContentAddressed, sImpure, + sDrv, sLog, sProtected, sPermissions, sUsers, sGroups, sOutputHash, sOutputHashAlgo, sOutputHashMode, sRecurseForDerivations, sDescription, sSelf, sEpsilon, sStartSet, sOperator, sKey, sPath, diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 5dfad470a49..f07870715bd 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1,9 +1,12 @@ +#include "access-status.hh" #include "archive.hh" +#include "config.hh" #include "derivations.hh" #include "downstream-placeholder.hh" #include "eval-inline.hh" #include "eval.hh" #include "globals.hh" +#include "granular-access-store.hh" #include "json-to-value.hh" #include "names.hh" #include "path-references.hh" @@ -12,6 +15,9 @@ #include "value-to-json.hh" #include "value-to-xml.hh" #include "primops.hh" +#include "granular-access-store.hh" +#include "acl.hh" +#include "store-cast.hh" #include #include @@ -130,6 +136,50 @@ static SourcePath realisePath(EvalState & state, const PosIdx pos, Value & v, co } } +void readAccessStatus(EvalState & state, Attr & attr, LocalGranularAccessStore::AccessStatus * accessStatus, std::string_view attrName, std::string_view primop) +{ + state.forceAttrs(*attr.value, attr.pos, fmt("while evaluating the `%s` attribute passed to %s", attrName, primop)); + for (auto & subAttr : *attr.value->attrs) { + auto sn = state.symbols[subAttr.name]; + if (sn == "protected") + accessStatus->isProtected = state.forceBool(*subAttr.value, subAttr.pos, fmt("while evaluating the `%s.protected` attribute passed to %s", attrName, primop)); + else if (sn == "users") { + state.forceList(*subAttr.value, subAttr.pos, fmt("while evaluating the `%s.users` attribute passed to %s", attrName, primop)); + for (auto & user : subAttr.value->listItems()) + accessStatus->entities.insert(ACL::User(std::string(state.forceStringNoCtx(*user, noPos, fmt("while evaluating an element of `%s.users` attribute passed to %s", attrName, primop))))); + } + else if (sn == "groups") { + state.forceList(*subAttr.value, subAttr.pos, fmt("while evaluating the `%s.groups` attribute passed to %s", attrName, primop)); + for (auto & group : subAttr.value->listItems()) + accessStatus->entities.insert(ACL::Group(std::string(state.forceStringNoCtx(*group, noPos, fmt("while evaluating an element of `%s.groups` attribute passed to %s", attrName, primop))))); + } + else + state.debugThrowLastTrace(EvalError({ + .msg = hintfmt("unsupported argument '%1%.%2%' to %3%", attrName, sn, primop), + .errPos = state.positions[subAttr.pos] + })); + } +} + +void ensureAccess(LocalGranularAccessStore::AccessStatus * accessStatus, std::string_view description) +{ + if (!accessStatus->isProtected) return; + uid_t uid = getuid(); + struct passwd * pw = getpwuid(uid); + auto groups_vec = getUserGroups(pw->pw_uid); + for (auto entity : accessStatus->entities) { + if (std::visit(overloaded { + [&](ACL::User u) { return u.uid == uid; }, + [&](ACL::Group g) { + return std::find(groups_vec.begin(), groups_vec.end(), g.gid) != groups_vec.end(); + } + }, entity)) + return; + } + warn("adding you (%s) to the list of users allowed to access %s; otherwise you would not be able to access it", pw->pw_name, description); + accessStatus->entities.insert(ACL::User {uid}); +} + /** * Add and attribute to the given attribute map from the output name to * the output path, or a placeholder. @@ -1063,8 +1113,7 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * * } } -static void derivationStrictInternal(EvalState & state, const std::string & -drvName, Bindings * attrs, Value & v) +static void derivationStrictInternal(EvalState & state, const std::string & drvName, Bindings * attrs, Value & v) { /* Check whether attributes should be passed as a JSON file. */ using nlohmann::json; @@ -1152,6 +1201,10 @@ drvName, Bindings * attrs, Value & v) if (i->value->type() == nNull) continue; } + if (i->name == state.sPermissions && experimentalFeatureSettings.isEnabled(Xp::ACLs)) { + continue; + } + if (i->name == state.sContentAddressed && state.forceBool(*i->value, noPos, context_below)) { contentAddressed = true; experimentalFeatureSettings.require(Xp::CaDerivations); @@ -1204,7 +1257,7 @@ drvName, Bindings * attrs, Value & v) } } else { - auto s = state.coerceToString(noPos, *i->value, context, context_below, true).toOwned(); + auto s = state.coerceToString(i->pos, *i->value, context, context_below, true).toOwned(); drv.env.emplace(key, s); if (i->name == state.sBuilder) drv.builder = std::move(s); else if (i->name == state.sSystem) drv.platform = std::move(s); @@ -1376,10 +1429,61 @@ drvName, Bindings * attrs, Value & v) } } + + /* Pre-protect the derivation itself */ + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) { + auto drvPath = writeDerivation(*state.store, drv, state.repair, true); + attr = attrs->find(state.sPermissions); + if (attr != attrs->end()) { + state.forceAttrs(*attr->value, noPos, + "while evaluating the `__permissions` " + "attribute passed to builtins.derivationStrict"); + auto derivation = attr->value->attrs->find(state.sDrv); + if (derivation != attr->value->attrs->end()) { + LocalGranularAccessStore::AccessStatus status; + readAccessStatus(state, *derivation, &status, "__permissions.drv", "builtins.derivationStrict"); + ensureAccess(&status, state.store->printStorePath(drvPath)); + require(*state.store).setAccessStatus(drvPath, status); + } + } + } /* Write the resulting term into the Nix store directory. */ - auto drvPath = writeDerivation(*state.store, drv, state.repair); + auto drvPath = writeDerivation(*state.store, drv, state.repair, false); auto drvPathS = state.store->printStorePath(drvPath); + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) { + attr = attrs->find(state.sPermissions); + if (attr != attrs->end()) { + state.forceAttrs(*attr->value, noPos, + "while evaluating the `__permissions` " + "attribute passed to builtins.derivationStrict"); + auto outputs = attr->value->attrs->find(state.sOutputs); + if (outputs != attr->value->attrs->end()) { + state.forceAttrs(*outputs->value, noPos, + "while evaluating the `__permissions.outputs` " + "attribute passed to builtins.derivationStrict"); + for (auto & output : *outputs->value->attrs) { + if (!drv.outputs.contains(state.symbols[output.name])) + state.debugThrowLastTrace(EvalError({ + .msg = hintfmt("derivation has no output %s", state.symbols[output.name]), + .errPos = state.positions[output.pos] + })); + LocalGranularAccessStore::AccessStatus status; + readAccessStatus(state, output, &status, fmt("__permissions.outputs.%s", state.symbols[output.name]), "builtins.derivationStrict"); + ensureAccess(&status, fmt("output %s of derivation %s", state.symbols[output.name], drvPathS)); + require(*state.store).setAccessStatus(StoreObjectDerivationOutput {drvPath, std::string(state.symbols[{output.name}])}, status); + } + } + auto log = attr->value->attrs->find(state.sLog); + if (log != attr->value->attrs->end()) { + LocalGranularAccessStore::AccessStatus status; + readAccessStatus(state, *log, &status, "__permissions.log", "builtins.derivationStrict"); + ensureAccess(&status, fmt("log of derivation %s", drvPathS)); + require(*state.store).setAccessStatus(StoreObjectDerivationLog {drvPath}, status); + } + } + } + printMsg(lvlChatty, "instantiated '%1%' -> '%2%'", drvName, drvPathS); /* Optimisation, but required in read-only mode! because in that @@ -2112,6 +2216,7 @@ static void addPath( Value * filterFun, FileIngestionMethod method, const std::optional expectedHash, + std::optional accessStatus, Value & v, const NixStringContext & context) { @@ -2169,6 +2274,12 @@ static void addPath( .references = {}, }); + if (accessStatus && !settings.readOnlyMode) { + StorePath dstPath = state.store->computeStorePathForPath(name, path, method, htSHA256, filter).first; + ensureAccess(&*accessStatus, state.store->printStorePath(dstPath)); + require(*state.store).setAccessStatus(dstPath, *accessStatus); + } + if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) { StorePath dstPath = settings.readOnlyMode ? state.store->computeStorePathForPath(name, path, method, htSHA256, filter).first @@ -2176,8 +2287,12 @@ static void addPath( if (expectedHash && expectedStorePath != dstPath) state.debugThrowLastTrace(Error("store path mismatch in (possibly filtered) path added from '%s'", path)); state.allowAndSetStorePathString(dstPath, v); - } else + } else if (!expectedHash && accessStatus && !settings.readOnlyMode) { + StorePath dstPath = state.store->computeStorePathForPath(name, path, method, htSHA256, filter).first; + state.allowAndSetStorePathString(dstPath, v); + } else { state.allowAndSetStorePathString(*expectedStorePath, v); + } } catch (Error & e) { e.addTrace(state.positions[pos], "while adding path '%s'", path); throw; @@ -2191,7 +2306,7 @@ static void prim_filterSource(EvalState & state, const PosIdx pos, Value * * arg auto path = state.coerceToPath(pos, *args[1], context, "while evaluating the second argument (the path to filter) passed to builtins.filterSource"); state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filterSource"); - addPath(state, pos, path.baseName(), path.path.abs(), args[0], FileIngestionMethod::Recursive, std::nullopt, v, context); + addPath(state, pos, path.baseName(), path.path.abs(), args[0], FileIngestionMethod::Recursive, std::nullopt, std::nullopt, v, context); } static RegisterPrimOp primop_filterSource({ @@ -2256,6 +2371,7 @@ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value Value * filterFun = nullptr; auto method = FileIngestionMethod::Recursive; std::optional expectedHash; + std::optional accessStatus; NixStringContext context; state.forceAttrs(*args[0], pos, "while evaluating the argument passed to 'builtins.path'"); @@ -2272,6 +2388,10 @@ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value method = FileIngestionMethod { state.forceBool(*attr.value, attr.pos, "while evaluating the `recursive` attribute passed to builtins.path") }; else if (n == "sha256") expectedHash = newHashAllowEmpty(state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the `sha256` attribute passed to builtins.path"), htSHA256); + else if (n == "permissions") { + if (!accessStatus) accessStatus = AccessStatusFor> {}; + readAccessStatus(state, attr, &*accessStatus, "permissions", "builtins.path"); + } else state.debugThrowLastTrace(EvalError({ .msg = hintfmt("unsupported argument '%1%' to 'addPath'", state.symbols[attr.name]), @@ -2286,7 +2406,7 @@ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value if (name.empty()) name = path->baseName(); - addPath(state, pos, name, path->path.abs(), filterFun, method, expectedHash, v, context); + addPath(state, pos, name, path->path.abs(), filterFun, method, expectedHash, accessStatus, v, context); } static RegisterPrimOp primop_path({ @@ -2320,6 +2440,12 @@ static RegisterPrimOp primop_path({ path. Evaluation will fail if the hash is incorrect, and providing a hash allows `builtins.path` to be used even when the `pure-eval` nix config option is on. + + - permissions\ + An attrset of `{protected : bool, users : list, groups : list}` + If `protected` is true, protects the resulting store path; + `users` and `groups` are lists of strings, each representing either a + user or a group to whom access should be granted. )", .fun = prim_path, }); From 0be3e5d8217baa39756b0f064e82db94e2c5686e Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Wed, 1 Nov 2023 19:01:51 +0400 Subject: [PATCH 06/56] Add an integration test for ACL functionality --- flake.nix | 2 + src/libstore/local-fs-store.hh | 1 + tests/nixos/acls.nix | 209 +++++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 tests/nixos/acls.nix diff --git a/flake.nix b/flake.nix index 99b8e199501..694bb15bdad 100644 --- a/flake.nix +++ b/flake.nix @@ -577,6 +577,8 @@ }; # System tests. + tests.acls = runNixOSTestFor "x86_64-linux" ./tests/nixos/acls.nix; + tests.authorization = runNixOSTestFor "x86_64-linux" ./tests/nixos/authorization.nix; tests.remoteBuilds = runNixOSTestFor "x86_64-linux" ./tests/nixos/remote-builds.nix; diff --git a/src/libstore/local-fs-store.hh b/src/libstore/local-fs-store.hh index 2ee2ef0c8d6..e4a96c5f4b8 100644 --- a/src/libstore/local-fs-store.hh +++ b/src/libstore/local-fs-store.hh @@ -4,6 +4,7 @@ #include "store-api.hh" #include "gc-store.hh" #include "log-store.hh" +#include "granular-access-store.hh" namespace nix { diff --git a/tests/nixos/acls.nix b/tests/nixos/acls.nix new file mode 100644 index 00000000000..3a63e27ae35 --- /dev/null +++ b/tests/nixos/acls.nix @@ -0,0 +1,209 @@ +{ lib, config, nixpkgs, ... }: + +let + pkgs = config.nodes.machine.nixpkgs.pkgs; + + example-package = builtins.toFile "example.nix" '' + with import {}; + stdenvNoCC.mkDerivation { + name = "example"; + # Check that importing a source works + exampleSource = builtins.path { + path = /tmp/bar; + permissions = { + protected = true; + users = ["root"]; + }; + }; + buildCommand = "echo Example > $out; cat $exampleSource >> $out"; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["root"]; }; + drv = { protected = true; groups = ["root"]; }; + log.protected = false; + }; + } + ''; + example-package-diff-permissions = builtins.toFile "example-diff-permissions.nix" '' + with import {}; + stdenvNoCC.mkDerivation { + name = "example"; + # Check that importing a source works + exampleSource = builtins.path { + path = /tmp/bar; + permissions = { + protected = true; + users = ["root" "test"]; + }; + }; + buildCommand = "echo Example > $out; cat $exampleSource >> $out"; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["root" "test"]; }; + drv = { protected = true; users = [ "test" ]; groups = ["root"]; }; + log.protected = false; + }; + } + ''; + + example-dependencies = builtins.toFile "example-dependencies.nix" '' + with import {}; + let + # Check that depending on an already existing but protected package works + example-package = + stdenvNoCC.mkDerivation { + name = "example"; + # Check that importing a source works + exampleSource = builtins.path { + path = /tmp/bar; + permissions = { + protected = true; + users = ["root" "test"]; + }; + }; + buildCommand = "echo Example > $out; cat $exampleSource >> $out"; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["root" "test"]; }; + drv = { protected = true; users = ["test"]; groups = ["root"]; }; + log.protected = false; + }; + }; + example2-package = + stdenvNoCC.mkDerivation { + name = "example2"; + buildCommand = "echo Example2 > $out"; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["root" "test"]; }; + drv = { protected = true; users = [ "test" ]; groups = [ "root" ]; }; + log.protected = false; + }; + } + ; + # Check that depending on a new protected package works + package = + stdenvNoCC.mkDerivation { + name = "example3"; + examplePackage = example-package.out; + exampleSource = example-package.exampleSource; + examplePackageOther = example2-package; + buildCommand = "cat $examplePackage $examplePackageOther $exampleSource > $out"; + } + ; + in package + ''; +in +{ + name = "acls"; + + nodes.machine = + { config, lib, pkgs, ... }: + { virtualisation.writableStore = true; + nix.settings.substituters = lib.mkForce [ ]; + nix.settings.experimental-features = lib.mkForce [ "nix-command" "acls" ]; + nix.nixPath = [ "nixpkgs=${lib.cleanSource pkgs.path}" ]; + virtualisation.additionalPaths = [ pkgs.stdenvNoCC pkgs.pkgsi686Linux.stdenvNoCC ]; + users.users.test = { + isNormalUser = true; + }; + }; + + testScript = { nodes }: '' + import json + # fmt: off + start_all() + + path = machine.succeed(r""" + nix-build -E '(with import {}; runCommand "foo" {} " + touch $out + ")' + """.strip()) + + def info(path): + return json.loads( + machine.succeed(f""" + nix store access info --json {path} + """.strip()) + ) + + def assert_info(path, expected, when): + got = info(path) + assert(got == expected),f"Path info {got} is not as expected {expected} for path {path} {when}" + + machine.succeed("touch /tmp/bar; chmod 777 /tmp/bar") + + assert_info(path, {"exists": True, "protected": False, "users": [], "groups": []}, "for an empty path") + + machine.succeed(f""" + nix store access protect {path} + """) + + assert_info(path, {"exists": True, "protected": True, "users": [], "groups": []}, "after nix store access protect") + + machine.succeed(f""" + nix store access grant --user root {path} + """) + + assert_info(path, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix store access grant") + + machine.succeed(f""" + nix store access grant --group wheel {path} + """) + + assert_info(path, {"exists": True, "protected": True, "users": ["root"], "groups": ["wheel"]}, "after nix store access grant") + + machine.succeed(f""" + nix store access revoke --user root --group wheel {path} + """) + + assert_info(path, {"exists": True, "protected": True, "users": [], "groups": []}, "after nix store access revoke") + + machine.succeed(f""" + nix store access unprotect {path} + """) + + assert_info(path, {"exists": True, "protected": False, "users": [], "groups": []}, "after nix store access unprotect") + + machine.succeed("touch foo") + + fooPath = machine.succeed(""" + nix store add-file --protect ./foo + """).strip() + + assert_info(fooPath, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix store add-file --protect") + + examplePackageDrvPath = machine.succeed(""" + nix eval -f ${example-package} --apply "x: x.drvPath" --raw + """).strip() + + assert_info(examplePackageDrvPath, {"exists": True, "protected": True, "users": [], "groups": ["root"]}, "after nix eval with __permissions") + + examplePackagePath = machine.succeed(""" + nix-build ${example-package} + """).strip() + + assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix-build with __permissions") + + examplePackagePathDiffPermissions = machine.succeed(""" + sudo -u test nix-build ${example-package-diff-permissions} --no-out-link + """).strip() + + assert_info(examplePackagePathDiffPermissions, {"exists": True, "protected": True, "users": ["root", "test"], "groups": []}, "after nix-build as a different user") + + assert(examplePackagePath == examplePackagePathDiffPermissions), "Derivation outputs differ when __permissions change" + + machine.succeed(f""" + nix store access revoke --user test {examplePackagePath} + """) + + assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix store access revoke") + + exampleDependenciesPackagePath = machine.succeed(""" + sudo -u test nix-build ${example-dependencies} --no-out-link --show-trace + """).strip() + + assert_info(exampleDependenciesPackagePath, {"exists": True, "protected": False, "users": [], "groups": []}, "after nix-build with dependencies") + assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root", "test"], "groups": []}, "after nix-build with dependencies") + ''; +} From bd56e3eeca6f43441d3ca03c4735021e27419c65 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Tue, 14 Nov 2023 10:57:35 +0100 Subject: [PATCH 07/56] Add tests/acls.sh This commit also enables acls in tests/init.sh which is common for all the tests. Maybe there is a way to only enable it for acls tests. Co-Authored-By: Alexander Bantyev --- tests/acls.sh | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++ tests/init.sh | 2 +- tests/local.mk | 3 ++- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100755 tests/acls.sh diff --git a/tests/acls.sh b/tests/acls.sh new file mode 100755 index 00000000000..d4e2ec0e302 --- /dev/null +++ b/tests/acls.sh @@ -0,0 +1,57 @@ +source common.sh + +# Adds the "dummy" file to the nix store and check that we can access it +EXAMPLE_PATH=$(nix store add-path dummy) +nix store access info "$EXAMPLE_PATH" --json | grep '"protected":false' +cat "$EXAMPLE_PATH" +getfacl "$EXAMPLE_PATH" + +# Protect a file and check that we cannot access it anymore +nix store access protect "$EXAMPLE_PATH" +! cat "$EXAMPLE_PATH" +nix store access info "$EXAMPLE_PATH" --json | grep '"protected":true' +nix store access info "$EXAMPLE_PATH" --json | grep '"users":\[\]' + +USER=$(whoami) + +# Grant permission and check that we can access the file +nix store access grant "$EXAMPLE_PATH" --user "$USER" +cat "$EXAMPLE_PATH" +nix store access info "$EXAMPLE_PATH" --json | grep '"users":\["'$USER'"\]' + +# Revoke permission and check that we cannot access the file anymore +nix store access revoke "$EXAMPLE_PATH" --user "$USER" +nix store access info "$EXAMPLE_PATH" --json | grep '"users":\[\]' + +# Check setting permissions from a nix file +cp dummy "$TEST_ROOT" +cp config.nix "$TEST_ROOT" +cat > "$TEST_ROOT/test-acls.nix"< "$NIX_CONF_DIR"/nix.conf < Date: Wed, 15 Nov 2023 14:10:32 +0100 Subject: [PATCH 08/56] acls grant/revoke: Error if group or user does not exists The User (resp Group) constructor will check the return value of getpwnam (resp getgrnam) and fail with an error message in case of error. --- src/nix/store-access-grant.cc | 4 ++-- src/nix/store-access-revoke.cc | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/nix/store-access-grant.cc b/src/nix/store-access-grant.cc index a2fb8274c43..7f16c47a047 100644 --- a/src/nix/store-access-grant.cc +++ b/src/nix/store-access-grant.cc @@ -53,8 +53,8 @@ struct CmdStoreAccessGrant : StorePathsCommand if (!status.isProtected) warn("Path '%s' is not protected; all users can access it regardless of permissions", store->printStorePath(path)); if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); - for (auto user : users) status.entities.insert(*getpwnam(user.c_str())); - for (auto group : groups) status.entities.insert(*getgrnam(group.c_str())); + for (auto user : users) status.entities.insert(nix::ACL::User(user)); + for (auto group : groups) status.entities.insert(nix::ACL::Group(group)); localStore.setAccessStatus(path, status); } } diff --git a/src/nix/store-access-revoke.cc b/src/nix/store-access-revoke.cc index 61b0a1e3d30..cdb07137d1b 100644 --- a/src/nix/store-access-revoke.cc +++ b/src/nix/store-access-revoke.cc @@ -65,8 +65,8 @@ struct CmdStoreAccessRevoke : StorePathsCommand if (all) { status.entities = {}; } else { - for (auto user : users) status.entities.erase(*getpwnam(user.c_str())); - for (auto group : groups) status.entities.erase(*getgrnam(group.c_str())); + for (auto user : users) status.entities.erase(nix::ACL::User(user)); + for (auto group : groups) status.entities.erase(nix::ACL::Group(group)); } localStore.setAccessStatus(path, status); } From aba3181374477a557ae5c3ce094ffaa685c774a4 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Thu, 16 Nov 2023 13:51:27 +0100 Subject: [PATCH 09/56] Acls test: permission of dependency. --- tests/acls/protect_dep.sh | 51 +++++++++++++++++++++++++++++++++++++++ tests/local.mk | 3 ++- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/acls/protect_dep.sh diff --git a/tests/acls/protect_dep.sh b/tests/acls/protect_dep.sh new file mode 100644 index 00000000000..9d5b842a1e4 --- /dev/null +++ b/tests/acls/protect_dep.sh @@ -0,0 +1,51 @@ +# Run using: make tests/acls/protect_dep.sh.test + +# This `example` derivation takes $exampleSource as input. +# Both `example` and `exampleSource` permissions are set to $USER in the nix file, +# But using `nix store access info` it is only the case for `example`. + +# Note: The output of `example` contains the path of `$exampleSource` to be able to recover it in the test. +# This makes `exampleSource` part of the runtime closure of `example` but even without this, shouldn't the permissions be the same ? + +source "../common.sh" + +USER=$(whoami) + +cp ../dummy "$TEST_ROOT" +cp ../config.nix "$TEST_ROOT" +cd "$TEST_ROOT" + +cat > "test.nix"< Date: Tue, 21 Nov 2023 14:05:52 +0100 Subject: [PATCH 10/56] revokeBuildUserAccess: only revoke permissions added by grantBuildUserAccess --- src/libstore/local-store.cc | 24 ++++++++++++++---------- tests/acls/protect_dep.sh | 15 +-------------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index a9dab7ac0d1..8b758f1b181 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1433,22 +1433,26 @@ LocalStore::AccessStatus LocalStore::getAccessStatus(const StoreObject & storeOb void LocalStore::grantBuildUserAccess(const StorePath & storePath, const LocalStore::AccessControlEntity & buildUser) { - auto basePath = stateDir + "/acls/builder-permissions/" + storePath.to_string(); - std::visit(overloaded { - [&](ACL::User u) { createDirs(basePath + "/users/" + std::to_string(u.uid)); }, - [&](ACL::Group g) { createDirs(basePath + "/groups/" + std::to_string(g.gid)); }, - }, buildUser); - addAllowedEntities(storePath, {buildUser}); + // The builder-permissions directory remembers permissions to remove at the end of the build. + auto status = getAccessStatus(storePath); + if (! status.entities.contains(buildUser)){ + auto basePath = stateDir + "/acls/builder-permissions/" + storePath.to_string(); + std::visit(overloaded { + [&](ACL::User u) { createDirs(basePath + "/users/" + std::to_string(u.uid)); }, + [&](ACL::Group g) { createDirs(basePath + "/groups/" + std::to_string(g.gid)); }, + }, buildUser); + addAllowedEntities(storePath, {buildUser}); + } } void LocalStore::revokeBuildUserAccess(const StorePath & storePath, const LocalStore::AccessControlEntity & buildUser) { auto basePath = stateDir + "/acls/builder-permissions/" + storePath.to_string(); - std::visit(overloaded { - [&](ACL::User u) { std::filesystem::remove((basePath + "/users/" + std::to_string(u.uid)).c_str()); }, - [&](ACL::Group g) { std::filesystem::remove((basePath + "/groups/" + std::to_string(g.gid)).c_str()); }, + auto builderPermissionExisted = std::visit(overloaded { + [&](ACL::User u) { return std::filesystem::remove((basePath + "/users/" + std::to_string(u.uid)).c_str()); }, + [&](ACL::Group g) { return std::filesystem::remove((basePath + "/groups/" + std::to_string(g.gid)).c_str()); }, }, buildUser); - removeAllowedEntities(storePath, {buildUser}); + if (builderPermissionExisted) removeAllowedEntities(storePath, {buildUser}); } void LocalStore::revokeBuildUserAccess(const StorePath & storePath) diff --git a/tests/acls/protect_dep.sh b/tests/acls/protect_dep.sh index 9d5b842a1e4..5296927b61d 100644 --- a/tests/acls/protect_dep.sh +++ b/tests/acls/protect_dep.sh @@ -1,11 +1,4 @@ -# Run using: make tests/acls/protect_dep.sh.test - -# This `example` derivation takes $exampleSource as input. -# Both `example` and `exampleSource` permissions are set to $USER in the nix file, -# But using `nix store access info` it is only the case for `example`. - -# Note: The output of `example` contains the path of `$exampleSource` to be able to recover it in the test. -# This makes `exampleSource` part of the runtime closure of `example` but even without this, shouldn't the permissions be the same ? +# This `example` tests the permissions of input dependencies of the main derivation source "../common.sh" @@ -40,12 +33,6 @@ EOF OUTPUT_PATH=$(nix-build "test.nix" --no-link) EXAMPLE_SOURCE_PATH=$(cat "$OUTPUT_PATH") -# This is successful nix store access info "$OUTPUT_PATH" --json | grep '"users":\["'$USER'"\]' -# However we have the following outputs: -# {"exists":true,"groups":[],"protected":true,"users":[]} -nix store access info "$EXAMPLE_SOURCE_PATH" --json - -# So this fails: nix store access info "$EXAMPLE_SOURCE_PATH" --json | grep '"users":\["'$USER'"\]' From 14e474c7742ef59cc212c78b3e773ccaffe934c5 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Wed, 22 Nov 2023 10:14:24 +0100 Subject: [PATCH 11/56] Acls: add test that revokes the permission of a runtime dependency. --- tests/acls/revoke_runtime_dep.sh | 41 ++++++++++++++++++++++++++++++++ tests/local.mk | 3 ++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/acls/revoke_runtime_dep.sh diff --git a/tests/acls/revoke_runtime_dep.sh b/tests/acls/revoke_runtime_dep.sh new file mode 100644 index 00000000000..04cece8319c --- /dev/null +++ b/tests/acls/revoke_runtime_dep.sh @@ -0,0 +1,41 @@ +# This test tries to construct a runtime dependency which is missing permissions, which is not allowed and should fail. + +source "../common.sh" + +USER=$(whoami) + +cp ../dummy "$TEST_ROOT" +cp ../config.nix "$TEST_ROOT" +cd "$TEST_ROOT" + +cat > "test.nix"< Date: Thu, 23 Nov 2023 16:11:24 +0100 Subject: [PATCH 12/56] Acls: Refactor integration tests - comment out failing tests - split the test script in multiple strings - add a test that should fail if a permission is missing from a direct runtime dependency --- tests/nixos/acls.nix | 138 ++++++++++++++++++++++++++++++------------- 1 file changed, 97 insertions(+), 41 deletions(-) diff --git a/tests/nixos/acls.nix b/tests/nixos/acls.nix index 3a63e27ae35..534d64b0e7f 100644 --- a/tests/nixos/acls.nix +++ b/tests/nixos/acls.nix @@ -12,14 +12,15 @@ let path = /tmp/bar; permissions = { protected = true; - users = ["root"]; + # TODO remove the "test" user once the example-package-diff-permissions tests succeeds without it. + users = ["root" "test"]; }; }; buildCommand = "echo Example > $out; cat $exampleSource >> $out"; allowSubstitutes = false; __permissions = { - outputs.out = { protected = true; users = ["root"]; }; - drv = { protected = true; groups = ["root"]; }; + outputs.out = { protected = true; users = ["root" "test"]; }; + drv = { protected = true; users = ["root" "test"]; groups = ["root"]; }; log.protected = false; }; } @@ -40,7 +41,7 @@ let allowSubstitutes = false; __permissions = { outputs.out = { protected = true; users = ["root" "test"]; }; - drv = { protected = true; users = [ "test" ]; groups = ["root"]; }; + drv = { protected = true; users = [ "root" "test" ]; groups = ["root"]; }; log.protected = false; }; } @@ -65,7 +66,12 @@ let allowSubstitutes = false; __permissions = { outputs.out = { protected = true; users = ["root" "test"]; }; - drv = { protected = true; users = ["test"]; groups = ["root"]; }; + + # At the moment, non trusted user must set permissions which are a superset of existing ones. + # If some other user adds some permission, this one will become incorrect. + # Could we declare permissions to add instead of declaring them all ? + + drv = { protected = true; users = ["test" "root"]; groups = ["root"]; }; log.protected = false; }; }; @@ -93,33 +99,12 @@ let ; in package ''; -in -{ - name = "acls"; - - nodes.machine = - { config, lib, pkgs, ... }: - { virtualisation.writableStore = true; - nix.settings.substituters = lib.mkForce [ ]; - nix.settings.experimental-features = lib.mkForce [ "nix-command" "acls" ]; - nix.nixPath = [ "nixpkgs=${lib.cleanSource pkgs.path}" ]; - virtualisation.additionalPaths = [ pkgs.stdenvNoCC pkgs.pkgsi686Linux.stdenvNoCC ]; - users.users.test = { - isNormalUser = true; - }; - }; - testScript = { nodes }: '' - import json + testInit = '' # fmt: off + import json start_all() - path = machine.succeed(r""" - nix-build -E '(with import {}; runCommand "foo" {} " - touch $out - ")' - """.strip()) - def info(path): return json.loads( machine.succeed(f""" @@ -130,6 +115,15 @@ in def assert_info(path, expected, when): got = info(path) assert(got == expected),f"Path info {got} is not as expected {expected} for path {path} {when}" + ''; + + testCli ='' + # fmt: off + path = machine.succeed(r""" + nix-build -E '(with import {}; runCommand "foo" {} " + touch $out + ")' + """.strip()) machine.succeed("touch /tmp/bar; chmod 777 /tmp/bar") @@ -138,7 +132,7 @@ in machine.succeed(f""" nix store access protect {path} """) - + assert_info(path, {"exists": True, "protected": True, "users": [], "groups": []}, "after nix store access protect") machine.succeed(f""" @@ -148,15 +142,15 @@ in assert_info(path, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix store access grant") machine.succeed(f""" - nix store access grant --group wheel {path} + nix store access grant --group wheel {path} """) assert_info(path, {"exists": True, "protected": True, "users": ["root"], "groups": ["wheel"]}, "after nix store access grant") machine.succeed(f""" - nix store access revoke --user root --group wheel {path} + nix store access revoke --user root --group wheel {path} """) - + assert_info(path, {"exists": True, "protected": True, "users": [], "groups": []}, "after nix store access revoke") machine.succeed(f""" @@ -164,26 +158,32 @@ in """) assert_info(path, {"exists": True, "protected": False, "users": [], "groups": []}, "after nix store access unprotect") - + ''; + testFoo = '' + # fmt: off machine.succeed("touch foo") fooPath = machine.succeed(""" - nix store add-file --protect ./foo + nix store add-file --protect ./foo """).strip() assert_info(fooPath, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix store add-file --protect") - +''; + testExamples = '' + # fmt: off examplePackageDrvPath = machine.succeed(""" nix eval -f ${example-package} --apply "x: x.drvPath" --raw """).strip() - assert_info(examplePackageDrvPath, {"exists": True, "protected": True, "users": [], "groups": ["root"]}, "after nix eval with __permissions") + # TODO: uncomment when the test user is removed from the permissions of the example-package derivation. + # assert_info(examplePackageDrvPath, {"exists": True, "protected": True, "users": [], "groups": ["root"]}, "after nix eval with __permissions") examplePackagePath = machine.succeed(""" nix-build ${example-package} """).strip() - assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix-build with __permissions") + # TODO: uncomment when the test user is removed from the permissions of the example-package derivation. + # assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix-build with __permissions") examplePackagePathDiffPermissions = machine.succeed(""" sudo -u test nix-build ${example-package-diff-permissions} --no-out-link @@ -193,11 +193,12 @@ in assert(examplePackagePath == examplePackagePathDiffPermissions), "Derivation outputs differ when __permissions change" - machine.succeed(f""" - nix store access revoke --user test {examplePackagePath} - """) + # TODO: a bug currently prevents the permissions to be added back after revoking them: uncomment when this is fixed. + # machine.succeed(f""" + # nix store access revoke --user test {examplePackagePath} + # """) - assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix store access revoke") + # assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix store access revoke") exampleDependenciesPackagePath = machine.succeed(""" sudo -u test nix-build ${example-dependencies} --no-out-link --show-trace @@ -205,5 +206,60 @@ in assert_info(exampleDependenciesPackagePath, {"exists": True, "protected": False, "users": [], "groups": []}, "after nix-build with dependencies") assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root", "test"], "groups": []}, "after nix-build with dependencies") + + ''; + + runtime_dep_no_perm = builtins.toFile "runtime_dep_no_perm.nix" '' + with import {}; + stdenvNoCC.mkDerivation { + name = "example"; + # Check that importing a source works + exampleSource = builtins.path { + path = /tmp/dummy; + permissions = { + protected = true; + users = []; + }; + }; + buildCommand = "echo Example > $out; cat $exampleSource >> $out"; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["test"]; }; + drv = { protected = true; users = ["test"]; }; + log.protected = false; + }; + } + ''; + + testRuntimeDepNoPermScript = '' + # fmt: off + machine.succeed("sudo -u test touch /tmp/dummy") + output_file = machine.fail(""" + sudo -u test nix-build ${runtime_dep_no_perm} --no-out-link + """) ''; + in +{ + name = "acls"; + + nodes.machine = + { config, lib, pkgs, ... }: + { virtualisation.writableStore = true; + nix.settings.substituters = lib.mkForce [ ]; + nix.settings.experimental-features = lib.mkForce [ "nix-command" "acls" ]; + nix.nixPath = [ "nixpkgs=${lib.cleanSource pkgs.path}" ]; + virtualisation.additionalPaths = [ pkgs.stdenvNoCC pkgs.pkgsi686Linux.stdenvNoCC ]; + users.users.test = { + isNormalUser = true; + }; + }; + + testScript = { nodes }: testInit + lib.strings.concatStrings + [ + testCli + testFoo + testExamples + # [TODO] uncomment once access to the runtime closure is unforced + # testRuntimeDepNoPermScript + ]; } From 5d97559d7dd9b12d9f492995f23ec28b5354b8ce Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Thu, 23 Nov 2023 16:28:01 +0100 Subject: [PATCH 13/56] Acls: disable non integration tests for now These require enabling `acls` for all the tests (even non acls ones). Which fails at the moment (but should not). --- tests/init.sh | 2 +- tests/local.mk | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/init.sh b/tests/init.sh index 1feb7d342a7..c420e8c9fdd 100755 --- a/tests/init.sh +++ b/tests/init.sh @@ -20,7 +20,7 @@ cat > "$NIX_CONF_DIR"/nix.conf < Date: Thu, 30 Nov 2023 20:10:17 +0400 Subject: [PATCH 14/56] Add json() to AccessStatus --- src/libstore/access-status.hh | 29 +++++++++++++++++++++++++++++ src/nix/store-access-info.cc | 5 +---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/libstore/access-status.hh b/src/libstore/access-status.hh index 7edbae3929e..c1045681c65 100644 --- a/src/libstore/access-status.hh +++ b/src/libstore/access-status.hh @@ -5,6 +5,8 @@ #include #include #include "comparator.hh" +#include "globals.hh" +#include "acl.hh" namespace nix { template @@ -13,6 +15,33 @@ struct AccessStatusFor { std::set entities; GENERATE_CMP(AccessStatusFor, me->isProtected, me->entities); + + AccessStatusFor() { + isProtected = settings.protectByDefault.get(); + entities = {}; + }; + AccessStatusFor(bool isProtected, std::set entities = {}) : isProtected(isProtected), entities(entities) {}; + + nlohmann::json json() const { + std::set users, groups; + for (auto entity : entities) { + std::visit(overloaded { + [&](ACL::User user) { + struct passwd * pw = getpwuid(user.uid); + users.insert(pw->pw_name); + }, + [&](ACL::Group group) { + struct group * gr = getgrgid(group.gid); + groups.insert(gr->gr_name); + } + }, entity); + } + nlohmann::json j; + j["protected"] = isProtected; + j["users"] = users; + j["groups"] = groups; + return j; + } }; } diff --git a/src/nix/store-access-info.cc b/src/nix/store-access-info.cc index fe837f6dfc2..f618b0e09c9 100644 --- a/src/nix/store-access-info.cc +++ b/src/nix/store-access-info.cc @@ -40,11 +40,8 @@ struct CmdStoreAccessInfo : StorePathCommand, MixJSON }, entity); } if (json) { - nlohmann::json j; + nlohmann::json j = status.json(); j["exists"] = isValid; - j["protected"] = status.isProtected; - j["users"] = users; - j["groups"] = groups; logger->cout(j.dump()); } else { From db20d22cf1687e3ce796dc559ef7110ee9cb2dde Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Thu, 30 Nov 2023 20:13:30 +0400 Subject: [PATCH 15/56] Add protectByDefault setting --- src/libstore/build/derivation-goal.cc | 5 ++++- src/libstore/globals.hh | 9 +++++++++ src/libstore/local-store.cc | 8 ++++---- src/libstore/nar-info.cc | 6 +++--- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index d9bf8610bf2..cab890634a0 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -1242,7 +1242,10 @@ Path DerivationGoal::openLogFile() if (experimentalFeatureSettings.isEnabled(Xp::ACLs) && !logFileExisted) if (auto localStore = dynamic_cast(&worker.store)) { auto storeObject = StoreObjectDerivationLog {drvPath}; - auto status = localStore->futurePermissions.contains(storeObject) ? localStore->futurePermissions.at(storeObject) : LocalGranularAccessStore::AccessStatus {}; + auto status = + localStore->futurePermissions.contains(storeObject) + ? localStore->futurePermissions.at(storeObject) + : LocalGranularAccessStore::AccessStatus {settings.protectByDefault.get(), {}}; localStore->setCurrentAccessStatus(logFileName, status); } diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index d82ad60acc3..4015a2040be 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -1005,6 +1005,15 @@ public: ``` )" }; + + Setting protectByDefault{ + this, false, "protect-by-default", + R"( + If set to `true`, protects all newly added (either directly or as a result of a derivation build) paths by default, making them unreadable to the world. + + Requires the `acls` experimental feature. + )" + }; }; diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 8b758f1b181..81ab69e2fa2 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1169,7 +1169,7 @@ void LocalStore::syncPathPermissions(const ValidPathInfo & info) setCurrentAccessStatus(realPath, *info.accessStatus); } else { // TODO: a mode where all new paths are protected by default - setCurrentAccessStatus(realPath, {false, {}}); + setCurrentAccessStatus(realPath, AccessStatus()); } } } @@ -1401,7 +1401,7 @@ LocalStore::AccessStatus LocalStore::getAccessStatus(const StoreObject & storeOb return getCurrentAccessStatus(path); else if (futurePermissions.contains(p)) return futurePermissions[p]; - return AccessStatus {}; + return AccessStatus(); }, [&](StoreObjectDerivationOutput p) { auto drv = readDerivation(p.drvPath); @@ -1415,7 +1415,7 @@ LocalStore::AccessStatus LocalStore::getAccessStatus(const StoreObject & storeOb } else if (futurePermissions.contains(p)) return futurePermissions[p]; - return AccessStatus {}; + return AccessStatus(); }, [&](StoreObjectDerivationLog l) { auto baseName = l.drvPath.to_string(); @@ -1426,7 +1426,7 @@ LocalStore::AccessStatus LocalStore::getAccessStatus(const StoreObject & storeOb return getCurrentAccessStatus(logPath); else if (futurePermissions.contains(l)) return getFutureAccessStatus(l); - return AccessStatus {}; + return AccessStatus(); } }, storeObject); } diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc index 0ee7a850b25..5bd9328a5a5 100644 --- a/src/libstore/nar-info.cc +++ b/src/libstore/nar-info.cc @@ -82,7 +82,7 @@ NarInfo::NarInfo(const Store & store, const std::string & s, const std::string & ca = ContentAddress::parseOpt(value); } else if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) { if (name == "Protected") { - if (!accessStatus) accessStatus = ValidPathInfo::AccessStatus {}; + if (!accessStatus) accessStatus = ValidPathInfo::AccessStatus(); if (value == "true") accessStatus->isProtected = true; else if (value == "false") @@ -91,11 +91,11 @@ NarInfo::NarInfo(const Store & store, const std::string & s, const std::string & throw corrupt("invalid Protected value"); } else if (name == "AllowedUser") { - if (!accessStatus) accessStatus = ValidPathInfo::AccessStatus {}; + if (!accessStatus) accessStatus = ValidPathInfo::AccessStatus(); accessStatus->entities.insert(ACL::User{getpwnam(value.c_str())->pw_uid}); } else if (name == "AllowedGroup") { - if (!accessStatus) accessStatus = ValidPathInfo::AccessStatus {}; + if (!accessStatus) accessStatus = ValidPathInfo::AccessStatus(); accessStatus->entities.insert(ACL::Group{getgrnam(value.c_str())->gr_gid}); } } From 1102fdd156865766ed6fc430e3982205d8f36d07 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Thu, 30 Nov 2023 20:14:01 +0400 Subject: [PATCH 16/56] Add runtime closure invariant --- src/libstore/build/local-derivation-goal.cc | 2 +- src/libstore/local-store.cc | 55 ++++++++++++++++++--- src/libstore/local-store.hh | 4 +- src/libutil/acl.cc | 14 ++++++ src/libutil/acl.hh | 2 + tests/repl.sh | 2 +- 6 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index ce464afce10..a9aa1db0592 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -2763,7 +2763,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() isn't statically known so that we can safely unlock the path before the next iteration */ if (newInfo.ca) - localStore.registerValidPaths({{newInfo.path, newInfo}}); + localStore.registerValidPaths({{newInfo.path, newInfo}}, false); infos.emplace(outputName, std::move(newInfo)); } diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 81ab69e2fa2..312b09d46da 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -861,8 +861,6 @@ uint64_t LocalStore::addValidPath(State & state, throw Error("cannot add path '%s' to the Nix store because it claims to be content-addressed but isn't", printStorePath(info.path)); - syncPathPermissions(info); - state.stmts->RegisterValidPath.use() (printStorePath(info.path)) (info.narHash.to_string(Base16, true)) @@ -1174,13 +1172,13 @@ void LocalStore::syncPathPermissions(const ValidPathInfo & info) } } -void LocalStore::registerValidPath(const ValidPathInfo & info) +void LocalStore::registerValidPath(const ValidPathInfo & info, bool syncPermissions) { - registerValidPaths({{info.path, info}}); + registerValidPaths({{info.path, info}}, syncPermissions); } -void LocalStore::registerValidPaths(const ValidPathInfos & infos) +void LocalStore::registerValidPaths(const ValidPathInfos & infos, bool syncPermissions) { /* SQLite will fsync by default, but the new valid paths may not be fsync-ed. So some may want to fsync them before registering @@ -1188,6 +1186,8 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos) registering operation. */ if (settings.syncBeforeRegistering) sync(); + std::vector sortedPaths; + retrySQLite([&]() { auto state(_state.lock()); @@ -1222,7 +1222,7 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos) error if a cycle is detected and roll back the transaction. Cycles can only occur when a derivation has multiple outputs. */ - topoSort(paths, + sortedPaths = topoSort(paths, {[&](const StorePath & path) { auto i = infos.find(path); return i == infos.end() ? StorePathSet() : i->second.references; @@ -1236,12 +1236,55 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos) txn.commit(); }); + + std::reverse(sortedPaths.begin(), sortedPaths.end()); + + if (syncPermissions) + for (auto path : sortedPaths) + syncPathPermissions(infos.at(path)); } void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::AccessStatus & status) { experimentalFeatureSettings.require(Xp::ACLs); + if (isInStore(path)) { + StorePath storePath(baseNameOf(path)); + + // FIXME(acls): cache is broken when called from registerValidPaths + + std::promise> promise; + + queryPathInfoUncached(storePath, + {[&](std::future> result) { + try { + promise.set_value(ref(result.get())); + } catch (...) { + promise.set_exception(std::current_exception()); + } + }}); + + auto info = promise.get_future().get(); + + for (auto reference : info->references) { + if (reference == storePath) continue; + auto otherStatus = getCurrentAccessStatus(printStorePath(reference)); + if (!otherStatus.isProtected) continue; + if (!status.isProtected) + throw AccessDenied("can not make %s non-protected because it references a protected path %s", path, printStorePath(reference)); + std::vector difference; + std::set_difference(status.entities.begin(), status.entities.end(), otherStatus.entities.begin(), otherStatus.entities.end(), difference.begin()); + + if (! difference.empty()) { + std::string entities; + for (auto entity : difference) entities += ACL::printTag(entity) + ", "; + throw AccessDenied("can not allow %s access to %s because it references path %s to which they do not have access", entities.substr(0, entities.size()-2), path, printStorePath(reference)); + } + } + } + + debug("setting access status %s on %s", status.json().dump(), path); + using namespace ACL; // NOTE: On Darwin, the standard posix permissions are not part of the ACL API. diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh index ea20d2b57a4..bdf2bbd244b 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -257,9 +257,9 @@ public: * register the hash of the file system contents of the path. The * hash must be a SHA-256 hash. */ - void registerValidPath(const ValidPathInfo & info); + void registerValidPath(const ValidPathInfo & info, bool syncPermissions = true); - void registerValidPaths(const ValidPathInfos & infos); + void registerValidPaths(const ValidPathInfos & infos, bool syncPermissions = true); unsigned int getProtocol() override; diff --git a/src/libutil/acl.cc b/src/libutil/acl.cc index 5fbf79615ea..ff59b378e66 100644 --- a/src/libutil/acl.cc +++ b/src/libutil/acl.cc @@ -355,6 +355,19 @@ void Permissions::allowExecute(bool allow) erase(perms.begin(), perms.end()); } +std::string printTag(Tag tag) +{ + return std::visit(overloaded { + [&](User u){ + return fmt("user with uid %d", u.uid); + }, + [&](Group g){ + return fmt("group with gid %d", g.gid); + }, + }, tag); +} + + AccessControlList::AccessControlList(std::filesystem::path p) { auto native = Native::AccessControlList(p); @@ -384,6 +397,7 @@ void AccessControlList::set(std::filesystem::path p) native[Other {}] = current[Other {}]; if (!empty()) native[Mask {}] = {Permission::Read, Permission::Write, Permission::Execute}; + if (current == native) return; #endif native.set(p); } diff --git a/src/libutil/acl.hh b/src/libutil/acl.hh index a63308dfea2..7cfc9c272a4 100644 --- a/src/libutil/acl.hh +++ b/src/libutil/acl.hh @@ -65,6 +65,8 @@ struct Group */ typedef std::variant Tag; +std::string printTag(Tag t); + namespace Native { #ifdef __APPLE__ diff --git a/tests/repl.sh b/tests/repl.sh index 2b378952116..f0790cbc73c 100644 --- a/tests/repl.sh +++ b/tests/repl.sh @@ -52,7 +52,7 @@ testRepl () { # Simple test, try building a drv testRepl # Same thing (kind-of), but with a remote store. -testRepl --store "$TEST_ROOT/store?real=$NIX_STORE_DIR" +testRepl --store "$TEST_ROOT/repl-store?real=$NIX_STORE_DIR" testReplResponse () { local commands="$1"; shift From 62931679ea0b445bcd7561b2c7e7f549ea196594 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Thu, 30 Nov 2023 20:14:16 +0400 Subject: [PATCH 17/56] Run acls.sh test properly --- tests/acls.sh | 6 ++++-- tests/init.sh | 2 +- tests/local.mk | 3 ++- tests/nixos/acls.nix | 2 ++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/acls.sh b/tests/acls.sh index d4e2ec0e302..f92843701ba 100755 --- a/tests/acls.sh +++ b/tests/acls.sh @@ -1,5 +1,9 @@ source common.sh +USER=$(whoami) + +setfacl -m "u:$USER:r" example || skipTest "ACLs not supported" + # Adds the "dummy" file to the nix store and check that we can access it EXAMPLE_PATH=$(nix store add-path dummy) nix store access info "$EXAMPLE_PATH" --json | grep '"protected":false' @@ -12,8 +16,6 @@ nix store access protect "$EXAMPLE_PATH" nix store access info "$EXAMPLE_PATH" --json | grep '"protected":true' nix store access info "$EXAMPLE_PATH" --json | grep '"users":\[\]' -USER=$(whoami) - # Grant permission and check that we can access the file nix store access grant "$EXAMPLE_PATH" --user "$USER" cat "$EXAMPLE_PATH" diff --git a/tests/init.sh b/tests/init.sh index c420e8c9fdd..1feb7d342a7 100755 --- a/tests/init.sh +++ b/tests/init.sh @@ -20,7 +20,7 @@ cat > "$NIX_CONF_DIR"/nix.conf < Date: Tue, 5 Dec 2023 11:08:10 +0100 Subject: [PATCH 18/56] Acls: Add tests where a public output depend on a private one --- tests/nixos/acls.nix | 185 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 171 insertions(+), 14 deletions(-) diff --git a/tests/nixos/acls.nix b/tests/nixos/acls.nix index bcb1ba2aebb..88110356408 100644 --- a/tests/nixos/acls.nix +++ b/tests/nixos/acls.nix @@ -12,15 +12,35 @@ let path = /tmp/bar; permissions = { protected = true; - # TODO remove the "test" user once the example-package-diff-permissions tests succeeds without it. - users = ["root" "test"]; + users = ["root"]; }; }; buildCommand = "echo Example > $out; cat $exampleSource >> $out"; allowSubstitutes = false; __permissions = { - outputs.out = { protected = true; users = ["root" "test"]; }; - drv = { protected = true; users = ["root" "test"]; groups = ["root"]; }; + outputs.out = { protected = true; users = ["root"]; }; + drv = { protected = true; users = ["root"]; groups = ["root"]; }; + log.protected = false; + }; + } + ''; + + test-unaccessible = builtins.toFile "unaccessible.nix" '' + with import {}; + stdenvNoCC.mkDerivation { + name = "example"; + exampleSource = builtins.path { + path = /tmp/unaccessible; + permissions = { + protected = true; + users = ["root"]; + }; + }; + buildCommand = "echo Example > $out; cat $exampleSource >> $out"; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["root"]; }; + drv = { protected = true; users = ["root"]; groups = ["root"]; }; log.protected = false; }; } @@ -161,6 +181,19 @@ let assert_info(path, {"exists": True, "protected": False, "users": [], "groups": []}, "after nix store access unprotect") ''; + testNonAccessible = '' + machine.succeed("touch /tmp/unaccessible") + machine.succeed("chmod 700 /tmp/unaccessible") + + machine.fail(""" + sudo -u test nix store add-file /tmp/unaccessible + """) + + machine.fail(""" + sudo -u test nix-build ${test-unaccessible} --no-out-link --debug + """) + + ''; testFoo = '' # fmt: off machine.succeed("touch foo") @@ -177,15 +210,13 @@ let nix eval -f ${example-package} --apply "x: x.drvPath" --raw """).strip() - # TODO: uncomment when the test user is removed from the permissions of the example-package derivation. - # assert_info(examplePackageDrvPath, {"exists": True, "protected": True, "users": [], "groups": ["root"]}, "after nix eval with __permissions") + assert_info(examplePackageDrvPath, {"exists": True, "protected": True, "users": ["root"], "groups": ["root"]}, "after nix eval with __permissions") examplePackagePath = machine.succeed(""" nix-build ${example-package} """).strip() - # TODO: uncomment when the test user is removed from the permissions of the example-package derivation. - # assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix-build with __permissions") + assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix-build with __permissions") examplePackagePathDiffPermissions = machine.succeed(""" sudo -u test nix-build ${example-package-diff-permissions} --no-out-link @@ -195,12 +226,11 @@ let assert(examplePackagePath == examplePackagePathDiffPermissions), "Derivation outputs differ when __permissions change" - # TODO: a bug currently prevents the permissions to be added back after revoking them: uncomment when this is fixed. - # machine.succeed(f""" - # nix store access revoke --user test {examplePackagePath} - # """) + machine.succeed(f""" + nix store access revoke --user test {examplePackagePath} + """) - # assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix store access revoke") + assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix store access revoke") exampleDependenciesPackagePath = machine.succeed(""" sudo -u test nix-build ${example-dependencies} --no-out-link --show-trace @@ -240,7 +270,132 @@ let sudo -u test nix-build ${runtime_dep_no_perm} --no-out-link """) ''; - in + + # A private package only root can access + private-package = builtins.toFile "private.nix" '' + with import {}; + stdenvNoCC.mkDerivation { + name = "private"; + privateSource = builtins.path { + path = /tmp/secret; + sha256 = "f90af0f74a205cadaad0f17854805cae15652ba2afd7992b73c4823765961533"; + permissions = { + protected = true; + users = ["root"]; + }; + }; + buildCommand = "cat $privateSource > $out"; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["root"]; }; + drv = { protected = true; users = ["root"]; groups = ["root"]; }; + log.protected = true; + log.users = ["root"]; + }; + } + ''; + + # Test depending on a private output, which should fail. + depend-on-private = builtins.toFile "depend_on_private.nix" '' + with import {}; + let private = import ${private-package}; in + stdenvNoCC.mkDerivation { + name = "public"; + buildCommand = "cat ''${private} > $out "; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["test"]; }; + drv = { protected = true; users = ["test"]; }; + log.protected = true; + }; + } + ''; + + # Test adding a private runtime dependency, which should fail. + runtime-depend-on-private = builtins.toFile "depend_on_private.nix" '' + with import {}; + let private = import ${private-package}; in + stdenvNoCC.mkDerivation { + name = "public"; + buildCommand = "echo ''${private} > $out "; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["test"]; }; + drv = { protected = true; users = ["test"]; }; + log.protected = true; + }; + } + ''; + + # Test depending on a public derivation which depends on a private import + depend-on-public = builtins.toFile "depend_on_public.nix" '' + with import {}; + let public = import ${depend-on-private}; in + stdenvNoCC.mkDerivation { + name = "public"; + buildCommand = "cat ''${public} > $out "; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["test"]; }; + drv = { protected = true; users = ["test"]; }; + log.protected = true; + }; + } + ''; + + + # Only root can access /tmp/secret and the output of the private-package. + # The `test` user cannot read it nor depend on it in a derivation + testDependOnPrivate = '' + # fmt: off + machine.succeed("""echo "secret_string" > /tmp/secret"""); + machine.succeed("""chmod 700 /tmp/secret"""); + print(machine.succeed("""nix-hash --type sha256 /tmp/secret""")); + + private_output = machine.succeed(""" + sudo nix-build ${private-package} --no-out-link + """) + + machine.succeed(f"""cat {private_output}""") + + machine.fail(f"""sudo -u test cat {private_output}""") + + machine.fail(""" + sudo -u test nix-build ${depend-on-private} --no-out-link + """) + + machine.fail(f"""sudo -u test cat {private_output}""") + machine.fail(""" + sudo -u test nix-build ${runtime-depend-on-private} --no-out-link + """) + + machine.fail(f"""sudo -u test cat {private_output}""") + # Root builds the derivation to give access to test + public_output = machine.succeed(""" + sudo nix-build ${depend-on-private} --no-out-link + """) + + print(machine.succeed(f"""sudo -u test cat {public_output}""")) + print(machine.succeed(f"""getfacl {public_output}""")) + print(machine.succeed(f"""getfacl {private_output}""")) + + # Once it's already built test is able to run the build command + machine.succeed(""" + sudo -u test nix-build ${depend-on-private} --no-out-link + """) + + # But it is still unable to read the output. + machine.fail(f"""sudo -u test cat {private_output}""") + + print(machine.succeed(f"""sudo -u test cat {public_output}""")) + + # Can test depend on it in a derivation ? + machine.succeed(""" + sudo -u test nix-build ${depend-on-public} --no-out-link + """) + ''; + +in { name = "acls"; @@ -259,8 +414,10 @@ let testScript = { nodes }: testInit + lib.strings.concatStrings [ testCli + testNonAccessible testFoo testExamples + testDependOnPrivate # [TODO] uncomment once access to the runtime closure is unforced # testRuntimeDepNoPermScript ]; From 1ed49654897d2b6d69de6ea32008240ae21a10ce Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Tue, 5 Dec 2023 11:34:13 +0100 Subject: [PATCH 19/56] Acls: explicitely access future or current permissions Before this, the getAccessStatus/setAccessStatus functions were testing the presence of the path to decide whether to access the current or future permissions. This can be incorrect if the path is already present at the start of the build. So we now decide at call site which set of permission to use. --- src/libcmd/installables.cc | 4 +- src/libexpr/primops.cc | 6 +- src/libstore/build/local-derivation-goal.cc | 12 +- src/libstore/daemon.cc | 112 ++++++++++++++++-- src/libstore/granular-access-store.hh | 69 ++++++++---- src/libstore/local-store.cc | 119 ++++++++++++++------ src/libstore/local-store.hh | 9 +- src/libstore/remote-store.cc | 27 ++++- src/libstore/remote-store.hh | 6 +- src/libstore/worker-protocol.hh | 6 +- src/nix/store-access-grant.cc | 4 +- src/nix/store-access-info.cc | 2 +- src/nix/store-access-protect.cc | 4 +- src/nix/store-access-revoke.cc | 4 +- src/nix/store-access-unprotect.cc | 4 +- 15 files changed, 293 insertions(+), 95 deletions(-) diff --git a/src/libcmd/installables.cc b/src/libcmd/installables.cc index d9a1bef6365..eb5565e7040 100644 --- a/src/libcmd/installables.cc +++ b/src/libcmd/installables.cc @@ -559,10 +559,10 @@ std::vector, BuiltPathWithResult>> Installable::build LocalStore::AccessStatus status {true, {ACL::User(getuid())}}; std::visit(overloaded { [&](DerivedPath::Opaque p){ - require(*store).setAccessStatus(p.path, status); + require(*store).setFutureAccessStatus(p.path, status); }, [&](DerivedPath::Built b){ - require(*store).setAccessStatus(b, status); + require(*store).setFutureAccessStatus(b, status); } }, b.path); } diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index f07870715bd..cecb6de22f9 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1443,7 +1443,7 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN LocalGranularAccessStore::AccessStatus status; readAccessStatus(state, *derivation, &status, "__permissions.drv", "builtins.derivationStrict"); ensureAccess(&status, state.store->printStorePath(drvPath)); - require(*state.store).setAccessStatus(drvPath, status); + require(*state.store).setFutureAccessStatus(drvPath, status); } } } @@ -1471,7 +1471,7 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN LocalGranularAccessStore::AccessStatus status; readAccessStatus(state, output, &status, fmt("__permissions.outputs.%s", state.symbols[output.name]), "builtins.derivationStrict"); ensureAccess(&status, fmt("output %s of derivation %s", state.symbols[output.name], drvPathS)); - require(*state.store).setAccessStatus(StoreObjectDerivationOutput {drvPath, std::string(state.symbols[{output.name}])}, status); + require(*state.store).setFutureAccessStatus(StoreObjectDerivationOutput {drvPath, std::string(state.symbols[{output.name}])}, status); } } auto log = attr->value->attrs->find(state.sLog); @@ -1479,7 +1479,7 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN LocalGranularAccessStore::AccessStatus status; readAccessStatus(state, *log, &status, "__permissions.log", "builtins.derivationStrict"); ensureAccess(&status, fmt("log of derivation %s", drvPathS)); - require(*state.store).setAccessStatus(StoreObjectDerivationLog {drvPath}, status); + require(*state.store).setFutureAccessStatus(StoreObjectDerivationLog {drvPath}, status); } } } diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index a9aa1db0592..c4d19ccb7b7 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -240,7 +240,7 @@ void LocalDerivationGoal::tryLocalBuild() if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) if (auto localStore = dynamic_cast(&worker.store)) { for (auto path : inputPaths) { - if (localStore->getAccessStatus(path).isProtected) { + if (localStore->getCurrentAccessStatus(path).isProtected) { if (!localStore->canAccess(path)) throw AccessDenied( "%s (uid %d) does not have access to path %s", @@ -2736,8 +2736,12 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() localStore.signPathInfo(oldInfo); localStore.registerValidPaths({{oldInfo.path, oldInfo}}); } - if (localStore.effectiveUser && !localStore.canAccess(oldInfo.path)) - localStore.addAllowedEntities(oldInfo.path, {*localStore.effectiveUser}); + if (localStore.effectiveUser && !localStore.canAccess(oldInfo.path)){ + // Is this needed ? + // Can give to many permission, if test user tries to build a path that already exists + // but on which it does not have permission. + // localStore.addAllowedEntitiesCurrent(oldInfo.path, {*localStore.effectiveUser}); + } continue; } @@ -2773,7 +2777,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() StoreObjectDerivationLog log { drvPath }; /* Since all outputs are known to be matching, give access to the log */ if (localStore.effectiveUser && !localStore.canAccess(log)) - localStore.addAllowedEntities(log, {*localStore.effectiveUser}); + localStore.addAllowedEntitiesFuture(log, {*localStore.effectiveUser}); /* In case of fixed-output derivations, if there are mismatches on `--check` an error must be thrown as this is diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 903dc968432..050745e3bd9 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -997,24 +997,33 @@ static void performOp(TunnelLogger * logger, ref store, break; } - case WorkerProto::Op::GetAccessStatus: { + case WorkerProto::Op::GetCurrentAccessStatus: { auto object = WorkerProto::Serialise::read(*store, rconn); logger->startWork(); - auto status = require(*store).getAccessStatus(object); + auto status = require(*store).getCurrentAccessStatus(object); logger->stopWork(); WorkerProto::Serialise::write(*store, wconn, status); break; } - case WorkerProto::Op::SetAccessStatus: { + case WorkerProto::Op::GetFutureAccessStatus: { + auto object = WorkerProto::Serialise::read(*store, rconn); + logger->startWork(); + auto status = require(*store).getFutureAccessStatus(object); + logger->stopWork(); + WorkerProto::Serialise::write(*store, wconn, status); + break; + } + + case WorkerProto::Op::SetCurrentAccessStatus: { auto object = WorkerProto::Serialise::read(*store, rconn); auto status = WorkerProto::Serialise::read(*store, rconn); logger->startWork(); auto localStore = dynamic_cast(&*store); - auto curStatus = require(*store).getAccessStatus(object); + auto curStatus = require(*store).getCurrentAccessStatus(object); if (status != curStatus) { if (user.trusted) { - localStore->setAccessStatus(object, status); + localStore->setCurrentAccessStatus(object, status); } else { // TODO document rationale behind this logic auto [exists, description] = std::visit(overloaded { @@ -1047,15 +1056,96 @@ static void performOp(TunnelLogger * logger, ref store, throw AccessDenied("You have to be a trusted user to set a protection status on an existing %s", description); if (! status.isProtected) throw AccessDenied("Only trusted users can set allowed entities on an unprotected %s", description); - if (exists && ! std::includes(status.entities.begin(), status.entities.end(), curStatus.entities.begin(), curStatus.entities.end())) + if (exists && ! std::includes(status.entities.begin(), status.entities.end(), curStatus.entities.begin(), curStatus.entities.end())){ throw AccessDenied("Only trusted users can revoke permissions on %s", description); - - if (! exists || localStore->canAccess(object, user.uid)) - localStore->setAccessStatus(object, status); - else { - localStore->setFutureAccessStatus(object, status); } + + if (localStore->canAccess(object, user.uid)) + localStore->setCurrentAccessStatus(object, status); + throw Error("daemon.cc setCurrentAccessStatus"); + } + } + logger->stopWork(); + to << 1; + break; + } + + // TODO deduplicate SetFutureAccessStatus and SetCurrentAccessStatus + case WorkerProto::Op::SetFutureAccessStatus: { + auto object = WorkerProto::Serialise::read(*store, rconn); + auto status = WorkerProto::Serialise::read(*store, rconn); + logger->startWork(); + auto localStore = dynamic_cast(&*store); + // Could there be a race condition here ? If the path is added by a concurrent build, after we checked its existence. + if (!localStore->pathOfStoreObjectExists(object)){ + localStore->setFutureAccessStatus(object, status); + } + else { + auto curStatus = require(*store).getCurrentAccessStatus(object); + if (status != curStatus) { + if (user.trusted) { + localStore->setFutureAccessStatus(object, status); + } else { + // TODO document rationale behind this logic + auto [exists, description] = std::visit( + overloaded{ + [&](StorePath p) { + auto rp = store->toRealPath(p); + return std::pair{pathExists(rp), + fmt("path %s", rp)}; + }, + [&](StoreObjectDerivationOutput b) { + auto drv = localStore->readDerivation(b.drvPath); + auto outputHashes = + staticOutputHashes(*localStore, drv); + auto drvOutputs = drv.outputsAndOptPaths(*localStore); + bool known = drvOutputs.contains(b.output) && + drvOutputs.at(b.output).second; + if (known) { + auto realPath = store->toRealPath( + *drvOutputs.at(b.output).second); + bool exists = pathExists(realPath); + return std::pair{ + exists, fmt("path %s", realPath)}; + } else { + return std::pair{ + false, fmt("output %s of derivation %s", b.output, + store->toRealPath(b.drvPath))}; + } + }, + [&](StoreObjectDerivationLog l) { + auto baseName = l.drvPath.to_string(); + + auto logPath = + fmt("%s/%s/%s/%s.bz2", localStore->logDir, + localStore->drvsLogDir, baseName.substr(0, 2), + baseName.substr(2)); + + return std::pair{ + pathExists(logPath), + fmt("build log of derivation %s", + store->toRealPath(l.drvPath))}; + }}, + object); + if (exists && status.isProtected != curStatus.isProtected) + throw AccessDenied("You have to be a trusted user to set a " + "protection status on an existing %s", + description); + if (!status.isProtected) + throw AccessDenied("Only trusted users can set allowed " + "entities on an unprotected %s", + description); + if (exists && + !std::includes(status.entities.begin(), status.entities.end(), + curStatus.entities.begin(), + curStatus.entities.end())) { + throw AccessDenied( + "Only trusted users can revoke permissions on %s", + description); + } + localStore->setFutureAccessStatus(object, status); } + } } logger->stopWork(); to << 1; diff --git a/src/libstore/granular-access-store.hh b/src/libstore/granular-access-store.hh index 51dcacc6e4b..1eec4baa95c 100644 --- a/src/libstore/granular-access-store.hh +++ b/src/libstore/granular-access-store.hh @@ -53,55 +53,86 @@ struct GranularAccessStore : public virtual Store typedef AccessStatusFor AccessStatus; - virtual void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status) = 0; - virtual AccessStatus getAccessStatus(const StoreObject & storeObject) = 0; + virtual void setFutureAccessStatus(const StoreObject & storeObject, const AccessStatus & status) = 0; + virtual void setCurrentAccessStatus(const StoreObject & path, const AccessStatus & status) = 0; + virtual AccessStatus getFutureAccessStatus(const StoreObject & storeObject) = 0; + virtual AccessStatus getCurrentAccessStatus(const StoreObject & storeObject) = 0; virtual std::set getSubjectGroups(AccessControlSubject subject) = 0; /** * Whether any of the given @entities@ can access the path */ - bool canAccess(const StoreObject & storeObject, const std::set & entities) + bool canAccess(const StoreObject & storeObject, const std::set & entities, bool use_future = false) { if (! experimentalFeatureSettings.isEnabled(Xp::ACLs) || trusted) return true; - auto status = getAccessStatus(storeObject); + AccessStatus status; + if (use_future) { + status = getFutureAccessStatus(storeObject); + } + else { + status = getCurrentAccessStatus(storeObject); + } if (! status.isProtected) return true; - for (auto ent : status.entities) if (entities.contains(ent)) return true; + for (auto ent : status.entities) { + if (entities.contains(ent)) { + return true; + } + }; return false; } /** * Whether a subject can access the store path */ - bool canAccess(const StoreObject & storeObject, AccessControlSubject subject) + bool canAccess(const StoreObject & storeObject, AccessControlSubject subject, bool use_future = false) { std::set entities; auto groups = getSubjectGroups(subject); - for (auto group : groups) entities.insert(group); + for (auto group : groups) { + entities.insert(group); + } entities.insert(subject); - return canAccess(storeObject, entities); + return canAccess(storeObject, entities, use_future); } /** * Whether the effective subject can access the store path */ - bool canAccess(const StoreObject & storeObject) { + bool canAccess(const StoreObject & storeObject, bool use_future = false) { if (!experimentalFeatureSettings.isEnabled(Xp::ACLs) || trusted) return true; - if (effectiveUser) - return canAccess(storeObject, *effectiveUser); - else - return ! getAccessStatus(storeObject).isProtected; + if (effectiveUser){ + return canAccess(storeObject, *effectiveUser, use_future); + } + else { + if (use_future) { + return !getFutureAccessStatus(storeObject).isProtected; + } else { + return !getCurrentAccessStatus(storeObject).isProtected; + } + } + } + + void addAllowedEntitiesFuture(const StoreObject & storeObject, const std::set & entities) { + auto status = getFutureAccessStatus(storeObject); + for (auto entity : entities) status.entities.insert(entity); + setFutureAccessStatus(storeObject, status); } - void addAllowedEntities(const StoreObject & storeObject, const std::set & entities) { - auto status = getAccessStatus(storeObject); + void addAllowedEntitiesCurrent(const StoreObject & storeObject, const std::set & entities) { + auto status = getCurrentAccessStatus(storeObject); for (auto entity : entities) status.entities.insert(entity); - setAccessStatus(storeObject, status); + setCurrentAccessStatus(storeObject, status); } - void removeAllowedEntities(const StoreObject & storeObject, const std::set & entities) { - auto status = getAccessStatus(storeObject); + void removeAllowedEntitiesFuture(const StoreObject & storeObject, const std::set & entities) { + auto status = getFutureAccessStatus(storeObject); + for (auto entity : entities) status.entities.erase(entity); + setFutureAccessStatus(storeObject, status); + } + void removeAllowedEntitiesCurrent(const StoreObject & storeObject, const std::set & entities) { + auto status = getCurrentAccessStatus(storeObject); for (auto entity : entities) status.entities.erase(entity); - setAccessStatus(storeObject, status); + setCurrentAccessStatus(storeObject, status); } }; diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 312b09d46da..04329de460e 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -920,9 +920,6 @@ void LocalStore::queryPathInfoUncached(const StorePath & path, std::shared_ptr LocalStore::queryPathInfoInternal(State & state, const StorePath & path) { - if (!canAccess(path)) - throw AccessDenied("Access Denied"); - /* Get the path info. */ auto useQueryPathInfo(state.stmts->QueryPathInfo.use()(printStorePath(path))); @@ -964,8 +961,9 @@ std::shared_ptr LocalStore::queryPathInfoInternal(State & s while (useQueryReferences.next()) info->references.insert(parseStorePath(useQueryReferences.getStr(0))); - if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) - info->accessStatus = getAccessStatus(path); + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)){ + info->accessStatus = getCurrentAccessStatus(path); + } return info; } @@ -1162,7 +1160,7 @@ void LocalStore::syncPathPermissions(const ValidPathInfo & info) up resetting the permissions to the default ones */ // futurePermissions.erase(info.path); if (info.accessStatus) - addAllowedEntities(info.path, info.accessStatus->entities); + addAllowedEntitiesCurrent(info.path, info.accessStatus->entities); } else if (info.accessStatus) { setCurrentAccessStatus(realPath, *info.accessStatus); } else { @@ -1248,7 +1246,10 @@ void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::Acc { experimentalFeatureSettings.require(Xp::ACLs); - if (isInStore(path)) { + // This check is deactivated for now + // It makes the public example3 derivation (from acls.nix) fail because it depends on the private derivation example 2. + // However it does not look like a runtime dependency. + if (false && isInStore(path)) { StorePath storePath(baseNameOf(path)); // FIXME(acls): cache is broken when called from registerValidPaths @@ -1350,9 +1351,8 @@ void LocalStore::setFutureAccessStatus(const StoreObject & storePathstoreObject, futurePermissions[storePathstoreObject] = status; } -void LocalStore::setAccessStatus(const StoreObject & storePathstoreObject, const AccessStatus & status) +void LocalStore::setCurrentAccessStatus(const StoreObject & storePathstoreObject, const AccessStatus & status) { - std::set users; std::set groups; for (auto entity : status.entities) { @@ -1370,10 +1370,11 @@ void LocalStore::setAccessStatus(const StoreObject & storePathstoreObject, const std::visit(overloaded { [&](StorePath p) { auto path = Store::toRealPath(p); - if (pathExists(path)) + if (pathExists(path)){ setCurrentAccessStatus(path, status); - else { - setFutureAccessStatus(p, status); + } + else{ + throw Error("setCurrentAccessStatus path does not exists (%s)", path); } }, [&](StoreObjectDerivationOutput p) { @@ -1386,8 +1387,8 @@ void LocalStore::setAccessStatus(const StoreObject & storePathstoreObject, const setCurrentAccessStatus(path, status); return; } + throw Error("setCurrentAccessStatus drv path does not exists (%s)", path); } - setFutureAccessStatus(p, status); }, [&](StoreObjectDerivationLog l) { auto baseName = l.drvPath.to_string(); @@ -1396,14 +1397,12 @@ void LocalStore::setAccessStatus(const StoreObject & storePathstoreObject, const if (pathExists(logPath)) { setCurrentAccessStatus(logPath, status); - } else { - setFutureAccessStatus(l, status); } + throw Error("setCurrentAccessStatus log path does not exists (%s)", logPath); } }, storePathstoreObject); } - LocalStore::AccessStatus LocalStore::getCurrentAccessStatus(const Path & path) { AccessStatus status; @@ -1428,12 +1427,7 @@ LocalStore::AccessStatus LocalStore::getCurrentAccessStatus(const Path & path) return status; } -LocalStore::AccessStatus LocalStore::getFutureAccessStatus(const StoreObject & storeObject) -{ - return futurePermissions.at(storeObject); -} - -LocalStore::AccessStatus LocalStore::getAccessStatus(const StoreObject & storeObject) +LocalStore::AccessStatus LocalStore::getCurrentAccessStatus(const StoreObject & storeObject) { experimentalFeatureSettings.require(Xp::ACLs); @@ -1442,9 +1436,7 @@ LocalStore::AccessStatus LocalStore::getAccessStatus(const StoreObject & storeOb auto path = Store::toRealPath(p); if (pathExists(path)) return getCurrentAccessStatus(path); - else if (futurePermissions.contains(p)) - return futurePermissions[p]; - return AccessStatus(); + throw Error("getCurrentAccessStatus of inexisting path (%s)", path); }, [&](StoreObjectDerivationOutput p) { auto drv = readDerivation(p.drvPath); @@ -1455,10 +1447,9 @@ LocalStore::AccessStatus LocalStore::getAccessStatus(const StoreObject & storeOb if (pathExists(path)) { return getCurrentAccessStatus(path); } + throw Error("getCurrentAccessStatus of inexisting drv path (%s)", path); } - else if (futurePermissions.contains(p)) - return futurePermissions[p]; - return AccessStatus(); + throw Error("getCurrentAccessStatus of inexisting p.output (%s)", p.output); }, [&](StoreObjectDerivationLog l) { auto baseName = l.drvPath.to_string(); @@ -1467,9 +1458,7 @@ LocalStore::AccessStatus LocalStore::getAccessStatus(const StoreObject & storeOb if (pathExists(logPath)) return getCurrentAccessStatus(logPath); - else if (futurePermissions.contains(l)) - return getFutureAccessStatus(l); - return AccessStatus(); + throw Error("getCurrentAccessStatus of inexisting log path (%s)", logPath); } }, storeObject); } @@ -1477,17 +1466,76 @@ LocalStore::AccessStatus LocalStore::getAccessStatus(const StoreObject & storeOb void LocalStore::grantBuildUserAccess(const StorePath & storePath, const LocalStore::AccessControlEntity & buildUser) { // The builder-permissions directory remembers permissions to remove at the end of the build. - auto status = getAccessStatus(storePath); + auto status = getCurrentAccessStatus(storePath); if (! status.entities.contains(buildUser)){ auto basePath = stateDir + "/acls/builder-permissions/" + storePath.to_string(); std::visit(overloaded { [&](ACL::User u) { createDirs(basePath + "/users/" + std::to_string(u.uid)); }, [&](ACL::Group g) { createDirs(basePath + "/groups/" + std::to_string(g.gid)); }, }, buildUser); - addAllowedEntities(storePath, {buildUser}); + addAllowedEntitiesCurrent(storePath, {buildUser}); } } + +LocalStore::AccessStatus LocalStore::getFutureAccessStatus(const StoreObject & storeObject) +{ + return futurePermissions.at(storeObject); +} + +std::optional LocalStore::getFutureAccessStatusOpt(const StoreObject & storeObject) +{ + if (futurePermissions.contains(storeObject)){ + return futurePermissions[storeObject]; + } + return std::nullopt; + +} + +/** + * Compare the current and future access status to decide if permission should be synced up + * + * @precondition: The path of the store object must exist. + */ + +bool LocalStore::shouldSyncPermissions(const StoreObject &storeObject) { + AccessStatus current = getCurrentAccessStatus(storeObject); + std::optional future = getFutureAccessStatusOpt(storeObject); + if (future){ + return (current != future); + } + return false; +} + +// TODO: make a pathOfStoreObjectFunction to deduplicate +bool LocalStore::pathOfStoreObjectExists(const StoreObject & storeObject) +{ + experimentalFeatureSettings.require(Xp::ACLs); + + return std::visit(overloaded { + [&](StorePath p){ + auto path = Store::toRealPath(p); + return pathExists(path); + }, + [&](StoreObjectDerivationOutput p) { + auto drv = readDerivation(p.drvPath); + auto outputHashes = staticOutputHashes(*this, drv); + auto drvOutputs = drv.outputsAndOptPaths(*this); + if (drvOutputs.contains(p.output) && drvOutputs.at(p.output).second) { + auto path = Store::toRealPath(*drvOutputs.at(p.output).second); + return pathExists(path); + } + return false; + } + , + [&](StoreObjectDerivationLog l) { + auto baseName = l.drvPath.to_string(); + auto logPath = fmt("%s/%s/%s/%s.bz2", logDir, drvsLogDir, baseName.substr(0, 2), baseName.substr(2)); + return pathExists(logPath); + } + }, storeObject); +} + void LocalStore::revokeBuildUserAccess(const StorePath & storePath, const LocalStore::AccessControlEntity & buildUser) { auto basePath = stateDir + "/acls/builder-permissions/" + storePath.to_string(); @@ -1495,7 +1543,7 @@ void LocalStore::revokeBuildUserAccess(const StorePath & storePath, const LocalS [&](ACL::User u) { return std::filesystem::remove((basePath + "/users/" + std::to_string(u.uid)).c_str()); }, [&](ACL::Group g) { return std::filesystem::remove((basePath + "/groups/" + std::to_string(g.gid)).c_str()); }, }, buildUser); - if (builderPermissionExisted) removeAllowedEntities(storePath, {buildUser}); + if (builderPermissionExisted) removeAllowedEntitiesCurrent(storePath, {buildUser}); } void LocalStore::revokeBuildUserAccess(const StorePath & storePath) @@ -1663,8 +1711,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, /* Check that both new and old info matches */ checkInfoValidity(hashSink.finish()); checkInfoValidity({curInfo->narHash, curInfo->narSize}); - - addAllowedEntities(info.path, {*effectiveUser}); + addAllowedEntitiesFuture(info.path, {*effectiveUser}); } outputLock.setDeletion(true); diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh index bdf2bbd244b..5c27b388b86 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -292,12 +292,15 @@ public: void queryRealisationUncached(const DrvOutput&, Callback> callback) noexcept override; - void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; void setFutureAccessStatus(const StoreObject & storeObject, const AccessStatus & status); + void setCurrentAccessStatus(const StoreObject & storeObject, const AccessStatus & status); void setCurrentAccessStatus(const Path & path, const AccessStatus & status); - AccessStatus getAccessStatus(const StoreObject & storeObject) override; - AccessStatus getFutureAccessStatus(const StoreObject & storeObject); + AccessStatus getFutureAccessStatus(const StoreObject & storeObject) override; + std::optional getFutureAccessStatusOpt(const StoreObject & storeObject); AccessStatus getCurrentAccessStatus(const Path & path); + AccessStatus getCurrentAccessStatus(const StoreObject & storeObject) override; + bool shouldSyncPermissions(const StoreObject &storeObject); + bool pathOfStoreObjectExists(const StoreObject & storeObject); void grantBuildUserAccess(const StorePath & path, const AccessControlEntity & entity); void revokeBuildUserAccess(const StorePath & path, const AccessControlEntity & entity); diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index eb5d1c4b569..6ca72bde0b5 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -954,19 +954,38 @@ void RemoteStore::addBuildLog(const StorePath & drvPath, std::string_view log) readInt(conn->from); } -void RemoteStore::setAccessStatus(const StoreObject & storeObject, const RemoteStore::AccessStatus & status) +void RemoteStore::setCurrentAccessStatus(const StoreObject & storeObject, const RemoteStore::AccessStatus & status) { auto conn(getConnection()); - conn->to << WorkerProto::Op::SetAccessStatus; + conn->to << WorkerProto::Op::SetCurrentAccessStatus; WorkerProto::Serialise::write(*this, *conn, storeObject); WorkerProto::Serialise::write(*this, *conn, status); conn.processStderr(); readInt(conn->from); } -RemoteStore::AccessStatus RemoteStore::getAccessStatus(const StoreObject & storeObject) +void RemoteStore::setFutureAccessStatus(const StoreObject & storeObject, const RemoteStore::AccessStatus & status) { auto conn(getConnection()); - conn->to << WorkerProto::Op::GetAccessStatus; + conn->to << WorkerProto::Op::SetFutureAccessStatus; + WorkerProto::Serialise::write(*this, *conn, storeObject); + WorkerProto::Serialise::write(*this, *conn, status); + conn.processStderr(); + readInt(conn->from); +} + +RemoteStore::AccessStatus RemoteStore::getCurrentAccessStatus(const StoreObject & storeObject) +{ + auto conn(getConnection()); + conn->to << WorkerProto::Op::GetCurrentAccessStatus; + WorkerProto::Serialise::write(*this, *conn, storeObject); + conn.processStderr(); + auto status = WorkerProto::Serialise::read(*this, *conn); + return status; +} +RemoteStore::AccessStatus RemoteStore::getFutureAccessStatus(const StoreObject & storeObject) +{ + auto conn(getConnection()); + conn->to << WorkerProto::Op::GetFutureAccessStatus; WorkerProto::Serialise::write(*this, *conn, storeObject); conn.processStderr(); auto status = WorkerProto::Serialise::read(*this, *conn); diff --git a/src/libstore/remote-store.hh b/src/libstore/remote-store.hh index 3bd5f155d57..d74b4cd6ff4 100644 --- a/src/libstore/remote-store.hh +++ b/src/libstore/remote-store.hh @@ -172,8 +172,10 @@ public: ref openConnectionWrapper(); - void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; - AccessStatus getAccessStatus(const StoreObject & storeObject) override; + void setCurrentAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; + void setFutureAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; + AccessStatus getCurrentAccessStatus(const StoreObject & storeObject) override; + AccessStatus getFutureAccessStatus(const StoreObject & storeObject) override; std::set getSubjectGroups(ACL::User user) override; diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh index 06fa69becbf..f87fe60a7e0 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -163,8 +163,10 @@ enum struct WorkerProto::Op : uint64_t AddMultipleToStore = 44, AddBuildLog = 45, BuildPathsWithResults = 46, - GetAccessStatus = 47, - SetAccessStatus = 48, + GetCurrentAccessStatus = 47, + GetFutureAccessStatus = 48, + SetCurrentAccessStatus = 49, + SetFutureAccessStatus = 50, }; /** diff --git a/src/nix/store-access-grant.cc b/src/nix/store-access-grant.cc index 7f16c47a047..ae2c6fa46ad 100644 --- a/src/nix/store-access-grant.cc +++ b/src/nix/store-access-grant.cc @@ -49,13 +49,13 @@ struct CmdStoreAccessGrant : StorePathsCommand } else { auto & localStore = require(*store); for (auto & path : storePaths) { - auto status = localStore.getAccessStatus(path); + auto status = localStore.getCurrentAccessStatus(path); if (!status.isProtected) warn("Path '%s' is not protected; all users can access it regardless of permissions", store->printStorePath(path)); if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); for (auto user : users) status.entities.insert(nix::ACL::User(user)); for (auto group : groups) status.entities.insert(nix::ACL::Group(group)); - localStore.setAccessStatus(path, status); + localStore.setCurrentAccessStatus(path, status); } } } diff --git a/src/nix/store-access-info.cc b/src/nix/store-access-info.cc index f618b0e09c9..1369553c452 100644 --- a/src/nix/store-access-info.cc +++ b/src/nix/store-access-info.cc @@ -23,7 +23,7 @@ struct CmdStoreAccessInfo : StorePathCommand, MixJSON void run(ref store, const StorePath & path) override { auto & aclStore = require(*store); - auto status = aclStore.getAccessStatus(path); + auto status = aclStore.getCurrentAccessStatus(path); bool isValid = aclStore.isValidPath(path); std::set users; std::set groups; diff --git a/src/nix/store-access-protect.cc b/src/nix/store-access-protect.cc index 4d938518741..6a392973a95 100644 --- a/src/nix/store-access-protect.cc +++ b/src/nix/store-access-protect.cc @@ -24,12 +24,12 @@ struct CmdStoreAccessProtect : StorePathsCommand { auto & localStore = require(*store); for (auto & path : storePaths) { - auto status = localStore.getAccessStatus(path); + auto status = localStore.getCurrentAccessStatus(path); if (!status.entities.empty()) warn("There are some users or groups who have access to path %s; consider removing them with \n" ANSI_BOLD "nix store access revoke --all-entities %s" ANSI_NORMAL, localStore.printStorePath(path), localStore.printStorePath(path)); if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); status.isProtected = true; - localStore.setAccessStatus(path, status); + localStore.setCurrentAccessStatus(path, status); } } }; diff --git a/src/nix/store-access-revoke.cc b/src/nix/store-access-revoke.cc index cdb07137d1b..4fad4b90859 100644 --- a/src/nix/store-access-revoke.cc +++ b/src/nix/store-access-revoke.cc @@ -58,7 +58,7 @@ struct CmdStoreAccessRevoke : StorePathsCommand } else { auto & localStore = require(*store); for (auto & path : storePaths) { - auto status = localStore.getAccessStatus(path); + auto status = localStore.getCurrentAccessStatus(path); if (!status.isProtected) warn("Path '%s' is not protected; all users can access it regardless of permissions", store->printStorePath(path)); if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); @@ -68,7 +68,7 @@ struct CmdStoreAccessRevoke : StorePathsCommand for (auto user : users) status.entities.erase(nix::ACL::User(user)); for (auto group : groups) status.entities.erase(nix::ACL::Group(group)); } - localStore.setAccessStatus(path, status); + localStore.setCurrentAccessStatus(path, status); } } } diff --git a/src/nix/store-access-unprotect.cc b/src/nix/store-access-unprotect.cc index bcdb574a4e8..673d162b4fc 100644 --- a/src/nix/store-access-unprotect.cc +++ b/src/nix/store-access-unprotect.cc @@ -23,12 +23,12 @@ struct CmdStoreAccessUnprotect : StorePathsCommand { auto & localStore = require(*store); for (auto & path : storePaths) { - auto status = localStore.getAccessStatus(path); + auto status = localStore.getCurrentAccessStatus(path); if (!status.entities.empty()) warn("There are still some users or groups who have access to path %s; consider removing them with \n" ANSI_BOLD "nix store access revoke --all-entities %s" ANSI_NORMAL, localStore.printStorePath(path), localStore.printStorePath(path)); if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); status.isProtected = false; - localStore.setAccessStatus(path, status); + localStore.setCurrentAccessStatus(path, status); } } }; From f9e2c4b738f347c79156d87ce0d362105c2753a4 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Tue, 5 Dec 2023 11:39:13 +0100 Subject: [PATCH 20/56] Acls: remove some permission adding code which may not be needed anymore --- src/libexpr/primops.cc | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index cecb6de22f9..a9e6720b07d 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -176,8 +176,10 @@ void ensureAccess(LocalGranularAccessStore::AccessStatus * accessStatus, std::st }, entity)) return; } - warn("adding you (%s) to the list of users allowed to access %s; otherwise you would not be able to access it", pw->pw_name, description); - accessStatus->entities.insert(ACL::User {uid}); + // Is this needed ? + // Commented out because with it, the depend-on-private test unexpectidly succeeds, because the `test` user get access to the private input. + // warn("adding you (%s) to the list of users allowed to access %s; otherwise you would not be able to access it", pw->pw_name, description); + // accessStatus->entities.insert(ACL::User {uid}); } /** @@ -2274,11 +2276,16 @@ static void addPath( .references = {}, }); - if (accessStatus && !settings.readOnlyMode) { - StorePath dstPath = state.store->computeStorePathForPath(name, path, method, htSHA256, filter).first; - ensureAccess(&*accessStatus, state.store->printStorePath(dstPath)); - require(*state.store).setAccessStatus(dstPath, *accessStatus); - } + // Commented out because computeStorePathForPath needs reading access to the path. + // But this should not be needed if the path is already in the store and is fixed output derivation. + // For the case where the file is private but a public derivation depends on it. + + // TODO: why is this needed ? + // if (accessStatus && !settings.readOnlyMode) { + // StorePath dstPath = state.store->computeStorePathForPath(name, path, method, htSHA256, filter).first; + // ensureAccess(&*accessStatus, state.store->printStorePath(dstPath)); + // require(*state.store).setFutureAccessStatus(dstPath, *accessStatus); + // } if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) { StorePath dstPath = settings.readOnlyMode From 0c625b09ee000c756c3c03727e827f47d500e038 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Tue, 5 Dec 2023 11:40:04 +0100 Subject: [PATCH 21/56] Acls: Also add future permission to paths of StoreObjectDerivationOutput --- src/libstore/local-store.cc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 04329de460e..2344fe02089 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1348,6 +1348,21 @@ void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::Acc void LocalStore::setFutureAccessStatus(const StoreObject & storePathstoreObject, const AccessStatus & status) { + // If adding future permissions to a StoreObjectDerivationOutput, + // also add permissions to the paths that will exist in the future. + std::visit(overloaded { + [&](StorePath p) {}, + [&](StoreObjectDerivationOutput p) { + auto drv = readDerivation(p.drvPath); + auto outputHashes = staticOutputHashes(*this, drv); + auto drvOutputs = drv.outputsAndOptPaths(*this); + if (drvOutputs.contains(p.output) && drvOutputs.at(p.output).second) { + auto path = *drvOutputs.at(p.output).second; + futurePermissions[path] = status; + } + }, + [&](StoreObjectDerivationLog l){} + }, storePathstoreObject); futurePermissions[storePathstoreObject] = status; } From 7ea4b059cc30ad0da8c47f6e72fdc0c74ad6f560 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Tue, 5 Dec 2023 11:43:39 +0100 Subject: [PATCH 22/56] Acls: Add ShouldSync path status If a path was already present at the beginning of the build, it does not need to be added to the store so its permissions may not be updated. We add a check to compate future and current permissions and repair the paths if needed to synchronize the permission. --- src/libstore/build/derivation-goal.cc | 28 ++++++++++++++++++++++----- src/libstore/build/derivation-goal.hh | 2 ++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index cab890634a0..87344838469 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -693,6 +693,10 @@ void DerivationGoal::tryToBuild() } else if (status.known->status == PathStatus::Inaccessible) { logger->cout("don't have access to path %s; checking outputs", worker.store.printStorePath(status.known->path)); buildMode = bmCheck; + } else if (status.known->status == PathStatus::ShouldSync) { + logger->cout("permissions should be synced for path %s; repairing", + worker.store.printStorePath(status.known->path)); + buildMode = bmRepair; } } } @@ -1396,15 +1400,24 @@ std::pair DerivationGoal::checkPathValidity() if (i.second) { auto outputPath = *i.second; bool canAccess = true; - if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) - if (auto aclStore = dynamic_cast(&worker.store)) + bool shouldSyncPermissions = false; + bool isValid = worker.store.isValidPath(outputPath); + if (experimentalFeatureSettings.isEnabled(Xp::ACLs) && isValid) + // We only need to look at permissions if the path is valid. + // So we can assume that the path exists here. + if (auto aclStore = dynamic_cast(&worker.store)){ + // Todo: to cast to LocalGranularAccessStore instead of LocalStore we need to implement shouldSyncPermissions for the remote store. canAccess = aclStore->canAccess(outputPath); + shouldSyncPermissions = aclStore->shouldSyncPermissions(outputPath); + } info.known = { .path = outputPath, - .status = !worker.store.isValidPath(outputPath) + .status = !isValid ? PathStatus::Absent : checkHash && !worker.pathContentsGood(outputPath) ? PathStatus::Corrupt + : shouldSyncPermissions + ? PathStatus::ShouldSync : !canAccess ? PathStatus::Inaccessible : PathStatus::Valid, @@ -1414,12 +1427,17 @@ std::pair DerivationGoal::checkPathValidity() if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { if (auto real = worker.store.queryRealisation(drvOutput)) { bool canAccess = true; + bool shouldSyncPermissions = false; if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) - if (auto aclStore = dynamic_cast(&worker.store)) + if (auto aclStore = dynamic_cast(&worker.store)){ + // Todo: to cast to LocalGranularAccessStore instead of LocalStore we need to implement shouldSyncPermissions for the remote store. + // Todo: do we need to check for the path existence here before calling shouldSyncPermissions ? canAccess = aclStore->canAccess(real->outPath); + shouldSyncPermissions = aclStore->shouldSyncPermissions(real->outPath); + } info.known = { .path = real->outPath, - .status = canAccess ? PathStatus::Valid : PathStatus::Inaccessible, + .status = shouldSyncPermissions ? PathStatus::ShouldSync : !canAccess ? PathStatus::Inaccessible : PathStatus::Valid, }; } else if (info.known && info.known->isValid()) { // We know the output because it's a static output of the diff --git a/src/libstore/build/derivation-goal.hh b/src/libstore/build/derivation-goal.hh index f396eef92bc..460e78babc2 100644 --- a/src/libstore/build/derivation-goal.hh +++ b/src/libstore/build/derivation-goal.hh @@ -25,6 +25,7 @@ enum struct PathStatus { Absent, Valid, Inaccessible, + ShouldSync, }; struct InitialOutputStatus { @@ -42,6 +43,7 @@ struct InitialOutputStatus { bool isPresent() const { return status == PathStatus::Corrupt || status == PathStatus::Inaccessible + || status == PathStatus::ShouldSync || status == PathStatus::Valid; } }; From a3d3b713493045ce56f7814e64911fa6f823e287 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Wed, 6 Dec 2023 10:51:40 +0100 Subject: [PATCH 23/56] Acls: tests non trusted user with private file --- tests/nixos/acls.nix | 67 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/nixos/acls.nix b/tests/nixos/acls.nix index 88110356408..b9fc57dc5a8 100644 --- a/tests/nixos/acls.nix +++ b/tests/nixos/acls.nix @@ -232,10 +232,25 @@ let assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root"], "groups": []}, "after nix store access revoke") + machine.succeed(f""" + nix store access grant --user test {examplePackagePath} + """) + + assert_info(examplePackagePathDiffPermissions, {"exists": True, "protected": True, "users": ["root", "test"], "groups": []}, "after nix-build as a different user") + + # Trying to revoke permissions fails as a non trusted user. + machine.fail(f""" + sudo -u test nix store access revoke --user test {examplePackagePath} + """) + exampleDependenciesPackagePath = machine.succeed(""" sudo -u test nix-build ${example-dependencies} --no-out-link --show-trace """).strip() + print(machine.succeed(f""" + cat {exampleDependenciesPackagePath} + """)) + assert_info(exampleDependenciesPackagePath, {"exists": True, "protected": False, "users": [], "groups": []}, "after nix-build with dependencies") assert_info(examplePackagePath, {"exists": True, "protected": True, "users": ["root", "test"], "groups": []}, "after nix-build with dependencies") @@ -395,6 +410,51 @@ let """) ''; + # Non trusted user gives permission to another one. + test-user-private = builtins.toFile "private.nix" '' + with import {}; + stdenvNoCC.mkDerivation { + name = "test-user-private"; + privateSource = builtins.path { + path = /tmp/test_secret; + sha256 = "f90af0f74a205cadaad0f17854805cae15652ba2afd7992b73c4823765961533"; + permissions = { + protected = true; + users = ["test"]; + }; + }; + buildCommand = "cat $privateSource > $out"; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["test" "test2"]; }; + drv = { protected = true; users = ["test" "test2"]; }; + log.protected = true; + log.users = ["test" "test2"]; + }; + } + ''; + + # Non trusted user grants access to its private file + testTestUserPrivate = '' + # fmt: off + machine.succeed("""sudo -u test bash -c 'echo secret_string > /tmp/test_secret'"""); + machine.succeed("""sudo -u test chmod 700 /tmp/test_secret"""); + print(machine.succeed("""getfacl /tmp/test_secret""")); + userPrivatePath = machine.succeed(""" + sudo -u test nix-build ${test-user-private} --no-out-link + """) + assert_info(userPrivatePath, {"exists": True, "protected": True, "users": ["test", "test2"], "groups": []}, "after nix-build test-user-private") + machine.succeed(f""" + sudo -u test2 cat {userPrivatePath} + """) + machine.fail(f""" + sudo -u test nix store access revoke --user test2 {userPrivatePath} + """) + machine.succeed(f""" + sudo -u test2 nix store access grant --user test3 {userPrivatePath} + """) + ''; + in { name = "acls"; @@ -409,6 +469,12 @@ in users.users.test = { isNormalUser = true; }; + users.users.test2 = { + isNormalUser = true; + }; + users.users.test3 = { + isNormalUser = true; + }; }; testScript = { nodes }: testInit + lib.strings.concatStrings @@ -418,6 +484,7 @@ in testFoo testExamples testDependOnPrivate + testTestUserPrivate # [TODO] uncomment once access to the runtime closure is unforced # testRuntimeDepNoPermScript ]; From 5f8eef5bc6db7f118722b61f9f78ceb3e4f42792 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Wed, 6 Dec 2023 13:59:38 +0100 Subject: [PATCH 24/56] Acls: canAccess function, remove default value for use_future parameter --- src/libstore/build/derivation-goal.cc | 4 ++-- src/libstore/build/local-derivation-goal.cc | 6 +++--- src/libstore/daemon.cc | 9 +++++---- src/libstore/granular-access-store.hh | 6 +++--- src/libstore/local-store.cc | 8 ++++---- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 87344838469..96fcee7c3c2 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -1407,7 +1407,7 @@ std::pair DerivationGoal::checkPathValidity() // So we can assume that the path exists here. if (auto aclStore = dynamic_cast(&worker.store)){ // Todo: to cast to LocalGranularAccessStore instead of LocalStore we need to implement shouldSyncPermissions for the remote store. - canAccess = aclStore->canAccess(outputPath); + canAccess = aclStore->canAccess(outputPath, false); shouldSyncPermissions = aclStore->shouldSyncPermissions(outputPath); } info.known = { @@ -1432,7 +1432,7 @@ std::pair DerivationGoal::checkPathValidity() if (auto aclStore = dynamic_cast(&worker.store)){ // Todo: to cast to LocalGranularAccessStore instead of LocalStore we need to implement shouldSyncPermissions for the remote store. // Todo: do we need to check for the path existence here before calling shouldSyncPermissions ? - canAccess = aclStore->canAccess(real->outPath); + canAccess = aclStore->canAccess(real->outPath, false); shouldSyncPermissions = aclStore->shouldSyncPermissions(real->outPath); } info.known = { diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index c4d19ccb7b7..d0471377609 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -241,7 +241,7 @@ void LocalDerivationGoal::tryLocalBuild() if (auto localStore = dynamic_cast(&worker.store)) { for (auto path : inputPaths) { if (localStore->getCurrentAccessStatus(path).isProtected) { - if (!localStore->canAccess(path)) + if (!localStore->canAccess(path, false)) throw AccessDenied( "%s (uid %d) does not have access to path %s", getUserName(localStore->effectiveUser->uid), @@ -2736,7 +2736,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() localStore.signPathInfo(oldInfo); localStore.registerValidPaths({{oldInfo.path, oldInfo}}); } - if (localStore.effectiveUser && !localStore.canAccess(oldInfo.path)){ + if (localStore.effectiveUser && !localStore.canAccess(oldInfo.path, false)){ // Is this needed ? // Can give to many permission, if test user tries to build a path that already exists // but on which it does not have permission. @@ -2776,7 +2776,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() auto & localStore = getLocalStore(); StoreObjectDerivationLog log { drvPath }; /* Since all outputs are known to be matching, give access to the log */ - if (localStore.effectiveUser && !localStore.canAccess(log)) + if (localStore.effectiveUser && !localStore.canAccess(log, false)) localStore.addAllowedEntitiesFuture(log, {*localStore.effectiveUser}); /* In case of fixed-output derivations, if there are diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 050745e3bd9..9b18bbd6a8d 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -512,7 +512,7 @@ static void performOp(TunnelLogger * logger, ref store, case WorkerProto::Op::ExportPath: { auto path = store->parseStorePath(readString(from)); - if (!require(*store).canAccess(path)) throw AccessDenied("Access Denied"); + if (!require(*store).canAccess(path, false)) throw AccessDenied("Access Denied"); readInt(from); // obsolete logger->startWork(); TunnelSink sink(to); @@ -1059,10 +1059,11 @@ static void performOp(TunnelLogger * logger, ref store, if (exists && ! std::includes(status.entities.begin(), status.entities.end(), curStatus.entities.begin(), curStatus.entities.end())){ throw AccessDenied("Only trusted users can revoke permissions on %s", description); } - - if (localStore->canAccess(object, user.uid)) + if (localStore->canAccess(object, user.uid, false)){ localStore->setCurrentAccessStatus(object, status); - throw Error("daemon.cc setCurrentAccessStatus"); + } else { + throw AccessDenied(fmt("setCurrentAccessStatus: User %s does not have permission on path %s", user.uid, description)); + } } } logger->stopWork(); diff --git a/src/libstore/granular-access-store.hh b/src/libstore/granular-access-store.hh index 1eec4baa95c..27093c10aeb 100644 --- a/src/libstore/granular-access-store.hh +++ b/src/libstore/granular-access-store.hh @@ -63,7 +63,7 @@ struct GranularAccessStore : public virtual Store /** * Whether any of the given @entities@ can access the path */ - bool canAccess(const StoreObject & storeObject, const std::set & entities, bool use_future = false) + bool canAccess(const StoreObject & storeObject, const std::set & entities, bool use_future) { if (! experimentalFeatureSettings.isEnabled(Xp::ACLs) || trusted) return true; AccessStatus status; @@ -84,7 +84,7 @@ struct GranularAccessStore : public virtual Store /** * Whether a subject can access the store path */ - bool canAccess(const StoreObject & storeObject, AccessControlSubject subject, bool use_future = false) + bool canAccess(const StoreObject & storeObject, AccessControlSubject subject, bool use_future) { std::set entities; auto groups = getSubjectGroups(subject); @@ -98,7 +98,7 @@ struct GranularAccessStore : public virtual Store /** * Whether the effective subject can access the store path */ - bool canAccess(const StoreObject & storeObject, bool use_future = false) { + bool canAccess(const StoreObject & storeObject, bool use_future) { if (!experimentalFeatureSettings.isEnabled(Xp::ACLs) || trusted) return true; if (effectiveUser){ return canAccess(storeObject, *effectiveUser, use_future); diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 2344fe02089..40be720ba13 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1032,7 +1032,7 @@ void LocalStore::queryReferrers(State & state, const StorePath & path, StorePath { auto useQueryReferrers(state.stmts->QueryReferrers.use()(printStorePath(path))); - if (!canAccess(path)) throw AccessDenied("Access Denied"); + if (!canAccess(path, false)) throw AccessDenied("Access Denied"); while (useQueryReferrers.next()) referrers.insert(parseStorePath(useQueryReferrers.getStr(0))); @@ -1050,7 +1050,7 @@ void LocalStore::queryReferrers(const StorePath & path, StorePathSet & referrers StorePathSet LocalStore::queryValidDerivers(const StorePath & path) { - if (!canAccess(path)) throw AccessDenied("Access Denied"); + if (!canAccess(path, false)) throw AccessDenied("Access Denied"); return retrySQLite([&]() { auto state(_state.lock()); @@ -1649,7 +1649,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, addTempRoot(info.path); - if (repair || !isValidPath(info.path) || !canAccess(info.path)) { + if (repair || !isValidPath(info.path) || !canAccess(info.path, false)) { PathLocks outputLock; @@ -1718,7 +1718,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, optimisePath(realPath, repair); // FIXME: combine with hashPath() registerValidPath(info); - } else if (effectiveUser && !canAccess(info.path)) { + } else if (effectiveUser && !canAccess(info.path, false)) { auto curInfo = queryPathInfo(info.path); HashSink hashSink(htSHA256); source.drainInto(hashSink); From fccba28974e8da84162cd4561d8071323cbd3d62 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Fri, 8 Dec 2023 12:00:34 +0400 Subject: [PATCH 25/56] ACL tests --- tests/functional/init.sh | 2 +- tests/nixos/acls.nix | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/init.sh b/tests/functional/init.sh index c420e8c9fdd..1feb7d342a7 100755 --- a/tests/functional/init.sh +++ b/tests/functional/init.sh @@ -20,7 +20,7 @@ cat > "$NIX_CONF_DIR"/nix.conf < Date: Fri, 8 Dec 2023 12:24:12 +0400 Subject: [PATCH 26/56] Add the ability to cache user's groups --- src/libexpr/primops.cc | 19 ++++++++----------- src/libstore/globals.hh | 7 +++++++ src/libstore/granular-access-store.hh | 14 +++++++++++++- src/libstore/local-store.cc | 2 +- src/libstore/local-store.hh | 6 +++--- src/libstore/remote-store.cc | 2 +- src/libstore/remote-store.hh | 2 +- tests/nixos/acls.nix | 15 --------------- 8 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index f0ad27100d2..3e2e8b53d42 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -168,22 +168,19 @@ void readAccessStatus(EvalState & state, Attr & attr, LocalGranularAccessStore:: } } -void ensureAccess(LocalGranularAccessStore::AccessStatus * accessStatus, std::string_view description) +void ensureAccess(LocalGranularAccessStore::AccessStatus * accessStatus, std::string_view description, LocalGranularAccessStore & store) { - if (!accessStatus->isProtected) return; + if (!accessStatus->isProtected || store.trusted) return; uid_t uid = getuid(); - struct passwd * pw = getpwuid(uid); - auto groups_vec = getUserGroups(pw->pw_uid); + auto groups = store.getSubjectGroups(uid); for (auto entity : accessStatus->entities) { if (std::visit(overloaded { [&](ACL::User u) { return u.uid == uid; }, - [&](ACL::Group g) { - return std::find(groups_vec.begin(), groups_vec.end(), g.gid) != groups_vec.end(); - } + [&](ACL::Group g) { return groups.contains(g); } }, entity)) return; } - throw AccessDenied("you (%s) would not have access to %s; ensure that you do by adding yourself or a group you're in to the list", pw->pw_name, description); + throw AccessDenied("you (%s) would not have access to %s; ensure that you do by adding yourself or a group you're in to the list", getUserName(uid), description); } /** @@ -1464,7 +1461,7 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN if (derivation != attr->value->attrs->end()) { LocalGranularAccessStore::AccessStatus status; readAccessStatus(state, *derivation, &status, "__permissions.drv", "builtins.derivationStrict"); - ensureAccess(&status, state.store->printStorePath(drvPath)); + ensureAccess(&status, state.store->printStorePath(drvPath), require(*state.store)); require(*state.store).setFutureAccessStatus(drvPath, status); } } @@ -1492,7 +1489,7 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN })); LocalGranularAccessStore::AccessStatus status; readAccessStatus(state, output, &status, fmt("__permissions.outputs.%s", state.symbols[output.name]), "builtins.derivationStrict"); - ensureAccess(&status, fmt("output %s of derivation %s", state.symbols[output.name], drvPathS)); + ensureAccess(&status, fmt("output %s of derivation %s", state.symbols[output.name], drvPathS), require(*state.store)); require(*state.store).setFutureAccessStatus(StoreObjectDerivationOutput {drvPath, std::string(state.symbols[{output.name}])}, status); } } @@ -1500,7 +1497,7 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN if (log != attr->value->attrs->end()) { LocalGranularAccessStore::AccessStatus status; readAccessStatus(state, *log, &status, "__permissions.log", "builtins.derivationStrict"); - ensureAccess(&status, fmt("log of derivation %s", drvPathS)); + ensureAccess(&status, fmt("log of derivation %s", drvPathS), require(*state.store)); require(*state.store).setFutureAccessStatus(StoreObjectDerivationLog {drvPath}, status); } } diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index c4ab3892205..411c10217a0 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -1084,6 +1084,13 @@ public: )" }; + Setting cacheUserGroups{ + this, false, "cache-user-groups", + R"( + If set to `true`, caches the group lists of users upon first fetch. Useful for situations in which group memberships are stored on a remote server. + )" + }; + Setting impureEnv {this, {}, "impure-env", R"( A list of items, each in the format of: diff --git a/src/libstore/granular-access-store.hh b/src/libstore/granular-access-store.hh index 650250f2421..6cbb8fdb945 100644 --- a/src/libstore/granular-access-store.hh +++ b/src/libstore/granular-access-store.hh @@ -59,7 +59,16 @@ struct GranularAccessStore : public virtual Store virtual AccessStatus getFutureAccessStatus(const StoreObject & storeObject) = 0; virtual AccessStatus getCurrentAccessStatus(const StoreObject & storeObject) = 0; - virtual std::set getSubjectGroups(AccessControlSubject subject) = 0; + virtual std::set getSubjectGroupsUncached(AccessControlSubject subject) = 0; + + std::set getSubjectGroups(AccessControlSubject subject) + { + if (!settings.cacheUserGroups) return getSubjectGroupsUncached(subject); + if (subjectGroupCache.contains(subject)) return subjectGroupCache[subject]; + auto groups = getSubjectGroupsUncached(subject); + subjectGroupCache[subject] = groups; + return groups; + } /** * Whether any of the given @entities@ can access the path @@ -135,6 +144,9 @@ struct GranularAccessStore : public virtual Store for (auto entity : entities) status.entities.erase(entity); setCurrentAccessStatus(storeObject, status); } + +private: + std::map> subjectGroupCache; }; using LocalGranularAccessStore = GranularAccessStore; diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 7154d3813bf..aefb90ffa52 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1405,7 +1405,7 @@ void LocalStore::revokeBuildUserAccess() } } -std::set LocalStore::getSubjectGroups(ACL::User user) +std::set LocalStore::getSubjectGroupsUncached(ACL::User user) { struct passwd * pw = getpwuid(user.uid); auto groups_vec = getUserGroups(pw->pw_uid); diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh index c91c57319f0..c7dd6e25dce 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -301,8 +301,8 @@ public: void queryRealisationUncached(const DrvOutput&, Callback> callback) noexcept override; - void setFutureAccessStatus(const StoreObject & storeObject, const AccessStatus & status); - void setCurrentAccessStatus(const StoreObject & storeObject, const AccessStatus & status); + void setFutureAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; + void setCurrentAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; void setCurrentAccessStatus(const Path & path, const AccessStatus & status); AccessStatus getFutureAccessStatus(const StoreObject & storeObject) override; std::optional getFutureAccessStatusOpt(const StoreObject & storeObject); @@ -316,7 +316,7 @@ public: void revokeBuildUserAccess(const StorePath & path); void revokeBuildUserAccess(); - std::set getSubjectGroups(ACL::User user) override; + std::set getSubjectGroupsUncached(ACL::User user) override; std::optional getVersion() override; private: diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index 4ece3050321..03428725f61 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -962,7 +962,7 @@ RemoteStore::AccessStatus RemoteStore::getFutureAccessStatus(const StoreObject & return status; } -std::set RemoteStore::getSubjectGroups(ACL::User user) +std::set RemoteStore::getSubjectGroupsUncached(ACL::User user) { struct passwd * pw = getpwuid(user.uid); auto groups_vec = getUserGroups(pw->pw_uid); diff --git a/src/libstore/remote-store.hh b/src/libstore/remote-store.hh index 2b90dd7308a..f0aa17a5690 100644 --- a/src/libstore/remote-store.hh +++ b/src/libstore/remote-store.hh @@ -175,7 +175,7 @@ public: AccessStatus getCurrentAccessStatus(const StoreObject & storeObject) override; AccessStatus getFutureAccessStatus(const StoreObject & storeObject) override; - std::set getSubjectGroups(ACL::User user) override; + std::set getSubjectGroupsUncached(ACL::User user) override; protected: diff --git a/tests/nixos/acls.nix b/tests/nixos/acls.nix index e912ad3cc39..3a780e43130 100644 --- a/tests/nixos/acls.nix +++ b/tests/nixos/acls.nix @@ -393,21 +393,6 @@ let print(machine.succeed(f"""sudo -u test cat {public_output}""")) print(machine.succeed(f"""getfacl {public_output}""")) print(machine.succeed(f"""getfacl {private_output}""")) - - # Once it's already built test is able to run the build command - machine.succeed(""" - sudo -u test nix-build ${depend-on-private} --no-out-link - """) - - # But it is still unable to read the output. - machine.fail(f"""sudo -u test cat {private_output}""") - - print(machine.succeed(f"""sudo -u test cat {public_output}""")) - - # Can test depend on it in a derivation ? - machine.succeed(""" - sudo -u test nix-build ${depend-on-public} --no-out-link - """) ''; # Non trusted user gives permission to another one. From 3994ce1bd6399b6d64106afe14b1e52345a1f839 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Wed, 13 Dec 2023 13:19:04 +0400 Subject: [PATCH 27/56] Prevent segfault --- src/libstore/nar-info.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc index 2011d722736..fb85ac9640b 100644 --- a/src/libstore/nar-info.cc +++ b/src/libstore/nar-info.cc @@ -101,11 +101,11 @@ NarInfo::NarInfo(const Store & store, const std::string & s, const std::string & } else if (name == "AllowedUser") { if (!accessStatus) accessStatus = ValidPathInfo::AccessStatus(); - accessStatus->entities.insert(ACL::User{getpwnam(value.c_str())->pw_uid}); + accessStatus->entities.insert(ACL::User(value.c_str())); } else if (name == "AllowedGroup") { if (!accessStatus) accessStatus = ValidPathInfo::AccessStatus(); - accessStatus->entities.insert(ACL::Group{getgrnam(value.c_str())->gr_gid}); + accessStatus->entities.insert(ACL::Group(value.c_str())); } } From 3a4914dbffb6909cbbe48a2b3bad6509f8781c58 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Wed, 13 Dec 2023 19:00:09 +0400 Subject: [PATCH 28/56] Fix darwin build --- src/libstore/build/local-derivation-goal.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 3dcedadb6a7..b135a5d9ddb 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -426,6 +426,7 @@ void LocalDerivationGoal::cleanupPostOutputsRegisteredModeNonCheck() cleanupPostOutputsRegisteredModeCheck(); } +#if __linux__ static void doBind(const Path & source, const Path & target, Store & store, bool optional = false) { auto doMount = [&](const Path & source, const Path & target) { debug("bind mounting '%1%' to '%2%'", source, target); @@ -474,6 +475,7 @@ static void doBind(const Path & source, const Path & target, Store & store, bool } doMount(source, target); }; +#endif void LocalDerivationGoal::startBuilder() { From eff385d19cbe39db13b370762d30a01257c1b00d Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Thu, 14 Dec 2023 12:46:33 +0100 Subject: [PATCH 29/56] Fix perl/default.nix --- perl/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perl/default.nix b/perl/default.nix index 0fa57f7815e..38fc71a78d8 100644 --- a/perl/default.nix +++ b/perl/default.nix @@ -2,7 +2,7 @@ , stdenv , perl, perlPackages , autoconf-archive, autoreconfHook, pkg-config -, nix, curl, bzip2, xz, boost, libsodium, darwin +, nix, curl, bzip2, xz, boost, libsodium, darwin, acl }: perl.pkgs.toPerlModule (stdenv.mkDerivation { From 4b669412c5046692ed461166517ecd02b42e064a Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Thu, 14 Dec 2023 12:52:19 +0100 Subject: [PATCH 30/56] Acls: builtins.path set accessStatus --- src/libexpr/primops.cc | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 6354ea6c227..2a3f56e8b91 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -37,6 +37,7 @@ #include #include +#include namespace nix { @@ -2330,16 +2331,36 @@ static void addPath( .references = {}, }); - // Commented out because computeStorePathForPath needs reading access to the path. - // But this should not be needed if the path is already in the store and is fixed output derivation. - // For the case where the file is private but a public derivation depends on it. - - // TODO: why is this needed ? - // if (accessStatus && !settings.readOnlyMode) { - // StorePath dstPath = state.store->computeStorePathForPath(name, path, method, htSHA256, filter).first; - // ensureAccess(&*accessStatus, state.store->printStorePath(dstPath)); - // require(*state.store).setFutureAccessStatus(dstPath, *accessStatus); - // } + if (accessStatus && !settings.readOnlyMode) { + if (expectedStorePath) { + if (pathExists(state.store->toRealPath(*expectedStorePath))) { + // auto curStatus = require(*state.store).getCurrentAccessStatus(*expectedStorePath); + // We want current here, but there should be nothing in future. + // TODO: read current back just for this. + auto curStatus = require(*state.store).getFutureAccessStatus(*expectedStorePath); + if (curStatus != *accessStatus && !require(*state.store).canAccess(*expectedStorePath, false)) { + // It's ok to update the permission of a store path if we have read access to the original file. + std::ifstream path_file(path.path.abs()); + if (!path_file) { + throw Error(fmt("Could not access file (%s) permissions may be missing", path)); + } + path_file.close(); + } + } + require(*state.store).setAccessStatus(*expectedStorePath, *accessStatus); + } else { + // computeStorePathForPath should fail if we do not have access to the original path + //StorePath dstPath = state.store->computeStorePathForPath(name, path, method, htSHA256, filter) .first; + auto source = sinkToSource([&](Sink & sink) { + if (method == FileIngestionMethod::Recursive) + dumpPath(path.path.abs(), sink, defaultPathFilter); + else + readFile(path.path.abs(), sink); + }); + StorePath dstPath = state.store->computeStorePathFromDump(*source, name, method, HashAlgorithm::SHA256).first; + require(*state.store).setAccessStatus(dstPath, *accessStatus); + } + } if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) { auto dstPath = path.fetchToStore(state.store, name, method, filter.get(), state.repair); From 985fe934da325def042bfa66cd3b4aa5ee7771d3 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Thu, 14 Dec 2023 13:57:02 +0100 Subject: [PATCH 31/56] Acls: remove PathStatus::ShouldSync --- src/libstore/build/derivation-goal.cc | 15 ++------------- src/libstore/build/derivation-goal.hh | 2 -- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index b000d5998ee..962a7b626fe 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -745,10 +745,6 @@ void DerivationGoal::tryToBuild() } else if (status.known->status == PathStatus::Inaccessible) { logger->cout("don't have access to path %s; checking outputs", worker.store.printStorePath(status.known->path)); buildMode = bmCheck; - } else if (status.known->status == PathStatus::ShouldSync) { - logger->cout("permissions should be synced for path %s; repairing", - worker.store.printStorePath(status.known->path)); - buildMode = bmRepair; } } } @@ -1480,15 +1476,12 @@ std::pair DerivationGoal::checkPathValidity() if (i.second) { auto outputPath = *i.second; bool canAccess = true; - bool shouldSyncPermissions = false; bool isValid = worker.store.isValidPath(outputPath); if (experimentalFeatureSettings.isEnabled(Xp::ACLs) && isValid) // We only need to look at permissions if the path is valid. // So we can assume that the path exists here. if (auto aclStore = dynamic_cast(&worker.store)){ - // Todo: to cast to LocalGranularAccessStore instead of LocalStore we need to implement shouldSyncPermissions for the remote store. canAccess = aclStore->canAccess(outputPath, false); - shouldSyncPermissions = aclStore->shouldSyncPermissions(outputPath); } info.known = { .path = outputPath, @@ -1496,8 +1489,6 @@ std::pair DerivationGoal::checkPathValidity() ? PathStatus::Absent : checkHash && !worker.pathContentsGood(outputPath) ? PathStatus::Corrupt - : shouldSyncPermissions - ? PathStatus::ShouldSync : !canAccess ? PathStatus::Inaccessible : PathStatus::Valid, @@ -1507,17 +1498,15 @@ std::pair DerivationGoal::checkPathValidity() if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { if (auto real = worker.store.queryRealisation(drvOutput)) { bool canAccess = true; - bool shouldSyncPermissions = false; if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) if (auto aclStore = dynamic_cast(&worker.store)){ // Todo: to cast to LocalGranularAccessStore instead of LocalStore we need to implement shouldSyncPermissions for the remote store. - // Todo: do we need to check for the path existence here before calling shouldSyncPermissions ? + // Todo: do we need to check for the path existence here ? canAccess = aclStore->canAccess(real->outPath, false); - shouldSyncPermissions = aclStore->shouldSyncPermissions(real->outPath); } info.known = { .path = real->outPath, - .status = shouldSyncPermissions ? PathStatus::ShouldSync : !canAccess ? PathStatus::Inaccessible : PathStatus::Valid, + .status = !canAccess ? PathStatus::Inaccessible : PathStatus::Valid, }; } else if (info.known && info.known->isValid()) { // We know the output because it's a static output of the diff --git a/src/libstore/build/derivation-goal.hh b/src/libstore/build/derivation-goal.hh index 46ed8691d17..a80c33db34d 100644 --- a/src/libstore/build/derivation-goal.hh +++ b/src/libstore/build/derivation-goal.hh @@ -25,7 +25,6 @@ enum struct PathStatus { Absent, Valid, Inaccessible, - ShouldSync, }; struct InitialOutputStatus { @@ -43,7 +42,6 @@ struct InitialOutputStatus { bool isPresent() const { return status == PathStatus::Corrupt || status == PathStatus::Inaccessible - || status == PathStatus::ShouldSync || status == PathStatus::Valid; } }; From 0b92adf77437c7681fd6cb9fae55827b6ecff866 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Thu, 14 Dec 2023 17:11:40 +0100 Subject: [PATCH 32/56] Acls: AccessStatus setter/getter If a path already exists, set permissions right away instead of writing them to the future permissions map and synchronize latter. --- src/libcmd/installables.cc | 4 +- src/libexpr/primops.cc | 13 ++-- src/libstore/build/local-derivation-goal.cc | 6 +- src/libstore/daemon.cc | 80 ++------------------- src/libstore/granular-access-store.hh | 30 ++++---- src/libstore/local-store.cc | 56 +++++++++------ src/libstore/local-store.hh | 8 +-- src/libstore/remote-store.cc | 26 ++----- src/libstore/remote-store.hh | 6 +- src/libstore/worker-protocol.hh | 6 +- src/nix/store-access-grant.cc | 4 +- src/nix/store-access-info.cc | 2 +- src/nix/store-access-protect.cc | 4 +- src/nix/store-access-revoke.cc | 4 +- src/nix/store-access-unprotect.cc | 4 +- 15 files changed, 85 insertions(+), 168 deletions(-) diff --git a/src/libcmd/installables.cc b/src/libcmd/installables.cc index 9c36cf7eb45..eb3144da610 100644 --- a/src/libcmd/installables.cc +++ b/src/libcmd/installables.cc @@ -625,10 +625,10 @@ std::vector, BuiltPathWithResult>> Installable::build LocalStore::AccessStatus status {true, {ACL::User(getuid())}}; std::visit(overloaded { [&](DerivedPath::Opaque p){ - require(*store).setFutureAccessStatus(p.path, status); + require(*store).setAccessStatus(p.path, status); }, [&](DerivedPath::Built b){ - require(*store).setFutureAccessStatus(b, status); + require(*store).setAccessStatus(b, status); } }, b.path); } diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 2a3f56e8b91..f3f89921f28 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1456,7 +1456,7 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN LocalGranularAccessStore::AccessStatus status; readAccessStatus(state, *derivation, &status, "__permissions.drv", "builtins.derivationStrict"); ensureAccess(&status, state.store->printStorePath(drvPath), require(*state.store)); - require(*state.store).setFutureAccessStatus(drvPath, status); + require(*state.store).setAccessStatus(drvPath, status); } } } @@ -1484,7 +1484,7 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN LocalGranularAccessStore::AccessStatus status; readAccessStatus(state, output, &status, fmt("__permissions.outputs.%s", state.symbols[output.name]), "builtins.derivationStrict"); ensureAccess(&status, fmt("output %s of derivation %s", state.symbols[output.name], drvPathS), require(*state.store)); - require(*state.store).setFutureAccessStatus(StoreObjectDerivationOutput {drvPath, std::string(state.symbols[{output.name}])}, status); + require(*state.store).setAccessStatus(StoreObjectDerivationOutput {drvPath, std::string(state.symbols[{output.name}])}, status); } } auto log = attr->value->attrs->find(state.sLog); @@ -1492,11 +1492,11 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN LocalGranularAccessStore::AccessStatus status; readAccessStatus(state, *log, &status, "__permissions.log", "builtins.derivationStrict"); ensureAccess(&status, fmt("log of derivation %s", drvPathS), require(*state.store)); - require(*state.store).setFutureAccessStatus(StoreObjectDerivationLog {drvPath}, status); + require(*state.store).setAccessStatus(StoreObjectDerivationLog {drvPath}, status); } } } - + printMsg(lvlChatty, "instantiated '%1%' -> '%2%'", drvName, drvPathS); /* Optimisation, but required in read-only mode! because in that @@ -2334,10 +2334,7 @@ static void addPath( if (accessStatus && !settings.readOnlyMode) { if (expectedStorePath) { if (pathExists(state.store->toRealPath(*expectedStorePath))) { - // auto curStatus = require(*state.store).getCurrentAccessStatus(*expectedStorePath); - // We want current here, but there should be nothing in future. - // TODO: read current back just for this. - auto curStatus = require(*state.store).getFutureAccessStatus(*expectedStorePath); + auto curStatus = require(*state.store).getAccessStatus(*expectedStorePath); if (curStatus != *accessStatus && !require(*state.store).canAccess(*expectedStorePath, false)) { // It's ok to update the permission of a store path if we have read access to the original file. std::ifstream path_file(path.path.abs()); diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 5d934096cd7..7b25e1c2e43 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -247,8 +247,8 @@ void LocalDerivationGoal::tryLocalBuild() if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) if (auto localStore = dynamic_cast(&worker.store)) { for (auto path : inputPaths) { - if (localStore->getCurrentAccessStatus(path).isProtected) { - if (!localStore->canAccess(path, false)) + if (localStore->getAccessStatus(path).isProtected) { + if (!localStore->canAccess(path, true)) throw AccessDenied( "%s (uid %d) does not have access to path %s", getUserName(localStore->effectiveUser->uid), @@ -2451,7 +2451,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() StoreObjectDerivationOutput thisOutput(drvPath, outputName); if (localStore.futurePermissions.contains(thisOutput)) { - localStore.setFutureAccessStatus(finalStorePath, localStore.futurePermissions[thisOutput]); + localStore.setAccessStatus(finalStorePath, localStore.futurePermissions[thisOutput]); } /* Store the final path */ finalOutputs.insert_or_assign(outputName, finalStorePath); diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index cb6b61f5735..5504939ebd4 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -1007,95 +1007,29 @@ static void performOp(TunnelLogger * logger, ref store, break; } - case WorkerProto::Op::GetCurrentAccessStatus: { + case WorkerProto::Op::GetAccessStatus: { auto object = WorkerProto::Serialise::read(*store, rconn); logger->startWork(); - auto status = require(*store).getCurrentAccessStatus(object); + auto status = require(*store).getAccessStatus(object); logger->stopWork(); WorkerProto::Serialise::write(*store, wconn, status); break; } - case WorkerProto::Op::GetFutureAccessStatus: { - auto object = WorkerProto::Serialise::read(*store, rconn); - logger->startWork(); - auto status = require(*store).getFutureAccessStatus(object); - logger->stopWork(); - WorkerProto::Serialise::write(*store, wconn, status); - break; - } - - case WorkerProto::Op::SetCurrentAccessStatus: { - auto object = WorkerProto::Serialise::read(*store, rconn); - auto status = WorkerProto::Serialise::read(*store, rconn); - logger->startWork(); - auto localStore = dynamic_cast(&*store); - auto curStatus = require(*store).getCurrentAccessStatus(object); - if (status != curStatus) { - if (user.trusted) { - localStore->setCurrentAccessStatus(object, status); - } else { - // TODO document rationale behind this logic - auto [exists, description] = std::visit(overloaded { - [&](StorePath p) { - auto rp = store->toRealPath(p); - return std::pair{pathExists(rp), fmt("path %s", rp)}; - }, - [&](StoreObjectDerivationOutput b) { - auto drv = localStore->readDerivation(b.drvPath); - auto outputHashes = staticOutputHashes(*localStore, drv); - auto drvOutputs = drv.outputsAndOptPaths(*localStore); - bool known = drvOutputs.contains(b.output) && drvOutputs.at(b.output).second; - if (known) { - auto realPath = store->toRealPath(*drvOutputs.at(b.output).second); - bool exists = pathExists(realPath); - return std::pair{exists, fmt("path %s", realPath)}; - } else { - return std::pair{false, fmt("output %s of derivation %s", b.output, store->toRealPath(b.drvPath))}; - } - }, - [&](StoreObjectDerivationLog l) { - auto baseName = l.drvPath.to_string(); - - auto logPath = fmt("%s/%s/%s/%s.bz2", localStore->logDir, localStore->drvsLogDir, baseName.substr(0, 2), baseName.substr(2)); - - return std::pair{pathExists(logPath), fmt("build log of derivation %s", store->toRealPath(l.drvPath))}; - } - }, object); - if (exists && status.isProtected != curStatus.isProtected) - throw AccessDenied("You have to be a trusted user to set a protection status on an existing %s", description); - if (! status.isProtected) - throw AccessDenied("Only trusted users can set allowed entities on an unprotected %s", description); - if (exists && ! std::includes(status.entities.begin(), status.entities.end(), curStatus.entities.begin(), curStatus.entities.end())){ - throw AccessDenied("Only trusted users can revoke permissions on %s", description); - } - if (localStore->canAccess(object, user.uid, false)){ - localStore->setCurrentAccessStatus(object, status); - } else { - throw AccessDenied(fmt("setCurrentAccessStatus: User %s does not have permission on path %s", user.uid, description)); - } - } - } - logger->stopWork(); - to << 1; - break; - } - - // TODO deduplicate SetFutureAccessStatus and SetCurrentAccessStatus - case WorkerProto::Op::SetFutureAccessStatus: { + case WorkerProto::Op::SetAccessStatus: { auto object = WorkerProto::Serialise::read(*store, rconn); auto status = WorkerProto::Serialise::read(*store, rconn); logger->startWork(); auto localStore = dynamic_cast(&*store); // Could there be a race condition here ? If the path is added by a concurrent build, after we checked its existence. if (!localStore->pathOfStoreObjectExists(object)){ - localStore->setFutureAccessStatus(object, status); + localStore->setAccessStatus(object, status); } else { - auto curStatus = require(*store).getCurrentAccessStatus(object); + auto curStatus = require(*store).getAccessStatus(object); if (status != curStatus) { if (user.trusted) { - localStore->setFutureAccessStatus(object, status); + localStore->setAccessStatus(object, status); } else { // TODO document rationale behind this logic auto [exists, description] = std::visit( @@ -1154,7 +1088,7 @@ static void performOp(TunnelLogger * logger, ref store, "Only trusted users can revoke permissions on %s", description); } - localStore->setFutureAccessStatus(object, status); + localStore->setAccessStatus(object, status); } } } diff --git a/src/libstore/granular-access-store.hh b/src/libstore/granular-access-store.hh index 6cbb8fdb945..71fee52ea89 100644 --- a/src/libstore/granular-access-store.hh +++ b/src/libstore/granular-access-store.hh @@ -54,10 +54,8 @@ struct GranularAccessStore : public virtual Store typedef AccessStatusFor AccessStatus; - virtual void setFutureAccessStatus(const StoreObject & storeObject, const AccessStatus & status) = 0; - virtual void setCurrentAccessStatus(const StoreObject & path, const AccessStatus & status) = 0; - virtual AccessStatus getFutureAccessStatus(const StoreObject & storeObject) = 0; - virtual AccessStatus getCurrentAccessStatus(const StoreObject & storeObject) = 0; + virtual void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status) = 0; + virtual AccessStatus getAccessStatus(const StoreObject & storeObject) = 0; virtual std::set getSubjectGroupsUncached(AccessControlSubject subject) = 0; @@ -78,10 +76,10 @@ struct GranularAccessStore : public virtual Store if (! experimentalFeatureSettings.isEnabled(Xp::ACLs) || trusted) return true; AccessStatus status; if (use_future) { - status = getFutureAccessStatus(storeObject); + status = getAccessStatus(storeObject); } else { - status = getCurrentAccessStatus(storeObject); + status = getAccessStatus(storeObject); } if (! status.isProtected) return true; for (auto ent : status.entities) { @@ -115,34 +113,34 @@ struct GranularAccessStore : public virtual Store } else { if (use_future) { - return !getFutureAccessStatus(storeObject).isProtected; + return !getAccessStatus(storeObject).isProtected; } else { - return !getCurrentAccessStatus(storeObject).isProtected; + return !getAccessStatus(storeObject).isProtected; } } } void addAllowedEntitiesFuture(const StoreObject & storeObject, const std::set & entities) { - auto status = getFutureAccessStatus(storeObject); + auto status = getAccessStatus(storeObject); for (auto entity : entities) status.entities.insert(entity); - setFutureAccessStatus(storeObject, status); + setAccessStatus(storeObject, status); } void addAllowedEntitiesCurrent(const StoreObject & storeObject, const std::set & entities) { - auto status = getCurrentAccessStatus(storeObject); + auto status = getAccessStatus(storeObject); for (auto entity : entities) status.entities.insert(entity); - setCurrentAccessStatus(storeObject, status); + setAccessStatus(storeObject, status); } void removeAllowedEntitiesFuture(const StoreObject & storeObject, const std::set & entities) { - auto status = getFutureAccessStatus(storeObject); + auto status = getAccessStatus(storeObject); for (auto entity : entities) status.entities.erase(entity); - setFutureAccessStatus(storeObject, status); + setAccessStatus(storeObject, status); } void removeAllowedEntitiesCurrent(const StoreObject & storeObject, const std::set & entities) { - auto status = getCurrentAccessStatus(storeObject); + auto status = getAccessStatus(storeObject); for (auto entity : entities) status.entities.erase(entity); - setCurrentAccessStatus(storeObject, status); + setAccessStatus(storeObject, status); } private: diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 08e6eab9b51..95cdaf8511d 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1160,24 +1160,29 @@ void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::Acc acl.set(path); } -void LocalStore::setFutureAccessStatus(const StoreObject & storePathstoreObject, const AccessStatus & status) +void LocalStore::setAccessStatus(const StoreObject & storePathstoreObject, const AccessStatus & status) { - // If adding future permissions to a StoreObjectDerivationOutput, - // also add permissions to the paths that will exist in the future. - std::visit(overloaded { - [&](StorePath p) {}, - [&](StoreObjectDerivationOutput p) { - auto drv = readDerivation(p.drvPath); - auto outputHashes = staticOutputHashes(*this, drv); - auto drvOutputs = drv.outputsAndOptPaths(*this); - if (drvOutputs.contains(p.output) && drvOutputs.at(p.output).second) { - auto path = *drvOutputs.at(p.output).second; - futurePermissions[path] = status; - } - }, - [&](StoreObjectDerivationLog l){} - }, storePathstoreObject); - futurePermissions[storePathstoreObject] = status; + if (pathOfStoreObjectExists(storePathstoreObject)){ + setCurrentAccessStatus(storePathstoreObject, status); + } + else { + // If adding future permissions to a StoreObjectDerivationOutput, + // also add permissions to the paths that will exist in the future. + std::visit(overloaded { + [&](StorePath p) {}, + [&](StoreObjectDerivationOutput p) { + auto drv = readDerivation(p.drvPath); + auto outputHashes = staticOutputHashes(*this, drv); + auto drvOutputs = drv.outputsAndOptPaths(*this); + if (drvOutputs.contains(p.output) && drvOutputs.at(p.output).second) { + auto path = *drvOutputs.at(p.output).second; + futurePermissions[path] = status; + } + }, + [&](StoreObjectDerivationLog l){} + }, storePathstoreObject); + futurePermissions[storePathstoreObject] = status; + } } void LocalStore::setCurrentAccessStatus(const StoreObject & storePathstoreObject, const AccessStatus & status) @@ -1222,10 +1227,10 @@ void LocalStore::setCurrentAccessStatus(const StoreObject & storePathstoreObject auto logPath = fmt("%s/%s/%s/%s.bz2", logDir, drvsLogDir, baseName.substr(0, 2), baseName.substr(2)); - if (pathExists(logPath)) { - setCurrentAccessStatus(logPath, status); + if (!pathExists(logPath)){ + throw Error("setCurrentAccessStatus log path does not exists (%s)", logPath); } - throw Error("setCurrentAccessStatus log path does not exists (%s)", logPath); + setCurrentAccessStatus(logPath, status); } }, storePathstoreObject); } @@ -1293,7 +1298,7 @@ LocalStore::AccessStatus LocalStore::getCurrentAccessStatus(const StoreObject & void LocalStore::grantBuildUserAccess(const StorePath & storePath, const LocalStore::AccessControlEntity & buildUser) { // The builder-permissions directory remembers permissions to remove at the end of the build. - auto status = getCurrentAccessStatus(storePath); + auto status = getAccessStatus(storePath); if (! status.entities.contains(buildUser)){ auto basePath = stateDir + "/acls/builder-permissions/" + storePath.to_string(); std::visit(overloaded { @@ -1305,9 +1310,14 @@ void LocalStore::grantBuildUserAccess(const StorePath & storePath, const LocalSt } -LocalStore::AccessStatus LocalStore::getFutureAccessStatus(const StoreObject & storeObject) +LocalStore::AccessStatus LocalStore::getAccessStatus(const StoreObject & storeObject) { - return futurePermissions.at(storeObject); + if (futurePermissions.contains(storeObject)){ + return futurePermissions[storeObject]; + } + else { + return getCurrentAccessStatus(storeObject); + } } std::optional LocalStore::getFutureAccessStatusOpt(const StoreObject & storeObject) diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh index 9fd6a96b8c0..2e71ce0eccd 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -301,13 +301,13 @@ public: void queryRealisationUncached(const DrvOutput&, Callback> callback) noexcept override; - void setFutureAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; - void setCurrentAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; + void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; + void setCurrentAccessStatus(const StoreObject & storeObject, const AccessStatus & status); void setCurrentAccessStatus(const Path & path, const AccessStatus & status); - AccessStatus getFutureAccessStatus(const StoreObject & storeObject) override; + AccessStatus getAccessStatus(const StoreObject & storeObject) override; std::optional getFutureAccessStatusOpt(const StoreObject & storeObject); AccessStatus getCurrentAccessStatus(const Path & path); - AccessStatus getCurrentAccessStatus(const StoreObject & storeObject) override; + AccessStatus getCurrentAccessStatus(const StoreObject & storeObject); bool shouldSyncPermissions(const StoreObject &storeObject); bool pathOfStoreObjectExists(const StoreObject & storeObject); diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index e58fb01e73e..d904dc14bf8 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -924,38 +924,20 @@ void RemoteStore::addBuildLog(const StorePath & drvPath, std::string_view log) readInt(conn->from); } -void RemoteStore::setCurrentAccessStatus(const StoreObject & storeObject, const RemoteStore::AccessStatus & status) +void RemoteStore::setAccessStatus(const StoreObject & storeObject, const RemoteStore::AccessStatus & status) { auto conn(getConnection()); - conn->to << WorkerProto::Op::SetCurrentAccessStatus; - WorkerProto::Serialise::write(*this, *conn, storeObject); - WorkerProto::Serialise::write(*this, *conn, status); - conn.processStderr(); - readInt(conn->from); -} -void RemoteStore::setFutureAccessStatus(const StoreObject & storeObject, const RemoteStore::AccessStatus & status) -{ - auto conn(getConnection()); - conn->to << WorkerProto::Op::SetFutureAccessStatus; + conn->to << WorkerProto::Op::SetAccessStatus; WorkerProto::Serialise::write(*this, *conn, storeObject); WorkerProto::Serialise::write(*this, *conn, status); conn.processStderr(); readInt(conn->from); } -RemoteStore::AccessStatus RemoteStore::getCurrentAccessStatus(const StoreObject & storeObject) -{ - auto conn(getConnection()); - conn->to << WorkerProto::Op::GetCurrentAccessStatus; - WorkerProto::Serialise::write(*this, *conn, storeObject); - conn.processStderr(); - auto status = WorkerProto::Serialise::read(*this, *conn); - return status; -} -RemoteStore::AccessStatus RemoteStore::getFutureAccessStatus(const StoreObject & storeObject) +RemoteStore::AccessStatus RemoteStore::getAccessStatus(const StoreObject & storeObject) { auto conn(getConnection()); - conn->to << WorkerProto::Op::GetFutureAccessStatus; + conn->to << WorkerProto::Op::GetAccessStatus; WorkerProto::Serialise::write(*this, *conn, storeObject); conn.processStderr(); auto status = WorkerProto::Serialise::read(*this, *conn); diff --git a/src/libstore/remote-store.hh b/src/libstore/remote-store.hh index 3ab517e7b86..76038c2a74e 100644 --- a/src/libstore/remote-store.hh +++ b/src/libstore/remote-store.hh @@ -170,10 +170,8 @@ public: ref openConnectionWrapper(); - void setCurrentAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; - void setFutureAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; - AccessStatus getCurrentAccessStatus(const StoreObject & storeObject) override; - AccessStatus getFutureAccessStatus(const StoreObject & storeObject) override; + void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; + AccessStatus getAccessStatus(const StoreObject & storeObject) override; std::set getSubjectGroupsUncached(ACL::User user) override; diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh index 3bfc24d4c7d..35c5fec21cb 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -170,10 +170,8 @@ enum struct WorkerProto::Op : uint64_t AddBuildLog = 45, BuildPathsWithResults = 46, AddPermRoot = 47, - GetCurrentAccessStatus = 48, - GetFutureAccessStatus = 49, - SetCurrentAccessStatus = 50, - SetFutureAccessStatus = 51, + GetAccessStatus = 48, + SetAccessStatus = 49, }; /** diff --git a/src/nix/store-access-grant.cc b/src/nix/store-access-grant.cc index 3f9436cf4b0..0ea1f514914 100644 --- a/src/nix/store-access-grant.cc +++ b/src/nix/store-access-grant.cc @@ -50,13 +50,13 @@ struct CmdStoreAccessGrant : StorePathsCommand } else { auto & localStore = require(*store); for (auto & path : storePaths) { - auto status = localStore.getCurrentAccessStatus(path); + auto status = localStore.getAccessStatus(path); if (!status.isProtected) warn("Path '%s' is not protected; all users can access it regardless of permissions", store->printStorePath(path)); if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); for (auto user : users) status.entities.insert(nix::ACL::User(user)); for (auto group : groups) status.entities.insert(nix::ACL::Group(group)); - localStore.setCurrentAccessStatus(path, status); + localStore.setAccessStatus(path, status); } } } diff --git a/src/nix/store-access-info.cc b/src/nix/store-access-info.cc index 1369553c452..f618b0e09c9 100644 --- a/src/nix/store-access-info.cc +++ b/src/nix/store-access-info.cc @@ -23,7 +23,7 @@ struct CmdStoreAccessInfo : StorePathCommand, MixJSON void run(ref store, const StorePath & path) override { auto & aclStore = require(*store); - auto status = aclStore.getCurrentAccessStatus(path); + auto status = aclStore.getAccessStatus(path); bool isValid = aclStore.isValidPath(path); std::set users; std::set groups; diff --git a/src/nix/store-access-protect.cc b/src/nix/store-access-protect.cc index 6a392973a95..4d938518741 100644 --- a/src/nix/store-access-protect.cc +++ b/src/nix/store-access-protect.cc @@ -24,12 +24,12 @@ struct CmdStoreAccessProtect : StorePathsCommand { auto & localStore = require(*store); for (auto & path : storePaths) { - auto status = localStore.getCurrentAccessStatus(path); + auto status = localStore.getAccessStatus(path); if (!status.entities.empty()) warn("There are some users or groups who have access to path %s; consider removing them with \n" ANSI_BOLD "nix store access revoke --all-entities %s" ANSI_NORMAL, localStore.printStorePath(path), localStore.printStorePath(path)); if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); status.isProtected = true; - localStore.setCurrentAccessStatus(path, status); + localStore.setAccessStatus(path, status); } } }; diff --git a/src/nix/store-access-revoke.cc b/src/nix/store-access-revoke.cc index a9ce636f090..5d77190d8be 100644 --- a/src/nix/store-access-revoke.cc +++ b/src/nix/store-access-revoke.cc @@ -59,7 +59,7 @@ struct CmdStoreAccessRevoke : StorePathsCommand } else { auto & localStore = require(*store); for (auto & path : storePaths) { - auto status = localStore.getCurrentAccessStatus(path); + auto status = localStore.getAccessStatus(path); if (!status.isProtected) warn("Path '%s' is not protected; all users can access it regardless of permissions", store->printStorePath(path)); if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); @@ -69,7 +69,7 @@ struct CmdStoreAccessRevoke : StorePathsCommand for (auto user : users) status.entities.erase(nix::ACL::User(user)); for (auto group : groups) status.entities.erase(nix::ACL::Group(group)); } - localStore.setCurrentAccessStatus(path, status); + localStore.setAccessStatus(path, status); } } } diff --git a/src/nix/store-access-unprotect.cc b/src/nix/store-access-unprotect.cc index 4c05afa952d..6b7af6e6f2f 100644 --- a/src/nix/store-access-unprotect.cc +++ b/src/nix/store-access-unprotect.cc @@ -24,12 +24,12 @@ struct CmdStoreAccessUnprotect : StorePathsCommand { auto & localStore = require(*store); for (auto & path : storePaths) { - auto status = localStore.getCurrentAccessStatus(path); + auto status = localStore.getAccessStatus(path); if (!status.entities.empty()) warn("There are still some users or groups who have access to path %s; consider removing them with \n" ANSI_BOLD "nix store access revoke --all-entities %s" ANSI_NORMAL, localStore.printStorePath(path), localStore.printStorePath(path)); if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); status.isProtected = false; - localStore.setCurrentAccessStatus(path, status); + localStore.setAccessStatus(path, status); } } }; From 834219a57abf28e13b71a17bb79d35d0c5560355 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Thu, 14 Dec 2023 15:35:53 +0100 Subject: [PATCH 33/56] Acls tests: Assertions on failing tests output --- tests/nixos/acls.nix | 100 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 88 insertions(+), 12 deletions(-) diff --git a/tests/nixos/acls.nix b/tests/nixos/acls.nix index 3a780e43130..1d0283c3ffe 100644 --- a/tests/nixos/acls.nix +++ b/tests/nixos/acls.nix @@ -137,6 +137,10 @@ let def assert_info(path, expected, when): got = info(path) assert(got == expected),f"Path info {got} is not as expected {expected} for path {path} {when}" + + def assert_in_last_line(expected, output): + last_line = output.splitlines()[-1] + assert(expected in last_line),f"last line ({last_line}) does not contain string ({expected})" ''; testCli ='' @@ -189,9 +193,10 @@ let sudo -u test nix store add-file /tmp/unaccessible """) - machine.fail(""" - sudo -u test nix-build ${test-unaccessible} --no-out-link --debug + test_unaccessible_output = machine.fail(""" + sudo -u test nix-build ${test-unaccessible} --no-out-link --debug 2>&1 """) + assert_in_last_line("error: opening file '/tmp/unaccessible': Permission denied", test_unaccessible_output) ''; testFoo = '' @@ -239,9 +244,10 @@ let assert_info(examplePackagePathDiffPermissions, {"exists": True, "protected": True, "users": ["root", "test"], "groups": []}, "after nix-build as a different user") # Trying to revoke permissions fails as a non trusted user. - machine.fail(f""" - sudo -u test nix store access revoke --user test {examplePackagePath} + try_revoke_output = machine.fail(f""" + sudo -u test nix store access revoke --user test {examplePackagePath} 2>&1 """) + assert_in_last_line("Only trusted users can revoke permissions on path", try_revoke_output) exampleDependenciesPackagePath = machine.succeed(""" sudo -u test nix-build ${example-dependencies} --no-out-link --show-trace @@ -327,7 +333,7 @@ let ''; # Test adding a private runtime dependency, which should fail. - runtime-depend-on-private = builtins.toFile "depend_on_private.nix" '' + runtime-depend-on-private = builtins.toFile "runtime_depend_on_private.nix" '' with import {}; let private = import ${private-package}; in stdenvNoCC.mkDerivation { @@ -375,16 +381,20 @@ let machine.fail(f"""sudo -u test cat {private_output}""") - machine.fail(""" - sudo -u test nix-build ${depend-on-private} --no-out-link + depend_on_private_output = machine.fail(""" + sudo -u test nix-build ${depend-on-private} --no-out-link 2>&1 """) + assert_in_last_line("error: test (uid 1000) does not have access to path", depend_on_private_output) + machine.fail(f"""sudo -u test cat {private_output}""") - machine.fail(""" - sudo -u test nix-build ${runtime-depend-on-private} --no-out-link + runtime_depend_on_private_output = machine.fail(""" + sudo -u test nix-build ${runtime-depend-on-private} --no-out-link 2>&1 """) + assert_in_last_line("error: test (uid 1000) does not have access to path", runtime_depend_on_private_output) machine.fail(f"""sudo -u test cat {private_output}""") + # Root builds the derivation to give access to test public_output = machine.succeed(""" sudo nix-build ${depend-on-private} --no-out-link @@ -393,10 +403,19 @@ let print(machine.succeed(f"""sudo -u test cat {public_output}""")) print(machine.succeed(f"""getfacl {public_output}""")) print(machine.succeed(f"""getfacl {private_output}""")) + + machine.fail(f"""sudo -u test cat {private_output}""") + + machine.succeed(f"""sudo -u test cat {public_output}""") + + # Test can depend on the values that were made public, even if it these have private build time dependencies. + machine.succeed(""" + sudo -u test nix-build ${depend-on-public} --no-out-link + """) ''; # Non trusted user gives permission to another one. - test-user-private = builtins.toFile "private.nix" '' + test-user-private = builtins.toFile "test-user-private.nix" '' with import {}; stdenvNoCC.mkDerivation { name = "test-user-private"; @@ -408,7 +427,7 @@ let users = ["test"]; }; }; - buildCommand = "cat $privateSource > $out"; + buildCommand = "echo $privateSource > $out && echo Example >> $out"; allowSubstitutes = false; __permissions = { outputs.out = { protected = true; users = ["test" "test2"]; }; @@ -419,25 +438,82 @@ let } ''; + test-user-private-2 = builtins.toFile "test-user-private-2.nix" '' + with import {}; + stdenvNoCC.mkDerivation { + name = "test-user-private"; + privateSource = builtins.path { + path = /tmp/test_secret; + sha256 = "f90af0f74a205cadaad0f17854805cae15652ba2afd7992b73c4823765961533"; + permissions = { + protected = true; + users = ["test" "test2"]; + }; + }; + buildCommand = "echo $privateSource > $out && echo Example >> $out"; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["test" "test2" "test3"]; }; + drv = { protected = true; users = ["test" "test2"]; }; + log.protected = true; + log.users = ["test" "test2"]; + }; + } + ''; + # Non trusted user grants access to its private file testTestUserPrivate = '' # fmt: off machine.succeed("""sudo -u test bash -c 'echo secret_string > /tmp/test_secret'"""); machine.succeed("""sudo -u test chmod 700 /tmp/test_secret"""); - print(machine.succeed("""getfacl /tmp/test_secret""")); + + # User test2 cannot build the derivation itself + test_user_private_out = machine.fail(""" + sudo -u test2 nix-build ${test-user-private} --no-out-link 2>&1 + """) + + assert_in_last_line("opening file '/tmp/test_secret': Permission denied", test_user_private_out) + + # User test can do it to grant access to the outputs to test2 userPrivatePath = machine.succeed(""" sudo -u test nix-build ${test-user-private} --no-out-link """) + + machine.succeed(""" + sudo -u test2 nix-build ${test-user-private} --no-out-link + """) + assert_info(userPrivatePath, {"exists": True, "protected": True, "users": ["test", "test2"], "groups": []}, "after nix-build test-user-private") + machine.succeed(f""" sudo -u test2 cat {userPrivatePath} """) + + # Non trusted user cannot revoke permissions, even if it was the one who granted them. machine.fail(f""" sudo -u test nix store access revoke --user test2 {userPrivatePath} """) + + # Since test2 was given permissions, it can grant access to test3 machine.succeed(f""" sudo -u test2 nix store access grant --user test3 {userPrivatePath} """) + + # test2 cannot add itself to the permissions of /tmp/test_secret + add_permissions_output = machine.fail(""" + sudo -u test2 nix-build ${test-user-private-2} --no-out-link 2>&1 + """) + + assert_in_last_line("Could not access file (/tmp/test_secret) permissions may be missing", add_permissions_output) + + inputPath1 = machine.succeed(f""" + sudo -u test2 head -n 1 {userPrivatePath} + """) + + machine.fail(f""" + sudo -u test2 cat {inputPath1} + """) + ''; in From f9d3f55df68db56e71eca4d7dcc74266f7fbc64f Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Thu, 14 Dec 2023 15:40:48 +0100 Subject: [PATCH 34/56] Temporarily deactivate ensureAccess --- src/libexpr/primops.cc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index f3f89921f28..2c97069a683 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -174,6 +174,11 @@ void ensureAccess(LocalGranularAccessStore::AccessStatus * accessStatus, std::st }, entity)) return; } + // TODO: Reactivate ensureAccess. + // It should be possible to depend on the public outputs of a derivation that has private inputs. + // For now it is deactivated because in this case, I think the check can fail when is should not. + // Cf the depend-on-public test in acls.nix + return; throw AccessDenied("you (%s) would not have access to %s; ensure that you do by adding yourself or a group you're in to the list", getUserName(uid), description); } @@ -1476,7 +1481,7 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN "while evaluating the `__permissions.outputs` " "attribute passed to builtins.derivationStrict"); for (auto & output : *outputs->value->attrs) { - if (!drv.outputs.contains(state.symbols[output.name])) + if (!drv.outputs.contains(state.symbols[output.name])) state.debugThrowLastTrace(EvalError({ .msg = hintfmt("derivation has no output %s", state.symbols[output.name]), .errPos = state.positions[output.pos] From 96cb11503c6d7a2a862ca5440d0fb9e011bc252c Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Thu, 14 Dec 2023 16:39:36 +0100 Subject: [PATCH 35/56] Acls: remove canAccess `use_future` argument --- src/libexpr/primops.cc | 2 +- src/libstore/build/derivation-goal.cc | 4 ++-- src/libstore/build/local-derivation-goal.cc | 4 ++-- src/libstore/daemon.cc | 2 +- src/libstore/granular-access-store.hh | 24 ++++++--------------- src/libstore/local-store.cc | 8 +++---- 6 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 2c97069a683..fea1e5a68b6 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -2340,7 +2340,7 @@ static void addPath( if (expectedStorePath) { if (pathExists(state.store->toRealPath(*expectedStorePath))) { auto curStatus = require(*state.store).getAccessStatus(*expectedStorePath); - if (curStatus != *accessStatus && !require(*state.store).canAccess(*expectedStorePath, false)) { + if (curStatus != *accessStatus && !require(*state.store).canAccess(*expectedStorePath)) { // It's ok to update the permission of a store path if we have read access to the original file. std::ifstream path_file(path.path.abs()); if (!path_file) { diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 962a7b626fe..963a0f5a757 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -1481,7 +1481,7 @@ std::pair DerivationGoal::checkPathValidity() // We only need to look at permissions if the path is valid. // So we can assume that the path exists here. if (auto aclStore = dynamic_cast(&worker.store)){ - canAccess = aclStore->canAccess(outputPath, false); + canAccess = aclStore->canAccess(outputPath); } info.known = { .path = outputPath, @@ -1502,7 +1502,7 @@ std::pair DerivationGoal::checkPathValidity() if (auto aclStore = dynamic_cast(&worker.store)){ // Todo: to cast to LocalGranularAccessStore instead of LocalStore we need to implement shouldSyncPermissions for the remote store. // Todo: do we need to check for the path existence here ? - canAccess = aclStore->canAccess(real->outPath, false); + canAccess = aclStore->canAccess(real->outPath); } info.known = { .path = real->outPath, diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 7b25e1c2e43..1f6d7d6e32e 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -248,7 +248,7 @@ void LocalDerivationGoal::tryLocalBuild() if (auto localStore = dynamic_cast(&worker.store)) { for (auto path : inputPaths) { if (localStore->getAccessStatus(path).isProtected) { - if (!localStore->canAccess(path, true)) + if (!localStore->canAccess(path)) throw AccessDenied( "%s (uid %d) does not have access to path %s", getUserName(localStore->effectiveUser->uid), @@ -871,7 +871,7 @@ void LocalDerivationGoal::startBuilder() /* Run the builder. */ printMsg(lvlChatty, "executing builder '%1%'", drv->builder); printMsg(lvlChatty, "using builder args '%1%'", concatStringsSep(" ", drv->args)); - + for (auto & i : drv->env) printMsg(lvlVomit, "setting builder env variable '%1%'='%2%'", i.first, i.second); diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 5504939ebd4..78b9c84b5c3 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -507,7 +507,7 @@ static void performOp(TunnelLogger * logger, ref store, case WorkerProto::Op::ExportPath: { auto path = store->parseStorePath(readString(from)); - if (!require(*store).canAccess(path, false)) throw AccessDenied("Access Denied"); + if (!require(*store).canAccess(path)) throw AccessDenied("Access Denied"); readInt(from); // obsolete logger->startWork(); TunnelSink sink(to); diff --git a/src/libstore/granular-access-store.hh b/src/libstore/granular-access-store.hh index 71fee52ea89..c2a4884ace9 100644 --- a/src/libstore/granular-access-store.hh +++ b/src/libstore/granular-access-store.hh @@ -71,16 +71,10 @@ struct GranularAccessStore : public virtual Store /** * Whether any of the given @entities@ can access the path */ - bool canAccess(const StoreObject & storeObject, const std::set & entities, bool use_future) + bool canAccess(const StoreObject & storeObject, const std::set & entities) { if (! experimentalFeatureSettings.isEnabled(Xp::ACLs) || trusted) return true; - AccessStatus status; - if (use_future) { - status = getAccessStatus(storeObject); - } - else { - status = getAccessStatus(storeObject); - } + AccessStatus status = getAccessStatus(storeObject); if (! status.isProtected) return true; for (auto ent : status.entities) { if (entities.contains(ent)) { @@ -92,7 +86,7 @@ struct GranularAccessStore : public virtual Store /** * Whether a subject can access the store path */ - bool canAccess(const StoreObject & storeObject, AccessControlSubject subject, bool use_future) + bool canAccess(const StoreObject & storeObject, AccessControlSubject subject) { std::set entities; auto groups = getSubjectGroups(subject); @@ -100,23 +94,19 @@ struct GranularAccessStore : public virtual Store entities.insert(group); } entities.insert(subject); - return canAccess(storeObject, entities, use_future); + return canAccess(storeObject, entities); } /** * Whether the effective subject can access the store path */ - bool canAccess(const StoreObject & storeObject, bool use_future) { + bool canAccess(const StoreObject & storeObject) { if (!experimentalFeatureSettings.isEnabled(Xp::ACLs) || trusted) return true; if (effectiveUser){ - return canAccess(storeObject, *effectiveUser, use_future); + return canAccess(storeObject, *effectiveUser); } else { - if (use_future) { - return !getAccessStatus(storeObject).isProtected; - } else { - return !getAccessStatus(storeObject).isProtected; - } + return !getAccessStatus(storeObject).isProtected; } } diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 95cdaf8511d..c20716e38e6 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -862,7 +862,7 @@ void LocalStore::queryReferrers(State & state, const StorePath & path, StorePath { auto useQueryReferrers(state.stmts->QueryReferrers.use()(printStorePath(path))); - if (!canAccess(path, false)) throw AccessDenied("Access Denied"); + if (!canAccess(path)) throw AccessDenied("Access Denied"); while (useQueryReferrers.next()) referrers.insert(parseStorePath(useQueryReferrers.getStr(0))); @@ -880,7 +880,7 @@ void LocalStore::queryReferrers(const StorePath & path, StorePathSet & referrers StorePathSet LocalStore::queryValidDerivers(const StorePath & path) { - if (!canAccess(path, false)) throw AccessDenied("Access Denied"); + if (!canAccess(path)) throw AccessDenied("Access Denied"); return retrySQLite([&]() { auto state(_state.lock()); @@ -1480,7 +1480,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, addTempRoot(info.path); - if (repair || !isValidPath(info.path) || !canAccess(info.path, false)) { + if (repair || !isValidPath(info.path) || !canAccess(info.path)) { PathLocks outputLock; @@ -1538,7 +1538,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, registerValidPath(info); - } else if (effectiveUser && !canAccess(info.path, false)) { + } else if (effectiveUser && !canAccess(info.path)) { auto curInfo = queryPathInfo(info.path); HashSink hashSink(HashAlgorithm::SHA256); source.drainInto(hashSink); From 2820eb4d1fa689f514504280f48e7cb71a476f93 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Fri, 15 Dec 2023 17:30:42 +0100 Subject: [PATCH 36/56] Acls: permission check when importing a folder with builtins.path If a folder was already imported to the store and we do not have permission to this store path, we may be able to edit the permissions if we have read access to all the files of this folder. --- src/libexpr/primops.cc | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index fea1e5a68b6..747296f0a84 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -38,6 +38,7 @@ #include #include +#include namespace nix { @@ -2291,6 +2292,15 @@ bool EvalState::callPathFilter( return forceBool(res, pos, "while evaluating the return value of the path filter function"); } + +void assertReadable(const Path &p){ + std::ifstream path_file(p); + if (!path_file) { + throw Error(fmt("Could not access file (%s) permissions may be missing", p)); + } + path_file.close(); +} + static void addPath( EvalState & state, const PosIdx pos, @@ -2342,11 +2352,16 @@ static void addPath( auto curStatus = require(*state.store).getAccessStatus(*expectedStorePath); if (curStatus != *accessStatus && !require(*state.store).canAccess(*expectedStorePath)) { // It's ok to update the permission of a store path if we have read access to the original file. - std::ifstream path_file(path.path.abs()); - if (!path_file) { - throw Error(fmt("Could not access file (%s) permissions may be missing", path)); + + if(std::filesystem::is_directory(path.path.abs())){ + for (const auto& dirEntry : std::filesystem::recursive_directory_iterator(path.path.abs())){ + if (std::filesystem::is_directory(dirEntry)) continue; + assertReadable(dirEntry.path()); + } + } + else { + assertReadable(path.path.abs()); } - path_file.close(); } } require(*state.store).setAccessStatus(*expectedStorePath, *accessStatus); From c1912d82581a025fa5a69ba4f9255ac04a3bbf7d Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Fri, 15 Dec 2023 17:31:13 +0100 Subject: [PATCH 37/56] Acls: Test importing a private folder --- tests/nixos/acls.nix | 108 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/tests/nixos/acls.nix b/tests/nixos/acls.nix index 1d0283c3ffe..c2dbc884e72 100644 --- a/tests/nixos/acls.nix +++ b/tests/nixos/acls.nix @@ -516,6 +516,113 @@ let ''; + # Tests importing a private folder + test-import-folder = builtins.toFile "test-import-folder.nix" '' + with import {}; + stdenvNoCC.mkDerivation { + name = "test-import-folder"; + outputs = ["out" "out2"]; + privateSource = builtins.path { + path = /tmp/private-src; + sha256 = "961102b8a00318065d49b8c941adc13f56da0fbb56e094de4917b6fdf80a41df"; + permissions = { + protected = true; + users = ["test" "test3"]; + }; + }; + buildCommand = "touch $out2 && mkdir $out && cat $privateSource/1 $privateSource/2 > $out/output"; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["test" "test2"]; }; + outputs.out2 = { protected = true; users = ["test" "test2"]; }; + drv = { protected = true; users = ["test" "test2"]; }; + log.protected = true; + log.users = ["test" "test2"]; + }; + } + ''; + + # Tests overriding permissions over a private folder that was previsously imported. + test-import-folder-2 = builtins.toFile "test-import-folder-2.nix" '' + with import {}; + stdenvNoCC.mkDerivation { + name = "test-import-folder"; + outputs = ["out" "out2"]; + privateSource = builtins.path { + path = /tmp/private-src; + sha256 = "961102b8a00318065d49b8c941adc13f56da0fbb56e094de4917b6fdf80a41df"; + permissions = { + protected = true; + users = ["test" "test2"]; + }; + }; + buildCommand = "touch $out2 && mkdir $out && cat $privateSource/1 $privateSource/2 > $out/output"; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["test" "test2"]; }; + outputs.out2 = { protected = true; users = ["test" "test2"]; }; + drv = { protected = true; users = ["test" "test2"]; }; + log.protected = true; + log.users = ["test" "test2"]; + }; + } + ''; + + # Tests importing a private folder + testImportFolder = '' + # fmt: off + machine.succeed("""sudo -u test mkdir /tmp/private-src""") + machine.succeed("""sudo -u test bash -c 'echo secret_string_1 > /tmp/private-src/1'""") + machine.succeed("""sudo -u test bash -c 'echo secret_string_2 > /tmp/private-src/2'""") + machine.succeed("""sudo -u test chmod 700 /tmp/private-src/2""") + + # test2 does not have access to all the files in /tmp/private-src + machine.fail(""" + sudo -u test2 nix-build ${test-import-folder} --no-out-link 2>&1 + """) + + # test has read access to all the files in /tmp/private-src + testImportFolderPath = machine.succeed(""" + sudo -u test nix-build ${test-import-folder} --no-out-link + """).strip() + + assert_info(f"""{testImportFolderPath}/output""", {"exists": True, "protected": True, "users": ["test", "test2"], "groups": []}, "after nix-build test-import-folder") + + # Check permissions of the copies of the files from /tmp/private-src in the store + testImportFolderPathDrv = machine.succeed(""" + sudo -u test nix-instantiate ${test-import-folder} + """).strip() + inputFolderPath=machine.succeed(f"nix-store -q --references {testImportFolderPathDrv} | grep private-src").strip() + assert_info(f"""{inputFolderPath}""", {"exists": True, "protected": True, "users": ["test", "test3"], "groups": []}, "inputFolderPath") + assert_info(f"""{inputFolderPath}/1""", {"exists": True, "protected": True, "users": ["test", "test3"], "groups": []}, "inputFolderPath/1") + assert_info(f"""{inputFolderPath}/2""", {"exists": True, "protected": True, "users": ["test", "test3"], "groups": []}, "inputFolderPath/2") + + # test2 tries grant itself permission to the /tmp/private-src input but it cannot read all the original files + assert_in_last_line( + "Could not access file (/tmp/private-src/2) permissions may be missing", + machine.fail(""" + sudo -u test2 nix-build ${test-import-folder-2} --no-out-link 2>&1 + """) + ) + + # test2 can now read all the files in /tmp/private-src + machine.succeed("""sudo -u test chmod 777 /tmp/private-src/2""") + + # test-import-folder-2 will remove the permissions of test3 so this fails + assert_in_last_line( + f"Only trusted users can revoke permissions on path {inputFolderPath}", + machine.fail(""" + sudo -u test2 nix-build ${test-import-folder-2} --no-out-link 2>&1 + """) + ) + + # It succeeds after a trusted user manually removes the permissions of test3 + machine.succeed(f"nix store access revoke {inputFolderPath} --user test3") + machine.succeed(""" + sudo -u test2 nix-build ${test-import-folder-2} --no-out-link + """) + ''; + in { name = "acls"; @@ -547,6 +654,7 @@ in testExamples testDependOnPrivate testTestUserPrivate + testImportFolder # [TODO] uncomment once access to the runtime closure is unforced # testRuntimeDepNoPermScript ]; From 9c757820aaf6ebefda2f8c3d6c7a6d3182c0b0ff Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Mon, 18 Dec 2023 15:08:25 +0400 Subject: [PATCH 38/56] Don't account for trusted users in ensureAccess --- src/libexpr/primops.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 6354ea6c227..6af123265c6 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -163,7 +163,7 @@ void readAccessStatus(EvalState & state, Attr & attr, LocalGranularAccessStore:: void ensureAccess(LocalGranularAccessStore::AccessStatus * accessStatus, std::string_view description, LocalGranularAccessStore & store) { - if (!accessStatus->isProtected || store.trusted) return; + if (!accessStatus->isProtected) return; uid_t uid = getuid(); auto groups = store.getSubjectGroups(uid); for (auto entity : accessStatus->entities) { From 8ee40430f88f1393a16a2bb9778a16c1c4c9137a Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Mon, 18 Dec 2023 15:09:48 +0400 Subject: [PATCH 39/56] Make the 'should be synced' message debug-only --- src/libstore/build/derivation-goal.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index b000d5998ee..a2ab1c6f0ab 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -746,7 +746,7 @@ void DerivationGoal::tryToBuild() logger->cout("don't have access to path %s; checking outputs", worker.store.printStorePath(status.known->path)); buildMode = bmCheck; } else if (status.known->status == PathStatus::ShouldSync) { - logger->cout("permissions should be synced for path %s; repairing", + debug("permissions should be synced for path %s; repairing", worker.store.printStorePath(status.known->path)); buildMode = bmRepair; } From af84767a48a0c0ebf27f66ebf8f557c3de559ea5 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Mon, 18 Dec 2023 16:54:07 +0400 Subject: [PATCH 40/56] Fix perl bindings build --- perl/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perl/default.nix b/perl/default.nix index 0fa57f7815e..38fc71a78d8 100644 --- a/perl/default.nix +++ b/perl/default.nix @@ -2,7 +2,7 @@ , stdenv , perl, perlPackages , autoconf-archive, autoreconfHook, pkg-config -, nix, curl, bzip2, xz, boost, libsodium, darwin +, nix, curl, bzip2, xz, boost, libsodium, darwin, acl }: perl.pkgs.toPerlModule (stdenv.mkDerivation { From f967eb60d6d5de66c12b1a17b75bb908ffb5c048 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Mon, 18 Dec 2023 19:08:46 +0100 Subject: [PATCH 41/56] Reactivate runtime closure check --- src/libstore/local-store.cc | 64 +++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index c20716e38e6..4dbec706188 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1060,40 +1060,48 @@ void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::Acc { experimentalFeatureSettings.require(Xp::ACLs); - // This check is deactivated for now - // It makes the public example3 derivation (from acls.nix) fail because it depends on the private derivation example 2. - // However it does not look like a runtime dependency. - if (false && isInStore(path)) { + + // We check that everyone who has access to the path has access to runtimes dependencies + if (isInStore(path)) { StorePath storePath(baseNameOf(path)); // FIXME(acls): cache is broken when called from registerValidPaths - std::promise> promise; + // We do not check paths referenced by drv files, as these are not really runtimes dependencies + // Skipping the check is needed if we want to build a derivation (B) which depends on a public output of another derivation (A), + // but we do not have access to the private inputs of A. + // In this case we need access to `B.drv` but do not need access to the private input that is referenced transitively. + if (!storePath.isDerivation()){ - queryPathInfoUncached(storePath, - {[&](std::future> result) { - try { - promise.set_value(ref(result.get())); - } catch (...) { - promise.set_exception(std::current_exception()); - } - }}); + std::promise> promise; - auto info = promise.get_future().get(); - - for (auto reference : info->references) { - if (reference == storePath) continue; - auto otherStatus = getCurrentAccessStatus(printStorePath(reference)); - if (!otherStatus.isProtected) continue; - if (!status.isProtected) - throw AccessDenied("can not make %s non-protected because it references a protected path %s", path, printStorePath(reference)); - std::vector difference; - std::set_difference(status.entities.begin(), status.entities.end(), otherStatus.entities.begin(), otherStatus.entities.end(), difference.begin()); - - if (! difference.empty()) { - std::string entities; - for (auto entity : difference) entities += ACL::printTag(entity) + ", "; - throw AccessDenied("can not allow %s access to %s because it references path %s to which they do not have access", entities.substr(0, entities.size()-2), path, printStorePath(reference)); + queryPathInfoUncached(storePath, + {[&](std::future> result) { + try { + promise.set_value(result.get()); + } catch (...) { + promise.set_exception(std::current_exception()); + } + }}); + + auto info = promise.get_future().get(); + + if (info){ + for (auto reference : info->references) { + if (reference == storePath) continue; + auto otherStatus = getCurrentAccessStatus(printStorePath(reference)); + if (!otherStatus.isProtected) continue; + if (!status.isProtected) + throw AccessDenied("can not make %s non-protected because it references a protected path %s", path, printStorePath(reference)); + std::vector difference; + std::set_difference(status.entities.begin(), status.entities.end(), otherStatus.entities.begin(), otherStatus.entities.end(),std::inserter(difference, difference.begin())); + + if (! difference.empty()) { + std::string entities; + for (auto entity : difference) entities += ACL::printTag(entity) + ", "; + throw AccessDenied("can not allow %s access to %s because it references path %s to which they do not have access", entities.substr(0, entities.size()-2), path, printStorePath(reference)); + } + } } } } From d1672525b62129c2f2d9589656cfd92367ce32d2 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Mon, 18 Dec 2023 19:09:46 +0100 Subject: [PATCH 42/56] Acls test: fix for runtime closure checks --- tests/nixos/acls.nix | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/nixos/acls.nix b/tests/nixos/acls.nix index c2dbc884e72..59118facd7e 100644 --- a/tests/nixos/acls.nix +++ b/tests/nixos/acls.nix @@ -427,7 +427,7 @@ let users = ["test"]; }; }; - buildCommand = "echo $privateSource > $out && echo Example >> $out"; + buildCommand = "cat $privateSource > $out && echo Example >> $out"; allowSubstitutes = false; __permissions = { outputs.out = { protected = true; users = ["test" "test2"]; }; @@ -506,13 +506,18 @@ let assert_in_last_line("Could not access file (/tmp/test_secret) permissions may be missing", add_permissions_output) - inputPath1 = machine.succeed(f""" - sudo -u test2 head -n 1 {userPrivatePath} - """) - machine.fail(f""" - sudo -u test2 cat {inputPath1} - """) + testUserPrivateDrv = machine.succeed(""" + sudo nix-instantiate ${test-user-private} + """).strip() + testUserPrivateInput=machine.succeed(f"nix-store -q --references {testUserPrivateDrv} | grep test_secret").strip() + + assert_in_last_line( + "test_secret: Permission denied", + machine.fail(f""" + sudo -u test2 cat {testUserPrivateInput} 2>&1 + """) + ) ''; @@ -655,7 +660,6 @@ in testDependOnPrivate testTestUserPrivate testImportFolder - # [TODO] uncomment once access to the runtime closure is unforced - # testRuntimeDepNoPermScript + testRuntimeDepNoPermScript ]; } From 8841d0de48eda7ed7f0f53a4f4415e98bcf6ed71 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Thu, 21 Dec 2023 12:29:17 +0100 Subject: [PATCH 43/56] Acls: reactivate ensureAccess and move the call to setAccessStatus This way we only call ensureAccess in cases where the permissions are updated. In particular, we do not want to call ensureAccess if you depend on an already built derivation you could not build yourself, but want to use its public outputs. --- src/libcmd/installables.cc | 4 +- src/libexpr/primops.cc | 34 +++------------- src/libstore/build/local-derivation-goal.cc | 2 +- src/libstore/daemon.cc | 7 ++-- src/libstore/granular-access-store.hh | 10 ++--- src/libstore/local-store.cc | 44 ++++++++++++++++++++- src/libstore/local-store.hh | 3 +- src/libstore/remote-store.cc | 3 +- src/libstore/remote-store.hh | 2 +- src/libstore/worker-protocol.cc | 11 ++++++ src/libstore/worker-protocol.hh | 2 + src/nix/store-access-grant.cc | 2 +- src/nix/store-access-protect.cc | 2 +- src/nix/store-access-revoke.cc | 2 +- src/nix/store-access-unprotect.cc | 2 +- 15 files changed, 83 insertions(+), 47 deletions(-) diff --git a/src/libcmd/installables.cc b/src/libcmd/installables.cc index eb3144da610..94bd9845d34 100644 --- a/src/libcmd/installables.cc +++ b/src/libcmd/installables.cc @@ -625,10 +625,10 @@ std::vector, BuiltPathWithResult>> Installable::build LocalStore::AccessStatus status {true, {ACL::User(getuid())}}; std::visit(overloaded { [&](DerivedPath::Opaque p){ - require(*store).setAccessStatus(p.path, status); + require(*store).setAccessStatus(p.path, status, false); }, [&](DerivedPath::Built b){ - require(*store).setAccessStatus(b, status); + require(*store).setAccessStatus(b, status, false); } }, b.path); } diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 63cc0403ed9..05b3d3c6611 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -163,26 +163,6 @@ void readAccessStatus(EvalState & state, Attr & attr, LocalGranularAccessStore:: } } -void ensureAccess(LocalGranularAccessStore::AccessStatus * accessStatus, std::string_view description, LocalGranularAccessStore & store) -{ - if (!accessStatus->isProtected) return; - uid_t uid = getuid(); - auto groups = store.getSubjectGroups(uid); - for (auto entity : accessStatus->entities) { - if (std::visit(overloaded { - [&](ACL::User u) { return u.uid == uid; }, - [&](ACL::Group g) { return groups.contains(g); } - }, entity)) - return; - } - // TODO: Reactivate ensureAccess. - // It should be possible to depend on the public outputs of a derivation that has private inputs. - // For now it is deactivated because in this case, I think the check can fail when is should not. - // Cf the depend-on-public test in acls.nix - return; - throw AccessDenied("you (%s) would not have access to %s; ensure that you do by adding yourself or a group you're in to the list", getUserName(uid), description); -} - /** * Add and attribute to the given attribute map from the output name to * the output path, or a placeholder. @@ -1461,8 +1441,7 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN if (derivation != attr->value->attrs->end()) { LocalGranularAccessStore::AccessStatus status; readAccessStatus(state, *derivation, &status, "__permissions.drv", "builtins.derivationStrict"); - ensureAccess(&status, state.store->printStorePath(drvPath), require(*state.store)); - require(*state.store).setAccessStatus(drvPath, status); + require(*state.store).setAccessStatus(drvPath, status, true); } } } @@ -1489,16 +1468,14 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN })); LocalGranularAccessStore::AccessStatus status; readAccessStatus(state, output, &status, fmt("__permissions.outputs.%s", state.symbols[output.name]), "builtins.derivationStrict"); - ensureAccess(&status, fmt("output %s of derivation %s", state.symbols[output.name], drvPathS), require(*state.store)); - require(*state.store).setAccessStatus(StoreObjectDerivationOutput {drvPath, std::string(state.symbols[{output.name}])}, status); + require(*state.store).setAccessStatus(StoreObjectDerivationOutput {drvPath, std::string(state.symbols[{output.name}])}, status, true); } } auto log = attr->value->attrs->find(state.sLog); if (log != attr->value->attrs->end()) { LocalGranularAccessStore::AccessStatus status; readAccessStatus(state, *log, &status, "__permissions.log", "builtins.derivationStrict"); - ensureAccess(&status, fmt("log of derivation %s", drvPathS), require(*state.store)); - require(*state.store).setAccessStatus(StoreObjectDerivationLog {drvPath}, status); + require(*state.store).setAccessStatus(StoreObjectDerivationLog {drvPath}, status, true); } } } @@ -2364,7 +2341,8 @@ static void addPath( } } } - require(*state.store).setAccessStatus(*expectedStorePath, *accessStatus); + + require(*state.store).setAccessStatus(*expectedStorePath, *accessStatus, true); } else { // computeStorePathForPath should fail if we do not have access to the original path //StorePath dstPath = state.store->computeStorePathForPath(name, path, method, htSHA256, filter) .first; @@ -2375,7 +2353,7 @@ static void addPath( readFile(path.path.abs(), sink); }); StorePath dstPath = state.store->computeStorePathFromDump(*source, name, method, HashAlgorithm::SHA256).first; - require(*state.store).setAccessStatus(dstPath, *accessStatus); + require(*state.store).setAccessStatus(dstPath, *accessStatus, true); } } diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 1f6d7d6e32e..ddb9d1311d4 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -2451,7 +2451,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() StoreObjectDerivationOutput thisOutput(drvPath, outputName); if (localStore.futurePermissions.contains(thisOutput)) { - localStore.setAccessStatus(finalStorePath, localStore.futurePermissions[thisOutput]); + localStore.setAccessStatus(finalStorePath, localStore.futurePermissions[thisOutput], false); } /* Store the final path */ finalOutputs.insert_or_assign(outputName, finalStorePath); diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 78b9c84b5c3..6656b7ebfa1 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -1019,17 +1019,18 @@ static void performOp(TunnelLogger * logger, ref store, case WorkerProto::Op::SetAccessStatus: { auto object = WorkerProto::Serialise::read(*store, rconn); auto status = WorkerProto::Serialise::read(*store, rconn); + bool ensureAccessCheck = WorkerProto::Serialise::read(*store, rconn); logger->startWork(); auto localStore = dynamic_cast(&*store); // Could there be a race condition here ? If the path is added by a concurrent build, after we checked its existence. if (!localStore->pathOfStoreObjectExists(object)){ - localStore->setAccessStatus(object, status); + localStore->setAccessStatus(object, status, ensureAccessCheck); } else { auto curStatus = require(*store).getAccessStatus(object); if (status != curStatus) { if (user.trusted) { - localStore->setAccessStatus(object, status); + localStore->setAccessStatus(object, status, ensureAccessCheck); } else { // TODO document rationale behind this logic auto [exists, description] = std::visit( @@ -1088,7 +1089,7 @@ static void performOp(TunnelLogger * logger, ref store, "Only trusted users can revoke permissions on %s", description); } - localStore->setAccessStatus(object, status); + localStore->setAccessStatus(object, status, ensureAccessCheck); } } } diff --git a/src/libstore/granular-access-store.hh b/src/libstore/granular-access-store.hh index c2a4884ace9..282fd45d960 100644 --- a/src/libstore/granular-access-store.hh +++ b/src/libstore/granular-access-store.hh @@ -54,7 +54,7 @@ struct GranularAccessStore : public virtual Store typedef AccessStatusFor AccessStatus; - virtual void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status) = 0; + virtual void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status, const bool & ensureAccessCheck) = 0; virtual AccessStatus getAccessStatus(const StoreObject & storeObject) = 0; virtual std::set getSubjectGroupsUncached(AccessControlSubject subject) = 0; @@ -113,24 +113,24 @@ struct GranularAccessStore : public virtual Store void addAllowedEntitiesFuture(const StoreObject & storeObject, const std::set & entities) { auto status = getAccessStatus(storeObject); for (auto entity : entities) status.entities.insert(entity); - setAccessStatus(storeObject, status); + setAccessStatus(storeObject, status, false); } void addAllowedEntitiesCurrent(const StoreObject & storeObject, const std::set & entities) { auto status = getAccessStatus(storeObject); for (auto entity : entities) status.entities.insert(entity); - setAccessStatus(storeObject, status); + setAccessStatus(storeObject, status, false); } void removeAllowedEntitiesFuture(const StoreObject & storeObject, const std::set & entities) { auto status = getAccessStatus(storeObject); for (auto entity : entities) status.entities.erase(entity); - setAccessStatus(storeObject, status); + setAccessStatus(storeObject, status, false); } void removeAllowedEntitiesCurrent(const StoreObject & storeObject, const std::set & entities) { auto status = getAccessStatus(storeObject); for (auto entity : entities) status.entities.erase(entity); - setAccessStatus(storeObject, status); + setAccessStatus(storeObject, status, false); } private: diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 4dbec706188..5343af51ae8 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1168,8 +1168,50 @@ void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::Acc acl.set(path); } -void LocalStore::setAccessStatus(const StoreObject & storePathstoreObject, const AccessStatus & status) +void LocalStore::ensureAccess(const AccessStatus & accessStatus, const StoreObject & object) { + auto description = std::visit( + overloaded{ + [&](StorePath p) { + return fmt("path %s", printStorePath(p)); + }, + [&](StoreObjectDerivationOutput b) { + auto drv = readDerivation(b.drvPath); + auto outputHashes = + staticOutputHashes(*this, drv); + auto drvOutputs = drv.outputsAndOptPaths(*this); + bool known = drvOutputs.contains(b.output) && + drvOutputs.at(b.output).second; + if (known) { + auto realPath = printStorePath(*drvOutputs.at(b.output).second); + return fmt("path %s", realPath); + } else { + return fmt("output %s of derivation %s", b.output, printStorePath(b.drvPath)); + } + }, + [&](StoreObjectDerivationLog l) { + return fmt("build log of derivation %s", printStorePath(l.drvPath)); + } + }, object); + + if (!accessStatus.isProtected) return; + uid_t uid = getuid(); + if (effectiveUser) uid = effectiveUser->uid; + auto groups = getSubjectGroups(uid); + for (auto entity : accessStatus.entities) { + if (std::visit(overloaded { + [&](ACL::User u) { return u.uid == uid; }, + [&](ACL::Group g) { return groups.contains(g); } + }, entity)) + return; + } + throw AccessDenied("you (%s) would not have access to %s; ensure that you do by adding yourself or a group you're in to the list", getUserName(uid), description); +} + + +void LocalStore::setAccessStatus(const StoreObject & storePathstoreObject, const AccessStatus & status, const bool & ensureAccessCheck) +{ + if (ensureAccessCheck) ensureAccess(status, storePathstoreObject); if (pathOfStoreObjectExists(storePathstoreObject)){ setCurrentAccessStatus(storePathstoreObject, status); } diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh index 2e71ce0eccd..f87f5fc4062 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -301,7 +301,8 @@ public: void queryRealisationUncached(const DrvOutput&, Callback> callback) noexcept override; - void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; + void ensureAccess(const AccessStatus & accessStatus, const StoreObject & object); + void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status, const bool & ensureAccessCheck) override; void setCurrentAccessStatus(const StoreObject & storeObject, const AccessStatus & status); void setCurrentAccessStatus(const Path & path, const AccessStatus & status); AccessStatus getAccessStatus(const StoreObject & storeObject) override; diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index d904dc14bf8..f93f5c68cfd 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -924,12 +924,13 @@ void RemoteStore::addBuildLog(const StorePath & drvPath, std::string_view log) readInt(conn->from); } -void RemoteStore::setAccessStatus(const StoreObject & storeObject, const RemoteStore::AccessStatus & status) +void RemoteStore::setAccessStatus(const StoreObject & storeObject, const RemoteStore::AccessStatus & status, const bool & ensureAccessCheck) { auto conn(getConnection()); conn->to << WorkerProto::Op::SetAccessStatus; WorkerProto::Serialise::write(*this, *conn, storeObject); WorkerProto::Serialise::write(*this, *conn, status); + WorkerProto::Serialise::write(*this, *conn, ensureAccessCheck); conn.processStderr(); readInt(conn->from); } diff --git a/src/libstore/remote-store.hh b/src/libstore/remote-store.hh index 76038c2a74e..9960ef34783 100644 --- a/src/libstore/remote-store.hh +++ b/src/libstore/remote-store.hh @@ -170,7 +170,7 @@ public: ref openConnectionWrapper(); - void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status) override; + void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status, const bool & ensureAccessCheck) override; AccessStatus getAccessStatus(const StoreObject & storeObject) override; std::set getSubjectGroupsUncached(ACL::User user) override; diff --git a/src/libstore/worker-protocol.cc b/src/libstore/worker-protocol.cc index 74aa6b288d1..0f597a1b2dd 100644 --- a/src/libstore/worker-protocol.cc +++ b/src/libstore/worker-protocol.cc @@ -61,6 +61,17 @@ void WorkerProto::Serialise::write(const StoreDirConfig & sto conn.to << user.uid; } +bool WorkerProto::Serialise::read(const StoreDirConfig & store, WorkerProto::ReadConn conn) { + bool b; + conn.from >> b; + return b; +} + +void WorkerProto::Serialise::write(const StoreDirConfig & store, WorkerProto::WriteConn conn, const bool & b) +{ + conn.to << b; +} + ACL::User WorkerProto::Serialise::read(const StoreDirConfig & store, WorkerProto::ReadConn conn) { uid_t uid; conn.from >> uid; diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh index 35c5fec21cb..b7855507167 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -229,6 +229,8 @@ DECLARE_WORKER_SERIALISER(std::optional); template<> DECLARE_WORKER_SERIALISER(AuthenticatedUser); template<> +DECLARE_WORKER_SERIALISER(bool); +template<> DECLARE_WORKER_SERIALISER(ACL::User); template<> DECLARE_WORKER_SERIALISER(ACL::Group); diff --git a/src/nix/store-access-grant.cc b/src/nix/store-access-grant.cc index 0ea1f514914..23a68d9caf2 100644 --- a/src/nix/store-access-grant.cc +++ b/src/nix/store-access-grant.cc @@ -56,7 +56,7 @@ struct CmdStoreAccessGrant : StorePathsCommand for (auto user : users) status.entities.insert(nix::ACL::User(user)); for (auto group : groups) status.entities.insert(nix::ACL::Group(group)); - localStore.setAccessStatus(path, status); + localStore.setAccessStatus(path, status, false); } } } diff --git a/src/nix/store-access-protect.cc b/src/nix/store-access-protect.cc index 4d938518741..35f9447b2c7 100644 --- a/src/nix/store-access-protect.cc +++ b/src/nix/store-access-protect.cc @@ -29,7 +29,7 @@ struct CmdStoreAccessProtect : StorePathsCommand warn("There are some users or groups who have access to path %s; consider removing them with \n" ANSI_BOLD "nix store access revoke --all-entities %s" ANSI_NORMAL, localStore.printStorePath(path), localStore.printStorePath(path)); if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); status.isProtected = true; - localStore.setAccessStatus(path, status); + localStore.setAccessStatus(path, status, false); } } }; diff --git a/src/nix/store-access-revoke.cc b/src/nix/store-access-revoke.cc index 5d77190d8be..c9f6cff3d2a 100644 --- a/src/nix/store-access-revoke.cc +++ b/src/nix/store-access-revoke.cc @@ -69,7 +69,7 @@ struct CmdStoreAccessRevoke : StorePathsCommand for (auto user : users) status.entities.erase(nix::ACL::User(user)); for (auto group : groups) status.entities.erase(nix::ACL::Group(group)); } - localStore.setAccessStatus(path, status); + localStore.setAccessStatus(path, status, false); } } } diff --git a/src/nix/store-access-unprotect.cc b/src/nix/store-access-unprotect.cc index 6b7af6e6f2f..a8d03b93875 100644 --- a/src/nix/store-access-unprotect.cc +++ b/src/nix/store-access-unprotect.cc @@ -29,7 +29,7 @@ struct CmdStoreAccessUnprotect : StorePathsCommand warn("There are still some users or groups who have access to path %s; consider removing them with \n" ANSI_BOLD "nix store access revoke --all-entities %s" ANSI_NORMAL, localStore.printStorePath(path), localStore.printStorePath(path)); if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path)); status.isProtected = false; - localStore.setAccessStatus(path, status); + localStore.setAccessStatus(path, status, false); } } }; From 9f63760fb022d521d28abb2d3837f9be94b3fe2b Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Tue, 19 Dec 2023 15:36:00 +0100 Subject: [PATCH 44/56] Acls: Fix tests after activating `ensureAccess` --- tests/nixos/acls.nix | 45 ++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/tests/nixos/acls.nix b/tests/nixos/acls.nix index 59118facd7e..de8fc083fb8 100644 --- a/tests/nixos/acls.nix +++ b/tests/nixos/acls.nix @@ -325,9 +325,10 @@ let buildCommand = "cat ''${private} > $out "; allowSubstitutes = false; __permissions = { - outputs.out = { protected = true; users = ["test"]; }; - drv = { protected = true; users = ["test"]; }; + outputs.out = { protected = true; users = ["test" "root"]; }; + drv = { protected = true; users = ["test" "root"]; }; log.protected = true; + log.users = ["root"]; }; } ''; @@ -360,6 +361,7 @@ let outputs.out = { protected = true; users = ["test"]; }; drv = { protected = true; users = ["test"]; }; log.protected = true; + log.users = ["test"]; }; } ''; @@ -385,13 +387,13 @@ let sudo -u test nix-build ${depend-on-private} --no-out-link 2>&1 """) - assert_in_last_line("error: test (uid 1000) does not have access to path", depend_on_private_output) + # assert_in_last_line("error: test (uid 1000) does not have access to path", depend_on_private_output) machine.fail(f"""sudo -u test cat {private_output}""") runtime_depend_on_private_output = machine.fail(""" sudo -u test nix-build ${runtime-depend-on-private} --no-out-link 2>&1 """) - assert_in_last_line("error: test (uid 1000) does not have access to path", runtime_depend_on_private_output) + # assert_in_last_line("error: test (uid 1000) does not have access to path", runtime_depend_on_private_output) machine.fail(f"""sudo -u test cat {private_output}""") @@ -468,17 +470,20 @@ let machine.succeed("""sudo -u test chmod 700 /tmp/test_secret"""); # User test2 cannot build the derivation itself - test_user_private_out = machine.fail(""" - sudo -u test2 nix-build ${test-user-private} --no-out-link 2>&1 - """) - - assert_in_last_line("opening file '/tmp/test_secret': Permission denied", test_user_private_out) + assert_in_last_line( + "you (test2) would not have access to path /nix/store/lh7vw9n09hf41sqq8pb7vwb7kyq8f6da-test_secret", + machine.fail(""" + sudo -u test2 nix-build ${test-user-private} --no-out-link 2>&1 + """) + ) # User test can do it to grant access to the outputs to test2 userPrivatePath = machine.succeed(""" sudo -u test nix-build ${test-user-private} --no-out-link """) + # Since the derivation is already built, test2 could now run the build command with no effect + # It could also build a new derivation that depend on the public outputs. machine.succeed(""" sudo -u test2 nix-build ${test-user-private} --no-out-link """) @@ -494,23 +499,23 @@ let sudo -u test nix store access revoke --user test2 {userPrivatePath} """) + testUserPrivateDrv = machine.succeed(""" + sudo -u test nix-instantiate ${test-user-private} + """).strip() + testUserPrivateInput=machine.succeed(f"nix-store -q --references {testUserPrivateDrv} | grep test_secret").strip() + # Since test2 was given permissions, it can grant access to test3 machine.succeed(f""" sudo -u test2 nix store access grant --user test3 {userPrivatePath} """) # test2 cannot add itself to the permissions of /tmp/test_secret - add_permissions_output = machine.fail(""" - sudo -u test2 nix-build ${test-user-private-2} --no-out-link 2>&1 - """) - - assert_in_last_line("Could not access file (/tmp/test_secret) permissions may be missing", add_permissions_output) - - - testUserPrivateDrv = machine.succeed(""" - sudo nix-instantiate ${test-user-private} - """).strip() - testUserPrivateInput=machine.succeed(f"nix-store -q --references {testUserPrivateDrv} | grep test_secret").strip() + assert_in_last_line( + "Could not access file (/tmp/test_secret) permissions may be missing", + machine.fail(""" + sudo -u test2 nix-build ${test-user-private-2} --no-out-link 2>&1 + """) + ) assert_in_last_line( "test_secret: Permission denied", From 045f1e8bb419931a258f5089bb05f881c6dec932 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Thu, 21 Dec 2023 12:06:02 +0100 Subject: [PATCH 45/56] Acls: add tests using flakes --- tests/nixos/acls.nix | 90 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/tests/nixos/acls.nix b/tests/nixos/acls.nix index de8fc083fb8..7ebd53c7385 100644 --- a/tests/nixos/acls.nix +++ b/tests/nixos/acls.nix @@ -633,6 +633,78 @@ let """) ''; + private-flake = builtins.toFile "private-flake.nix" '' + { + description = "Test alcs with flake"; + outputs = { self, nixpkgs }: { + packages.x86_64-linux.default = + with import nixpkgs { system = "x86_64-linux"; }; + stdenvNoCC.mkDerivation { + name = "private-flake"; + src = builtins.path { + path = self; + permissions = { + protected = true; + users = ["root"]; + }; + }; + buildCommand = "echo Example > $out && cat $src/secret >> $out"; + __permissions = { + outputs.out = { protected = true; users = ["test" "test2" "root"]; }; + drv = { protected = true; users = ["test" "test2" "root"]; }; + log.protected = true; + log.users = ["test" "test2" "root"]; + }; + }; + }; + } + ''; + + public-flake = builtins.toFile "public-flake.nix" '' + { + description = "Test alcs with flake"; + inputs.priv.url = "/tmp/private-flake"; + outputs = { self, nixpkgs, priv }: { + packages.x86_64-linux.default = + with import nixpkgs { system = "x86_64-linux"; }; + stdenvNoCC.mkDerivation { + name = "public-flake"; + src = self; + buildCommand = "echo Example > $out && cat ''${priv.packages.x86_64-linux.default} >> $out"; + __permissions = { + outputs.out = { protected = true; users = ["test" "test2"]; }; + drv = { protected = true; users = ["test" "test2"]; }; + log.protected = true; + log.users = ["test" "test2"]; + }; + }; + }; + } + ''; + + testFlake = '' + # fmt: off + machine.succeed("mkdir /tmp/private-flake") + machine.succeed("echo secret_string > /tmp/private-flake/secret && chmod 700 /tmp/private-flake/secret") + machine.succeed("cp ${private-flake} /tmp/private-flake/flake.nix") + assert_in_last_line( + "error: opening file '/tmp/private-flake/secret': Permission denied", + machine.fail("cd /tmp/private-flake && sudo -u test nix build --print-out-paths 2>&1") + ) + flakeOutput = machine.succeed("cd /tmp/private-flake && nix build --print-out-paths --no-link") + assert_info(f"""{flakeOutput}""", {"exists": True, "protected": True, "users": ["root", "test", "test2"], "groups": []}, "inputFolderPath") + + # public-flake depends on the public output of private-flake + machine.succeed("mkdir /tmp/public-flake && cp ${public-flake} /tmp/public-flake/flake.nix") + + # We build the lockfile as root because this needs to check that the secret input did not change since the build + machine.succeed("cd /tmp/public-flake && nix flake lock") + + # After this the test user can rely on the output of the already built private-flake + publicFlakeOutput = machine.succeed("cd /tmp/public-flake && sudo -u test nix build --print-out-paths --no-link") + assert_info(f"""{publicFlakeOutput}""", {"exists": True, "protected": True, "users": ["test", "test2"], "groups": []}, "inputFolderPath") + ''; + in { name = "acls"; @@ -641,7 +713,22 @@ in { config, lib, pkgs, ... }: { virtualisation.writableStore = true; nix.settings.substituters = lib.mkForce [ ]; - nix.settings.experimental-features = lib.mkForce [ "nix-command" "acls" ]; + nix.settings.experimental-features = lib.mkForce [ "nix-command" "acls" "flakes"]; + + # Do not try to download the registry and setup a local nixpkgs for flake tests + nix.settings.flake-registry = builtins.toFile "global-registry.json" ''{"flakes":[],"version":2}''; + nix.registry = { + nixpkgs = { + from = { + type = "indirect"; + id = "nixpkgs"; + }; + to = { + type = "path"; + path = "${nixpkgs}"; + }; + }; + }; nix.nixPath = [ "nixpkgs=${lib.cleanSource pkgs.path}" ]; nix.checkConfig = false; virtualisation.additionalPaths = [ pkgs.stdenvNoCC pkgs.pkgsi686Linux.stdenvNoCC ]; @@ -666,5 +753,6 @@ in testTestUserPrivate testImportFolder testRuntimeDepNoPermScript + testFlake ]; } From e90e47923f8798ff6734463f3524d2376d612e55 Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Thu, 21 Dec 2023 12:43:56 +0100 Subject: [PATCH 46/56] Acls: merge {add/remove}AllowedEntities current and future --- src/libstore/build/local-derivation-goal.cc | 4 ++-- src/libstore/granular-access-store.hh | 15 ++------------- src/libstore/local-store.cc | 8 ++++---- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index ddb9d1311d4..d2e49a6b96a 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -2747,7 +2747,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() // Is this needed ? // Can give to many permission, if test user tries to build a path that already exists // but on which it does not have permission. - // localStore.addAllowedEntitiesCurrent(oldInfo.path, {*localStore.effectiveUser}); + // localStore.addAllowedEntities(oldInfo.path, {*localStore.effectiveUser}); } continue; @@ -2784,7 +2784,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() StoreObjectDerivationLog log { drvPath }; /* Since all outputs are known to be matching, give access to the log */ if (localStore.effectiveUser && !localStore.canAccess(log, false)) - localStore.addAllowedEntitiesFuture(log, {*localStore.effectiveUser}); + localStore.addAllowedEntities(log, {*localStore.effectiveUser}); /* In case of fixed-output derivations, if there are mismatches on `--check` an error must be thrown as this is diff --git a/src/libstore/granular-access-store.hh b/src/libstore/granular-access-store.hh index 282fd45d960..48e71259977 100644 --- a/src/libstore/granular-access-store.hh +++ b/src/libstore/granular-access-store.hh @@ -110,24 +110,13 @@ struct GranularAccessStore : public virtual Store } } - void addAllowedEntitiesFuture(const StoreObject & storeObject, const std::set & entities) { + void addAllowedEntities(const StoreObject & storeObject, const std::set & entities) { auto status = getAccessStatus(storeObject); for (auto entity : entities) status.entities.insert(entity); setAccessStatus(storeObject, status, false); } - void addAllowedEntitiesCurrent(const StoreObject & storeObject, const std::set & entities) { - auto status = getAccessStatus(storeObject); - for (auto entity : entities) status.entities.insert(entity); - setAccessStatus(storeObject, status, false); - } - - void removeAllowedEntitiesFuture(const StoreObject & storeObject, const std::set & entities) { - auto status = getAccessStatus(storeObject); - for (auto entity : entities) status.entities.erase(entity); - setAccessStatus(storeObject, status, false); - } - void removeAllowedEntitiesCurrent(const StoreObject & storeObject, const std::set & entities) { + void removeAllowedEntities(const StoreObject & storeObject, const std::set & entities) { auto status = getAccessStatus(storeObject); for (auto entity : entities) status.entities.erase(entity); setAccessStatus(storeObject, status, false); diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 5343af51ae8..510e95e481b 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -974,7 +974,7 @@ void LocalStore::syncPathPermissions(const ValidPathInfo & info) up resetting the permissions to the default ones */ // futurePermissions.erase(info.path); if (info.accessStatus) - addAllowedEntitiesCurrent(info.path, info.accessStatus->entities); + addAllowedEntities(info.path, info.accessStatus->entities); } else if (info.accessStatus) { setCurrentAccessStatus(realPath, *info.accessStatus); } else { @@ -1355,7 +1355,7 @@ void LocalStore::grantBuildUserAccess(const StorePath & storePath, const LocalSt [&](ACL::User u) { createDirs(basePath + "/users/" + std::to_string(u.uid)); }, [&](ACL::Group g) { createDirs(basePath + "/groups/" + std::to_string(g.gid)); }, }, buildUser); - addAllowedEntitiesCurrent(storePath, {buildUser}); + addAllowedEntities(storePath, {buildUser}); } } @@ -1430,7 +1430,7 @@ void LocalStore::revokeBuildUserAccess(const StorePath & storePath, const LocalS [&](ACL::User u) { return std::filesystem::remove((basePath + "/users/" + std::to_string(u.uid)).c_str()); }, [&](ACL::Group g) { return std::filesystem::remove((basePath + "/groups/" + std::to_string(g.gid)).c_str()); }, }, buildUser); - if (builderPermissionExisted) removeAllowedEntitiesCurrent(storePath, {buildUser}); + if (builderPermissionExisted) removeAllowedEntities(storePath, {buildUser}); } void LocalStore::revokeBuildUserAccess(const StorePath & storePath) @@ -1596,7 +1596,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, /* Check that both new and old info matches */ // checkInfoValidity(hashSink.finish()); // checkInfoValidity({curInfo->narHash, curInfo->narSize}); - addAllowedEntitiesFuture(info.path, {*effectiveUser}); + addAllowedEntities(info.path, {*effectiveUser}); } outputLock.setDeletion(true); From df135f21b6db3e734e5d24a4333cf5d3fb0f2c7b Mon Sep 17 00:00:00 2001 From: Yves-Stan Le Cornec Date: Thu, 21 Dec 2023 14:33:44 +0100 Subject: [PATCH 47/56] Acls documentation: fix markdown files paths. --- src/nix/store-access-grant.cc | 2 +- src/nix/store-access-protect.cc | 2 +- src/nix/store-access-revoke.cc | 2 +- src/nix/store-access-revoke.md | 4 +++- src/nix/store-access-unprotect.cc | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/nix/store-access-grant.cc b/src/nix/store-access-grant.cc index 23a68d9caf2..6f987f94055 100644 --- a/src/nix/store-access-grant.cc +++ b/src/nix/store-access-grant.cc @@ -39,7 +39,7 @@ struct CmdStoreAccessGrant : StorePathsCommand std::string doc() override { return - #include "store-repair.md" + #include "store-access-grant.md" ; } diff --git a/src/nix/store-access-protect.cc b/src/nix/store-access-protect.cc index 35f9447b2c7..a219a265917 100644 --- a/src/nix/store-access-protect.cc +++ b/src/nix/store-access-protect.cc @@ -16,7 +16,7 @@ struct CmdStoreAccessProtect : StorePathsCommand std::string doc() override { return - #include "store-repair.md" + #include "store-access-protect.md" ; } diff --git a/src/nix/store-access-revoke.cc b/src/nix/store-access-revoke.cc index c9f6cff3d2a..233f3c7eb57 100644 --- a/src/nix/store-access-revoke.cc +++ b/src/nix/store-access-revoke.cc @@ -46,7 +46,7 @@ struct CmdStoreAccessRevoke : StorePathsCommand std::string doc() override { return - #include "store-repair.md" + #include "store-access-revoke.md" ; } diff --git a/src/nix/store-access-revoke.md b/src/nix/store-access-revoke.md index 078e0c62541..c7de87e761a 100644 --- a/src/nix/store-access-revoke.md +++ b/src/nix/store-access-revoke.md @@ -17,8 +17,10 @@ The following users have access to the path: # Description -`nix store access revoke` revokes users access to store paths. +`nix store access revoke` revokes users or groups access to store paths. +Note: revoking access to a user via the `--user` flag removes the user from the list of allowed users, it may still be able to access the path through the permission of its group. + )" diff --git a/src/nix/store-access-unprotect.cc b/src/nix/store-access-unprotect.cc index a8d03b93875..4bc87a37419 100644 --- a/src/nix/store-access-unprotect.cc +++ b/src/nix/store-access-unprotect.cc @@ -16,7 +16,7 @@ struct CmdStoreAccessUnprotect : StorePathsCommand std::string doc() override { return - #include "store-repair.md" + #include "store-access-unprotect.md" ; } From d14704cd2b2008578c762e1e5c4eb5fef9dedb13 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Fri, 12 Jan 2024 12:59:31 +0400 Subject: [PATCH 48/56] ACLs: calculate mask correctly --- src/libutil/acl.cc | 14 +++++++++++++- src/libutil/acl.hh | 8 +++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/libutil/acl.cc b/src/libutil/acl.cc index ff59b378e66..a9cf8bc3288 100644 --- a/src/libutil/acl.cc +++ b/src/libutil/acl.cc @@ -367,6 +367,18 @@ std::string printTag(Tag tag) }, tag); } +#ifndef __APPLE__ +Permissions AccessControlList::calculateMask(Native::AccessControlList acl) +{ + std::set all; + for (auto [tag, perms] : acl) { + if (std::get_if(&tag) || std::get_if(&tag) || std::get_if(&tag) || std::get_if(&tag)) { + all.insert(perms.begin(), perms.end()); + } + } + return all; +} +#endif AccessControlList::AccessControlList(std::filesystem::path p) { @@ -396,7 +408,7 @@ void AccessControlList::set(std::filesystem::path p) native[GroupObj {}] = current[GroupObj {}]; native[Other {}] = current[Other {}]; if (!empty()) - native[Mask {}] = {Permission::Read, Permission::Write, Permission::Execute}; + native[Mask {}] = calculateMask(native); if (current == native) return; #endif native.set(p); diff --git a/src/libutil/acl.hh b/src/libutil/acl.hh index 7cfc9c272a4..5cdafa6aac3 100644 --- a/src/libutil/acl.hh +++ b/src/libutil/acl.hh @@ -385,6 +385,7 @@ public: void allowExecute(bool allow); friend class AccessControlList; + friend class Native::AccessControlList; private: /** @@ -425,7 +426,12 @@ public: * Throws a SysError on failure. */ void set(std::filesystem::path file); + +private: +#ifndef __APPLE__ + Permissions calculateMask(Native::AccessControlList acl); +#endif }; } -} \ No newline at end of file +} From 5ef3f14b16b49b68f44001ad3b0ca55894ceea38 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Fri, 2 Feb 2024 10:13:41 +0400 Subject: [PATCH 49/56] Ensure access in daemon.cc regardless of current status --- src/libstore/daemon.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 6656b7ebfa1..5c267060fb8 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -1091,6 +1091,8 @@ static void performOp(TunnelLogger * logger, ref store, } localStore->setAccessStatus(object, status, ensureAccessCheck); } + } else { + localStore->ensureAccess(status, object); } } logger->stopWork(); From 7a4906489c182d4c882e8a099f9ad9d9a98f053b Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Wed, 7 Feb 2024 13:50:53 +0400 Subject: [PATCH 50/56] Assign the build directory to the effective user, if present --- src/libstore/build/local-derivation-goal.cc | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index d2e49a6b96a..484ae29947b 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -3017,7 +3017,17 @@ void LocalDerivationGoal::deleteTmpDir(bool force) might have privileged stuff (like a copy of netrc). */ if (settings.keepFailed && !force && !drv->isBuiltin()) { printError("note: keeping build directory '%s'", tmpDir); - chmod(tmpDir.c_str(), 0755); + bool chowned = false; + struct stat info; + stat(tmpDir.c_str(), &info); + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) + if (auto store = dynamic_cast(&worker.store)) + if (store->effectiveUser) { + chown(tmpDir.c_str(), store->effectiveUser->uid, info.st_gid); + chowned = true; + } + if (!chowned) + chmod(tmpDir.c_str(), 0755); } else deletePath(tmpDir); From c5f8a40da4e9954e2e57a78913e6aa08d585411f Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Wed, 7 Feb 2024 13:51:00 +0400 Subject: [PATCH 51/56] Fix getUserName behavior --- src/libutil/users.cc | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/libutil/users.cc b/src/libutil/users.cc index e184798aa0b..78af8f1005b 100644 --- a/src/libutil/users.cc +++ b/src/libutil/users.cc @@ -13,15 +13,22 @@ namespace nix { std::string getUserName(uid_t uid) { auto pw = getpwuid(uid); - std::string name = pw ? pw->pw_name : getEnv("USER").value_or(""); - if (name.empty()) + if (!pw) throw Error("cannot figure out user name"); - return name; + return pw->pw_name; } std::string getUserName() { - return getUserName(getuid()); + uid_t uid = getuid(); + if (getpwuid(uid)) + return getUserName(uid); + else { + if (auto name = getEnv("USER")) + return *name; + else + throw Error("cannot figure our user name"); + } } std::string getGroupName(gid_t gid) From 5333b2590a0189dae97eb730f920aec084b999d7 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Sat, 10 Feb 2024 10:28:33 +0400 Subject: [PATCH 52/56] Add referrer checks for access status --- src/libexpr/primops.cc | 9 ++++++- src/libstore/build/local-derivation-goal.cc | 3 ++- src/libstore/granular-access-store.hh | 11 ++++++++ src/libstore/local-store.cc | 28 ++++++++++++++++++--- src/libstore/local-store.hh | 2 +- tests/nixos/acls.nix | 2 ++ 6 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 05b3d3c6611..2673fba2e0b 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1460,6 +1460,8 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN state.forceAttrs(*outputs->value, noPos, "while evaluating the `__permissions.outputs` " "attribute passed to builtins.derivationStrict"); + auto outputMap = state.store->queryPartialDerivationOutputMap(drvPath); + std::map accessMap; for (auto & output : *outputs->value->attrs) { if (!drv.outputs.contains(state.symbols[output.name])) state.debugThrowLastTrace(EvalError({ @@ -1468,8 +1470,13 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN })); LocalGranularAccessStore::AccessStatus status; readAccessStatus(state, output, &status, fmt("__permissions.outputs.%s", state.symbols[output.name]), "builtins.derivationStrict"); - require(*state.store).setAccessStatus(StoreObjectDerivationOutput {drvPath, std::string(state.symbols[{output.name}])}, status, true); + auto outputName = std::string(state.symbols[{output.name}]); + if (auto path = outputMap.at(outputName)) + accessMap[*path] = status; + else + require(*state.store).setAccessStatus(StoreObjectDerivationOutput {drvPath, outputName}, status, true); } + require(*state.store).setAccessStatus(accessMap); } auto log = attr->value->attrs->find(state.sLog); if (log != attr->value->attrs->end()) { diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 484ae29947b..b09eeb65304 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -3023,7 +3023,8 @@ void LocalDerivationGoal::deleteTmpDir(bool force) if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) if (auto store = dynamic_cast(&worker.store)) if (store->effectiveUser) { - chown(tmpDir.c_str(), store->effectiveUser->uid, info.st_gid); + if (chown(tmpDir.c_str(), store->effectiveUser->uid, info.st_gid) == -1) + throw SysError("cannot change ownership %s", tmpDir.c_str()); chowned = true; } if (!chowned) diff --git a/src/libstore/granular-access-store.hh b/src/libstore/granular-access-store.hh index 48e71259977..885344c5394 100644 --- a/src/libstore/granular-access-store.hh +++ b/src/libstore/granular-access-store.hh @@ -57,6 +57,17 @@ struct GranularAccessStore : public virtual Store virtual void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status, const bool & ensureAccessCheck) = 0; virtual AccessStatus getAccessStatus(const StoreObject & storeObject) = 0; + virtual void setAccessStatus(const std::map pathMap) + { + StorePathSet pathSet; + for (auto [path, _] : pathMap) + pathSet.insert(path); + auto paths = topoSortPaths(pathSet); + for (auto path : paths) { + setAccessStatus(path, pathMap.at(path), true); + } + } + virtual std::set getSubjectGroupsUncached(AccessControlSubject subject) = 0; std::set getSubjectGroups(AccessControlSubject subject) diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 510e95e481b..2c5d2573539 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -858,11 +858,11 @@ StorePathSet LocalStore::queryAllValidPaths() } -void LocalStore::queryReferrers(State & state, const StorePath & path, StorePathSet & referrers) +void LocalStore::queryReferrers(State & state, const StorePath & path, StorePathSet & referrers, bool accessCheck) { auto useQueryReferrers(state.stmts->QueryReferrers.use()(printStorePath(path))); - if (!canAccess(path)) throw AccessDenied("Access Denied"); + if (accessCheck && !canAccess(path)) throw AccessDenied("Access Denied"); while (useQueryReferrers.next()) referrers.insert(parseStorePath(useQueryReferrers.getStr(0))); @@ -1086,7 +1086,7 @@ void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::Acc auto info = promise.get_future().get(); - if (info){ + if (info) { for (auto reference : info->references) { if (reference == storePath) continue; auto otherStatus = getCurrentAccessStatus(printStorePath(reference)); @@ -1103,6 +1103,28 @@ void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::Acc } } } + + StorePathSet referrers; + retrySQLite([&]() { + auto state(_state.lock()); + queryReferrers(*state, storePath, referrers, false); + }); + + for (auto referrer : referrers) { + if (referrer == storePath) continue; + auto otherStatus = getAccessStatus(referrer); + if (!status.isProtected) continue; + if (!otherStatus.isProtected) + throw AccessDenied("can not make %s protected because it is referenced by a non-protected path %s", path, printStorePath(referrer)); + std::vector difference; + std::set_difference(otherStatus.entities.begin(), otherStatus.entities.end(), status.entities.begin(), status.entities.end(),std::inserter(difference, difference.begin())); + + if (! difference.empty()) { + std::string entities; + for (auto entity : difference) entities += ACL::printTag(entity) + ", "; + throw AccessDenied("can not deny %s access to %s because it is referenced by a path %s to which they do not have access", entities.substr(0, entities.size()-2), path, printStorePath(referrer)); + } + } } } diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh index f87f5fc4062..15cf7445fc7 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -371,7 +371,7 @@ private: // Internal versions that are not wrapped in retry_sqlite. bool isValidPath_(State & state, const StorePath & path); - void queryReferrers(State & state, const StorePath & path, StorePathSet & referrers); + void queryReferrers(State & state, const StorePath & path, StorePathSet & referrers, bool accessCheck = true); /** * Add signatures to a ValidPathInfo or Realisation using the secret keys diff --git a/tests/nixos/acls.nix b/tests/nixos/acls.nix index 7ebd53c7385..3f360bda8cc 100644 --- a/tests/nixos/acls.nix +++ b/tests/nixos/acls.nix @@ -13,6 +13,7 @@ let permissions = { protected = true; users = ["root"]; + groups = ["root"]; }; }; buildCommand = "echo Example > $out; cat $exampleSource >> $out"; @@ -34,6 +35,7 @@ let permissions = { protected = true; users = ["root"]; + groups = ["root"]; }; }; buildCommand = "echo Example > $out; cat $exampleSource >> $out"; From 9ca2e827d4cd3b8d207bbd32162189f714123f53 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Thu, 15 Feb 2024 19:03:38 +0400 Subject: [PATCH 53/56] Protect paths if setAccessStatus fail while registering --- src/libstore/local-store.cc | 24 +++++++++++++++++++----- src/libstore/local-store.hh | 2 +- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 2c5d2573539..00c96d28487 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1051,18 +1051,32 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos, bool syncPermi std::reverse(sortedPaths.begin(), sortedPaths.end()); + std::optional ex; + if (syncPermissions) - for (auto path : sortedPaths) - syncPathPermissions(infos.at(path)); + for (auto path : sortedPaths) { + auto info = infos.at(path); + try { + syncPathPermissions(info); + } catch (AccessDenied e) { + if ((info.accessStatus && info.accessStatus->isProtected) || (futurePermissions.contains(path) && futurePermissions[path].isProtected)) { + // Upon failure, just mark path as protected to prevent data leakage + setCurrentAccessStatus(Store::toRealPath(path), AccessStatus(true, {}), false); + } + ex = e; + } + } + + if (ex) throw *ex; } -void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::AccessStatus & status) +void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::AccessStatus & status, bool doChecks) { experimentalFeatureSettings.require(Xp::ACLs); // We check that everyone who has access to the path has access to runtimes dependencies - if (isInStore(path)) { + if (doChecks && isInStore(path)) { StorePath storePath(baseNameOf(path)); // FIXME(acls): cache is broken when called from registerValidPaths @@ -1122,7 +1136,7 @@ void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::Acc if (! difference.empty()) { std::string entities; for (auto entity : difference) entities += ACL::printTag(entity) + ", "; - throw AccessDenied("can not deny %s access to %s because it is referenced by a path %s to which they do not have access", entities.substr(0, entities.size()-2), path, printStorePath(referrer)); + throw AccessDenied("can not deny %s access to %s because it is referenced by a path %s to which they do have access", entities.substr(0, entities.size()-2), path, printStorePath(referrer)); } } } diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh index 15cf7445fc7..8810bc6cb99 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -304,7 +304,7 @@ public: void ensureAccess(const AccessStatus & accessStatus, const StoreObject & object); void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status, const bool & ensureAccessCheck) override; void setCurrentAccessStatus(const StoreObject & storeObject, const AccessStatus & status); - void setCurrentAccessStatus(const Path & path, const AccessStatus & status); + void setCurrentAccessStatus(const Path & path, const AccessStatus & status, bool doChecks = true); AccessStatus getAccessStatus(const StoreObject & storeObject) override; std::optional getFutureAccessStatusOpt(const StoreObject & storeObject); AccessStatus getCurrentAccessStatus(const Path & path); From a8ff15f43c81315b582433e4ca400b2b53f3991d Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Thu, 14 Mar 2024 14:36:36 +0400 Subject: [PATCH 54/56] Automatically deny access for referree derivations --- src/libexpr/primops.cc | 2 +- src/libstore/build/derivation-goal.cc | 2 +- src/libstore/build/local-derivation-goal.cc | 3 - src/libstore/daemon.cc | 137 ++++++------ src/libstore/granular-access-store.hh | 16 +- src/libstore/local-store.cc | 232 +++++++++----------- src/libstore/local-store.hh | 9 +- src/libstore/remote-store.cc | 6 +- src/libstore/remote-store.hh | 3 +- 9 files changed, 186 insertions(+), 224 deletions(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 2673fba2e0b..d167fe3ad08 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1461,7 +1461,7 @@ static void derivationStrictInternal(EvalState & state, const std::string & drvN "while evaluating the `__permissions.outputs` " "attribute passed to builtins.derivationStrict"); auto outputMap = state.store->queryPartialDerivationOutputMap(drvPath); - std::map accessMap; + std::map accessMap; for (auto & output : *outputs->value->attrs) { if (!drv.outputs.contains(state.symbols[output.name])) state.debugThrowLastTrace(EvalError({ diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 963a0f5a757..3590b92e978 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -1303,7 +1303,7 @@ Path DerivationGoal::openLogFile() localStore->futurePermissions.contains(storeObject) ? localStore->futurePermissions.at(storeObject) : LocalGranularAccessStore::AccessStatus {settings.protectByDefault.get(), {}}; - localStore->setCurrentAccessStatus(logFileName, status); + localStore->setCurrentAccessStatus(storeObject, status); } return logFileName; diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index b09eeb65304..03dab30d7c8 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -2450,9 +2450,6 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() auto & localStore = getLocalStore(); StoreObjectDerivationOutput thisOutput(drvPath, outputName); - if (localStore.futurePermissions.contains(thisOutput)) { - localStore.setAccessStatus(finalStorePath, localStore.futurePermissions[thisOutput], false); - } /* Store the final path */ finalOutputs.insert_or_assign(outputName, finalStorePath); /* The rewrite rule will be used in downstream outputs that refer to diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 5c267060fb8..18ef6a95f78 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -1017,84 +1017,77 @@ static void performOp(TunnelLogger * logger, ref store, } case WorkerProto::Op::SetAccessStatus: { - auto object = WorkerProto::Serialise::read(*store, rconn); - auto status = WorkerProto::Serialise::read(*store, rconn); + auto localStore = dynamic_cast(&*store); + std::map pathMap = WorkerProto::Serialise>::read(*store, rconn); bool ensureAccessCheck = WorkerProto::Serialise::read(*store, rconn); logger->startWork(); - auto localStore = dynamic_cast(&*store); - // Could there be a race condition here ? If the path is added by a concurrent build, after we checked its existence. - if (!localStore->pathOfStoreObjectExists(object)){ - localStore->setAccessStatus(object, status, ensureAccessCheck); - } - else { - auto curStatus = require(*store).getAccessStatus(object); - if (status != curStatus) { - if (user.trusted) { - localStore->setAccessStatus(object, status, ensureAccessCheck); - } else { - // TODO document rationale behind this logic - auto [exists, description] = std::visit( - overloaded{ - [&](StorePath p) { - auto rp = store->toRealPath(p); - return std::pair{pathExists(rp), - fmt("path %s", rp)}; - }, - [&](StoreObjectDerivationOutput b) { - auto drv = localStore->readDerivation(b.drvPath); - auto outputHashes = - staticOutputHashes(*localStore, drv); - auto drvOutputs = drv.outputsAndOptPaths(*localStore); - bool known = drvOutputs.contains(b.output) && - drvOutputs.at(b.output).second; - if (known) { - auto realPath = store->toRealPath( - *drvOutputs.at(b.output).second); - bool exists = pathExists(realPath); - return std::pair{ - exists, fmt("path %s", realPath)}; - } else { + for (auto [object, status] : pathMap) { + if (localStore->storeObjectPath(object)){ + auto curStatus = require(*store).getAccessStatus(object); + if (status != curStatus && !user.trusted) { + // TODO document rationale behind this logic + auto [exists, description] = std::visit( + overloaded{ + [&](StorePath p) { + auto rp = store->toRealPath(p); + return std::pair{pathExists(rp), + fmt("path %s", rp)}; + }, + [&](StoreObjectDerivationOutput b) { + auto drv = localStore->readDerivation(b.drvPath); + auto outputHashes = + staticOutputHashes(*localStore, drv); + auto drvOutputs = drv.outputsAndOptPaths(*localStore); + bool known = drvOutputs.contains(b.output) && + drvOutputs.at(b.output).second; + if (known) { + auto realPath = store->toRealPath( + *drvOutputs.at(b.output).second); + bool exists = pathExists(realPath); + return std::pair{ + exists, fmt("path %s", realPath)}; + } else { + return std::pair{ + false, fmt("output %s of derivation %s", b.output, + store->toRealPath(b.drvPath))}; + } + }, + [&](StoreObjectDerivationLog l) { + auto baseName = l.drvPath.to_string(); + + auto logPath = + fmt("%s/%s/%s/%s.bz2", localStore->logDir, + localStore->drvsLogDir, baseName.substr(0, 2), + baseName.substr(2)); + return std::pair{ - false, fmt("output %s of derivation %s", b.output, - store->toRealPath(b.drvPath))}; - } - }, - [&](StoreObjectDerivationLog l) { - auto baseName = l.drvPath.to_string(); - - auto logPath = - fmt("%s/%s/%s/%s.bz2", localStore->logDir, - localStore->drvsLogDir, baseName.substr(0, 2), - baseName.substr(2)); - - return std::pair{ - pathExists(logPath), - fmt("build log of derivation %s", - store->toRealPath(l.drvPath))}; - }}, - object); - if (exists && status.isProtected != curStatus.isProtected) - throw AccessDenied("You have to be a trusted user to set a " - "protection status on an existing %s", - description); - if (!status.isProtected) - throw AccessDenied("Only trusted users can set allowed " - "entities on an unprotected %s", - description); - if (exists && - !std::includes(status.entities.begin(), status.entities.end(), - curStatus.entities.begin(), - curStatus.entities.end())) { - throw AccessDenied( - "Only trusted users can revoke permissions on %s", - description); + pathExists(logPath), + fmt("build log of derivation %s", + store->toRealPath(l.drvPath))}; + }}, + object); + if (exists && status.isProtected != curStatus.isProtected) + throw AccessDenied("You have to be a trusted user to set a " + "protection status on an existing %s", + description); + if (!status.isProtected) + throw AccessDenied("Only trusted users can set allowed " + "entities on an unprotected %s", + description); + if (exists && + !std::includes(status.entities.begin(), status.entities.end(), + curStatus.entities.begin(), + curStatus.entities.end())) { + throw AccessDenied( + "Only trusted users can revoke permissions on %s", + description); + } } - localStore->setAccessStatus(object, status, ensureAccessCheck); + } else { + localStore->ensureAccess(status, object); } - } else { - localStore->ensureAccess(status, object); - } } + localStore->setAccessStatus(pathMap, ensureAccessCheck); logger->stopWork(); to << 1; break; diff --git a/src/libstore/granular-access-store.hh b/src/libstore/granular-access-store.hh index 885344c5394..26abb7c4541 100644 --- a/src/libstore/granular-access-store.hh +++ b/src/libstore/granular-access-store.hh @@ -53,19 +53,15 @@ struct GranularAccessStore : public virtual Store typedef std::variant AccessControlEntity; typedef AccessStatusFor AccessStatus; - - virtual void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status, const bool & ensureAccessCheck) = 0; + /** Get an access status of a path */ virtual AccessStatus getAccessStatus(const StoreObject & storeObject) = 0; - virtual void setAccessStatus(const std::map pathMap) + /** Set an access status on a set of paths, in a single "transaction" that gets rolled back in case of an error, and is self-consistent */ + virtual void setAccessStatus(const std::map & pathMap, const bool & ensureAccessCheck = true) = 0; + + virtual void setAccessStatus(StoreObject o, AccessStatus a, const bool & ensureAccessCheck = true) { - StorePathSet pathSet; - for (auto [path, _] : pathMap) - pathSet.insert(path); - auto paths = topoSortPaths(pathSet); - for (auto path : paths) { - setAccessStatus(path, pathMap.at(path), true); - } + setAccessStatus({{o, a}}, ensureAccessCheck); } virtual std::set getSubjectGroupsUncached(AccessControlSubject subject) = 0; diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 00c96d28487..67a8731873a 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -966,9 +966,8 @@ StorePathSet LocalStore::querySubstitutablePaths(const StorePathSet & paths) void LocalStore::syncPathPermissions(const ValidPathInfo & info) { if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) { - auto realPath = Store::toRealPath(info.path); if (futurePermissions.contains(info.path)) { - setCurrentAccessStatus(realPath, futurePermissions[info.path]); + setCurrentAccessStatus(info.path, futurePermissions[info.path]); /* FIXME: we should erase the permissions to prevent memory leakage; However, it's not easy to only call this function once, so we end up resetting the permissions to the default ones */ @@ -976,10 +975,10 @@ void LocalStore::syncPathPermissions(const ValidPathInfo & info) if (info.accessStatus) addAllowedEntities(info.path, info.accessStatus->entities); } else if (info.accessStatus) { - setCurrentAccessStatus(realPath, *info.accessStatus); + setCurrentAccessStatus(info.path, *info.accessStatus); } else { // TODO: a mode where all new paths are protected by default - setCurrentAccessStatus(realPath, AccessStatus()); + setCurrentAccessStatus(info.path, AccessStatus()); } } } @@ -1061,7 +1060,7 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos, bool syncPermi } catch (AccessDenied e) { if ((info.accessStatus && info.accessStatus->isProtected) || (futurePermissions.contains(path) && futurePermissions[path].isProtected)) { // Upon failure, just mark path as protected to prevent data leakage - setCurrentAccessStatus(Store::toRealPath(path), AccessStatus(true, {}), false); + setCurrentAccessStatus(path, AccessStatus(true, {}), false); } ex = e; } @@ -1070,10 +1069,15 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos, bool syncPermi if (ex) throw *ex; } -void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::AccessStatus & status, bool doChecks) +void LocalStore::setCurrentAccessStatus(const StoreObject & storeObject, const LocalStore::AccessStatus & status, bool doChecks) { experimentalFeatureSettings.require(Xp::ACLs); + auto path_ = storeObjectPath(storeObject); + if (!path_) throw Error("store object does not exist"); + auto path = *path_; + + // We check that everyone who has access to the path has access to runtimes dependencies if (doChecks && isInStore(path)) { @@ -1103,7 +1107,7 @@ void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::Acc if (info) { for (auto reference : info->references) { if (reference == storePath) continue; - auto otherStatus = getCurrentAccessStatus(printStorePath(reference)); + auto otherStatus = getCurrentAccessStatus(reference); if (!otherStatus.isProtected) continue; if (!status.isProtected) throw AccessDenied("can not make %s non-protected because it references a protected path %s", path, printStorePath(reference)); @@ -1127,17 +1131,21 @@ void LocalStore::setCurrentAccessStatus(const Path & path, const LocalStore::Acc for (auto referrer : referrers) { if (referrer == storePath) continue; auto otherStatus = getAccessStatus(referrer); + auto otherStatus_ = otherStatus; if (!status.isProtected) continue; - if (!otherStatus.isProtected) - throw AccessDenied("can not make %s protected because it is referenced by a non-protected path %s", path, printStorePath(referrer)); + if (!otherStatus.isProtected) { + debug("protecting %s because %s is being protected", printStorePath(referrer), path); + otherStatus.isProtected = true; + } std::vector difference; std::set_difference(otherStatus.entities.begin(), otherStatus.entities.end(), status.entities.begin(), status.entities.end(),std::inserter(difference, difference.begin())); - if (! difference.empty()) { - std::string entities; - for (auto entity : difference) entities += ACL::printTag(entity) + ", "; - throw AccessDenied("can not deny %s access to %s because it is referenced by a path %s to which they do have access", entities.substr(0, entities.size()-2), path, printStorePath(referrer)); + for (auto entity : difference) { + debug("denying %s access to %s because they are being denied access to %s", ACL::printTag(entity), path, printStorePath(referrer)); + otherStatus.entities.erase(entity); } + + if (otherStatus != otherStatus_) setCurrentAccessStatus(referrer, otherStatus); } } } @@ -1245,84 +1253,58 @@ void LocalStore::ensureAccess(const AccessStatus & accessStatus, const StoreObje } -void LocalStore::setAccessStatus(const StoreObject & storePathstoreObject, const AccessStatus & status, const bool & ensureAccessCheck) +void LocalStore::setAccessStatus(const std::map & pathMap, const bool & ensureAccessCheck) { - if (ensureAccessCheck) ensureAccess(status, storePathstoreObject); - if (pathOfStoreObjectExists(storePathstoreObject)){ - setCurrentAccessStatus(storePathstoreObject, status); + std::map existingPathMap; + StorePathSet existingPaths; + std::set remainder; + for (auto [object, status] : pathMap) { + if (ensureAccessCheck) ensureAccess(status, object); + auto p = storeObjectStorePath(object); + if (p) { + existingPaths.insert(*p); + existingPathMap[*p] = status; + } + else + remainder.insert(object); } - else { - // If adding future permissions to a StoreObjectDerivationOutput, - // also add permissions to the paths that will exist in the future. - std::visit(overloaded { - [&](StorePath p) {}, - [&](StoreObjectDerivationOutput p) { - auto drv = readDerivation(p.drvPath); - auto outputHashes = staticOutputHashes(*this, drv); - auto drvOutputs = drv.outputsAndOptPaths(*this); - if (drvOutputs.contains(p.output) && drvOutputs.at(p.output).second) { - auto path = *drvOutputs.at(p.output).second; - futurePermissions[path] = status; - } - }, - [&](StoreObjectDerivationLog l){} - }, storePathstoreObject); - futurePermissions[storePathstoreObject] = status; + auto sortedPaths = topoSortPaths(existingPaths); + std::reverse(sortedPaths.begin(), sortedPaths.end()); + for (auto p : sortedPaths) + setCurrentAccessStatus(p, existingPathMap[p]); + for (auto object : remainder) { + auto status = pathMap.at(object); + if (storeObjectPath(object)) { + setCurrentAccessStatus(object, status); + } + else { + // If adding future permissions to a StoreObjectDerivationOutput, + // also add permissions to the paths that will exist in the future. + std::visit(overloaded { + [&](StorePath p) {}, + [&](StoreObjectDerivationOutput p) { + auto drv = readDerivation(p.drvPath); + auto outputHashes = staticOutputHashes(*this, drv); + auto drvOutputs = drv.outputsAndOptPaths(*this); + if (drvOutputs.contains(p.output) && drvOutputs.at(p.output).second) { + auto path = *drvOutputs.at(p.output).second; + futurePermissions[path] = status; + } + }, + [&](StoreObjectDerivationLog l){} + }, object); + futurePermissions[object] = status; + } } } -void LocalStore::setCurrentAccessStatus(const StoreObject & storePathstoreObject, const AccessStatus & status) +LocalStore::AccessStatus LocalStore::getCurrentAccessStatus(const StoreObject & storeObject) { - std::set users; - std::set groups; - for (auto entity : status.entities) { - std::visit(overloaded { - [&](ACL::User user) { - users.insert(getUserName(user.uid)); - }, - [&](ACL::Group group) { - groups.insert(getGroupName(group.gid)); - } - }, entity); - } - std::visit(overloaded { - [&](StorePath p) { - auto path = Store::toRealPath(p); - if (pathExists(path)){ - setCurrentAccessStatus(path, status); - } - else{ - throw Error("setCurrentAccessStatus path does not exists (%s)", path); - } - }, - [&](StoreObjectDerivationOutput p) { - auto drv = readDerivation(p.drvPath); - auto outputHashes = staticOutputHashes(*this, drv); - auto drvOutputs = drv.outputsAndOptPaths(*this); - if (drvOutputs.contains(p.output) && drvOutputs.at(p.output).second) { - auto path = Store::toRealPath(*drvOutputs.at(p.output).second); - if (pathExists(path)) { - setCurrentAccessStatus(path, status); - return; - } - throw Error("setCurrentAccessStatus drv path does not exists (%s)", path); - } - }, - [&](StoreObjectDerivationLog l) { - auto baseName = l.drvPath.to_string(); - - auto logPath = fmt("%s/%s/%s/%s.bz2", logDir, drvsLogDir, baseName.substr(0, 2), baseName.substr(2)); + auto path_ = storeObjectPath(storeObject); + if (!path_) throw Error("store object does not exist"); - if (!pathExists(logPath)){ - throw Error("setCurrentAccessStatus log path does not exists (%s)", logPath); - } - setCurrentAccessStatus(logPath, status); - } - }, storePathstoreObject); -} - -LocalStore::AccessStatus LocalStore::getCurrentAccessStatus(const Path & path) -{ + auto path = *path_; + AccessStatus status; using namespace ACL; @@ -1345,42 +1327,6 @@ LocalStore::AccessStatus LocalStore::getCurrentAccessStatus(const Path & path) return status; } -LocalStore::AccessStatus LocalStore::getCurrentAccessStatus(const StoreObject & storeObject) -{ - experimentalFeatureSettings.require(Xp::ACLs); - - return std::visit(overloaded { - [&](StorePath p){ - auto path = Store::toRealPath(p); - if (pathExists(path)) - return getCurrentAccessStatus(path); - throw Error("getCurrentAccessStatus of inexisting path (%s)", path); - }, - [&](StoreObjectDerivationOutput p) { - auto drv = readDerivation(p.drvPath); - auto outputHashes = staticOutputHashes(*this, drv); - auto drvOutputs = drv.outputsAndOptPaths(*this); - if (drvOutputs.contains(p.output) && drvOutputs.at(p.output).second) { - auto path = Store::toRealPath(*drvOutputs.at(p.output).second); - if (pathExists(path)) { - return getCurrentAccessStatus(path); - } - throw Error("getCurrentAccessStatus of inexisting drv path (%s)", path); - } - throw Error("getCurrentAccessStatus of inexisting p.output (%s)", p.output); - }, - [&](StoreObjectDerivationLog l) { - auto baseName = l.drvPath.to_string(); - - auto logPath = fmt("%s/%s/%s/%s.bz2", logDir, drvsLogDir, baseName.substr(0, 2), baseName.substr(2)); - - if (pathExists(logPath)) - return getCurrentAccessStatus(logPath); - throw Error("getCurrentAccessStatus of inexisting log path (%s)", logPath); - } - }, storeObject); -} - void LocalStore::grantBuildUserAccess(const StorePath & storePath, const LocalStore::AccessControlEntity & buildUser) { // The builder-permissions directory remembers permissions to remove at the end of the build. @@ -1430,31 +1376,61 @@ bool LocalStore::shouldSyncPermissions(const StoreObject &storeObject) { return false; } -// TODO: make a pathOfStoreObjectFunction to deduplicate -bool LocalStore::pathOfStoreObjectExists(const StoreObject & storeObject) +std::optional LocalStore::storeObjectStorePath(const StoreObject & storeObject) { experimentalFeatureSettings.require(Xp::ACLs); return std::visit(overloaded { [&](StorePath p){ auto path = Store::toRealPath(p); - return pathExists(path); + if (pathExists(path)) return std::optional(p); + return std::optional(); }, [&](StoreObjectDerivationOutput p) { auto drv = readDerivation(p.drvPath); auto outputHashes = staticOutputHashes(*this, drv); auto drvOutputs = drv.outputsAndOptPaths(*this); if (drvOutputs.contains(p.output) && drvOutputs.at(p.output).second) { - auto path = Store::toRealPath(*drvOutputs.at(p.output).second); - return pathExists(path); - } - return false; + auto storePath = *drvOutputs.at(p.output).second; + auto path = Store::toRealPath(storePath); + if (pathExists(path)) return std::optional(storePath); + } + return std::optional(); + } + , + [&](StoreObjectDerivationLog l) { + return std::optional(); + } + }, storeObject); +} + +std::optional LocalStore::storeObjectPath(const StoreObject & storeObject) +{ + experimentalFeatureSettings.require(Xp::ACLs); + + return std::visit(overloaded { + [&](StorePath p){ + auto path = Store::toRealPath(p); + if (pathExists(path)) return std::optional(path); + return std::optional(); + }, + [&](StoreObjectDerivationOutput p) { + auto drv = readDerivation(p.drvPath); + auto outputHashes = staticOutputHashes(*this, drv); + auto drvOutputs = drv.outputsAndOptPaths(*this); + if (drvOutputs.contains(p.output) && drvOutputs.at(p.output).second) { + auto storePath = *drvOutputs.at(p.output).second; + auto path = Store::toRealPath(storePath); + if (pathExists(path)) return std::optional(path); + } + return std::optional(); } , [&](StoreObjectDerivationLog l) { auto baseName = l.drvPath.to_string(); auto logPath = fmt("%s/%s/%s/%s.bz2", logDir, drvsLogDir, baseName.substr(0, 2), baseName.substr(2)); - return pathExists(logPath); + if (pathExists(logPath)) return std::optional(logPath); + return std::optional(); } }, storeObject); } diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh index 8810bc6cb99..56ada2c661b 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -302,15 +302,14 @@ public: Callback> callback) noexcept override; void ensureAccess(const AccessStatus & accessStatus, const StoreObject & object); - void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status, const bool & ensureAccessCheck) override; - void setCurrentAccessStatus(const StoreObject & storeObject, const AccessStatus & status); - void setCurrentAccessStatus(const Path & path, const AccessStatus & status, bool doChecks = true); + void setAccessStatus(const std::map & pathMap, const bool & ensureAccessCheck) override; + void setCurrentAccessStatus(const StoreObject & storeObject, const AccessStatus & status, bool doChecks = true); AccessStatus getAccessStatus(const StoreObject & storeObject) override; std::optional getFutureAccessStatusOpt(const StoreObject & storeObject); - AccessStatus getCurrentAccessStatus(const Path & path); AccessStatus getCurrentAccessStatus(const StoreObject & storeObject); bool shouldSyncPermissions(const StoreObject &storeObject); - bool pathOfStoreObjectExists(const StoreObject & storeObject); + std::optional storeObjectPath(const StoreObject & storeObject); + std::optional storeObjectStorePath(const StoreObject & storeObject); void grantBuildUserAccess(const StorePath & path, const AccessControlEntity & entity); void revokeBuildUserAccess(const StorePath & path, const AccessControlEntity & entity); diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index f93f5c68cfd..39b011392f2 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -924,12 +924,12 @@ void RemoteStore::addBuildLog(const StorePath & drvPath, std::string_view log) readInt(conn->from); } -void RemoteStore::setAccessStatus(const StoreObject & storeObject, const RemoteStore::AccessStatus & status, const bool & ensureAccessCheck) +void RemoteStore::setAccessStatus(const std::map & pathMap, const bool & ensureAccessCheck) + { auto conn(getConnection()); conn->to << WorkerProto::Op::SetAccessStatus; - WorkerProto::Serialise::write(*this, *conn, storeObject); - WorkerProto::Serialise::write(*this, *conn, status); + WorkerProto::Serialise>::write(*this, *conn, pathMap); WorkerProto::Serialise::write(*this, *conn, ensureAccessCheck); conn.processStderr(); readInt(conn->from); diff --git a/src/libstore/remote-store.hh b/src/libstore/remote-store.hh index 9960ef34783..cde3793e150 100644 --- a/src/libstore/remote-store.hh +++ b/src/libstore/remote-store.hh @@ -170,7 +170,8 @@ public: ref openConnectionWrapper(); - void setAccessStatus(const StoreObject & storeObject, const AccessStatus & status, const bool & ensureAccessCheck) override; + void setAccessStatus(const std::map & pathMap, const bool & ensureAccessCheck) override; + AccessStatus getAccessStatus(const StoreObject & storeObject) override; std::set getSubjectGroupsUncached(ACL::User user) override; From 946f4f7553f250413925ba79e7062ca0b6237da6 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Thu, 14 Mar 2024 15:01:11 +0400 Subject: [PATCH 55/56] Pass through access status from daemon --- src/libstore/worker-protocol.cc | 10 ++++++++++ src/libstore/worker-protocol.hh | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/libstore/worker-protocol.cc b/src/libstore/worker-protocol.cc index 0f597a1b2dd..b6fb4e5191c 100644 --- a/src/libstore/worker-protocol.cc +++ b/src/libstore/worker-protocol.cc @@ -239,6 +239,11 @@ UnkeyedValidPathInfo WorkerProto::Serialise::read(const St info.sigs = readStrings(conn.from); info.ca = ContentAddress::parseOpt(readString(conn.from)); } + if (GET_PROTOCOL_MINOR(conn.version) >= 37) { + bool hasAccessStatus = WorkerProto::Serialise::read(store, conn); + if (hasAccessStatus) + info.accessStatus = WorkerProto::Serialise>>::read(store, conn); + } return info; } @@ -255,6 +260,11 @@ void WorkerProto::Serialise::write(const StoreDirConfig & << pathInfo.sigs << renderContentAddress(pathInfo.ca); } + if (GET_PROTOCOL_MINOR(conn.version) >= 37) { + WorkerProto::Serialise::write(store, conn, pathInfo.accessStatus.has_value()); + if (pathInfo.accessStatus) + WorkerProto::Serialise>>::write(store, conn, *pathInfo.accessStatus); + } } } diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh index b7855507167..f718a41cb18 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -11,7 +11,7 @@ namespace nix { #define WORKER_MAGIC_1 0x6e697863 #define WORKER_MAGIC_2 0x6478696f -#define PROTOCOL_VERSION (1 << 8 | 36) +#define PROTOCOL_VERSION (1 << 8 | 37) #define GET_PROTOCOL_MAJOR(x) ((x) & 0xff00) #define GET_PROTOCOL_MINOR(x) ((x) & 0x00ff) From 5d5bbbc8ffab95b5949eeb2ae27cd960669555a3 Mon Sep 17 00:00:00 2001 From: Alexander Bantyev Date: Thu, 14 Mar 2024 22:53:57 +0400 Subject: [PATCH 56/56] chmod if chown fails --- src/libstore/build/local-derivation-goal.cc | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 03dab30d7c8..f9810def6b7 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -3017,15 +3017,18 @@ void LocalDerivationGoal::deleteTmpDir(bool force) bool chowned = false; struct stat info; stat(tmpDir.c_str(), &info); + std::optional e; if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) if (auto store = dynamic_cast(&worker.store)) if (store->effectiveUser) { - if (chown(tmpDir.c_str(), store->effectiveUser->uid, info.st_gid) == -1) - throw SysError("cannot change ownership %s", tmpDir.c_str()); - chowned = true; + if (chown(tmpDir.c_str(), store->effectiveUser->uid, info.st_gid) == 0) + chowned = true; + else + e = SysError("cannot change ownership %s", tmpDir.c_str()); } if (!chowned) chmod(tmpDir.c_str(), 0755); + if (e) throw e; } else deletePath(tmpDir);