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