diff --git a/src/libstore/auth-tunnel.cc b/src/libstore/auth-tunnel.cc new file mode 100644 index 00000000000..7c1c891fbea --- /dev/null +++ b/src/libstore/auth-tunnel.cc @@ -0,0 +1,148 @@ +#include "auth-tunnel.hh" +#include "serialise.hh" +#include "auth.hh" +#include "store-api.hh" +#include "unix-domain-socket.hh" + +#include + +namespace nix { + +AuthTunnel::AuthTunnel(StoreDirConfig & storeConfig, WorkerProto::Version clientVersion) + : clientVersion(clientVersion) +{ + auto sockets = socketPair(); + serverFd = std::move(sockets.first); + clientFd = std::move(sockets.second); + + serverThread = std::thread([this, clientVersion, &storeConfig]() { + try { + FdSource fromSource(serverFd.get()); + WorkerProto::ReadConn from{ + .from = fromSource, + .version = clientVersion, + }; + FdSink toSource(serverFd.get()); + WorkerProto::WriteConn to{ + .to = toSource, + .version = clientVersion, + }; + + while (true) { + auto op = (WorkerProto::CallbackOp) readInt(from.from); + + switch (op) { + + case WorkerProto::CallbackOp::FillAuth: { + auto authRequest = WorkerProto::Serialise::read(storeConfig, from); + bool required; + from.from >> required; + debug("tunneling auth request: %s", authRequest); + // FIXME: handle exceptions + auto authData = auth::getAuthenticator()->fill(authRequest, required); + if (authData) + debug("tunneling auth response: %s", *authData); + to.to << 1; + WorkerProto::Serialise>::write(storeConfig, to, authData); + toSource.flush(); + break; + } + + case WorkerProto::CallbackOp::RejectAuth: { + auto authData = WorkerProto::Serialise::read(storeConfig, from); + debug("tunneling auth data erase: %s", authData); + auth::getAuthenticator()->reject(authData); + to.to << 1; + toSource.flush(); + break; + } + + default: + throw Error("invalid callback operation %1%", (int) op); + } + } + } catch (EndOfFile &) { + } catch (...) { + ignoreException(); + } + }); +} + +AuthTunnel::~AuthTunnel() +{ + if (serverFd) + shutdown(serverFd.get(), SHUT_RDWR); + + if (serverThread.joinable()) + serverThread.join(); +} + +struct TunneledAuthSource : auth::AuthSource +{ + struct State + { + /** + * File descriptor to send requests to the client. + */ + AutoCloseFD fd; + + FdSource from; + FdSink to; + + WorkerProto::ReadConn fromConn; + WorkerProto::WriteConn toConn; + + State(WorkerProto::Version clientVersion, AutoCloseFD && fd) + : fd(std::move(fd)) + , from(this->fd.get()) + , to(this->fd.get()) + , fromConn{.from = from, .version = clientVersion} + , toConn{.to = to, .version = clientVersion} + { + } + }; + + Sync state_; + + ref storeConfig; + + TunneledAuthSource(ref storeConfig, WorkerProto::Version clientVersion, AutoCloseFD && fd) + : state_(clientVersion, std::move(fd)) + , storeConfig(storeConfig) + { + } + + std::optional get(const auth::AuthData & request, bool required) override + { + auto state(state_.lock()); + + state->to << (int) WorkerProto::CallbackOp::FillAuth; + WorkerProto::Serialise::write(*storeConfig, state->toConn, request); + state->to << required; + state->to.flush(); + + if (readInt(state->from)) + return WorkerProto::Serialise>::read(*storeConfig, state->fromConn); + else + return std::nullopt; + } + + void erase(const auth::AuthData & authData) override + { + auto state(state_.lock()); + + state->to << (int) WorkerProto::CallbackOp::RejectAuth; + WorkerProto::Serialise::write(*storeConfig, state->toConn, authData); + state->to.flush(); + + readInt(state->from); + } +}; + +ref +makeTunneledAuthSource(ref storeConfig, WorkerProto::Version clientVersion, AutoCloseFD && clientFd) +{ + return make_ref(storeConfig, clientVersion, std::move(clientFd)); +} + +} diff --git a/src/libstore/auth-tunnel.hh b/src/libstore/auth-tunnel.hh new file mode 100644 index 00000000000..50152805ae8 --- /dev/null +++ b/src/libstore/auth-tunnel.hh @@ -0,0 +1,27 @@ +#pragma once + +#include "file-descriptor.hh" +#include "worker-protocol.hh" +#include "ref.hh" + +#include + +namespace nix { + +struct AuthTunnel +{ + AutoCloseFD clientFd, serverFd; + std::thread serverThread; + const WorkerProto::Version clientVersion; + AuthTunnel(StoreDirConfig & storeConfig, WorkerProto::Version clientVersion); + ~AuthTunnel(); +}; + +namespace auth { +struct AuthSource; +} + +ref +makeTunneledAuthSource(ref storeConfig, WorkerProto::Version clientVersion, AutoCloseFD && clientFd); + +} diff --git a/src/libstore/builtins.hh b/src/libstore/builtins.hh index 93558b49e23..80db56753d8 100644 --- a/src/libstore/builtins.hh +++ b/src/libstore/builtins.hh @@ -5,11 +5,13 @@ namespace nix { +namespace auth { class Authenticator; } + // TODO: make pluggable. void builtinFetchurl( const BasicDerivation & drv, const std::map & outputs, - const std::string & netrcData); + ref authenticator); void builtinUnpackChannel( const BasicDerivation & drv, diff --git a/src/libstore/builtins/fetchurl.cc b/src/libstore/builtins/fetchurl.cc index b9dfeba2f8e..4560b0f9239 100644 --- a/src/libstore/builtins/fetchurl.cc +++ b/src/libstore/builtins/fetchurl.cc @@ -9,16 +9,8 @@ namespace nix { void builtinFetchurl( const BasicDerivation & drv, const std::map & outputs, - const std::string & netrcData) + ref authenticator) { - /* Make the host's netrc data available. Too bad curl requires - this to be stored in a file. It would be nice if we could just - pass a pointer to the data. */ - if (netrcData != "") { - settings.netrcFile = "netrc"; - writeFile(settings.netrcFile, netrcData, 0600); - } - auto out = get(drv.outputs, "out"); if (!out) throw Error("'builtin:fetchurl' requires an 'out' output"); @@ -41,6 +33,7 @@ void builtinFetchurl( /* No need to do TLS verification, because we check the hash of the result anyway. */ FileTransferRequest request(url); + request.authenticator = authenticator; request.verifyTLS = false; request.decompress = false; diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 40163a6214d..a2e1ae4f10d 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -15,6 +15,10 @@ #include "derivations.hh" #include "args.hh" #include "git.hh" +#include "auth.hh" +#include "auth-tunnel.hh" + +#include #ifndef _WIN32 // TODO need graceful async exit support on Windows? # include "monitor-fd.hh" @@ -271,7 +275,7 @@ struct ClientSettings static void performOp(TunnelLogger * logger, ref store, TrustedFlag trusted, RecursiveFlag recursive, WorkerProto::Version clientVersion, - Source & from, BufferedSink & to, WorkerProto::Op op) + FdSource & from, BufferedSink & to, WorkerProto::Op op) { WorkerProto::ReadConn rconn { .from = from, @@ -1014,6 +1018,43 @@ static void performOp(TunnelLogger * logger, ref store, case WorkerProto::Op::ClearFailedPaths: throw Error("Removed operation %1%", op); + case WorkerProto::Op::InitCallback: { + // Indicate that we're ready to receive the file descriptor. + to << 0; + to.flush(); + + struct msghdr msg = {0}; + + char msgData[256]; + struct iovec io = { .iov_base = msgData, .iov_len = sizeof(msgData) }; + msg.msg_iov = &io; + msg.msg_iovlen = 1; + + char controlData[256]; + msg.msg_control = controlData; + msg.msg_controllen = sizeof(controlData); + + if (recvmsg(from.fd, &msg, 0) < 0) + throw SysError("receiving callback socket"); + + AutoCloseFD fd(*((int *) CMSG_DATA(CMSG_FIRSTHDR(&msg)))); + debug("received file descriptor %d from client", fd.get()); + + logger->startWork(); + + if (experimentalFeatureSettings.isEnabled(Xp::AuthForwarding) + && ((auth::authSettings.authForwarding == auth::AuthForwarding::TrustedUsers && trusted) + || (auth::authSettings.authForwarding == auth::AuthForwarding::AllUsers))) + auth::getAuthenticator()->addAuthSource( + makeTunneledAuthSource(store, clientVersion, std::move(fd))); + + logger->stopWork(); + to << 1; + to.flush(); + + break; + } + default: throw Error("invalid operation %1%", op); } diff --git a/src/libstore/filetransfer.cc b/src/libstore/filetransfer.cc index cbbb0fe7a34..4e068a82c33 100644 --- a/src/libstore/filetransfer.cc +++ b/src/libstore/filetransfer.cc @@ -7,6 +7,8 @@ #include "finally.hh" #include "callback.hh" #include "signals.hh" +#include "auth.hh" +#include "url.hh" #if ENABLE_S3 #include @@ -38,6 +40,12 @@ FileTransferSettings fileTransferSettings; static GlobalConfig::Register rFileTransferSettings(&fileTransferSettings); +FileTransferRequest::FileTransferRequest(std::string_view uri) + : uri(uri) + , parentAct(getCurActivity()) + , authenticator(auth::getAuthenticator()) +{ } + struct curlFileTransfer : public FileTransfer { CURLM * curlm = 0; @@ -71,6 +79,8 @@ struct curlFileTransfer : public FileTransfer curl_off_t writtenToSink = 0; + std::optional authData; + inline static const std::set successfulStatuses {200, 201, 204, 206, 304, 0 /* other protocol */}; /* Get the HTTP status code, or 0 for other protocols. */ long getHTTPStatus() @@ -359,10 +369,23 @@ struct curlFileTransfer : public FileTransfer curl_easy_setopt(req, CURLOPT_LOW_SPEED_LIMIT, 1L); curl_easy_setopt(req, CURLOPT_LOW_SPEED_TIME, fileTransferSettings.stalledDownloadTimeout.get()); - /* If no file exist in the specified path, curl continues to work - anyway as if netrc support was disabled. */ - curl_easy_setopt(req, CURLOPT_NETRC_FILE, settings.netrcFile.get().c_str()); - curl_easy_setopt(req, CURLOPT_NETRC, CURL_NETRC_OPTIONAL); + auto url = parseURL(request.uri); + auth::AuthData authRequest = { + .protocol = url.scheme, + .host = url.authority, + .path = request.authPath.value_or(url.path), + // FIXME: add username + }; + authData = request.authenticator->fill(authRequest, request.requireAuth); + + if (authData) { + if (authData->userName) + curl_easy_setopt(req, CURLOPT_USERNAME, authData->userName->c_str()); + if (authData->password) + curl_easy_setopt(req, CURLOPT_PASSWORD, authData->password->c_str()); + } + else + debug("no auth data for '%s'", request.uri); if (writtenToSink) curl_easy_setopt(req, CURLOPT_RESUME_FROM_LARGE, writtenToSink); @@ -418,7 +441,17 @@ struct curlFileTransfer : public FileTransfer if (httpStatus == 404 || httpStatus == 410 || code == CURLE_FILE_COULDNT_READ_FILE) { // The file is definitely not there err = NotFound; - } else if (httpStatus == 401 || httpStatus == 403 || httpStatus == 407) { + } else if (httpStatus == 401) { + if (authData) + /* This authentication data didn't work, so + erase it. */ + request.authenticator->reject(*authData); + if (authData || request.requireAuth) + // FIXME: call erase() on the auth and retry. + err = Forbidden; + else + request.requireAuth = true; + } else if (httpStatus == 403 || httpStatus == 407) { // Don't retry on authentication/authorization failures err = Forbidden; } else if (httpStatus >= 400 && httpStatus < 500 && httpStatus != 408 && httpStatus != 429) { @@ -490,7 +523,10 @@ struct curlFileTransfer : public FileTransfer || writtenToSink == 0 || (acceptRanges && encoding.empty()))) { - int ms = request.baseRetryTimeMs * std::pow(2.0f, attempt - 1 + std::uniform_real_distribution<>(0.0, 0.5)(fileTransfer.mt19937)); + int ms = + httpStatus == 401 + ? 0 + : request.baseRetryTimeMs * std::pow(2.0f, attempt - 1 + std::uniform_real_distribution<>(0.0, 0.5)(fileTransfer.mt19937)); if (writtenToSink) warn("%s; retrying from offset %d in %d ms", exc.what(), writtenToSink, ms); else diff --git a/src/libstore/filetransfer.hh b/src/libstore/filetransfer.hh index 1f5b4ab934c..049db08825b 100644 --- a/src/libstore/filetransfer.hh +++ b/src/libstore/filetransfer.hh @@ -12,6 +12,8 @@ namespace nix { +namespace auth { class Authenticator; } + struct FileTransferSettings : Config { Setting enableHttp2{this, true, "http2", @@ -65,9 +67,24 @@ struct FileTransferRequest std::optional data; std::string mimeType; std::function dataCallback; + ref authenticator; + + /** + * The path to be used for authentication (replacing the path part + * of `uri`). This is needed for efficient authentication + * caching. E.g. for a binary cache, the `authPart` will typically + * be `/`, ensuring that all paths underneath `/` + * (e.g. `/nix-cache-info` or `/foo.narinfo`) can hit the same + * authentication cache entry. + */ + std::optional authPath; + + /** + * Whether the authenticator *must* return authentication data. + */ + bool requireAuth = false; - FileTransferRequest(std::string_view uri) - : uri(uri), parentAct(getCurActivity()) { } + FileTransferRequest(std::string_view uri); std::string verb() { diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index dfe25f31726..b927f0479e6 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -1038,31 +1038,6 @@ public: Nix to use for downloads. )"}; - Setting netrcFile{ - this, fmt("%s/%s", nixConfDir, "netrc"), "netrc-file", - R"( - If set to an absolute path to a `netrc` file, Nix will use the HTTP - authentication credentials in this file when trying to download from - a remote host through HTTP or HTTPS. Defaults to - `$NIX_CONF_DIR/netrc`. - - The `netrc` file consists of a list of accounts in the following - format: - - machine my-machine - login my-username - password my-password - - For the exact syntax, see [the `curl` - documentation](https://ec.haxx.se/usingcurl-netrc.html). - - > **Note** - > - > This must be an absolute path, and `~` is not resolved. For - > example, `~/.netrc` won't resolve to your home directory's - > `.netrc`. - )"}; - Setting caFile{ this, getDefaultSSLCertFile(), "ssl-cert-file", R"( diff --git a/src/libstore/http-binary-cache-store.cc b/src/libstore/http-binary-cache-store.cc index 3328caef9c4..04e0d524bd0 100644 --- a/src/libstore/http-binary-cache-store.cc +++ b/src/libstore/http-binary-cache-store.cc @@ -3,6 +3,7 @@ #include "globals.hh" #include "nar-info-disk-cache.hh" #include "callback.hh" +#include "url.hh" namespace nix { @@ -28,6 +29,8 @@ class HttpBinaryCacheStore : public virtual HttpBinaryCacheStoreConfig, public v Path cacheUri; + std::string authRoot; + struct State { bool enabled = true; @@ -57,6 +60,8 @@ class HttpBinaryCacheStore : public virtual HttpBinaryCacheStoreConfig, public v while (!cacheUri.empty() && cacheUri.back() == '/') cacheUri.pop_back(); + authRoot = parseURL(cacheUri).path; + diskCache = getNarInfoDiskCache(); } @@ -149,11 +154,13 @@ class HttpBinaryCacheStore : public virtual HttpBinaryCacheStoreConfig, public v FileTransferRequest makeRequest(const std::string & path) { - return FileTransferRequest( - hasPrefix(path, "https://") || hasPrefix(path, "http://") || hasPrefix(path, "file://") - ? path - : cacheUri + "/" + path); - + if (hasPrefix(path, "https://") || hasPrefix(path, "http://") || hasPrefix(path, "file://")) + return FileTransferRequest(path); + else { + auto request = FileTransferRequest(cacheUri + "/" + path); + request.authPath = parseURL(cacheUri).path; + return request; + } } void getFile(const std::string & path, Sink & sink) override diff --git a/src/libstore/meson.build b/src/libstore/meson.build index 297bf71870c..8fe58a44097 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -150,6 +150,7 @@ add_project_arguments( subdir('build-utils-meson/diagnostics') sources = files( + 'auth-tunnel.cc', 'binary-cache-store.cc', 'build-result.cc', 'build/derivation-goal.cc', @@ -222,6 +223,7 @@ include_dirs = [ ] headers = [config_h] + files( + 'auth-tunnel.hh', 'binary-cache-store.hh', 'build-result.hh', 'build/derivation-goal.hh', diff --git a/src/libstore/remote-store.hh b/src/libstore/remote-store.hh index 4e18962683d..7d96232f43a 100644 --- a/src/libstore/remote-store.hh +++ b/src/libstore/remote-store.hh @@ -171,7 +171,7 @@ protected: virtual ref openConnection() = 0; - void initConnection(Connection & conn); + virtual void initConnection(Connection & conn); ref> connections; diff --git a/src/libstore/serve-protocol.hh b/src/libstore/serve-protocol.hh index 8c112bb74c7..3b88c8f9103 100644 --- a/src/libstore/serve-protocol.hh +++ b/src/libstore/serve-protocol.hh @@ -136,7 +136,7 @@ struct ServeProto::BuildOptions { /** * Convenience for sending operation codes. * - * @todo Switch to using `ServeProto::Serialize` instead probably. But + * @todo Switch to using `ServeProto::Serialise` instead probably. But * this was not done at this time so there would be less churn. */ inline Sink & operator << (Sink & sink, ServeProto::Command op) diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 2c4dee518b4..b10f06e6b80 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -434,7 +434,7 @@ StringSet StoreConfig::getDefaultSystemFeatures() Store::Store(const Params & params) : StoreConfig(params) - , state({(size_t) pathInfoCacheSize}) + , state({.pathInfoCache{(size_t) pathInfoCacheSize}}) { assertLibStoreInitialized(); } diff --git a/src/libstore/uds-remote-store.cc b/src/libstore/uds-remote-store.cc index 499f7696712..231faa9271d 100644 --- a/src/libstore/uds-remote-store.cc +++ b/src/libstore/uds-remote-store.cc @@ -1,6 +1,9 @@ #include "uds-remote-store.hh" #include "unix-domain-socket.hh" #include "worker-protocol.hh" +#include "worker-protocol-impl.hh" +#include "auth.hh" +#include "auth-tunnel.hh" #include #include @@ -85,6 +88,57 @@ ref UDSRemoteStore::openConnection() } +void UDSRemoteStore::initConnection(RemoteStore::Connection & _conn) +{ + Connection & conn(*(Connection *) &_conn); + + RemoteStore::initConnection(conn); + + if (GET_PROTOCOL_MINOR(conn.daemonVersion) >= 38 + && conn.remoteTrustsUs + && experimentalFeatureSettings.isEnabled(Xp::AuthForwarding)) + { + conn.authTunnel = std::make_unique( + *this, ((WorkerProto::ReadConn) _conn).version); + + conn.to << WorkerProto::Op::InitCallback; + conn.to.flush(); + + // Wait until the daemon is ready to receive the file + // descriptor. This is so that the fd doesn't get lost in the + // daemon's regular read() calls. + readInt(conn.from); + + struct msghdr msg = { 0 }; + char buf[CMSG_SPACE(sizeof(int))]; + memset(buf, '\0', sizeof(buf)); + struct iovec io = { .iov_base = (void *) "xy", .iov_len = 2 }; + + msg.msg_iov = &io; + msg.msg_iovlen = 1; + msg.msg_control = buf; + msg.msg_controllen = sizeof(buf); + + struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + + auto clientFd = std::move(conn.authTunnel->clientFd); + + *((int *) CMSG_DATA(cmsg)) = clientFd.get(); + + msg.msg_controllen = CMSG_SPACE(sizeof(int)); + + if (sendmsg(conn.fd.get(), &msg, 0) < 0) + throw SysError("sending callback socket to the daemon"); + + conn.processStderrReturn(); + readInt(conn.from); + } +} + + void UDSRemoteStore::addIndirectRoot(const Path & path) { auto conn(getConnection()); diff --git a/src/libstore/uds-remote-store.hh b/src/libstore/uds-remote-store.hh index 6f0494bb63b..5b7bb7bc219 100644 --- a/src/libstore/uds-remote-store.hh +++ b/src/libstore/uds-remote-store.hh @@ -7,6 +7,8 @@ namespace nix { +struct AuthTunnel; + struct UDSRemoteStoreConfig : virtual LocalFSStoreConfig, virtual RemoteStoreConfig { UDSRemoteStoreConfig(const Params & params) @@ -59,10 +61,12 @@ private: struct Connection : RemoteStore::Connection { AutoCloseFD fd; + std::unique_ptr authTunnel; void closeWrite() override; }; ref openConnection() override; + void initConnection(RemoteStore::Connection & conn) override; std::optional path; }; diff --git a/src/libstore/unix/build/local-derivation-goal.cc b/src/libstore/unix/build/local-derivation-goal.cc index f968bbc5b7f..4e4151b4c2a 100644 --- a/src/libstore/unix/build/local-derivation-goal.cc +++ b/src/libstore/unix/build/local-derivation-goal.cc @@ -19,6 +19,8 @@ #include "unix-domain-socket.hh" #include "posix-fs-canonicalise.hh" #include "posix-source-accessor.hh" +#include "auth.hh" +#include "auth-tunnel.hh" #include #include @@ -1195,6 +1197,12 @@ void LocalDerivationGoal::initEnv() /* Trigger colored output in various tools. */ env["TERM"] = "xterm-256color"; + + /* Set up authentication tunneling for builtin:fetchurl. FIXME: + maybe we want to support this for arbitrary fixed-output + derivations. */ + if (drv->builder == "builtin:fetchurl") + authTunnel = std::make_shared(worker.store, 0); } @@ -1742,14 +1750,6 @@ void LocalDerivationGoal::runChild() bool setUser = true; - /* Make the contents of netrc available to builtin:fetchurl - (which may run under a different uid and/or in a sandbox). */ - std::string netrcData; - try { - if (drv->isBuiltin() && drv->builder == "builtin:fetchurl") - netrcData = readFile(settings.netrcFile); - } catch (SystemError &) { } - #if __linux__ if (useChroot) { @@ -1982,7 +1982,10 @@ void LocalDerivationGoal::runChild() throw SysError("changing into '%1%'", tmpDir); /* Close all other file descriptors. */ - unix::closeMostFDs({STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO}); + std::set fdsToKeep{STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO}; + if (authTunnel) + fdsToKeep.insert(authTunnel->clientFd.get()); + unix::closeMostFDs(fdsToKeep); #if __linux__ linux::setPersonality(drv->platform); @@ -2186,8 +2189,15 @@ void LocalDerivationGoal::runChild() outputs.insert_or_assign(e.first, worker.store.printStorePath(scratchOutputs.at(e.first))); - if (drv->builder == "builtin:fetchurl") - builtinFetchurl(*drv, outputs, netrcData); + if (drv->builder == "builtin:fetchurl") { + auto authSource = + makeTunneledAuthSource( + ref(worker.store.shared_from_this()), + authTunnel->clientVersion, + std::move(authTunnel->clientFd)); + std::vector> authSources{authSource}; + builtinFetchurl(*drv, outputs, make_ref(authSources)); + } else if (drv->builder == "builtin:buildenv") builtinBuildenv(*drv, outputs); else if (drv->builder == "builtin:unpack-channel") @@ -2993,7 +3003,7 @@ void LocalDerivationGoal::deleteTmpDir(bool force) { if (topTmpDir != "") { /* Don't keep temporary directories for builtins because they - might have privileged stuff (like a copy of netrc). */ + might have privileged stuff. */ if (settings.keepFailed && !force && !drv->isBuiltin()) { printError("note: keeping build directory '%s'", tmpDir); chmod(tmpDir.c_str(), 0755); diff --git a/src/libstore/unix/build/local-derivation-goal.hh b/src/libstore/unix/build/local-derivation-goal.hh index bf25cf2a60b..84d176b1895 100644 --- a/src/libstore/unix/build/local-derivation-goal.hh +++ b/src/libstore/unix/build/local-derivation-goal.hh @@ -7,6 +7,8 @@ namespace nix { +struct AuthTunnel; + struct LocalDerivationGoal : public DerivationGoal { LocalStore & getLocalStore(); @@ -167,6 +169,12 @@ struct LocalDerivationGoal : public DerivationGoal */ std::set addedDrvOutputs; + /** + * State for tunneling authentication requests between the child + * (currently just `builtin:fetchurl`) and the parent. + */ + std::shared_ptr authTunnel; + /** * Recursive Nix calls are only allowed to build or realize paths * in the original input closure or added via a recursive Nix call diff --git a/src/libstore/worker-protocol.cc b/src/libstore/worker-protocol.cc index f06fb2893c7..af0eec159d5 100644 --- a/src/libstore/worker-protocol.cc +++ b/src/libstore/worker-protocol.cc @@ -6,12 +6,41 @@ #include "worker-protocol-impl.hh" #include "archive.hh" #include "path-info.hh" +#include "auth.hh" #include #include namespace nix { + +template +std::optional readOptional(const StoreDirConfig & store, WorkerProto::ReadConn conn) +{ + auto tag = readNum(conn.from); + switch (tag) { + case 0: + return std::nullopt; + case 1: + return WorkerProto::Serialise::read(store, conn); + default: + throw Error("Invalid optional tag from remote"); + } +} + + +template +void writeOptional(const StoreDirConfig & store, WorkerProto::WriteConn conn, const std::optional & x) +{ + if (!x.has_value()) { + conn.to << uint8_t{0}; + } else { + conn.to << uint8_t{1}; + WorkerProto::Serialise::write(store, conn, *x); + } +} + + /* protocol-specific definitions */ BuildMode WorkerProto::Serialise::read(const StoreDirConfig & store, WorkerProto::ReadConn conn) @@ -250,7 +279,6 @@ void WorkerProto::Serialise::write(const StoreDirConfig & } } - WorkerProto::ClientHandshakeInfo WorkerProto::Serialise::read(const StoreDirConfig & store, ReadConn conn) { WorkerProto::ClientHandshakeInfo res; @@ -281,4 +309,36 @@ void WorkerProto::Serialise::write(const Store } } +auth::AuthData WorkerProto::Serialise::read(const StoreDirConfig & store, ReadConn conn) +{ + auth::AuthData authData; + conn.from + >> authData.protocol + >> authData.host + >> authData.path + >> authData.userName + >> authData.password; + return authData; +} + +void WorkerProto::Serialise::write(const StoreDirConfig & store, WriteConn conn, const auth::AuthData & authData) +{ + conn.to + << authData.protocol + << authData.host + << authData.path + << authData.userName + << authData.password; +} + +std::optional WorkerProto::Serialise>::read(const StoreDirConfig & store, ReadConn conn) +{ + return readOptional(store, conn); +} + +void WorkerProto::Serialise>::write(const StoreDirConfig & store, WriteConn conn, const std::optional & authData) +{ + writeOptional(store, conn, authData); +} + } diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh index 62a12d18201..e56a1a56ed5 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -11,7 +11,7 @@ namespace nix { #define WORKER_MAGIC_1 0x6e697863 #define WORKER_MAGIC_2 0x6478696f -#define PROTOCOL_VERSION (1 << 8 | 37) +#define PROTOCOL_VERSION (1 << 8 | 38) #define GET_PROTOCOL_MAJOR(x) ((x) & 0xff00) #define GET_PROTOCOL_MINOR(x) ((x) & 0x00ff) @@ -37,6 +37,7 @@ struct ValidPathInfo; struct UnkeyedValidPathInfo; enum BuildMode : uint8_t; enum TrustedFlag : bool; +namespace auth { struct AuthData; } /** @@ -52,6 +53,11 @@ struct WorkerProto */ enum struct Op : uint64_t; + /** + * Enumeration of all the request types for the callback mechanism. + */ + enum struct CallbackOp : uint64_t; + /** * Version type for the protocol. * @@ -178,6 +184,7 @@ enum struct WorkerProto::Op : uint64_t AddBuildLog = 45, BuildPathsWithResults = 46, AddPermRoot = 47, + InitCallback = 48, }; struct WorkerProto::ClientHandshakeInfo @@ -228,6 +235,12 @@ inline std::ostream & operator << (std::ostream & s, WorkerProto::Op op) return s << static_cast(op); } +enum struct WorkerProto::CallbackOp : uint64_t +{ + FillAuth = 1, + RejectAuth = 2, +}; + /** * Declare a canonical serialiser pair for the worker protocol. * @@ -263,6 +276,10 @@ template<> DECLARE_WORKER_SERIALISER(std::optional); template<> DECLARE_WORKER_SERIALISER(WorkerProto::ClientHandshakeInfo); +template<> +DECLARE_WORKER_SERIALISER(auth::AuthData); +template<> +DECLARE_WORKER_SERIALISER(std::optional); template DECLARE_WORKER_SERIALISER(std::vector); diff --git a/src/libutil/auth.cc b/src/libutil/auth.cc new file mode 100644 index 00000000000..9260929c810 --- /dev/null +++ b/src/libutil/auth.cc @@ -0,0 +1,462 @@ +#include "auth.hh" +#include "file-system.hh" +#include "users.hh" +#include "util.hh" +#include "processes.hh" +#include "environment-variables.hh" +#include "config-global.hh" +#include "config-impl.hh" +#include "abstract-setting-to-json.hh" + +#include + +namespace nix { + +using namespace auth; + +// FIXME: need to generalize defining enum settings. +template<> +AuthForwarding BaseSetting::parse(const std::string & str) const +{ + if (str == "false") + return AuthForwarding::Disabled; + else if (str == "trusted-users") + return AuthForwarding::TrustedUsers; + else if (str == "all-users") + return AuthForwarding::AllUsers; + else + throw UsageError("option '%s' has invalid value '%s'", name, str); +} + +template<> +struct BaseSetting::trait +{ + static constexpr bool appendable = false; +}; + +template<> +std::string BaseSetting::to_string() const +{ + if (value == AuthForwarding::Disabled) + return "false"; + else if (value == AuthForwarding::TrustedUsers) + return "trusted-users"; + else if (value == AuthForwarding::AllUsers) + return "all-users"; + else + abort(); +} + +NLOHMANN_JSON_SERIALIZE_ENUM( + AuthForwarding, + { + {AuthForwarding::Disabled, "false"}, + {AuthForwarding::TrustedUsers, "trusted-users"}, + {AuthForwarding::AllUsers, "all-users"}, + }); + +} + +namespace nix::auth { + +AuthSettings authSettings; + +static GlobalConfig::Register rAuthSettings(&authSettings); + +AuthData AuthData::parseGitAuthData(std::string_view raw) +{ + AuthData res; + + for (auto & line : tokenizeString>(raw, "\n")) { + auto eq = line.find('='); + if (eq == line.npos) + continue; + auto key = trim(line.substr(0, eq)); + auto value = trim(line.substr(eq + 1)); + if (key == "protocol") + res.protocol = value; + else if (key == "host") + res.host = value; + else if (key == "path") + res.path = value; + else if (key == "username") + res.userName = value; + else if (key == "password") + res.password = value; + } + + return res; +} + +std::optional AuthData::match(const AuthData & request) const +{ + if (protocol && request.protocol && *protocol != *request.protocol) + return std::nullopt; + + if (host && request.host && *host != *request.host) + return std::nullopt; + + // `request.path` must be within `path`. + if (path && request.path + && !(*path == *request.path || request.path->substr(0, path->size() + 1) == *request.path + "/")) + return std::nullopt; + + if (userName && request.userName && *userName != request.userName) + return std::nullopt; + + if (password && request.password && *password != request.password) + return std::nullopt; + + AuthData res{request}; + if (!res.userName) + res.userName = userName; + if (!res.password) + res.password = password; + return res; +} + +std::string AuthData::toGitAuthData() const +{ + std::string res; + if (protocol) + res += fmt("protocol=%s\n", *protocol); + if (host) + res += fmt("host=%s\n", *host); + if (path) + res += fmt("path=%s\n", *path); + if (userName) + res += fmt("username=%s\n", *userName); + if (password) + res += fmt("password=%s\n", *password); + return res; +} + +std::ostream & operator<<(std::ostream & str, const AuthData & authData) +{ + str << fmt( + "{protocol = %s, host=%s, path=%s, userName=%s, password=%s}", + authData.protocol.value_or(""), + authData.host.value_or(""), + authData.path.value_or(""), + authData.userName.value_or(""), + authData.password ? "..." : ""); + return str; +} + +struct NixAuthSource : AuthSource +{ + const std::filesystem::path authDir; + + std::vector authDatas; + + NixAuthSource() + : authDir(std::filesystem::path(getDataDir()) / "nix" / "auth") + { + if (pathExists(authDir)) + for (auto & file : std::filesystem::directory_iterator{authDir}) { + if (hasSuffix(file.path().filename().string(), "~")) + continue; + auto path = authDir / file.path().filename(); + auto authData = AuthData::parseGitAuthData(readFile(path)); + if (!authData.password) + warn("authentication file '%s' does not contain a password, skipping", path); + else + authDatas.push_back(authData); + } + } + + std::optional get(const AuthData & request, bool required) override + { + for (auto & authData : authDatas) + if (auto res = authData.match(request)) + return res; + + return std::nullopt; + } + + bool set(const AuthData & authData) override + { + if (get(authData, false)) + return true; + + auto authFile = authDir / fmt("auto-%s-%s", authData.host.value_or("none"), authData.userName.value_or("none")); + + writeFile(authFile, authData.toGitAuthData()); + + return true; + } +}; + +struct NetrcAuthSource : AuthSource +{ + const Path path; + std::vector authDatas; + + NetrcAuthSource(const Path & path) + : path(path) + { + // FIXME: read netrc lazily. + debug("reading netrc '%s'", path); + + if (!pathExists(path)) + return; + + auto raw = readFile(path); + + std::string_view remaining(raw); + + auto whitespace = "\n\r\t "; + + auto nextToken = [&]() -> std::optional { + // Skip whitespace. + auto n = remaining.find_first_not_of(whitespace); + if (n == remaining.npos) + return std::nullopt; + remaining = remaining.substr(n); + + if (remaining.substr(0, 1) == "\"") + throw UnimplementedError("quoted tokens in netrc are not supported yet"); + + n = remaining.find_first_of(whitespace); + auto token = remaining.substr(0, n); + remaining = remaining.substr(n == remaining.npos ? remaining.size() : n); + + return token; + }; + + std::optional curMachine; + + auto flushMachine = [&]() { + if (curMachine) { + authDatas.push_back(std::move(*curMachine)); + curMachine.reset(); + } + }; + + while (auto token = nextToken()) { + if (token == "machine") { + flushMachine(); + auto name = nextToken(); + if (!name) + throw Error("netrc 'machine' token requires a name"); + curMachine = AuthData{.protocol = "https", .host = std::string(*name)}; + } else if (token == "default") { + flushMachine(); + curMachine = AuthData{ + .protocol = "https", + }; + } else if (token == "login") { + if (!curMachine) + throw Error("netrc 'login' token must be preceded by a 'machine'"); + auto userName = nextToken(); + if (!userName) + throw Error("netrc 'login' token requires a user name"); + curMachine->userName = std::string(*userName); + } else if (token == "password") { + if (!curMachine) + throw Error("netrc 'password' token must be preceded by a 'machine'"); + auto password = nextToken(); + if (!password) + throw Error("netrc 'password' token requires a password"); + curMachine->password = std::string(*password); + } else if (token == "account") { + // Ignore this. + nextToken(); + } else + warn("unrecognized netrc token '%s'", *token); + } + + flushMachine(); + } + + std::optional get(const AuthData & request, bool required) override + { + for (auto & authData : authDatas) + if (auto res = authData.match(request)) + return res; + + return std::nullopt; + } +}; + +/** + * Authenticate using an external helper program via the + * `git-credential-*` protocol. + */ +struct ExternalAuthSource : AuthSource +{ + bool enabled = true; + Path program; + + ExternalAuthSource(Path program) + : program(program) + { + experimentalFeatureSettings.require(Xp::PluggableAuth); + } + + std::optional get(const AuthData & request, bool required) override + { + try { + if (!enabled) + return std::nullopt; + + auto response = AuthData::parseGitAuthData(runProgram(program, true, {"get"}, request.toGitAuthData())); + + if (!response.password) + return std::nullopt; + + AuthData res{request}; + if (response.userName) + res.userName = response.userName; + res.password = response.password; + return res; + } catch (SysError & e) { + ignoreException(); + if (e.errNo == ENOENT || e.errNo == EPIPE) + enabled = false; + return std::nullopt; + } catch (Error &) { + ignoreException(); + return std::nullopt; + } + } + + bool set(const AuthData & authData) override + { + try { + if (!enabled) + return false; + + runProgram(program, true, {"store"}, authData.toGitAuthData()); + + return true; + } catch (SysError & e) { + ignoreException(); + if (e.errNo == ENOENT || e.errNo == EPIPE) + enabled = false; + return false; + } catch (Error &) { + ignoreException(); + return false; + } + } + + void erase(const AuthData & authData) override + { + try { + if (!enabled) + return; + + runProgram(program, true, {"erase"}, authData.toGitAuthData()); + } catch (SysError & e) { + ignoreException(); + if (e.errNo == ENOENT || e.errNo == EPIPE) + enabled = false; + } catch (Error &) { + ignoreException(); + } + } +}; + +std::optional Authenticator::fill(const AuthData & request, bool required) +{ + if (!request.protocol) + throw Error("authentication data '%s' does not contain a protocol", request); + + if (!request.host) + throw Error("authentication data '%s' does not contain a host", request); + + for (auto & entry : cache) { + if (auto res = entry.match(request)) { + debug("authentication cache hit %s -> %s", entry, *res); + return res; + } + } + + for (auto & authSource : authSources) { + auto res = authSource->get(request, required); + if (res) { + cache.push_back(*res); + return res; + } + } + + if (required) { + auto askPassHelper = getEnvNonEmpty("SSH_ASKPASS"); + if (askPassHelper) { + /* Ask the user. */ + auto res = request; + + // Note: see + // https://github.com/KDE/ksshaskpass/blob/master/src/main.cpp + // for the expected format of the phrases. + + if (!request.userName) { + res.userName = chomp(runProgram( + *askPassHelper, true, {fmt("Username for '%s': ", request.host.value_or(""))}, std::nullopt, true)); + } + + if (!request.password) { + res.password = chomp(runProgram( + *askPassHelper, true, {fmt("Password for '%s': ", request.host.value_or(""))}, std::nullopt, true)); + } + + if (res.userName && res.password) { + cache.push_back(res); + + if (authSettings.storeAuth) { + for (auto & authSource : authSources) { + if (authSource->set(res)) + break; + } + } + } + + return res; + } + } + + return std::nullopt; +} + +void Authenticator::reject(const AuthData & authData) +{ + debug("erasing auth data %s", authData); + for (auto & authSource : authSources) + authSource->erase(authData); +} + +void Authenticator::addAuthSource(ref authSource) +{ + authSources.push_back(authSource); +} + +void Authenticator::setAuthSource(ref authSource) +{ + authSources = {authSource}; +} + +ref getAuthenticator() +{ + static auto authenticator = ({ + std::vector> authSources; + + for (auto & s : authSettings.authSources.get()) { + if (hasPrefix(s, "builtin:")) { + if (s == "builtin:nix") + authSources.push_back(make_ref()); + else if (s == "builtin:netrc") { + if (authSettings.netrcFile != "") + authSources.push_back(make_ref(authSettings.netrcFile)); + } else + warn("unknown authentication sources '%s'", s); + } else + authSources.push_back(make_ref(s)); + } + + make_ref(authSources); + }); + return authenticator; +} + +} diff --git a/src/libutil/auth.hh b/src/libutil/auth.hh new file mode 100644 index 00000000000..a7dedb2db45 --- /dev/null +++ b/src/libutil/auth.hh @@ -0,0 +1,154 @@ +#pragma once + +#include + +#include "types.hh" +#include "config.hh" +#include "ref.hh" + +namespace nix::auth { + +enum struct AuthForwarding { Disabled, TrustedUsers, AllUsers }; + +struct AuthSettings : Config +{ + Setting authSources{ + this, + {"builtin:nix", "builtin:netrc"}, + "auth-sources", + R"( + A list of helper programs from which to obtain + authentication data for HTTP requests. These helpers use + [the same protocol as Git's credential + helpers](https://git-scm.com/docs/gitcredentials#_custom_helpers), + so any Git credential helper can be used as an + authentication source. + + Nix has the following builtin helpers: + + * `builtin:nix`: Get authentication data from files in + `~/.local/share/nix/auth`. For example, the following sets + a username and password for `cache.example.org`: + + ``` + # cat < ~/.local/share/nix/auth/my-cache + protocol=https + host=cache.example.org + username=alice + password=foobar + EOF + ``` + + Example: `builtin:nix` `git-credential-libsecret` + )"}; + + Setting netrcFile{ + this, + "", + "netrc-file", + R"( + An absolute path to a `netrc` file. Nix will use the HTTP + authentication credentials in this file when trying to download from + a remote host through HTTP or HTTPS. Defaults to + `$NIX_CONF_DIR/netrc`. + + The `netrc` file consists of a list of accounts in the following + format: + + machine my-machine + login my-username + password my-password + + For the exact syntax, see [the `curl` + documentation](https://ec.haxx.se/usingcurl-netrc.html). + + > **Note** + > + > This must be an absolute path, and `~` is not resolved. For + > example, `~/.netrc` won't resolve to your home directory's + > `.netrc`. + )"}; + + Setting storeAuth{ + this, + false, + "store-auth", + R"( + Whether to store user names and passwords using the + authentication sources configured in [`auth-sources`](#conf-auth-sources). + )"}; + + Setting authForwarding{ + this, + AuthForwarding::TrustedUsers, + "auth-forwarding", + R"( + Whether to forward authentication data to the Nix daemon. This setting can have the following values: + + * `false`: Forwarding is disabled. + * `trusted-users`: Forwarding is only supported for [trusted users](#conf-trusted-users). + * `all-users`: Forwarding is supported for all users. + )", + {}, + true, + Xp::AuthForwarding}; +}; + +extern AuthSettings authSettings; + +struct AuthData +{ + std::optional protocol; + std::optional host; + std::optional path; + std::optional userName; + std::optional password; + + static AuthData parseGitAuthData(std::string_view raw); + + std::optional match(const AuthData & request) const; + + std::string toGitAuthData() const; +}; + +std::ostream & operator<<(std::ostream & str, const AuthData & authData); + +struct AuthSource +{ + virtual ~AuthSource() {} + + virtual std::optional get(const AuthData & request, bool required) = 0; + + virtual bool set(const AuthData & authData) + { + return false; + } + + virtual void erase(const AuthData & authData) {} +}; + +class Authenticator +{ + std::vector> authSources; + + std::vector cache; + +public: + + Authenticator(std::vector> authSources = {}) + : authSources(std::move(authSources)) + { + } + + std::optional fill(const AuthData & request, bool required); + + void reject(const AuthData & authData); + + void addAuthSource(ref authSource); + + void setAuthSource(ref authSource); +}; + +ref getAuthenticator(); + +} diff --git a/src/libutil/config-impl.hh b/src/libutil/config-impl.hh index 1d349fab5db..debdc3776ed 100644 --- a/src/libutil/config-impl.hh +++ b/src/libutil/config-impl.hh @@ -13,6 +13,7 @@ */ #include "config.hh" +#include "args.hh" namespace nix { diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index 1c080e372f6..b5ed6c6f4f5 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -24,7 +24,7 @@ struct ExperimentalFeatureDetails * feature, we either have no issue at all if few features are not added * at the end of the list, or a proper merge conflict if they are. */ -constexpr size_t numXpFeatures = 1 + static_cast(Xp::VerifiedFetches); +constexpr size_t numXpFeatures = 1 + static_cast(Xp::PluggableAuth); constexpr std::array xpFeatureDetails = {{ { @@ -294,6 +294,22 @@ constexpr std::array xpFeatureDetails )", .trackingUrl = "https://github.com/NixOS/nix/milestone/48", }, + { + .tag = Xp::AuthForwarding, + .name = "auth-forwarding", + .description = R"( + Whether to forward authentication data from the client to the daemon. + )", + .trackingUrl = "https://github.com/NixOS/nix/pull/9857", + }, + { + .tag = Xp::PluggableAuth, + .name = "pluggable-auth", + .description = R"( + Whether to support pluggable authentication via external credential helpers. + )", + .trackingUrl = "https://github.com/NixOS/nix/pull/9857", + }, }}; static_assert( diff --git a/src/libutil/experimental-features.hh b/src/libutil/experimental-features.hh index 6ffbc0c1028..57ec07fe160 100644 --- a/src/libutil/experimental-features.hh +++ b/src/libutil/experimental-features.hh @@ -35,6 +35,8 @@ enum struct ExperimentalFeature ConfigurableImpureEnv, MountedSSHStore, VerifiedFetches, + AuthForwarding, + PluggableAuth, }; /** diff --git a/src/libutil/meson.build b/src/libutil/meson.build index fbfcbe67c42..019e734a845 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -119,6 +119,7 @@ subdir('build-utils-meson/diagnostics') sources = files( 'archive.cc', 'args.cc', + 'auth.cc', 'canon-path.cc', 'compression.cc', 'compute-levels.cc', @@ -168,6 +169,7 @@ headers = [config_h] + files( 'archive.hh', 'args.hh', 'args/root.hh', + 'auth.hh', 'callback.hh', 'canon-path.hh', 'chunked-vector.hh', diff --git a/src/libutil/serialise.hh b/src/libutil/serialise.hh index 18f4a79c398..ea5d8f087c9 100644 --- a/src/libutil/serialise.hh +++ b/src/libutil/serialise.hh @@ -444,6 +444,41 @@ Source & operator >> (Source & in, bool & b) Error readError(Source & source); +template +inline Sink & operator << (Sink & sink, const std::optional & x) +{ + if (!x.has_value()) { + sink << uint8_t{0}; + } else { + sink + << uint8_t{1} + << *x; + } + return sink; +} + + +template +Source & operator >> (Source & in, std::optional & x) +{ + auto tag = readNum(in); + switch (tag) { + case 0: + x = std::nullopt; + break; + case 1: { + T x2; + in >> x2; + x = std::move(x2); + break; + } + default: + throw Error("Invalid optional tag from remote"); + } + return in; +} + + /** * An adapter that converts a std::basic_istream into a source. */ diff --git a/src/libutil/sync.hh b/src/libutil/sync.hh index 20dd6ee52bc..4c37ea37b3a 100644 --- a/src/libutil/sync.hh +++ b/src/libutil/sync.hh @@ -38,6 +38,9 @@ public: SyncBase(const T & data) : data(data) { } SyncBase(T && data) noexcept : data(std::move(data)) { } + template + SyncBase(Args &&... args) : data(std::forward(args)...) { } + template class Lock { diff --git a/src/libutil/unix-domain-socket.cc b/src/libutil/unix-domain-socket.cc index 1707fdb75e1..f81d97f3be6 100644 --- a/src/libutil/unix-domain-socket.cc +++ b/src/libutil/unix-domain-socket.cc @@ -14,13 +14,15 @@ namespace nix { +#ifdef SOCK_CLOEXEC +#define MAYBE_SOCK_CLOEXEC SOCK_CLOEXEC +#else +#define MAYBE_SOCK_CLOEXEC 0 +#endif + AutoCloseFD createUnixDomainSocket() { - AutoCloseFD fdSocket = toDescriptor(socket(PF_UNIX, SOCK_STREAM - #ifdef SOCK_CLOEXEC - | SOCK_CLOEXEC - #endif - , 0)); + AutoCloseFD fdSocket = toDescriptor(socket(PF_UNIX, SOCK_STREAM | MAYBE_SOCK_CLOEXEC, 0)); if (!fdSocket) throw SysError("cannot create Unix domain socket"); #ifndef _WIN32 @@ -114,4 +116,14 @@ void connect(Socket fd, const std::string & path) bindConnectProcHelper("connect", ::connect, fd, path); } +std::pair socketPair() +{ + int sockets[2]; + if (socketpair(PF_UNIX, SOCK_STREAM | MAYBE_SOCK_CLOEXEC, 0, sockets)) + throw SysError("creating a socket pair"); + unix::closeOnExec(sockets[0]); + unix::closeOnExec(sockets[1]); + return {AutoCloseFD(sockets[0]), AutoCloseFD(sockets[1])}; +} + } diff --git a/src/libutil/unix-domain-socket.hh b/src/libutil/unix-domain-socket.hh index ba2baeb1334..b6b23a4f42c 100644 --- a/src/libutil/unix-domain-socket.hh +++ b/src/libutil/unix-domain-socket.hh @@ -80,4 +80,9 @@ void bind(Socket fd, const std::string & path); */ void connect(Socket fd, const std::string & path); +/** + * Create an unnamed pair of connected sockets. + */ +std::pair socketPair(); + } diff --git a/src/nix/auth.cc b/src/nix/auth.cc new file mode 100644 index 00000000000..687dceb03bb --- /dev/null +++ b/src/nix/auth.cc @@ -0,0 +1,67 @@ +#include "auth.hh" +#include "command.hh" +#include "progress-bar.hh" + +using namespace nix; + +struct CmdAuthFill : Command +{ + bool require = false; + + CmdAuthFill() + { + addFlag({ + .longName = "require", + .description = "Prompt the user for authentication if no authentication source provides it.", + .handler = {&require, true}, + }); + } + + std::string description() override + { + return "obtain a user name and password from the configured authentication sources"; + } + +#if 0 + std::string doc() override + { + return +# include "auth-fill.md" + ; + } +#endif + + void run() override + { + using namespace auth; + stopProgressBar(); + auto authRequest = AuthData::parseGitAuthData(drainFD(STDIN_FILENO)); + auto authData = getAuthenticator()->fill(authRequest, require); + if (authData) + writeFull(STDOUT_FILENO, authData->toGitAuthData()); + } +}; + +struct CmdAuth : NixMultiCommand +{ + CmdAuth() + : NixMultiCommand( + "auth", + { + {"fill", []() { return make_ref(); }}, + }) + { + } + + std::string description() override + { + return "authentication-related commands"; + } + + Category category() override + { + return catUtility; + } +}; + +static auto rCmdAuth = registerCommand("auth"); diff --git a/src/nix/main.cc b/src/nix/main.cc index 00ad6fe2c97..80e88336f8c 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -19,6 +19,7 @@ #include "network-proxy.hh" #include "eval-cache.hh" #include "flake/flake.hh" +#include "auth.hh" #include #include @@ -406,6 +407,10 @@ void mainWrapped(int argc, char * * argv) verbosity = lvlInfo; } + // FIXME: this is a hack to work around the fact that nixConfDir + // is defined in libstore. Should move that to libutil. + auth::authSettings.netrcFile.setDefault(fmt("%s/%s", settings.nixConfDir, "netrc")); + NixArgs args; if (argc == 2 && std::string(argv[1]) == "__dump-cli") { diff --git a/src/nix/meson.build b/src/nix/meson.build index dd21c4b1bca..26234d29366 100644 --- a/src/nix/meson.build +++ b/src/nix/meson.build @@ -48,6 +48,7 @@ subdir('build-utils-meson/generate-header') nix_sources = files( 'add-to-store.cc', 'app.cc', + 'auth.cc', 'build.cc', 'bundle.cc', 'cat.cc', diff --git a/src/nix/unix/daemon.cc b/src/nix/unix/daemon.cc index 4a7997b1fb0..5d088e3d076 100644 --- a/src/nix/unix/daemon.cc +++ b/src/nix/unix/daemon.cc @@ -454,6 +454,9 @@ static void processStdioConnection(ref store, TrustedFlag trustClient) */ static void runDaemon(bool stdio, std::optional forceTrustClientOpt, bool processOps) { + /* Don't ask for authentication in auth.cc. */ + unsetenv("SSH_ASKPASS"); + if (stdio) { auto store = openUncachedStore(); diff --git a/tests/functional/auth.sh b/tests/functional/auth.sh new file mode 100644 index 00000000000..ad8ee3bca3a --- /dev/null +++ b/tests/functional/auth.sh @@ -0,0 +1,61 @@ +source common.sh + +authDir="$HOME/.local/share/nix/auth" +mkdir -p "$authDir" + +printf "protocol=https\nhost=example.org\nusername=alice\npassword=foobar\n" > $authDir/example.org + +[[ $(printf "protocol=https\nhost=example.org\nusername=alice\n" | nix auth fill) = $(printf "protocol=https\nhost=example.org\nusername=alice\npassword=foobar\n") ]] +[[ $(printf "protocol=https\nhost=example.org\n" | nix auth fill) = $(printf "protocol=https\nhost=example.org\nusername=alice\npassword=foobar\n") ]] +(! printf "host=example.org\n" | nix auth fill) +[[ $(printf "protocol=https\nhost=example.org\nusername=bob\n" | nix auth fill) = "" ]] + +# Test interactive prompting. +unset SSH_ASKPASS +[[ $(printf "protocol=https\nhost=fnord.org\nusername=bob\n" | nix auth fill --require) = "" ]] + +askpass="$TEST_ROOT/ask-pass" +cat > $askpass <&2 +if [[ \$prompt =~ Password ]]; then + printf "foobar" +elif [[ \$prompt =~ Username ]]; then + printf "alice" +else + exit 1 +fi +EOF +chmod +x $askpass +export SSH_ASKPASS=$askpass + +[[ $(printf "protocol=https\nhost=fnord.org\nusername=bob\n" | nix auth fill --require) = $(printf "protocol=https\nhost=fnord.org\nusername=bob\npassword=foobar\n") ]] + +[[ $(printf "protocol=https\nhost=fnord.org\n" | nix auth fill --require) = $(printf "protocol=https\nhost=fnord.org\nusername=alice\npassword=foobar\n") ]] + +# Test storing authentication. +[[ $(printf "protocol=https\nhost=fnord.org\n" | nix auth fill --require --store-auth) = $(printf "protocol=https\nhost=fnord.org\nusername=alice\npassword=foobar\n") ]] +unset SSH_ASKPASS +[[ $(printf "protocol=https\nhost=fnord.org\n" | nix auth fill --require --store-auth) = $(printf "protocol=https\nhost=fnord.org\nusername=alice\npassword=foobar\n") ]] + +# Test authentication helpers. +myHelper="$TEST_ROOT/auth-helper" +cat > $myHelper <