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

Support ZFS delegations #838

Open
iFreilicht opened this issue Oct 16, 2024 · 1 comment
Open

Support ZFS delegations #838

iFreilicht opened this issue Oct 16, 2024 · 1 comment
Labels
contributions welcome There's nothing left to discuss, feel free to submit a PR for this! enhancement New feature or request

Comments

@iFreilicht
Copy link
Contributor

I'm splitting this off from #298, which contains a lot of discussion about an unrelated feature, an delegations are the only thing missing.

“delegations” allow otherwise-unprivileged users to create children of a given dataset, snapshot it, set properties, etc. see zfs-allow(8)

The addition of this feature would be appreciated, but isn't a high priority.

@iFreilicht iFreilicht added enhancement New feature or request contributions welcome There's nothing left to discuss, feel free to submit a PR for this! labels Oct 16, 2024
@iFreilicht iFreilicht mentioned this issue Oct 16, 2024
2 tasks
@MaienM
Copy link
Contributor

MaienM commented Nov 16, 2024

I recently wrote something that manages this for my own infrastructure. The repo it's in is private, so I'll repost the relevant bits here. It'd need some work to be integrated, but it might be a good start for someone that wants to pick this up.

This allows one to set the delegated permissions on a dataset like this:

{
    zfs.datasets."dataset/name".allow.everyone.userprop = true;
    zfs.datasets."dataset/name".allow.user."username".refquota = "descedent";
    zfs.datasets."dataset/name".allow.group."groupname".refquota = "local";
}

The basic approach I've taken is to generate two lists of partial allow commands (without the zfs allow prefix), one for the desired state (wanted) and one for the current state (currrent). I then take the items in current that aren't in wanted and run them prefixed with zfs unallow, and I take the items in wanted that aren't in current and run them prefixed with zfs allow.

For reference, the above configuration would result in the following wanted list:

-e userprop dataset/name
-d -u username refquota dataset/name
-l -g groupname refquota dataset/name

delegated.nix

{ config, lib, pkgs, ... }:
{
  options.zfs.datasets = lib.mkOption {
    type = lib.types.attrsOf (lib.types.submodule {
      options = {
        allow =
          let
            permissionMapType = lib.types.attrsOf (lib.types.either
              lib.types.bool
              (lib.types.enum [
                "local+descendant"
                "local"
                "descendant"
              ])
            );
          in
          lib.mkOption {
            type = lib.types.submodule {
              options = {
                user = lib.mkOption {
                  type = lib.types.attrsOf permissionMapType;
                  default = { };
                };
                group = lib.mkOption {
                  type = lib.types.attrsOf permissionMapType;
                  default = { };
                };
                everyone = lib.mkOption {
                  type = permissionMapType;
                  default = { };
                };
              };
            };
            default = { };
          };
      };
    });
    default = { };
  };

  config = {
    systemd.services."zfs-setup-delegated-permissions" = {
      description = "Setup delegated ZFS permissions.";
      serviceConfig.Type = "oneshot";
      enable = true;
      wants = [ "zfs-volumes.target" ];
      path = with pkgs; [ zfs ];
      script =
        let
          concatMapAttrsToList = mapper: attrs: builtins.concatLists (lib.attrsets.mapAttrsToList mapper attrs);
          processPermissions = target: poolName: permissions: concatMapAttrsToList
            (permissionName: permission:
              let
                rest = "${target} ${permissionName} ${poolName}";
              in
              if builtins.typeOf permission == "bool"
              then
                if permission
                then [ rest ]
                else [ ]
              else
                if permission == "local+descendant" then [ rest ]
                else if permission == "local" then [ "-l ${rest}" ]
                else if permission == "descendant" then [ "-d ${rest}" ]
                else builtins.throw "Invalid permision ${permission}."
            )
            permissions;
          wanted = concatMapAttrsToList
            (poolName: pool:
              (concatMapAttrsToList (name: processPermissions "-u ${name}" poolName) pool.allow.user) ++
              (concatMapAttrsToList (name: processPermissions "-g ${name}" poolName) pool.allow.group) ++
              (processPermissions "-e" poolName pool.allow.everyone)
            )
            config.zfs.datasets;
        in
        ''
          ${lib.strings.toShellVar "wanted" wanted}
          ${builtins.readFile ./delegated.sh}
        '';
    };
    systemd.timers."zfs-setup-delegated-permissions" = {
      inherit (config.systemd.services."zfs-setup-delegated-permissions") description;
      wantedBy = [ "timers.target" ];
      after = [
        "zfs-volumes.target"
      ];
      timerConfig = {
        OnActiveSec = "10s";
        OnCalendar = "*-*-* 00:00:00";
        Unit = "zfs-setup-delegated-permissions.service";
      };
    };
  };
}

delegated.sh

transform() {
	while read -r -a line; do
		if [[ ${line[*]} == '---- Permissions on '* ]]; then
			dataset="${line[3]}"
			continue
		fi

		if [[ ${line[1]:-} == 'permissions:' ]]; then
			case "${line[0]}" in
				"Local+Descendent") pflags="" ;;
				"Local") pflags="-l " ;;
				"Descendent") pflags="-d " ;;
				*)
					>&2 echo "Unknown permissions '${line[0]}'."
					exit 1
					;;
			esac
			continue
		fi

		case "${line[0]}" in
			"user")
				aflags="-u ${line[1]} "
				perms="${line[2]}"
				;;
			"group")
				aflags="-g ${line[1]} "
				perms="${line[2]}"
				;;
			"everyone")
				aflags="-e "
				perms="${line[1]}"
				;;
			*)
				>&2 echo "Unknown target type '${line[1]}'."
				exit 1
				;;
		esac
		read -r -a permlist <<< "${perms//,/ }"

		for perm in "${permlist[@]}"; do
			echo "$pflags$aflags$perm $dataset"
		done
	done
}

mapfile -t current < <(
	zfs list -H -o name \
		| xargs -d'\n' -n1 zfs allow \
		| transform \
		| sort -u
)

mapfile -t to_remove < <(
	printf '%s\n' "${current[@]}" \
		| grep -vFxf <(printf '%s\n' "${wanted[@]}")
)
for perm in "${to_remove[@]}"; do
	# shellcheck disable=2086
	zfs unallow $perm
done

mapfile -t to_add < <(
	printf '%s\n' "${wanted[@]}" \
		| grep -vFxf <(printf '%s\n' "${current[@]}")
)
for perm in "${to_add[@]}"; do
	# shellcheck disable=2086
	zfs allow $perm
done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contributions welcome There's nothing left to discuss, feel free to submit a PR for this! enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants