Add Store::isTrustedClient()

This function returns true or false depending on whether the Nix client
is trusted or not. Mostly relevant when speaking to a remote store with
a daemon.

We include this information in `nix ping store` and `nix doctor`

Co-Authored-By: John Ericson <John.Ericson@Obsidian.Systems>
This commit is contained in:
matthewcroughan 2022-12-26 20:21:08 +00:00 committed by John Ericson
parent 9185639631
commit 9207f94582
21 changed files with 169 additions and 3 deletions

View file

@ -39,3 +39,9 @@
* `man nix-store-<operation>` and `man nix-env-<operation>` * `man nix-store-<operation>` and `man nix-env-<operation>`
* `nix-store --help --<operation>` and `nix-env --help --<operation>`. * `nix-store --help --<operation>` and `nix-env --help --<operation>`.
* Nix when used as a client now checks whether the store (the server) trusts the client.
(The store always had to check whether it trusts the client, but now the client is informed of the store's decision.)
This is useful for scripting interactions with (non-legacy-ssh) remote Nix stores.
`nix store ping` and `nix doctor` now display this information.

View file

@ -1415,6 +1415,9 @@ struct RestrictedStore : public virtual RestrictedStoreConfig, public virtual Lo
virtual void addBuildLog(const StorePath & path, std::string_view log) override virtual void addBuildLog(const StorePath & path, std::string_view log) override
{ unsupported("addBuildLog"); } { unsupported("addBuildLog"); }
std::optional<TrustedFlag> isTrustedClient() override
{ return NotTrusted; }
}; };
@ -1467,7 +1470,7 @@ void LocalDerivationGoal::startDaemon()
FdSink to(remote.get()); FdSink to(remote.get());
try { try {
daemon::processConnection(store, from, to, daemon::processConnection(store, from, to,
daemon::NotTrusted, daemon::Recursive); NotTrusted, daemon::Recursive);
debug("terminated daemon connection"); debug("terminated daemon connection");
} catch (SysError &) { } catch (SysError &) {
ignoreException(); ignoreException();

View file

@ -1032,6 +1032,15 @@ void processConnection(
if (GET_PROTOCOL_MINOR(clientVersion) >= 33) if (GET_PROTOCOL_MINOR(clientVersion) >= 33)
to << nixVersion; to << nixVersion;
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
? store->isTrustedClient()
: std::optional { NotTrusted };
worker_proto::write(*store, to, temp);
}
/* Send startup error messages to the client. */ /* Send startup error messages to the client. */
tunnelLogger->startWork(); tunnelLogger->startWork();

View file

@ -6,7 +6,6 @@
namespace nix::daemon { namespace nix::daemon {
enum TrustedFlag : bool { NotTrusted = false, Trusted = true };
enum RecursiveFlag : bool { NotRecursive = false, Recursive = true }; enum RecursiveFlag : bool { NotRecursive = false, Recursive = true };
void processConnection( void processConnection(

View file

@ -39,6 +39,14 @@ struct DummyStore : public virtual DummyStoreConfig, public virtual Store
callback(nullptr); callback(nullptr);
} }
/**
* The dummy store is incapable of *not* trusting! :)
*/
virtual std::optional<TrustedFlag> isTrustedClient() override
{
return Trusted;
}
static std::set<std::string> uriSchemes() { static std::set<std::string> uriSchemes() {
return {"dummy"}; return {"dummy"};
} }

View file

@ -194,6 +194,18 @@ protected:
}}); }});
} }
/**
* This isn't actually necessary read only. We support "upsert" now, so we
* have a notion of authentication via HTTP POST/PUT.
*
* For now, we conservatively say we don't know.
*
* \todo try to expose our HTTP authentication status.
*/
std::optional<TrustedFlag> isTrustedClient() override
{
return std::nullopt;
}
}; };
static RegisterStoreImplementation<HttpBinaryCacheStore, HttpBinaryCacheStoreConfig> regHttpBinaryCacheStore; static RegisterStoreImplementation<HttpBinaryCacheStore, HttpBinaryCacheStoreConfig> regHttpBinaryCacheStore;

