Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add granular access control for nix store #9287

Draft
wants to merge 61 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
20e574e
Add a library for ACL manipulation
balsoft Nov 1, 2023
337a127
Add a granular store interface
balsoft Nov 1, 2023
5024921
Implement granular access store
balsoft Nov 1, 2023
552e4e5
Add CLI commands to manipulate ACLs
balsoft Nov 1, 2023
61a3cea
Add __permissions to builtins.derivation and builtins.path
balsoft Nov 1, 2023
0be3e5d
Add an integration test for ACL functionality
balsoft Nov 1, 2023
bd56e3e
Add tests/acls.sh
ylecornec Nov 14, 2023
db3a522
acls grant/revoke: Error if group or user does not exists
ylecornec Nov 15, 2023
aba3181
Acls test: permission of dependency.
ylecornec Nov 16, 2023
8dbea38
revokeBuildUserAccess: only revoke permissions added by grantBuildUse…
ylecornec Nov 21, 2023
14e474c
Acls: add test that revokes the permission of a runtime dependency.
ylecornec Nov 22, 2023
afd828b
Acls: Refactor integration tests
ylecornec Nov 23, 2023
5d97559
Acls: disable non integration tests for now
ylecornec Nov 23, 2023
65fe86f
Add json() to AccessStatus
balsoft Nov 30, 2023
db20d22
Add protectByDefault setting
balsoft Nov 30, 2023
1102fdd
Add runtime closure invariant
balsoft Nov 30, 2023
6293167
Run acls.sh test properly
balsoft Nov 30, 2023
9d6c011
Acls: Add tests where a public output depend on a private one
ylecornec Dec 5, 2023
1ed4965
Acls: explicitely access future or current permissions
ylecornec Dec 5, 2023
f9e2c4b
Acls: remove some permission adding code which may not be needed anymore
ylecornec Dec 5, 2023
0c625b0
Acls: Also add future permission to paths of StoreObjectDerivationOutput
ylecornec Dec 5, 2023
7ea4b05
Acls: Add ShouldSync path status
ylecornec Dec 5, 2023
a3d3b71
Acls: tests non trusted user with private file
ylecornec Dec 6, 2023
5f8eef5
Acls: canAccess function, remove default value for use_future parameter
ylecornec Dec 6, 2023
2c00ec5
Merge remote-tracking branch 'origin/master' into acls
balsoft Dec 5, 2023
228d8af
Merge branch 'ylecornec/dep_perms' into acls
balsoft Dec 7, 2023
fccba28
ACL tests
balsoft Dec 8, 2023
7653b07
Add the ability to cache user's groups
balsoft Dec 8, 2023
3994ce1
Prevent segfault
balsoft Dec 13, 2023
3a4914d
Fix darwin build
balsoft Dec 13, 2023
cd72876
Merge remote-tracking branch 'origin/master' into acls
balsoft Dec 13, 2023
eff385d
Fix perl/default.nix
ylecornec Dec 14, 2023
4b66941
Acls: builtins.path set accessStatus
ylecornec Dec 14, 2023
985fe93
Acls: remove PathStatus::ShouldSync
ylecornec Dec 14, 2023
0b92adf
Acls: AccessStatus setter/getter
ylecornec Dec 14, 2023
834219a
Acls tests: Assertions on failing tests output
ylecornec Dec 14, 2023
f9d3f55
Temporarily deactivate ensureAccess
ylecornec Dec 14, 2023
96cb115
Acls: remove canAccess `use_future` argument
ylecornec Dec 14, 2023
2820eb4
Acls: permission check when importing a folder with builtins.path
ylecornec Dec 15, 2023
c1912d8
Acls: Test importing a private folder
ylecornec Dec 15, 2023
9c75782
Don't account for trusted users in ensureAccess
balsoft Dec 18, 2023
8ee4043
Make the 'should be synced' message debug-only
balsoft Dec 18, 2023
af84767
Fix perl bindings build
balsoft Dec 18, 2023
f967eb6
Reactivate runtime closure check
ylecornec Dec 18, 2023
d167252
Acls test: fix for runtime closure checks
ylecornec Dec 18, 2023
53c8eb5
Merge remote-tracking branch 'tweag/acls' into ylecornec/remove_curre…
ylecornec Dec 18, 2023
8841d0d
Acls: reactivate ensureAccess and move the call to setAccessStatus
ylecornec Dec 21, 2023
9f63760
Acls: Fix tests after activating `ensureAccess`
ylecornec Dec 19, 2023
045f1e8
Acls: add tests using flakes
ylecornec Dec 21, 2023
e90e479
Acls: merge {add/remove}AllowedEntities current and future
ylecornec Dec 21, 2023
df135f2
Acls documentation: fix markdown files paths.
ylecornec Dec 21, 2023
d14704c
ACLs: calculate mask correctly
balsoft Jan 12, 2024
51419e5
Merge branch 'ylecornec/remove_current_future' into selective-acl
balsoft Jan 16, 2024
5ef3f14
Ensure access in daemon.cc regardless of current status
balsoft Feb 2, 2024
7a49064
Assign the build directory to the effective user, if present
balsoft Feb 7, 2024
c5f8a40
Fix getUserName behavior
balsoft Feb 7, 2024
5333b25
Add referrer checks for access status
balsoft Feb 10, 2024
9ca2e82
Protect paths if setAccessStatus fail while registering
balsoft Feb 15, 2024
a8ff15f
Automatically deny access for referree derivations
balsoft Mar 14, 2024
946f4f7
Pass through access status from daemon
balsoft Mar 14, 2024
5d5bbbc
chmod if chown fails
balsoft Mar 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add CLI commands to manipulate ACLs
  • Loading branch information
balsoft committed Nov 1, 2023
commit 552e4e529c40535b464315b094b9f7db4e20113b
22 changes: 19 additions & 3 deletions src/libcmd/installables.cc
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -19,6 +20,8 @@
#include "url.hh"
#include "registry.hh"
#include "build-result.hh"
#include "store-cast.hh"
#include "local-store.hh"

#include <regex>
#include <queue>
Expand Down Expand Up @@ -519,10 +522,11 @@ std::vector<BuiltPathWithResult> Installable::build(
ref<Store> store,
Realise mode,
const Installables & installables,
BuildMode bMode)
BuildMode bMode,
bool protect)
{
std::vector<BuiltPathWithResult> 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;
}
Expand All @@ -532,7 +536,8 @@ std::vector<std::pair<ref<Installable>, BuiltPathWithResult>> Installable::build
ref<Store> store,
Realise mode,
const Installables & installables,
BuildMode bMode)
BuildMode bMode,
bool protect)
{
if (mode == Realise::Nothing)
settings.readOnlyMode = true;
Expand All @@ -550,6 +555,17 @@ std::vector<std::pair<ref<Installable>, 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<LocalGranularAccessStore>(*store).setAccessStatus(p.path, status);
},
[&](DerivedPath::Built b){
require<LocalGranularAccessStore>(*store).setAccessStatus(b, status);
}
}, b.path);
}
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/libcmd/installables.hh
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,16 @@ struct Installable
ref<Store> store,
Realise mode,
const Installables & installables,
BuildMode bMode = bmNormal);
BuildMode bMode = bmNormal,
bool protect = false);

static std::vector<std::pair<ref<Installable>, BuiltPathWithResult>> build2(
ref<Store> evalStore,
ref<Store> store,
Realise mode,
const Installables & installables,
BuildMode bMode = bmNormal);
BuildMode bMode = bmNormal,
bool protect = false);

