Hi,

Accidentally I wrote exactly the same yesterday. Sorry I was not aware of
your previous message as I only joined the mailing list this week.

See patch attached. A couple of things you might want to take over:
  * -f/--force
  * Follow links in "generate" and "edit" commands so that they can be
    used indifferently on the actual password or its alias.
  * A few test cases.

Kr
Lionel

On 13/03/2022 06:23, Radon Rosborough wrote:
Hi friends,

As promised in February [1] [2], I created a Pass extension that makes
it more convenient to manage symbolic links within the password store
(use case: websites that have more than one domain name using the same
login credentials). The project is available on GitHub [3], where you
can download releases packaged for Ubuntu/Debian, Red Hat/Fedora, Arch
Linux, and Homebrew, or install from source.

Any feedback or bug reports would be greatly appreciated [4].

Best regards,
Radon Rosborough

[1]: https://lists.zx2c4.com/pipermail/password-store/2022-January/004572.html [2]: https://lists.zx2c4.com/pipermail/password-store/2022-February/004581.html
[3]: https://github.com/raxod502/pass-ln
[4]: https://github.com/raxod502/pass-ln/issues
From f979d59701933e00c928941ab39ec47f64cac490 Mon Sep 17 00:00:00 2001
From: Lionel Van Bemten <[email protected]>
Date: Sat, 12 Mar 2022 15:00:08 +0100
Subject: [PATCH] Add command to create symlinks between passwords

---
 src/completion/pass.bash-completion |  4 +-
 src/completion/pass.fish-completion |  4 ++
 src/completion/pass.zsh-completion  |  3 +-
 src/password-store.sh               | 65 +++++++++++++++++++++++++++++
 tests/t0600-link.sh                 | 47 +++++++++++++++++++++
 5 files changed, 120 insertions(+), 3 deletions(-)
 create mode 100755 tests/t0600-link.sh

diff --git a/src/completion/pass.bash-completion b/src/completion/pass.bash-completion
index 2d23cbf..e7dc9dc 100644
--- a/src/completion/pass.bash-completion
+++ b/src/completion/pass.bash-completion
@@ -84,7 +84,7 @@ _pass()
 {
 	COMPREPLY=()
 	local cur="${COMP_WORDS[COMP_CWORD]}"
-	local commands="init ls find grep show insert generate edit rm mv cp git help version ${PASSWORD_STORE_EXTENSION_COMMANDS[*]}"
+	local commands="init ls find grep show insert generate edit rm mv cp ln git help version ${PASSWORD_STORE_EXTENSION_COMMANDS[*]}"
 	if [[ $COMP_CWORD -gt 1 ]]; then
 		local lastarg="${COMP_WORDS[$COMP_CWORD-1]}"
 		case "${COMP_WORDS[1]}" in
@@ -112,7 +112,7 @@ _pass()
 				COMPREPLY+=($(compgen -W "-n --no-symbols -c --clip -f --force -i --in-place" -- ${cur}))
 				_pass_complete_entries
 				;;
-			cp|copy|mv|rename)
+			cp|copy|mv|rename|ln|link)
 				COMPREPLY+=($(compgen -W "-f --force" -- ${cur}))
 				_pass_complete_entries
 				;;
diff --git a/src/completion/pass.fish-completion b/src/completion/pass.fish-completion
index 0f57dd2..f288880 100644
--- a/src/completion/pass.fish-completion
+++ b/src/completion/pass.fish-completion
@@ -87,6 +87,10 @@ complete -c $PROG -f -n '__fish_pass_needs_command' -a cp -d 'Command: copy exis
 complete -c $PROG -f -n '__fish_pass_uses_command cp' -s f -l force -d 'Force copy'
 complete -c $PROG -f -n '__fish_pass_uses_command cp' -a "(__fish_pass_print_entries_and_dirs)"
 
+complete -c $PROG -f -n '__fish_pass_needs_command' -a ln -d 'Command: create symlink to password'
+complete -c $PROG -f -n '__fish_pass_uses_command ln' -s f -l force -d 'Force create'
+complete -c $PROG -f -n '__fish_pass_uses_command ln' -a "(__fish_pass_print_entries_and_dirs)"
+
 complete -c $PROG -f -n '__fish_pass_needs_command' -a rm -d 'Command: remove existing password'
 complete -c $PROG -f -n '__fish_pass_uses_command rm' -s r -l recursive -d 'Remove password groups recursively'
 complete -c $PROG -f -n '__fish_pass_uses_command rm' -s f -l force -d 'Force removal'
diff --git a/src/completion/pass.zsh-completion b/src/completion/pass.zsh-completion
index d911e12..c8c284f 100644
--- a/src/completion/pass.zsh-completion
+++ b/src/completion/pass.zsh-completion
@@ -58,7 +58,7 @@ _pass () {
 					"--in-place[replace first line]"
 				_pass_complete_entries_with_subdirs
 				;;
-			cp|copy|mv|rename)
+			cp|copy|mv|rename|ln|link)
 				_arguments : \
 					"-f[force rename]" \
 					"--force[force rename]"
@@ -102,6 +102,7 @@ _pass () {
 			"mv:Rename the password"
 			"cp:Copy the password"
 			"rm:Remove the password"
+			"ln:Create link to the password"
 			"git:Call git on the password store"
 			"version:Output version information"
 			"help:Output help message"
diff --git a/src/password-store.sh b/src/password-store.sh
index 22e818f..a81e066 100755
--- a/src/password-store.sh
+++ b/src/password-store.sh
@@ -145,6 +145,21 @@ check_sneaky_paths() {
 		[[ $path =~ /\.\.$ || $path =~ ^\.\./ || $path =~ /\.\./ || $path =~ ^\.\.$ ]] && die "Error: You've attempted to pass a sneaky path to pass. Go home."
 	done
 }
+follow_link() {
+	local path="$1"
+	local prefix_realpath=$(readlink -f "$PREFIX")
+	if [[ -L "$PREFIX/$path.gpg" ]]; then
+		path=$(readlink -f "$PREFIX/$path.gpg")
+		[[ -e "$path" ]] || die "Error: broken link $1."
+		path=${path/$prefix_realpath\//}
+		path=${path%.gpg}
+	elif [[ -L "$PREFIX/$path" ]]; then
+		path=$(readlink -f "$PREFIX/$path")
+		[[ -e "$path" ]] || die "Error: broken link $1."
+		path=${path/$prefix_realpath\//}
+	fi
+	echo $path
+}
 
 #
 # END helper functions
@@ -306,6 +321,8 @@ cmd_usage() {
 	        Renames or moves old-path to new-path, optionally forcefully, selectively reencrypting.
 	    $PROGRAM cp [--force,-f] old-path new-path
 	        Copies old-path to new-path, optionally forcefully, selectively reencrypting.
+	    $PROGRAM ln [--force,-f] target link
+	        Creates a symbolic link from link to target. This allows to create aliases for passwords.
 	    $PROGRAM git git-command-args...
 	        If the password store is a git repository, execute a git command
 	        specified by git-command-args.
@@ -487,6 +504,7 @@ cmd_edit() {
 
 	local path="${1%/}"
 	check_sneaky_paths "$path"
+	path=$(follow_link "$path")
 	mkdir -p -v "$PREFIX/$(dirname -- "$path")"
 	set_gpg_recipients "$(dirname -- "$path")"
 	local passfile="$PREFIX/$path.gpg"
@@ -527,6 +545,7 @@ cmd_generate() {
 	local path="$1"
 	local length="${2:-$GENERATED_LENGTH}"
 	check_sneaky_paths "$path"
+	path=$(follow_link "$path")
 	[[ $length =~ ^[0-9]+$ ]] || die "Error: pass-length \"$length\" must be a number."
 	[[ $length -gt 0 ]] || die "Error: pass-length must be greater than zero."
 	mkdir -p -v "$PREFIX/$(dirname -- "$path")"
@@ -621,6 +640,8 @@ cmd_copy_move() {
 	mkdir -p -v "${new_path%/*}"
 	[[ -d $old_path || -d $new_path || $new_path == */ ]] || new_path="${new_path}.gpg"
 
+	[[ -L "$old_path" ]] && die "Error: cannot move or copy a link."
+
 	local interactive="-i"
 	[[ ! -t 0 || $force -eq 1 ]] && interactive="-f"
 
@@ -649,6 +670,49 @@ cmd_copy_move() {
 	fi
 }
 
+cmd_link() {
+	local opts force=0
+	opts="$($GETOPT -o f -l force -n "$PROGRAM" -- "$@")"
+	local err=$?
+	eval set -- "$opts"
+	while true; do case $1 in
+		-f|--force) force=1; shift ;;
+		--) shift; break ;;
+	esac done
+	[[ $# -ne 2 ]] && die "Usage: $PROGRAM $COMMAND [--force,-f] target link"
+	check_sneaky_paths "$@"
+	local target="$1"
+	local link="$2"
+
+	local target_path="$PREFIX/$target"
+	local link_path="$PREFIX/$link"
+	if [[ -f "$target_path.gpg" ]]; then
+		target="$target.gpg"
+		link_path="$link_path.gpg"
+	elif [[ ! -d "$target_path" ]]; then
+		die "Error: $target is not in the password store."
+	fi
+
+	while [[ "${link%%/*}" == "${target%%/*}" ]]; do
+		link="${link#*/}"
+		target="${target#*/}"
+	done
+
+	target_prefix=""
+	while [[ "${link#*/}" != "${link}" ]]; do
+		link="${link#*/}"
+		target_prefix="$target_prefix../"
+	done
+
+	local interactive="-i"
+	[[ ! -t 0 || $force -eq 1 ]] && interactive="-f"
+
+	mkdir -p -v "${link_path%/*}"
+	set_git "$link_path"
+	ln -s $interactive "${target_prefix}${target}" "$link_path" || exit 1
+	git_add_file "$link_path" "Link ${2} to ${1}."
+}
+
 cmd_git() {
 	set_git "$PREFIX/"
 	if [[ $1 == "init" ]]; then
@@ -715,6 +779,7 @@ case "$1" in
 	delete|rm|remove) shift;	cmd_delete "$@" ;;
 	rename|mv) shift;		cmd_copy_move "move" "$@" ;;
 	copy|cp) shift;			cmd_copy_move "copy" "$@" ;;
+	link|ln) shift;			cmd_link "$@" ;;
 	git) shift;			cmd_git "$@" ;;
 	*)				cmd_extension_or_show "$@" ;;
 esac
diff --git a/tests/t0600-link.sh b/tests/t0600-link.sh
new file mode 100755
index 0000000..249c760
--- /dev/null
+++ b/tests/t0600-link.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+
+test_description='Test ln command'
+cd "$(dirname "$0")"
+. ./setup.sh
+
+INITIAL_PASSWORD="Jonas"
+
+test_expect_success 'Create link and read password from it' '
+	"$PASS" init $KEY1 &&
+	"$PASS" git init &&
+	"$PASS" insert -e cred1 <<<"$INITIAL_PASSWORD" &&
+	"$PASS" ln cred1 cred2 &&
+	[[ $("$PASS" show cred2) == "$INITIAL_PASSWORD" ]]
+'
+
+test_expect_success 'Edit password in target file' '
+	export FAKE_EDITOR_PASSWORD="Wout" &&
+	export PATH="$TEST_HOME:$PATH" &&
+	export EDITOR="fake-editor-change-password.sh" &&
+	"$PASS" edit cred1 &&
+	[[ $("$PASS" show cred2) == "$FAKE_EDITOR_PASSWORD" ]]
+'
+
+test_expect_success 'Edit password in alias file' '
+	export FAKE_EDITOR_PASSWORD="Primoz" &&
+	"$PASS" edit cred2 &&
+	[[ $("$PASS" show cred1) == "$FAKE_EDITOR_PASSWORD" ]]
+'
+
+test_expect_success 'Symlink across directories' '
+	"$PASS" insert -e dir1/cred1 <<<"$INITIAL_PASSWORD" &&
+	"$PASS" ln dir1/cred1 dir2/cred2 &&
+	[[ $("$PASS" show dir2/cred2) == "$INITIAL_PASSWORD" ]]
+'
+
+test_expect_success 'Remove symlink and read original password' '
+	"$PASS" rm dir2/cred2 &&
+  [[ $("$PASS" show dir1/cred1) == "$INITIAL_PASSWORD" ]] &&
+  test_must_fail "$PASS" show dir2/cred2
+'
+
+test_expect_success 'Symlink to nonexistent password' '
+	test_must_fail "$PASS" ln cred9
+'
+
+test_done
-- 
2.35.1

Reply via email to