View file

@ -389,6 +389,15 @@ public:
return conn->remoteVersion; return conn->remoteVersion;
} }
/**
* The legacy ssh protocol doesn't support checking for trusted-user.
* Try using ssh-ng:// instead if you want to know.
*/
std::optional<TrustedFlag> isTrustedClient() override
{
return std::nullopt;
}
void queryRealisationUncached(const DrvOutput &, void queryRealisationUncached(const DrvOutput &,
Callback<std::shared_ptr<const Realisation>> callback) noexcept override Callback<std::shared_ptr<const Realisation>> callback) noexcept override
// TODO: Implement // TODO: Implement

View file

@ -95,6 +95,10 @@ protected:
return paths; return paths;
} }
std::optional<TrustedFlag> isTrustedClient() override
{
return Trusted;
}
}; };
void LocalBinaryCacheStore::init() void LocalBinaryCacheStore::init()

View file

@ -1685,6 +1685,11 @@ unsigned int LocalStore::getProtocol()
return PROTOCOL_VERSION; return PROTOCOL_VERSION;
} }
std::optional<TrustedFlag> LocalStore::isTrustedClient()
{
return Trusted;
}
#if defined(FS_IOC_SETFLAGS) && defined(FS_IOC_GETFLAGS) && defined(FS_IMMUTABLE_FL) #if defined(FS_IOC_SETFLAGS) && defined(FS_IOC_GETFLAGS) && defined(FS_IMMUTABLE_FL)

View file

@ -204,6 +204,8 @@ public:
unsigned int getProtocol() override; unsigned int getProtocol() override;
std::optional<TrustedFlag> isTrustedClient() override;
void vacuumDB(); void vacuumDB();
void repairPath(const StorePath & path) override; void repairPath(const StorePath & path) override;

View file

@ -42,6 +42,40 @@ void write(const Store & store, Sink & out, const StorePath & storePath)
} }
std::optional<TrustedFlag> read(const Store & store, Source & from, Phantom<std::optional<TrustedFlag>> _)
{
auto temp = readNum<uint8_t>(from);
switch (temp) {
case 0:
return std::nullopt;
case 1:
return { Trusted };
case 2:
return { NotTrusted };
default:
throw Error("Invalid trusted status from remote");
}
}
void write(const Store & store, Sink & out, const std::optional<TrustedFlag> & optTrusted)
{
if (!optTrusted)
out << (uint8_t)0;
else {
switch (*optTrusted) {
case Trusted:
out << (uint8_t)1;
break;
case NotTrusted:
out << (uint8_t)2;
break;
default:
assert(false);
};
}
}
ContentAddress read(const Store & store, Source & from, Phantom<ContentAddress> _) ContentAddress read(const Store & store, Source & from, Phantom<ContentAddress> _)
{ {
return parseContentAddress(readString(from)); return parseContentAddress(readString(from));
@ -226,6 +260,13 @@ void RemoteStore::initConnection(Connection & conn)
conn.daemonNixVersion = readString(conn.from); conn.daemonNixVersion = readString(conn.from);
} }
if (GET_PROTOCOL_MINOR(conn.daemonVersion) >= 35) {
conn.remoteTrustsUs = worker_proto::read(*this, conn.from, Phantom<std::optional<TrustedFlag>> {});
} else {
// We don't know the answer; protocol to old.
conn.remoteTrustsUs = std::nullopt;
}
auto ex = conn.processStderr(); auto ex = conn.processStderr();
if (ex) std::rethrow_exception(ex); if (ex) std::rethrow_exception(ex);
} }
@ -1082,6 +1123,11 @@ unsigned int RemoteStore::getProtocol()
return conn->daemonVersion; return conn->daemonVersion;
} }
std::optional<TrustedFlag> RemoteStore::isTrustedClient()
{
auto conn(getConnection());
return conn->remoteTrustsUs;
}
void RemoteStore::flushBadConnections() void RemoteStore::flushBadConnections()
{ {

View file

@ -144,6 +144,8 @@ public:
unsigned int getProtocol() override; unsigned int getProtocol() override;
std::optional<TrustedFlag> isTrustedClient() override;
void flushBadConnections(); void flushBadConnections();
struct Connection struct Connection
@ -151,6 +153,7 @@ public:
FdSink to; FdSink to;
FdSource from; FdSource from;
unsigned int daemonVersion; unsigned int daemonVersion;
std::optional<TrustedFlag> remoteTrustsUs;
std::optional<std::string> daemonNixVersion; std::optional<std::string> daemonNixVersion;
std::chrono::time_point<std::chrono::steady_clock> startTime; std::chrono::time_point<std::chrono::steady_clock> startTime;

View file

@ -509,6 +509,16 @@ struct S3BinaryCacheStoreImpl : virtual S3BinaryCacheStoreConfig, public virtual
return paths; return paths;
} }
/**
* For now, we conservatively say we don't know.
*
* \todo try to expose our S3 authentication status.
*/
std::optional<TrustedFlag> isTrustedClient() override
{
return std::nullopt;
}
static std::set<std::string> uriSchemes() { return {"s3"}; } static std::set<std::string> uriSchemes() { return {"s3"}; }
}; };