static std::set<StorePath> toStorePaths(
ref<Store> evalStore,
Expand Down
14 changes: 14 additions & 0 deletions src/libmain/common-args.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 10 additions & 1 deletion src/nix/add-to-store.cc
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#include "command.hh"
#include "common-args.hh"
#include "granular-access-store.hh"
#include "store-api.hh"
#include "archive.hh"
#include "store-cast.hh"

using namespace nix;

struct CmdAddToStore : MixDryRun, StoreCommand
struct CmdAddToStore : MixDryRun, MixProtect, StoreCommand
{
Path path;
std::optional<std::string> namePart;
Expand Down Expand Up @@ -56,6 +58,13 @@ struct CmdAddToStore : MixDryRun, StoreCommand
info.narSize = sink.s.size();

if (!dryRun) {
if (protect) {
LocalGranularAccessStore::AccessStatus status;
status.isProtected = true;
status.entities = {ACL::User(getuid())};
info.accessStatus = status;
}

auto source = StringSource(sink.s);
store->addToStore(info, source);
}
Expand Down
5 changes: 3 additions & 2 deletions src/nix/build.cc
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ static void createOutLinks(const Path& outLink, const std::vector<BuiltPathWithR
}
}

struct CmdBuild : InstallablesCommand, MixDryRun, MixJSON, MixProfile
struct CmdBuild : InstallablesCommand, MixDryRun, MixJSON, MixProfile, MixProtect
{
Path outLink = "result";
bool printOutputPaths = false;
Expand Down Expand Up @@ -134,7 +134,8 @@ struct CmdBuild : InstallablesCommand, MixDryRun, MixJSON, MixProfile
getEvalStore(), store,
Realise::Outputs,
installables,
repair ? bmRepair : buildMode);
repair ? bmRepair : buildMode,
protect);

if (json) logger->cout("%s", builtPathsWithResultToJSON(buildables, store).dump());

Expand Down
64 changes: 64 additions & 0 deletions src/nix/store-access-grant.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include "command.hh"
#include "store-api.hh"
#include "local-fs-store.hh"
#include "store-cast.hh"

using namespace nix;

struct CmdStoreAccessGrant : StorePathsCommand
{
std::set<std::string> users;
std::set<std::string> groups;
CmdStoreAccessGrant()
{
addFlag({
.longName = "user",
.shortName = 'u',
.description = "User to whom access should be granted",
.labels = {"user"},
.handler = {[&](std::string _user){
users.insert(_user);
}}
});
addFlag({
.longName = "group",
.shortName = 'g',
.description = "Group to which access should be granted",
.labels = {"group"},
.handler = {[&](std::string _group){
groups.insert(_group);
}}
});
}
std::string description() override
{
return "grant a user access to store paths";
}

std::string doc() override
{
return
#include "store-repair.md"
;
}

void run(ref<Store> 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<LocalGranularAccessStore>(*store);
for (auto & path : storePaths) {
auto status = localStore.getAccessStatus(path);
if (!status.isProtected) warn("Path '%s' is not protected; all users can access it regardless of permissions", store->printStorePath(path));
if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path));

for (auto user : users) status.entities.insert(*getpwnam(user.c_str()));
for (auto group : groups) status.entities.insert(*getgrnam(group.c_str()));
localStore.setAccessStatus(path, status);
}
}
}
};

static auto rStoreAccessGrant = registerCommand2<CmdStoreAccessGrant>({"store", "access", "grant"});
24 changes: 24 additions & 0 deletions src/nix/store-access-grant.md
Original file line number Diff line number Diff line change
@@ -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.

<!-- FIXME moar docs -->

)"
103 changes: 103 additions & 0 deletions src/nix/store-access-info.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#include "ansicolor.hh"
#include "command.hh"
#include "store-api.hh"
#include "local-store.hh"
#include "store-cast.hh"

using namespace nix;

struct CmdStoreAccessInfo : StorePathCommand, MixJSON
{
std::string description() override
{
return "get information about store path access";
}

std::string doc() override
{
return
#include "store-access-info.md"
;
}

void run(ref<Store> store, const StorePath & path) override
{
auto & aclStore = require<LocalGranularAccessStore>(*store);
auto status = aclStore.getAccessStatus(path);
bool isValid = aclStore.isValidPath(path);
std::set<std::string> users;
std::set<std::string> groups;
for (auto entity : status.entities) {
std::visit(overloaded {
[&](ACL::User user) {
struct passwd * pw = getpwuid(user.uid);
users.insert(pw->pw_name);
},
[&](ACL::Group group) {
struct group * gr = getgrgid(group.gid);
groups.insert(gr->gr_name);
}
}, entity);
}
if (json) {
nlohmann::json j;
j["exists"] = isValid;
j["protected"] = status.isProtected;
j["users"] = users;
j["groups"] = groups;
logger->cout(j.dump());
}
else {
std::string be, have, has;
if (isValid) {
be = "is";
have = "have";
has = "has";
}
else {
be = "will be";
have = "will have";
has = "will have";

logger->cout("The path does not exist yet; the permissions will be applied when it is added to the store.\n");
}

if (status.isProtected)
logger->cout("The path " + be + " " ANSI_BOLD ANSI_GREEN "protected" ANSI_NORMAL);
else
logger->cout("The path " + be + " " ANSI_BOLD ANSI_RED "not" ANSI_NORMAL " protected");

if (users.empty() && groups.empty()) {
if (status.isProtected) { logger->cout(""); logger->cout("Nobody " + has + " access to the path"); };
} else {
logger->cout("");
if (!status.isProtected) {
logger->warn("Despite this path not being protected, some users and groups " + have + " additional access to it.");
logger->cout("");
}

if (!users.empty()) {
if (status.isProtected)
logger->cout("The following users " + have + " access to the path:");
else
logger->cout(ANSI_BOLD "If the path was protected" ANSI_NORMAL ", the following users would have access to it:");

for (auto user : users)
logger->cout(ANSI_MAGENTA " %s" ANSI_NORMAL, user);
}
if (! (users.empty() && groups.empty())) logger->cout("");
if (!groups.empty()) {
if (status.isProtected)
logger->cout("Users in the following groups " + have + " access to the path:");
else
logger->cout(ANSI_BOLD "If the path was protected" ANSI_NORMAL ", users in the following groups would have access to it:");
for (auto group : groups)
logger->cout(ANSI_CYAN " %s" ANSI_NORMAL, group);
}
}
}
}

};

static auto rStoreAccessInfo = registerCommand2<CmdStoreAccessInfo>({"store", "access", "info"});
17 changes: 17 additions & 0 deletions src/nix/store-access-info.md
Original file line number Diff line number Diff line change
@@ -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.

)"
37 changes: 37 additions & 0 deletions src/nix/store-access-protect.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include "command.hh"
#include "granular-access-store.hh"
#include "local-fs-store.hh"
#include "store-api.hh"
#include "store-cast.hh"

using namespace nix;

struct CmdStoreAccessProtect : StorePathsCommand
{
std::string description() override
{
return "protect store paths";
}

std::string doc() override
{
return
#include "store-repair.md"
;
}

void run(ref<Store> store, StorePaths && storePaths) override
{
auto & localStore = require<LocalGranularAccessStore>(*store);
for (auto & path : storePaths) {
auto status = localStore.getAccessStatus(path);
if (!status.entities.empty())
warn("There are some users or groups who have access to path %s; consider removing them with \n" ANSI_BOLD "nix store access revoke --all-entities %s" ANSI_NORMAL, localStore.printStorePath(path), localStore.printStorePath(path));
if (!localStore.isValidPath(path)) warn("Path %s does not exist yet; permissions will be applied as soon as it is added to the store", localStore.printStorePath(path));
status.isProtected = true;
localStore.setAccessStatus(path, status);
}
}
};

static auto rStoreAccessProtect = registerCommand2<CmdStoreAccessProtect>({"store", "access", "protect"});
Loading