diff --git a/flake.nix b/flake.nix index ada52c05d72..6b9c289391b 100644 --- a/flake.nix +++ b/flake.nix @@ -232,11 +232,11 @@ boost libsodium ] + ++ lib.optionals stdenv.isLinux [libseccomp acl] ++ lib.optionals (!stdenv.hostPlatform.isWindows) [ editline lowdown-nix ] - ++ lib.optional stdenv.isLinux libseccomp ++ lib.optional stdenv.hostPlatform.isx86_64 libcpuid; checkDeps = [ diff --git a/perl/default.nix b/perl/default.nix index 4687976a168..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 { @@ -38,6 +38,7 @@ perl.pkgs.toPerlModule (stdenv.mkDerivation { boost ] ++ lib.optional (stdenv.isLinux || stdenv.isDarwin) libsodium + ++ lib.optional stdenv.isLinux acl ++ lib.optional stdenv.isDarwin darwin.apple_sdk.frameworks.Security; configureFlags = [ diff --git a/src/libcmd/installables.cc b/src/libcmd/installables.cc index 6b3c82374b6..94bd9845d34 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" @@ -21,6 +22,8 @@ #include "url.hh" #include "registry.hh" #include "build-result.hh" +#include "store-cast.hh" +#include "local-store.hh" #include #include @@ -585,10 +588,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; } @@ -598,7 +602,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; @@ -616,6 +621,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, false); + }, + [&](DerivedPath::Built b){ + require(*store).setAccessStatus(b, status, false); + } + }, b.path); + } } } diff --git a/src/libcmd/installables.hh b/src/libcmd/installables.hh index e087f935c85..db411145cd9 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/libexpr/eval.cc b/src/libexpr/eval.cc index 9e494148e34..e50dea9825a 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -495,6 +495,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 f452dcb9f4f..a8daaa1751c 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -204,6 +204,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 89d5492da23..d167fe3ad08 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1,4 +1,6 @@ +#include "access-status.hh" #include "archive.hh" +#include "config.hh" #include "derivations.hh" #include "downstream-placeholder.hh" #include "eval-inline.hh" @@ -6,6 +8,8 @@ #include "eval-settings.hh" #include "gc-small-vector.hh" #include "globals.hh" +#include "granular-access-store.hh" +#include "hash.hh" #include "json-to-value.hh" #include "names.hh" #include "path-references.hh" @@ -15,6 +19,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 "fs-input-accessor.hh" #include @@ -30,6 +37,8 @@ #include #include +#include +#include namespace nix { @@ -129,6 +138,31 @@ static SourcePath realisePath(EvalState & state, const PosIdx pos, Value & v, bo } } +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] + })); + } +} + /** * Add and attribute to the given attribute map from the output name to * the output path, or a placeholder. @@ -1076,8 +1110,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; @@ -1165,6 +1198,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); @@ -1217,7 +1254,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); @@ -1391,10 +1428,65 @@ 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"); + require(*state.store).setAccessStatus(drvPath, status, true); + } + } + } /* 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"); + 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({ + .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"); + 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()) { + LocalGranularAccessStore::AccessStatus status; + readAccessStatus(state, *log, &status, "__permissions.log", "builtins.derivationStrict"); + require(*state.store).setAccessStatus(StoreObjectDerivationLog {drvPath}, status, true); + } + } + } + printMsg(lvlChatty, "instantiated '%1%' -> '%2%'", drvName, drvPathS); /* Optimisation, but required in read-only mode! because in that @@ -2184,6 +2276,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, @@ -2192,6 +2293,7 @@ static void addPath( Value * filterFun, FileIngestionMethod method, const std::optional expectedHash, + std::optional accessStatus, Value & v, const NixStringContext & context) { @@ -2228,13 +2330,57 @@ static void addPath( .references = {}, }); + if (accessStatus && !settings.readOnlyMode) { + if (expectedStorePath) { + if (pathExists(state.store->toRealPath(*expectedStorePath))) { + 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. + + 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()); + } + } + } + + 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; + 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, true); + } + } + if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) { auto dstPath = path.fetchToStore(state.store, name, method, filter.get(), state.repair); 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) { + 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; + state.allowAndSetStorePathString(dstPath, v); + } else { state.allowAndSetStorePathString(*expectedStorePath, v); + } } catch (Error & e) { e.addTrace(state.positions[pos], "while adding path '%s'", path); throw; @@ -2248,8 +2394,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, args[0], FileIngestionMethod::Recursive, std::nullopt, v, context); + addPath(state, pos, path.baseName(), path, args[0], FileIngestionMethod::Recursive, std::nullopt, std::nullopt, v, context); } static RegisterPrimOp primop_filterSource({ @@ -2314,6 +2459,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'"); @@ -2330,6 +2476,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"), HashAlgorithm::SHA256); + 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]), @@ -2344,7 +2494,7 @@ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value if (name.empty()) name = path->baseName(); - addPath(state, pos, name, *path, filterFun, method, expectedHash, v, context); + addPath(state, pos, name, *path, filterFun, method, expectedHash, accessStatus, v, context); } static RegisterPrimOp primop_path({ @@ -2378,6 +2528,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, }); 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/libstore/access-status.hh b/src/libstore/access-status.hh new file mode 100644 index 00000000000..4c45761830c --- /dev/null +++ b/src/libstore/access-status.hh @@ -0,0 +1,46 @@ +#pragma once +///@file + + +#include +#include +#include "comparator.hh" +#include "globals.hh" +#include "acl.hh" +#include "util.hh" + +namespace nix { +template +struct AccessStatusFor { + bool isProtected = false; + 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) { + users.insert(getUserName(user.uid)); + }, + [&](ACL::Group group) { + groups.insert(getGroupName(group.gid)); + } + }, entity); + } + nlohmann::json j; + j["protected"] = isProtected; + j["users"] = users; + j["groups"] = groups; + return j; + } +}; +} + diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index f8728ed4a68..3590b92e978 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" @@ -731,13 +733,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 @@ -1273,7 +1282,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()); @@ -1283,6 +1296,16 @@ 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 {settings.protectByDefault.get(), {}}; + localStore->setCurrentAccessStatus(storeObject, status); + } + return logFileName; } @@ -1452,21 +1475,38 @@ std::pair DerivationGoal::checkPathValidity() wantedOutputsLeft.erase(i.first); if (i.second) { auto outputPath = *i.second; + bool canAccess = true; + 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)){ + canAccess = aclStore->canAccess(outputPath); + } info.known = { .path = outputPath, - .status = !worker.store.isValidPath(outputPath) + .status = !isValid ? 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)){ + // 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); + } info.known = { .path = real->outPath, - .status = 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 ddb5ee1e34a..a80c33db34d 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 802b39f8459..f9810def6b7 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -1,6 +1,11 @@ #include "local-derivation-goal.hh" +#include "acl.hh" +#include "config.hh" +#include "gc-store.hh" +#include "granular-access-store.hh" #include "indirect-root-store.hh" #include "hook-instance.hh" +#include "local-fs-store.hh" #include "worker.hh" #include "builtins.hh" #include "builtins/buildenv.hh" @@ -21,11 +26,13 @@ #include "unix-domain-socket.hh" #include "posix-fs-canonicalise.hh" +#include #include #include #include #include +#include #include #include #include @@ -237,6 +244,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 { @@ -271,17 +294,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); } @@ -304,6 +329,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. */ @@ -364,8 +398,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; @@ -391,23 +427,53 @@ void LocalDerivationGoal::cleanupPostOutputsRegisteredModeNonCheck() } #if __linux__ -static void 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) +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); + 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) && store.isInStore(source)) { + auto [storePath, subPath] = 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); }; #endif @@ -712,6 +778,7 @@ 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); @@ -804,6 +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); @@ -1500,9 +1568,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(); @@ -1585,7 +1659,7 @@ void LocalDerivationGoal::addDependency(const StorePath & path) if (setns(sandboxMountNamespace.get(), 0) == -1) throw SysError("entering sandbox mount namespace"); - doBind(source, target); + doBind(source, target, worker.store); _exit(0); })); @@ -1609,7 +1683,6 @@ void LocalDerivationGoal::chownToBuilder(const Path & path) throw SysError("cannot change ownership of '%1%'", path); } - void setupSeccomp() { #if __linux__ @@ -1841,7 +1914,7 @@ void LocalDerivationGoal::runChild() chmod_(dst, 0555); } else #endif - doBind(i.second.source, chrootRootDir + i.first, i.second.optional); + doBind(i.second.source, chrootRootDir + i.first, worker.store, i.second.optional); } /* Bind a new instance of procfs on /proc. */ @@ -1880,8 +1953,8 @@ void LocalDerivationGoal::runChild() } else { if (errno != EINVAL) throw SysError("mounting /dev/pts"); - doBind("/dev/pts", chrootRootDir + "/dev/pts"); - doBind("/dev/ptmx", chrootRootDir + "/dev/ptmx"); + doBind("/dev/pts", chrootRootDir + "/dev/pts", worker.store); + doBind("/dev/ptmx", chrootRootDir + "/dev/ptmx", worker.store); } } @@ -2374,6 +2447,9 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() auto actualPath = toRealPathChroot(worker.store.printStorePath(*scratchPath)); auto finish = [&](StorePath finalStorePath) { + auto & localStore = getLocalStore(); + + StoreObjectDerivationOutput thisOutput(drvPath, outputName); /* Store the final path */ finalOutputs.insert_or_assign(outputName, finalStorePath); /* The rewrite rule will be used in downstream outputs that refer to @@ -2657,12 +2733,19 @@ 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, 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. + // localStore.addAllowedEntities(oldInfo.path, {*localStore.effectiveUser}); + } continue; } @@ -2688,12 +2771,18 @@ 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)); } 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, false)) + 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. */ @@ -2925,7 +3014,21 @@ 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); + 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) == 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); diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index a112d6d31d3..18ef6a95f78 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" @@ -262,7 +265,7 @@ struct ClientSettings }; static void performOp(TunnelLogger * logger, ref store, - TrustedFlag trusted, RecursiveFlag recursive, WorkerProto::Version clientVersion, + AuthenticatedUser user, RecursiveFlag recursive, WorkerProto::Version clientVersion, Source & from, BufferedSink & to, WorkerProto::Op op) { WorkerProto::ReadConn rconn { @@ -477,7 +480,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(); @@ -504,6 +507,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); @@ -517,7 +521,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)); @@ -540,7 +544,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(); @@ -559,7 +563,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(); @@ -621,15 +625,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 @@ -667,7 +671,7 @@ static void performOp(TunnelLogger * logger, ref store, } case WorkerProto::Op::AddPermRoot: { - if (!trusted) + if (!user.trusted) throw Error( "you are not privileged to create perm roots\n\n" "hint: you can just do this client-side without special privileges, and probably want to do that instead."); @@ -704,7 +708,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; @@ -775,7 +779,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; @@ -862,7 +866,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(); @@ -901,9 +905,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) { @@ -989,7 +993,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); { @@ -1003,6 +1007,92 @@ 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 localStore = dynamic_cast(&*store); + std::map pathMap = WorkerProto::Serialise>::read(*store, rconn); + bool ensureAccessCheck = WorkerProto::Serialise::read(*store, rconn); + logger->startWork(); + 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{ + 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); + } + } + } else { + localStore->ensureAccess(status, object); + } + } + localStore->setAccessStatus(pathMap, ensureAccessCheck); + logger->stopWork(); + to << 1; + break; + } + case WorkerProto::Op::QueryFailedPaths: case WorkerProto::Op::ClearFailedPaths: throw Error("Removed operation %1%", op); @@ -1016,9 +1106,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. */ @@ -1058,7 +1153,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 { @@ -1078,6 +1173,7 @@ void processConnection( /* Process client requests. */ while (true) { + printMsgUsing(prevLogger, lvlDebug, "waiting for op"); WorkerProto::Op op; try { op = (enum WorkerProto::Op) readInt(from); @@ -1094,7 +1190,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 f401d076dde..8fa546462e5 100644 --- a/src/libstore/globals.cc +++ b/src/libstore/globals.cc @@ -427,5 +427,4 @@ void initLibStore() { initLibStoreDone = true; } - } diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index df977e294ff..9b52747123d 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -1070,6 +1070,22 @@ 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. + )" + }; + + 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: @@ -1129,4 +1145,11 @@ void initLibStore(); */ void assertLibStoreInitialized(); +enum TrustedFlag : bool { NotTrusted = false, Trusted = true }; + +struct AuthenticatedUser { + TrustedFlag trusted; + uid_t uid; +}; + } diff --git a/src/libstore/granular-access-store.hh b/src/libstore/granular-access-store.hh new file mode 100644 index 00000000000..26abb7c4541 --- /dev/null +++ b/src/libstore/granular-access-store.hh @@ -0,0 +1,139 @@ +#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->getBaseStorePath()) + { + if (auto names = std::get_if(&p.outputs.raw)) + if (names->size() == 1) { + output = *names->begin(); + return; + } + throw Error("StoreObjectDerivationOutput requires a DerivedPathBuilt with just one named output"); + }; + StoreObjectDerivationOutput(SingleDerivedPathBuilt p) : drvPath(p.drvPath->getBaseStorePath()), output(p.output) { }; + StoreObjectDerivationOutput(StorePath drvPath, std::string output) : 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; + + /** Get an access status of a path */ + virtual AccessStatus getAccessStatus(const StoreObject & storeObject) = 0; + + /** 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) + { + setAccessStatus({{o, a}}, ensureAccessCheck); + } + + 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 + */ + bool canAccess(const StoreObject & storeObject, const std::set & entities) + { + if (! experimentalFeatureSettings.isEnabled(Xp::ACLs) || trusted) return true; + AccessStatus 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, false); + } + + 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); + } + +private: + std::map> subjectGroupCache; +}; + +using LocalGranularAccessStore = GranularAccessStore; + + +} diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 7e82bae282e..67a8731873a 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1,6 +1,11 @@ #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 "hash.hh" #include "pathlocks.hh" #include "worker-protocol.hh" #include "derivations.hh" @@ -14,10 +19,13 @@ #include "signals.hh" #include "posix-fs-canonicalise.hh" +#include #include #include #include +#include +#include #include #include #include @@ -29,6 +37,7 @@ #include #include #include +#include #if __linux__ #include @@ -197,6 +206,10 @@ LocalStore::LocalStore(const Params & params) } else { makeStoreWritable(); } + + effectiveUser = getuid(); + trusted = true; + createDirs(linksDir); Path profilesDir = stateDir + "/profiles"; createDirs(profilesDir); @@ -253,6 +266,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 @@ -643,6 +665,7 @@ void LocalStore::registerDrvOutput(const Realisation & info) .exec(); } }); + /* FIXME(ACL) set ACLs correctly */ } void LocalStore::cacheDrvOutputMapping( @@ -768,6 +791,10 @@ std::shared_ptr LocalStore::queryPathInfoInternal(State & s while (useQueryReferences.next()) info->references.insert(parseStorePath(useQueryReferences.getStr(0))); + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)){ + info->accessStatus = getCurrentAccessStatus(path); + } + return info; } @@ -831,10 +858,12 @@ 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 (accessCheck && !canAccess(path)) throw AccessDenied("Access Denied"); + while (useQueryReferrers.next()) referrers.insert(parseStorePath(useQueryReferrers.getStr(0))); } @@ -851,6 +880,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()); @@ -933,14 +963,33 @@ StorePathSet LocalStore::querySubstitutablePaths(const StorePathSet & paths) return res; } +void LocalStore::syncPathPermissions(const ValidPathInfo & info) +{ + if (experimentalFeatureSettings.isEnabled(Xp::ACLs)) { + if (futurePermissions.contains(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 */ + // futurePermissions.erase(info.path); + if (info.accessStatus) + addAllowedEntities(info.path, info.accessStatus->entities); + } else if (info.accessStatus) { + setCurrentAccessStatus(info.path, *info.accessStatus); + } else { + // TODO: a mode where all new paths are protected by default + setCurrentAccessStatus(info.path, AccessStatus()); + } + } +} -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 @@ -948,7 +997,9 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos) registering operation. */ if (settings.syncBeforeRegistering) sync(); - return retrySQLite([&]() { + std::vector sortedPaths; + + retrySQLite([&]() { auto state(_state.lock()); SQLiteTxn txn(state->db); @@ -982,7 +1033,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; @@ -996,8 +1047,448 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos) txn.commit(); }); + + std::reverse(sortedPaths.begin(), sortedPaths.end()); + + std::optional ex; + + if (syncPermissions) + 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(path, AccessStatus(true, {}), false); + } + ex = e; + } + } + + if (ex) throw *ex; +} + +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)) { + StorePath storePath(baseNameOf(path)); + + // FIXME(acls): cache is broken when called from registerValidPaths + + // 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()){ + + std::promise> promise; + + 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(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)); + } + } + } + + StorePathSet referrers; + retrySQLite([&]() { + auto state(_state.lock()); + queryReferrers(*state, storePath, referrers, false); + }); + + for (auto referrer : referrers) { + if (referrer == storePath) continue; + auto otherStatus = getAccessStatus(referrer); + auto otherStatus_ = otherStatus; + if (!status.isProtected) continue; + 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())); + + 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); + } + } + } + + 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. + // 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::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 std::map & pathMap, const bool & ensureAccessCheck) +{ + 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); + } + 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; + } + } +} + +LocalStore::AccessStatus LocalStore::getCurrentAccessStatus(const StoreObject & storeObject) +{ + auto path_ = storeObjectPath(storeObject); + if (!path_) throw Error("store object does not exist"); + + auto 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; +} + +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); + 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}); + } +} + + +LocalStore::AccessStatus LocalStore::getAccessStatus(const StoreObject & storeObject) +{ + if (futurePermissions.contains(storeObject)){ + return futurePermissions[storeObject]; + } + else { + return getCurrentAccessStatus(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; +} + +std::optional LocalStore::storeObjectStorePath(const StoreObject & storeObject) +{ + experimentalFeatureSettings.require(Xp::ACLs); + + return std::visit(overloaded { + [&](StorePath p){ + auto path = Store::toRealPath(p); + 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 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)); + if (pathExists(logPath)) return std::optional(logPath); + return std::optional(); + } + }, storeObject); +} + +void LocalStore::revokeBuildUserAccess(const StorePath & storePath, const LocalStore::AccessControlEntity & buildUser) +{ + auto basePath = stateDir + "/acls/builder-permissions/" + storePath.to_string(); + 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); + if (builderPermissionExisted) 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::getSubjectGroupsUncached(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. */ @@ -1051,7 +1542,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; @@ -1105,9 +1596,19 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, canonicalisePathMetaData(realPath, {}); - optimisePath(realPath, repair); // FIXME: combine with hashPath() + optimisePath(realPath, repair); registerValidPath(info); + + } else if (effectiveUser && !canAccess(info.path)) { + auto curInfo = queryPathInfo(info.path); + HashSink hashSink(HashAlgorithm::SHA256); + 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); @@ -1130,6 +1631,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 @@ -1167,9 +1670,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(); } @@ -1209,9 +1712,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); @@ -1272,7 +1775,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 ee605b5a2df..56ada2c661b 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/local-store.hh @@ -7,6 +7,7 @@ #include "store-api.hh" #include "indirect-root-store.hh" #include "sync.hh" +#include "granular-access-store.hh" #include #include @@ -67,8 +68,10 @@ struct LocalStoreConfig : virtual LocalFSStoreConfig }; class LocalStore : public virtual LocalStoreConfig - , public virtual IndirectRootStore + , public virtual LocalFSStore , public virtual GcStore + , public virtual LocalGranularAccessStore + , public virtual IndirectRootStore { private: @@ -113,6 +116,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; @@ -253,9 +266,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; @@ -288,8 +301,24 @@ public: void queryRealisationUncached(const DrvOutput&, Callback> callback) noexcept override; - std::optional getVersion() override; + void ensureAccess(const AccessStatus & accessStatus, const StoreObject & object); + 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 StoreObject & storeObject); + bool shouldSyncPermissions(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); + void revokeBuildUserAccess(const StorePath & path); + void revokeBuildUserAccess(); + + std::set getSubjectGroupsUncached(ACL::User user) override; + + std::optional getVersion() override; private: /** @@ -341,7 +370,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/src/libstore/local.mk b/src/libstore/local.mk index 68ccdc409be..b39d349bdad 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 87f55ce496b..41602d9e55c 100644 --- a/src/libstore/lock.cc +++ b/src/libstore/lock.cc @@ -40,11 +40,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() { @@ -118,13 +118,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) { @@ -181,6 +181,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 cc8ad3d0273..58337251b95 100644 --- a/src/libstore/misc.cc +++ b/src/libstore/misc.cc @@ -415,7 +415,6 @@ OutputPathMap resolveDerivedPath(Store & store, const DerivedPath::Built & bfd, return outputs; } - StorePath resolveDerivedPath(Store & store, const SingleDerivedPath & req, Store * evalStore_) { auto & evalStore = evalStore_ ? *evalStore_ : store; diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc index d9618d04c0e..83de878c199 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 { @@ -87,6 +89,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(value.c_str())); + } + else if (name == "AllowedGroup") { + if (!accessStatus) accessStatus = ValidPathInfo::AccessStatus(); + accessStatus->entities.insert(ACL::Group(value.c_str())); + } } pos = eol + 1; @@ -131,6 +151,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: " + getUserName(u.uid) + "\n"; }, + [&](ACL::Group g){ res += "AllowedGroup: " + getGroupName(g.gid) + "\n"; } + }, entity); + } + return res; } diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index f58e31bfd5f..90866dd9fd0 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -1,5 +1,6 @@ #include +#include "config.hh" #include "path-info.hh" #include "store-api.hh" #include "json-utils.hh" @@ -169,6 +170,15 @@ nlohmann::json UnkeyedValidPathInfo::toJSON( if (ca) jsonObject["ca"] = renderContentAddress(ca); + if (accessStatus) { + jsonObject["protected"] = accessStatus->isProtected; + for (auto & entity : accessStatus->entities) { + std::visit(overloaded { + [&](ACL::User u) { jsonObject["allowedUsers"].push_back(getUserName(u.uid)); }, + [&](ACL::Group g) { jsonObject["allowedGroups"].push_back(getGroupName(g.gid)); }, + }, entity); + } + } if (includeImpureInfo) { if (deriver) jsonObject["deriver"] = store.printStorePath(*deriver); @@ -234,6 +244,17 @@ UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON( if (json.contains("signatures")) res.sigs = valueAt(json, "signatures"); + if (experimentalFeatureSettings.isEnabled(Xp::ACLs) && json.contains("protected")) { + res.accessStatus = AccessStatus(); + res.accessStatus->isProtected = json.at("protected"); + if (json.contains("allowedUsers")) + for (std::string user : json.at("allowedUsers")) + res.accessStatus->entities.insert(ACL::User(user)); + if (json.contains("allowedGroups")) + for (std::string group : json.at("allowedGroups")) + res.accessStatus->entities.insert(ACL::Group(group)); + } + return res; } diff --git a/src/libstore/path-info.hh b/src/libstore/path-info.hh index 077abc7e15b..144a6fc1165 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; @@ -71,6 +72,9 @@ struct UnkeyedValidPathInfo */ std::optional ca; + using AccessStatus = AccessStatusFor>; + std::optional accessStatus; + UnkeyedValidPathInfo(const UnkeyedValidPathInfo & other) = default; UnkeyedValidPathInfo(Hash narHash) : narHash(narHash) { }; @@ -134,7 +138,6 @@ struct ValidPathInfo : UnkeyedValidPathInfo { * Verify a single signature. */ bool checkSignature(const Store & store, const PublicKeys & publicKeys, const std::string & sig) const; - Strings shortRefs() const; ValidPathInfo(const ValidPathInfo & other) = default; diff --git a/src/libstore/posix-fs-canonicalise.cc b/src/libstore/posix-fs-canonicalise.cc index f38fa83690b..853d46cc6f3 100644 --- a/src/libstore/posix-fs-canonicalise.cc +++ b/src/libstore/posix-fs-canonicalise.cc @@ -18,15 +18,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); + } } } diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index cc26c2a9436..39b011392f2 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; @@ -921,6 +924,38 @@ void RemoteStore::addBuildLog(const StorePath & drvPath, std::string_view log) readInt(conn->from); } +void RemoteStore::setAccessStatus(const std::map & pathMap, const bool & ensureAccessCheck) + +{ + auto conn(getConnection()); + conn->to << WorkerProto::Op::SetAccessStatus; + WorkerProto::Serialise>::write(*this, *conn, pathMap); + WorkerProto::Serialise::write(*this, *conn, ensureAccessCheck); + 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::getSubjectGroupsUncached(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 f2e34c1a3f6..cde3793e150 100644 --- a/src/libstore/remote-store.hh +++ b/src/libstore/remote-store.hh @@ -4,9 +4,11 @@ #include #include +#include "local-fs-store.hh" #include "store-api.hh" #include "gc-store.hh" #include "log-store.hh" +#include "granular-access-store.hh" namespace nix { @@ -38,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: @@ -167,6 +170,12 @@ public: ref openConnectionWrapper(); + void setAccessStatus(const std::map & pathMap, const bool & ensureAccessCheck) override; + + AccessStatus getAccessStatus(const StoreObject & storeObject) override; + + std::set getSubjectGroupsUncached(ACL::User user) override; + protected: virtual ref openConnection() = 0; diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 7f35e74afaa..671c7490a1b 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -953,7 +953,6 @@ StorePathSet Store::exportReferences(const StorePathSet & storePaths, const Stor return paths; } - const Store::Stats & Store::getStats() { { diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index 2c883ce97ec..e96f518b826 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -14,10 +14,12 @@ #include "repair-flag.hh" #include "store-dir-config.hh" +#include #include #include #include #include +#include #include #include #include @@ -66,6 +68,8 @@ MakeError(SubstituterDisabled, Error); MakeError(InvalidStoreURI, Error); +MakeError(AccessDenied, Error); + struct Realisation; struct RealisedPath; struct DrvOutput; @@ -84,6 +88,7 @@ typedef std::map OutputPathMap; enum CheckSigsFlag : bool { NoCheckSigs = false, CheckSigs = true }; enum SubstituteFlag : bool { NoSubstitute = false, Substitute = true }; + /** * Magic header of exportPath() output (obsolete). */ @@ -91,7 +96,6 @@ const uint32_t exportMagic = 0x4558494e; enum BuildMode { bmNormal, bmRepair, bmCheck }; -enum TrustedFlag : bool { NotTrusted = false, Trusted = true }; struct BuildResult; struct KeyedBuildResult; diff --git a/src/libstore/worker-protocol-impl.hh b/src/libstore/worker-protocol-impl.hh index 026cc37bc78..1ac2f934f02 100644 --- a/src/libstore/worker-protocol-impl.hh +++ b/src/libstore/worker-protocol-impl.hh @@ -9,6 +9,7 @@ */ #include "worker-protocol.hh" +#include "granular-access-store.hh" #include "length-prefixed-protocol-helper.hh" namespace nix { @@ -56,4 +57,89 @@ struct WorkerProto::Serialise /* protocol-specific templates */ +template +AccessStatusFor WorkerProto::Serialise>::read(const StoreDirConfig & 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 StoreDirConfig & store, WorkerProto::WriteConn conn, const AccessStatusFor & status) +{ + conn.to << status.isProtected; + WorkerProto::Serialise>::write(store, conn, status.entities); +} + +template +std::variant WorkerProto::Serialise>::read(const StoreDirConfig & 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 StoreDirConfig & 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 StoreDirConfig & 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 StoreDirConfig & 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"); + } +} + } diff --git a/src/libstore/worker-protocol.cc b/src/libstore/worker-protocol.cc index 2a379e75e31..b6fb4e5191c 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 "path-with-outputs.hh" #include "store-api.hh" @@ -46,6 +48,51 @@ void WorkerProto::Serialise>::write(const StoreDirCon } } +AuthenticatedUser WorkerProto::Serialise::read(const StoreDirConfig & store, WorkerProto::ReadConn conn) { + AuthenticatedUser user; + user.trusted = *WorkerProto::Serialise>::read(store, conn); + conn.from >> user.uid; + return user; +} + +void WorkerProto::Serialise::write(const StoreDirConfig & store, WorkerProto::WriteConn conn, const AuthenticatedUser & user) +{ + WorkerProto::Serialise>::write(store, conn, user.trusted); + 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; + return uid; +} + +void WorkerProto::Serialise::write(const StoreDirConfig & store, WorkerProto::WriteConn conn, const ACL::User & user) +{ + conn.to << user.uid; +} + +ACL::Group WorkerProto::Serialise::read(const StoreDirConfig & store, WorkerProto::ReadConn conn) { + gid_t gid; + conn.from >> gid; + return gid; +} + +void WorkerProto::Serialise::write(const StoreDirConfig & store, WorkerProto::WriteConn conn, const ACL::Group & group) +{ + conn.to << group.gid; +} DerivedPath WorkerProto::Serialise::read(const StoreDirConfig & store, WorkerProto::ReadConn conn) { @@ -80,6 +127,28 @@ void WorkerProto::Serialise::write(const StoreDirConfig & store, Wo } } +StoreObjectDerivationOutput WorkerProto::Serialise::read(const StoreDirConfig & 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 StoreDirConfig & 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 StoreDirConfig & store, WorkerProto::ReadConn conn) +{ + return { WorkerProto::Serialise::read(store, conn) }; +} + +void WorkerProto::Serialise::write(const StoreDirConfig & store, WorkerProto::WriteConn conn, const StoreObjectDerivationLog & drvLog) +{ + WorkerProto::Serialise::write(store, conn, drvLog.drvPath); +} KeyedBuildResult WorkerProto::Serialise::read(const StoreDirConfig & store, WorkerProto::ReadConn conn) { @@ -170,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; } @@ -186,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 c269142896c..f718a41cb18 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -1,6 +1,8 @@ #pragma once ///@file +#include "serialise.hh" +#include "acl.hh" #include "common-protocol.hh" namespace nix { @@ -9,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) @@ -29,12 +31,18 @@ struct Source; // items being serialised struct DerivedPath; +struct StoreObjectDerivationOutput; +struct StoreObjectDerivationLog; +struct DrvOutput; +struct Realisation; struct BuildResult; struct KeyedBuildResult; struct ValidPathInfo; struct UnkeyedValidPathInfo; enum TrustedFlag : bool; - +struct AuthenticatedUser; +namespace acl { struct User; struct Group; }; +template struct AccessStatusFor; /** * The "worker protocol", used by unix:// and ssh-ng:// stores. @@ -162,6 +170,8 @@ enum struct WorkerProto::Op : uint64_t AddBuildLog = 45, BuildPathsWithResults = 46, AddPermRoot = 47, + GetAccessStatus = 48, + SetAccessStatus = 49, }; /** @@ -211,9 +221,23 @@ DECLARE_WORKER_SERIALISER(KeyedBuildResult); template<> DECLARE_WORKER_SERIALISER(ValidPathInfo); template<> -DECLARE_WORKER_SERIALISER(UnkeyedValidPathInfo); +DECLARE_WORKER_SERIALISER(StoreObjectDerivationOutput); +template<> +DECLARE_WORKER_SERIALISER(StoreObjectDerivationLog); template<> 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); +template +DECLARE_WORKER_SERIALISER(AccessStatusFor); +template<> +DECLARE_WORKER_SERIALISER(UnkeyedValidPathInfo); template DECLARE_WORKER_SERIALISER(std::vector); @@ -222,6 +246,15 @@ DECLARE_WORKER_SERIALISER(std::set); template DECLARE_WORKER_SERIALISER(std::tuple); +template +#define X_ std::variant +DECLARE_WORKER_SERIALISER(X_); +#undef X_ +template +#define X_ std::variant +DECLARE_WORKER_SERIALISER(X_); +#undef X_ + #define COMMA_ , template DECLARE_WORKER_SERIALISER(std::map); diff --git a/src/libutil/acl.cc b/src/libutil/acl.cc new file mode 100644 index 00000000000..a9cf8bc3288 --- /dev/null +++ b/src/libutil/acl.cc @@ -0,0 +1,417 @@ +#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()); +} + +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); +} + +#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) +{ + 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 {}] = calculateMask(native); + if (current == native) return; +#endif + native.set(p); +} + +} diff --git a/src/libutil/acl.hh b/src/libutil/acl.hh new file mode 100644 index 00000000000..5cdafa6aac3 --- /dev/null +++ b/src/libutil/acl.hh @@ -0,0 +1,437 @@ +#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; + +std::string printTag(Tag t); + +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; + friend class Native::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); + +private: +#ifndef __APPLE__ + Permissions calculateMask(Native::AccessControlList acl); +#endif +}; + +} +} diff --git a/src/libutil/archive.cc b/src/libutil/archive.cc index 465df2073d2..c1953bf9c51 100644 --- a/src/libutil/archive.cc +++ b/src/libutil/archive.cc @@ -281,10 +281,11 @@ void parseDump(ParseSink & sink, Source & source) } -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); } @@ -302,12 +303,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 2cf8ee89118..ededb72488b 100644 --- a/src/libutil/archive.hh +++ b/src/libutil/archive.hh @@ -75,14 +75,14 @@ void dumpString(std::string_view s, Sink & sink); 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/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index 9b46fc5b00a..59cf6526af5 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -25,6 +25,13 @@ struct ExperimentalFeatureDetails constexpr size_t numXpFeatures = 1 + static_cast(Xp::VerifiedFetches); constexpr std::array xpFeatureDetails = {{ + { + .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). + )", + }, { .tag = Xp::CaDerivations, .name = "ca-derivations", diff --git a/src/libutil/experimental-features.hh b/src/libutil/experimental-features.hh index eae4fa9b856..7f5d156121b 100644 --- a/src/libutil/experimental-features.hh +++ b/src/libutil/experimental-features.hh @@ -17,6 +17,7 @@ namespace nix { */ enum struct ExperimentalFeature { + ACLs, CaDerivations, ImpureDerivations, Flakes, diff --git a/src/libutil/fs-sink.cc b/src/libutil/fs-sink.cc index 925e6f05dc2..d1a77644852 100644 --- a/src/libutil/fs-sink.cc +++ b/src/libutil/fs-sink.cc @@ -67,14 +67,16 @@ static GlobalConfig::Register r1(&restoreSinkSettings); void RestoreSink::createDirectory(const Path & path) { 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 RestoreSink::createRegularFile(const Path & path) { 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); } diff --git a/src/libutil/fs-sink.hh b/src/libutil/fs-sink.hh index bf54b730193..9f337c84a5f 100644 --- a/src/libutil/fs-sink.hh +++ b/src/libutil/fs-sink.hh @@ -55,6 +55,8 @@ struct RestoreSink : ParseSink { Path dstPath; + bool protect; + void createDirectory(const Path & path) override; void createRegularFile(const Path & path) override; diff --git a/src/libutil/local.mk b/src/libutil/local.mk index 81efaafeca8..de44e59ea5e 100644 --- a/src/libutil/local.mk +++ b/src/libutil/local.mk @@ -10,6 +10,10 @@ libutil_CXXFLAGS += -I src/libutil libutil_LDFLAGS += -pthread $(OPENSSL_LIBS) $(LIBBROTLI_LIBS) $(LIBARCHIVE_LIBS) $(BOOST_LDFLAGS) -lboost_context +ifdef HOST_LINUX + libutil_LDFLAGS += -lacl +endif + $(foreach i, $(wildcard $(d)/args/*.hh), \ $(eval $(call install-file-in, $(i), $(includedir)/nix/args, 0644))) diff --git a/src/libutil/users.cc b/src/libutil/users.cc index 95a641322a4..78af8f1005b 100644 --- a/src/libutil/users.cc +++ b/src/libutil/users.cc @@ -4,18 +4,66 @@ #include "file-system.hh" #include +#include #include #include namespace nix { -std::string getUserName() +std::string getUserName(uid_t uid) { - auto pw = getpwuid(geteuid()); - std::string name = pw ? pw->pw_name : getEnv("USER").value_or(""); - if (name.empty()) + auto pw = getpwuid(uid); + if (!pw) throw Error("cannot figure out user name"); - return name; + return pw->pw_name; +} + +std::string getUserName() +{ + 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) +{ + auto gr = getgrgid(gid); + if (!gr) + throw Error("cannot figure out group name"); + return gr->gr_name; +} + + +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) diff --git a/src/libutil/util.cc b/src/libutil/util.cc index 5bb3f374ba7..9f5c66deea6 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include diff --git a/src/libutil/util.hh b/src/libutil/util.hh index 27faa4d6d22..0eb1e263587 100644 --- a/src/libutil/util.hh +++ b/src/libutil/util.hh @@ -23,6 +23,19 @@ void initLibUtil(); */ std::vector stringsToCharPtrs(const Strings & ss); +std::string getUserName(); +std::string getUserName(uid_t uid); +std::string getGroupName(gid_t gid); + +/** + * 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); MakeError(FormatError, Error); diff --git a/src/nix/add-to-store.cc b/src/nix/add-to-store.cc index 02de796b5c7..8d7576183ce 100644 --- a/src/nix/add-to-store.cc +++ b/src/nix/add-to-store.cc @@ -1,7 +1,9 @@ #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; @@ -16,7 +18,7 @@ static FileIngestionMethod parseIngestionMethod(std::string_view input) } } -struct CmdAddToStore : MixDryRun, StoreCommand +struct CmdAddToStore : MixDryRun, MixProtect, StoreCommand { Path path; std::optional namePart; @@ -82,6 +84,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 47910018659..1245ad0c779 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/daemon.cc b/src/nix/daemon.cc index 4dada8e0e61..e68a05c5519 100644 --- a/src/nix/daemon.cc +++ b/src/nix/daemon.cc @@ -133,21 +133,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? @@ -156,13 +141,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; @@ -171,12 +154,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; } @@ -251,7 +230,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; @@ -261,16 +240,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 }; } @@ -326,19 +308,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 : ""); @@ -367,7 +344,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); @@ -430,11 +407,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); } /** @@ -461,11 +438,13 @@ static void runDaemon(bool stdio, std::optional forceTrustClientOpt if (!processOps && (remoteStore = store.dynamic_pointer_cast())) 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); } diff --git a/src/nix/store-access-grant.cc b/src/nix/store-access-grant.cc new file mode 100644 index 00000000000..6f987f94055 --- /dev/null +++ b/src/nix/store-access-grant.cc @@ -0,0 +1,65 @@ +#include "command.hh" +#include "store-api.hh" +#include "local-fs-store.hh" +#include "store-cast.hh" +#include "granular-access-store.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-access-grant.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(nix::ACL::User(user)); + for (auto group : groups) status.entities.insert(nix::ACL::Group(group)); + localStore.setAccessStatus(path, status, false); + } + } + } +}; + +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..f618b0e09c9 --- /dev/null +++ b/src/nix/store-access-info.cc @@ -0,0 +1,100 @@ +#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 = status.json(); + j["exists"] = isValid; + 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..a219a265917 --- /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-access-protect.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, false); + } + } +}; + +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..233f3c7eb57 --- /dev/null +++ b/src/nix/store-access-revoke.cc @@ -0,0 +1,78 @@ +#include "command.hh" +#include "store-api.hh" +#include "local-fs-store.hh" +#include "store-cast.hh" +#include "granular-access-store.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-access-revoke.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(nix::ACL::User(user)); + for (auto group : groups) status.entities.erase(nix::ACL::Group(group)); + } + localStore.setAccessStatus(path, status, false); + } + } + } +}; + +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..c7de87e761a --- /dev/null +++ b/src/nix/store-access-revoke.md @@ -0,0 +1,26 @@ +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 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 new file mode 100644 index 00000000000..4bc87a37419 --- /dev/null +++ b/src/nix/store-access-unprotect.cc @@ -0,0 +1,37 @@ +#include "command.hh" +#include "store-api.hh" +#include "local-fs-store.hh" +#include "store-cast.hh" +#include "granular-access-store.hh" + +using namespace nix; + +struct CmdStoreAccessUnprotect : StorePathsCommand +{ + std::string description() override + { + return "unprotect store paths"; + } + + std::string doc() override + { + return + #include "store-access-unprotect.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, false); + } + } +}; + +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..b420eb43037 --- /dev/null +++ b/src/nix/store-access.cc @@ -0,0 +1,32 @@ +#include "command.hh" + +using namespace nix; + +struct CmdStoreAccess : virtual NixMultiCommand +{ + CmdStoreAccess() : NixMultiCommand("store access", 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. + + + +)" diff --git a/tests/functional/acls.sh b/tests/functional/acls.sh new file mode 100755 index 00000000000..71ca8e05cf8 --- /dev/null +++ b/tests/functional/acls.sh @@ -0,0 +1,60 @@ +source common.sh + +USER=$(whoami) + +touch example +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' +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":\[\]' + +# 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"< "test.nix"< "test.nix"< "$NIX_CONF_DIR"/nix.conf.extra < {}; + stdenvNoCC.mkDerivation { + name = "example"; + # Check that importing a source works + exampleSource = builtins.path { + path = /tmp/bar; + permissions = { + protected = true; + users = ["root"]; + groups = ["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; + }; + } + ''; + + test-unaccessible = builtins.toFile "unaccessible.nix" '' + with import {}; + stdenvNoCC.mkDerivation { + name = "example"; + exampleSource = builtins.path { + path = /tmp/unaccessible; + permissions = { + protected = true; + users = ["root"]; + groups = ["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; + }; + } + ''; + 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"]; + groups = ["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"]; }; + 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"]; + groups = ["root"]; + }; + }; + buildCommand = "echo Example > $out; cat $exampleSource >> $out"; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["root" "test"]; }; + + # 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; + }; + }; + 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 + ''; + + testInit = '' + # fmt: off + import json + start_all() + + 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}" + + 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 ='' + # 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") + + 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") + ''; + testNonAccessible = '' + machine.succeed("touch /tmp/unaccessible") + machine.succeed("chmod 700 /tmp/unaccessible") + + machine.fail(""" + sudo -u test nix store add-file /tmp/unaccessible + """) + + 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 = '' + # fmt: off + 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") +''; + 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": ["root"], "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") + + 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. + 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 + """).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") + + ''; + + 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 + """) + ''; + + # 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" "root"]; }; + drv = { protected = true; users = ["test" "root"]; }; + log.protected = true; + log.users = ["root"]; + }; + } + ''; + + # Test adding a private runtime dependency, which should fail. + runtime-depend-on-private = builtins.toFile "runtime_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; + log.users = ["test"]; + }; + } + ''; + + + # 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}""") + + 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}""") + 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 + """) + + 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 "test-user-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 && echo Example >> $out"; + allowSubstitutes = false; + __permissions = { + outputs.out = { protected = true; users = ["test" "test2"]; }; + drv = { protected = true; users = ["test" "test2"]; }; + log.protected = true; + log.users = ["test" "test2"]; + }; + } + ''; + + 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"""); + + # User test2 cannot build the derivation itself + 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 + """) + + 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} + """) + + 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 + 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", + machine.fail(f""" + sudo -u test2 cat {testUserPrivateInput} 2>&1 + """) + ) + + ''; + + # 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 + """) + ''; + + 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"; + + nodes.machine = + { config, lib, pkgs, ... }: + { virtualisation.writableStore = true; + nix.settings.substituters = lib.mkForce [ ]; + 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 ]; + users.users.test = { + isNormalUser = true; + }; + users.users.test2 = { + isNormalUser = true; + }; + users.users.test3 = { + isNormalUser = true; + }; + }; + + testScript = { nodes }: testInit + lib.strings.concatStrings + [ + testCli + testNonAccessible + testFoo + testExamples + testDependOnPrivate + testTestUserPrivate + testImportFolder + testRuntimeDepNoPermScript + testFlake + ]; +} diff --git a/tests/nixos/default.nix b/tests/nixos/default.nix index 2645cac8e70..a3df4f60b34 100644 --- a/tests/nixos/default.nix +++ b/tests/nixos/default.nix @@ -20,6 +20,8 @@ in { authorization = runNixOSTestFor "x86_64-linux" ./authorization.nix; + acls = runNixOSTestFor "x86_64-linux" ./acls.nix; + remoteBuilds = runNixOSTestFor "x86_64-linux" ./remote-builds.nix; remoteBuildsSshNg = runNixOSTestFor "x86_64-linux" ./remote-builds-ssh-ng.nix;