View file

@ -89,6 +89,7 @@ const uint32_t exportMagic = 0x4558494e;
enum BuildMode { bmNormal, bmRepair, bmCheck }; enum BuildMode { bmNormal, bmRepair, bmCheck };
enum TrustedFlag : bool { NotTrusted = false, Trusted = true };
struct BuildResult; struct BuildResult;
@ -815,6 +816,17 @@ public:
return 0; return 0;
}; };
/**
* @return/ whether store trusts *us*.
*
* `std::nullopt` means we do not know.
*
* @note This is the opposite of the StoreConfig::isTrusted
* store setting. That is about whether *we* trust the store.
*/
virtual std::optional<TrustedFlag> isTrustedClient() = 0;
virtual Path toRealPath(const Path & storePath) virtual Path toRealPath(const Path & storePath)
{ {
return storePath; return storePath;

View file

@ -10,7 +10,7 @@ namespace nix {
#define WORKER_MAGIC_1 0x6e697863 #define WORKER_MAGIC_1 0x6e697863
#define WORKER_MAGIC_2 0x6478696f #define WORKER_MAGIC_2 0x6478696f
#define PROTOCOL_VERSION (1 << 8 | 34) #define PROTOCOL_VERSION (1 << 8 | 35)
#define GET_PROTOCOL_MAJOR(x) ((x) & 0xff00) #define GET_PROTOCOL_MAJOR(x) ((x) & 0xff00)
#define GET_PROTOCOL_MINOR(x) ((x) & 0x00ff) #define GET_PROTOCOL_MINOR(x) ((x) & 0x00ff)
@ -103,6 +103,7 @@ MAKE_WORKER_PROTO(, DerivedPath);
MAKE_WORKER_PROTO(, Realisation); MAKE_WORKER_PROTO(, Realisation);
MAKE_WORKER_PROTO(, DrvOutput); MAKE_WORKER_PROTO(, DrvOutput);
MAKE_WORKER_PROTO(, BuildResult); MAKE_WORKER_PROTO(, BuildResult);
MAKE_WORKER_PROTO(, std::optional<TrustedFlag>);
MAKE_WORKER_PROTO(template<typename T>, std::vector<T>); MAKE_WORKER_PROTO(template<typename T>, std::vector<T>);
MAKE_WORKER_PROTO(template<typename T>, std::set<T>); MAKE_WORKER_PROTO(template<typename T>, std::set<T>);

View file

@ -33,6 +33,10 @@ bool checkFail(const std::string & msg) {
return false; return false;
} }
void checkInfo(const std::string & msg) {
notice(ANSI_BLUE "[INFO] " ANSI_NORMAL + msg);
}
} }
struct CmdDoctor : StoreCommand struct CmdDoctor : StoreCommand
@ -63,6 +67,7 @@ struct CmdDoctor : StoreCommand
success &= checkProfileRoots(store); success &= checkProfileRoots(store);
} }
success &= checkStoreProtocol(store->getProtocol()); success &= checkStoreProtocol(store->getProtocol());
checkTrustedUser(store);
if (!success) if (!success)
throw Exit(2); throw Exit(2);
@ -138,6 +143,14 @@ struct CmdDoctor : StoreCommand
return checkPass("Client protocol matches store protocol."); return checkPass("Client protocol matches store protocol.");
} }
void checkTrustedUser(ref<Store> store)
{
std::string_view trusted = store->isTrustedClient()
? "trusted"
: "not trusted";
checkInfo(fmt("You are %s by store uri: %s", trusted, store->getUri()));
}
}; };
static auto rCmdDoctor = registerCommand<CmdDoctor>("doctor"); static auto rCmdDoctor = registerCommand<CmdDoctor>("doctor");

View file

@ -28,15 +28,20 @@ struct CmdPingStore : StoreCommand, MixJSON
store->connect(); store->connect();
if (auto version = store->getVersion()) if (auto version = store->getVersion())
notice("Version: %s", *version); notice("Version: %s", *version);
if (auto trusted = store->isTrustedClient())
notice("Trusted: %s", *trusted);
} else { } else {
nlohmann::json res; nlohmann::json res;
Finally printRes([&]() { Finally printRes([&]() {
logger->cout("%s", res); logger->cout("%s", res);
}); });
res["url"] = store->getUri(); res["url"] = store->getUri();
store->connect(); store->connect();
if (auto version = store->getVersion()) if (auto version = store->getVersion())
res["version"] = *version; res["version"] = *version;
if (auto trusted = store->isTrustedClient())
res["trusted"] = *trusted;
} }
} }
}; };

View file

@ -0,0 +1,4 @@
source common.sh
# Check that store ping trusted doesn't yet work with ssh://
nix --store ssh://localhost?remote-store=$TEST_ROOT/other-store store ping --json | jq -e 'has("trusted") | not'

View file

@ -17,3 +17,6 @@ PATH2=$(nix path-info --store "$PWD/x" $CORRECT_PATH)
PATH3=$(nix path-info --store "local?root=$PWD/x" $CORRECT_PATH) PATH3=$(nix path-info --store "local?root=$PWD/x" $CORRECT_PATH)
[ $CORRECT_PATH == $PATH3 ] [ $CORRECT_PATH == $PATH3 ]
# Ensure store ping trusted works with local store
nix --store ./x store ping --json | jq -e '.trusted'

View file

@ -17,6 +17,7 @@ nix_tests = \
ca/gc.sh \ ca/gc.sh \
gc.sh \ gc.sh \
remote-store.sh \ remote-store.sh \
legacy-ssh-store.sh \
lang.sh \ lang.sh \
experimental-features.sh \ experimental-features.sh \
fetchMercurial.sh \ fetchMercurial.sh \

View file

@ -5,8 +5,19 @@ clearStore
# Ensure "fake ssh" remote store works just as legacy fake ssh would. # Ensure "fake ssh" remote store works just as legacy fake ssh would.
nix --store ssh-ng://localhost?remote-store=$TEST_ROOT/other-store doctor nix --store ssh-ng://localhost?remote-store=$TEST_ROOT/other-store doctor
# Ensure that store ping trusted works with ssh-ng://
nix --store ssh-ng://localhost?remote-store=$TEST_ROOT/other-store store ping --json | jq -e '.trusted'
startDaemon startDaemon
if isDaemonNewer "2.15pre0"; then
# Ensure that ping works trusted with new daemon
nix store ping --json | jq -e '.trusted'
else
# And the the field is absent with the old daemon
nix store ping --json | jq -e 'has("trusted") | not'
fi
# Test import-from-derivation through the daemon. # Test import-from-derivation through the daemon.
[[ $(nix eval --impure --raw --expr ' [[ $(nix eval --impure --raw --expr '
with import ./config.nix; with import ./config.nix;