Posts come up very frequently with people coming up with various
workarounds and hacks to support team workflows.

This month there have already been two threads requesting solutions for this:

https://lists.zx2c4.com/pipermail/password-store/2016-November/002463.html
https://lists.zx2c4.com/pipermail/password-store/2016-November/002482.html

For many it is necessary to maintain several passdb folders for
personal, family, work, projects, etc. Maintaining this typically
requires each of these with its own repo, users, and git origins.

One could come up with custom scripts/wrappers to support this (and
many do), but then those scripts must be signed and distributed and
updated properly with all members in a group which makes adoption of
pass that much more difficult. I feel this really should be solved in
pass itself and clearly others agree.

The pass maintainer has also expressed interest in in-tree solutions
to this problem but the last submission attempt fizzled out.

https://lists.zx2c4.com/pipermail/password-store/2015-April/001455.html

## Subtree Pattern ##

Currently the only workflow I have found that is natively supported by
pass today is using subtrees. I have been using the following approach
for over a year with 3 repos and 2 teams.

While it does work, it is fairly cumbersome.

Setup:

```
pass init ABCDE12345678901
pass git init
pass git remote add origin [email protected]:lrvick/passdb.git
pass git remote add -f project [email protected]:project/passdb.git
pass git remote add -f company [email protected]:company/passdb.git
cd ~/.password-store
git subtree add --prefix Project someproject master --squash
git subtree add --prefix Company someproject master --squash
```

Add/Edit

```
pass generate Project/somepass 50
pass rm Personal/somepass
pass edit Work/somepass
```

Sync to/from respective origins

```
pass git subtree push origin master
pass git subtree pull --prefix=Project project master --squash
pass git subtree pull --prefix=Company company master --squash
pass git subtree push --prefix=Project project master
pass git subtree push --prefix=Company company master
```

The above also has some particularly painful side effects when
enforcing commit signing + upstream merges. In practice this has
resulted in us having to tap a yubikey to sign once for every file in
the repo...


## Submodule Pattern ##

With the attached patch for submodule support the above could become
substantially more manageable.

Personal Setup:

```
pass init ABCDE12345678901
pass git init
git remote add origin [email protected]:lrvick/passdb.git
```

Existing shared repo setup:

```
pass git submodule add [email protected]:project/passdb.git Project
pass git submodule add [email protected]:work/passdb.git Work
pass git submodule foreach "git checkout origin master"
```

Add/Edit

```
pass generate Project/somepass 50
pass rm Personal/somepass
pass edit Work/somepass
```

Sync to/from respective origins

```
pass git push
pass git submodule foreach "git pull"
pass git submodule foreach "git push"
```

## Design and Implementation ##

This change makes `pass` commit to the innermost Git repository (as
identified by the presence of a `.git` file/folder) that contains a
file, when it is changed.

It was designed to minimize the number of changes to `pass` and not
break existing usages. A few extra tests have been supplied for the
new workflow.

Whenever a new commit is performed (upon add/edit/remove), `GIT_DIR`
and `GIT_WORK_TREE` are set to the relevant directories.

It is worth mentioning this patchset has already undergone a few
rounds of testing, bug-fixing, and code review here:

https://github.com/lrvick/password-store/pull/1

More input (or a merge!) would be very appreciated.

-- 
Lance R. Vick
__________________________________________________
Cell    -  650.686.8819
IRC     -  [email protected]
Website -  http://lrvick.net
PGP Key -  http://lrvick.net/0x36C8AAA9.asc
__________________________________________________

Attachment: submodules.patch.sig
Description: PGP signature

From 19ecdb6007896273103a91a9dcc5223f7ef4bc62 Mon Sep 17 00:00:00 2001
From: "Lance R. Vick" <[email protected]>
Date: Fri, 7 Oct 2016 01:18:23 -0700
Subject: [PATCH 1/7] multiple git repo support

---
 src/password-store.sh             | 34 ++++++++++++++++++++++++++++++----
 tests/t0600-git-multidir-tests.sh | 30 ++++++++++++++++++++++++++++++
 2 files changed, 60 insertions(+), 4 deletions(-)
 create mode 100755 tests/t0600-git-multidir-tests.sh

diff --git a/src/password-store.sh b/src/password-store.sh
index 63be840..5351073 100755
--- a/src/password-store.sh
+++ b/src/password-store.sh
@@ -17,14 +17,30 @@ X_SELECTION="${PASSWORD_STORE_X_SELECTION:-clipboard}"
 CLIP_TIME="${PASSWORD_STORE_CLIP_TIME:-45}"
 GENERATED_LENGTH="${PASSWORD_STORE_GENERATED_LENGTH:-25}"
 
-export GIT_DIR="${PASSWORD_STORE_GIT:-$PREFIX}/.git"
 export GIT_WORK_TREE="${PASSWORD_STORE_GIT:-$PREFIX}"
+export GIT_DIR="$GIT_WORK_TREE/.git"
 
 #
 # BEGIN helper functions
 #
-
+git_set_dir() {
+	local gwt="${PASSWORD_STORE_GIT:-$PREFIX}"; gwt="${gwt%/}"
+	[[ "$1" == "." ]] && return
+	[[ "$1" == "$gwt" ]] && return
+	if [ "$1" != "${1#/}" ]; then
+		local dir="$1"
+	else
+		local dir="$gwt/$1"
+	fi
+	if [[ -d "$dir/.git" ]]; then
+		export GIT_WORK_TREE="$dir"
+		export GIT_DIR="$dir/.git"
+	else
+		git_set_dir "$(dirname "$dir")"
+	fi
+}
 git_add_file() {
+	git_set_dir "$1"
 	[[ -d $GIT_DIR ]] || return
 	git add "$1" || return
 	[[ -n $(git status --porcelain "$1") ]] || return
@@ -450,6 +466,7 @@ cmd_generate() {
 	[[ ! $length =~ ^[0-9]+$ ]] && die "Error: pass-length \"$length\" must be a number."
 	mkdir -p -v "$PREFIX/$(dirname "$path")"
 	set_gpg_recipients "$(dirname "$path")"
+	git_set_dir "$path"
 	local passfile="$PREFIX/$path.gpg"
 
 	[[ $inplace -eq 0 && $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
@@ -500,6 +517,7 @@ cmd_delete() {
 	[[ $force -eq 1 ]] || yesno "Are you sure you would like to delete $path?"
 
 	rm $recursive -f -v "$passfile"
+	git_set_dir "$path"
 	if [[ -d $GIT_DIR && ! -e $passfile ]]; then
 		git rm -qr "$passfile"
 		git_commit "Remove $path from store."
@@ -555,10 +573,18 @@ cmd_copy_move() {
 
 cmd_git() {
 	if [[ $1 == "init" ]]; then
+		if [[ $2 ]]; then
+			local dir="$GIT_WORK_TREE/${@: -1}"
+			echo "init at $dir"
+			if [[ -d "$dir" ]]; then
+				export GIT_WORK_TREE="$dir"
+				export GIT_DIR="$GIT_WORK_TREE/.git"
+			fi
+		fi
 		git "$@" || exit 1
-		git_add_file "$PREFIX" "Add current contents of password store."
+		git_add_file "$GIT_WORK_TREE" "Add current contents of password store."
 
-		echo '*.gpg diff=gpg' > "$PREFIX/.gitattributes"
+		echo '*.gpg diff=gpg' > "$GIT_WORK_TREE/.gitattributes"
 		git_add_file .gitattributes "Configure git repository for gpg file diff."
 		git config --local diff.gpg.binary true
 		git config --local diff.gpg.textconv "$GPG -d ${GPG_OPTS[*]}"
diff --git a/tests/t0600-git-multidir-tests.sh b/tests/t0600-git-multidir-tests.sh
new file mode 100755
index 0000000..46d9c46
--- /dev/null
+++ b/tests/t0600-git-multidir-tests.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+
+test_description='Test multiple git repos'
+cd "$(dirname "$0")"
+. ./setup.sh
+
+test_expect_success 'Test init multiple repos' '
+	"$PASS" init -p personal $KEY1 &&
+	"$PASS" git init personal &&
+	"$PASS" init -p work $KEY2 &&
+	"$PASS" git init work &&
+	[[ -d $PASSWORD_STORE_DIR/work/.git ]] &&
+	[[ -d $PASSWORD_STORE_DIR/personal/.git ]]
+'
+
+test_expect_success 'Test alter passwords across multiple repos' '
+	"$PASS" generate personal/cred1 50 &&
+	"$PASS" generate work/sub/dir/cred2 50 &&
+	"$PASS" rm personal/cred1 &&
+	[[ ! -e $PASSWORD_STORE_DIR/personal/cred1.gpg ]] &&
+	[[ -e $PASSWORD_STORE_DIR/work/sub/dir/cred2.gpg ]] &&
+	export GIT_WORK_TREE="$PASSWORD_STORE_DIR/personal" &&
+	export GIT_DIR="$GIT_WORK_TREE/.git" &&
+	[[ "$(git rev-list --all --count)" == "4" ]] &&
+	export GIT_WORK_TREE="$PASSWORD_STORE_DIR/work" &&
+	export GIT_DIR="$GIT_WORK_TREE/.git" &&
+	[[ "$(git rev-list --all --count)" == "3" ]]
+'
+
+test_done

From 7182055b9b363f8ee0df312c561803adba8cc427 Mon Sep 17 00:00:00 2001
From: "Lance R. Vick" <[email protected]>
Date: Wed, 12 Oct 2016 01:15:17 -0700
Subject: [PATCH 2/7] handle submodules

---
 src/password-store.sh | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/password-store.sh b/src/password-store.sh
index 5351073..2e4e311 100755
--- a/src/password-store.sh
+++ b/src/password-store.sh
@@ -32,7 +32,7 @@ git_set_dir() {
 	else
 		local dir="$gwt/$1"
 	fi
-	if [[ -d "$dir/.git" ]]; then
+	if [[ -d "$dir/.git" ]] || [[ -f "$dir/.git" ]]; then
 		export GIT_WORK_TREE="$dir"
 		export GIT_DIR="$dir/.git"
 	else
@@ -41,14 +41,12 @@ git_set_dir() {
 }
 git_add_file() {
 	git_set_dir "$1"
-	[[ -d $GIT_DIR ]] || return
 	git add "$1" || return
 	[[ -n $(git status --porcelain "$1") ]] || return
 	git_commit "$2"
 }
 git_commit() {
 	local sign=""
-	[[ -d $GIT_DIR ]] || return
 	[[ $(git config --bool --get pass.signcommits) == "true" ]] && sign="-S"
 	git commit $sign -m "$1"
 }
@@ -588,6 +586,8 @@ cmd_git() {
 		git_add_file .gitattributes "Configure git repository for gpg file diff."
 		git config --local diff.gpg.binary true
 		git config --local diff.gpg.textconv "$GPG -d ${GPG_OPTS[*]}"
+	elif [[ $1 == "submodule" ]]; then
+		git -C "$GIT_WORK_TREE" "$@"
 	elif [[ -d $GIT_DIR ]]; then
 		tmpdir nowarn #Defines $SECURE_TMPDIR. We don't warn, because at most, this only copies encrypted files.
 		export TMPDIR="$SECURE_TMPDIR"

From 0a0c758f409c84bd2f0a711ec8eb3ceff7838fb1 Mon Sep 17 00:00:00 2001
From: "Lance R. Vick" <[email protected]>
Date: Wed, 12 Oct 2016 02:04:53 -0700
Subject: [PATCH 3/7] pass rm with submodules

---
 src/password-store.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/password-store.sh b/src/password-store.sh
index 2e4e311..fb227d8 100755
--- a/src/password-store.sh
+++ b/src/password-store.sh
@@ -516,7 +516,7 @@ cmd_delete() {
 
 	rm $recursive -f -v "$passfile"
 	git_set_dir "$path"
-	if [[ -d $GIT_DIR && ! -e $passfile ]]; then
+	if [[ ! -e $passfile ]]; then
 		git rm -qr "$passfile"
 		git_commit "Remove $path from store."
 	fi

From 9f198dae4b964f4ac1e2b5f8ee30e721ceb9e1ad Mon Sep 17 00:00:00 2001
From: "Lance R. Vick" <[email protected]>
Date: Wed, 12 Oct 2016 02:05:03 -0700
Subject: [PATCH 4/7] submodule tests

---
 tests/t0700-git-submodule-tests.sh | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)
 create mode 100755 tests/t0700-git-submodule-tests.sh

diff --git a/tests/t0700-git-submodule-tests.sh b/tests/t0700-git-submodule-tests.sh
new file mode 100755
index 0000000..3881455
--- /dev/null
+++ b/tests/t0700-git-submodule-tests.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+
+test_description='Test multiple git repos'
+cd "$(dirname "$0")"
+. ./setup.sh
+
+test_expect_success 'Test init multiple submodules' '
+	"$PASS" init $KEY1 &&
+	"$PASS" git init &&
+	"$PASS" git submodule add ./ personal &&
+	"$PASS" git submodule add ./ work &&
+	[[ -f $PASSWORD_STORE_DIR/work/.git ]] &&
+	[[ -f $PASSWORD_STORE_DIR/personal/.git ]]
+'
+
+test_expect_success 'Test alter passwords across multiple submodules' '
+	"$PASS" generate personal/cred1 50 &&
+	"$PASS" generate work/sub/dir/cred2 50 &&
+	"$PASS" rm personal/cred1 &&
+	[[ ! -e $PASSWORD_STORE_DIR/personal/cred1.gpg ]] &&
+	[[ -e $PASSWORD_STORE_DIR/work/sub/dir/cred2.gpg ]] &&
+	export GIT_WORK_TREE="$PASSWORD_STORE_DIR/personal" &&
+	export GIT_DIR="$GIT_WORK_TREE/.git" &&
+	git log >> /tmp/gitlog.txt &&
+	[[ "$(git rev-list --all --count)" == "4" ]] &&
+	export GIT_WORK_TREE="$PASSWORD_STORE_DIR/work" &&
+	export GIT_DIR="$GIT_WORK_TREE/.git" &&
+	[[ "$(git rev-list --all --count)" == "3" ]]
+'
+
+test_done

From 97c264be2f688b346a5b6a4653831ebadccfad99 Mon Sep 17 00:00:00 2001
From: "Lance R. Vick" <[email protected]>
Date: Wed, 12 Oct 2016 03:45:14 -0700
Subject: [PATCH 5/7] fix remaining explicit directory checks that break
 submodules

---
 src/password-store.sh | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/password-store.sh b/src/password-store.sh
index fb227d8..a228ae3 100755
--- a/src/password-store.sh
+++ b/src/password-store.sh
@@ -32,7 +32,7 @@ git_set_dir() {
 	else
 		local dir="$gwt/$1"
 	fi
-	if [[ -d "$dir/.git" ]] || [[ -f "$dir/.git" ]]; then
+	if [[ -e "$dir/.git" ]]; then
 		export GIT_WORK_TREE="$dir"
 		export GIT_DIR="$dir/.git"
 	else
@@ -291,7 +291,7 @@ cmd_init() {
 	if [[ $# -eq 1 && -z $1 ]]; then
 		[[ ! -f "$gpg_id" ]] && die "Error: $gpg_id does not exist and so cannot be removed."
 		rm -v -f "$gpg_id" || exit 1
-		if [[ -d $GIT_DIR ]]; then
+		if [[ -e $GIT_DIR ]]; then
 			git rm -qr "$gpg_id"
 			git_commit "Deinitialize ${gpg_id}${id_path:+ ($id_path)}."
 		fi
@@ -557,7 +557,7 @@ cmd_copy_move() {
 		mv $interactive -v "$old_path" "$new_path" || exit 1
 		[[ -e "$new_path" ]] && reencrypt_path "$new_path"
 
-		if [[ -d $GIT_DIR && ! -e $old_path ]]; then
+		if [[ -e $GIT_DIR && ! -e $old_path ]]; then
 			git rm -qr "$old_path"
 			git_add_file "$new_path" "Rename ${1} to ${2}."
 		fi
@@ -588,7 +588,7 @@ cmd_git() {
 		git config --local diff.gpg.textconv "$GPG -d ${GPG_OPTS[*]}"
 	elif [[ $1 == "submodule" ]]; then
 		git -C "$GIT_WORK_TREE" "$@"
-	elif [[ -d $GIT_DIR ]]; then
+	elif [[ -e $GIT_DIR ]]; then
 		tmpdir nowarn #Defines $SECURE_TMPDIR. We don't warn, because at most, this only copies encrypted files.
 		export TMPDIR="$SECURE_TMPDIR"
 		git "$@"

From 472b9b91def1f9c51f815404c304fcd149e230e2 Mon Sep 17 00:00:00 2001
From: "Lance R. Vick" <[email protected]>
Date: Wed, 12 Oct 2016 03:46:15 -0700
Subject: [PATCH 6/7] fix test naming

---
 tests/t0600-git-multidir-tests.sh  | 30 ------------------------------
 tests/t0600-git-multirepo-tests.sh | 30 ++++++++++++++++++++++++++++++
 tests/t0700-git-submodule-tests.sh |  2 +-
 3 files changed, 31 insertions(+), 31 deletions(-)
 delete mode 100755 tests/t0600-git-multidir-tests.sh
 create mode 100755 tests/t0600-git-multirepo-tests.sh

diff --git a/tests/t0600-git-multidir-tests.sh b/tests/t0600-git-multidir-tests.sh
deleted file mode 100755
index 46d9c46..0000000
--- a/tests/t0600-git-multidir-tests.sh
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/usr/bin/env bash
-
-test_description='Test multiple git repos'
-cd "$(dirname "$0")"
-. ./setup.sh
-
-test_expect_success 'Test init multiple repos' '
-	"$PASS" init -p personal $KEY1 &&
-	"$PASS" git init personal &&
-	"$PASS" init -p work $KEY2 &&
-	"$PASS" git init work &&
-	[[ -d $PASSWORD_STORE_DIR/work/.git ]] &&
-	[[ -d $PASSWORD_STORE_DIR/personal/.git ]]
-'
-
-test_expect_success 'Test alter passwords across multiple repos' '
-	"$PASS" generate personal/cred1 50 &&
-	"$PASS" generate work/sub/dir/cred2 50 &&
-	"$PASS" rm personal/cred1 &&
-	[[ ! -e $PASSWORD_STORE_DIR/personal/cred1.gpg ]] &&
-	[[ -e $PASSWORD_STORE_DIR/work/sub/dir/cred2.gpg ]] &&
-	export GIT_WORK_TREE="$PASSWORD_STORE_DIR/personal" &&
-	export GIT_DIR="$GIT_WORK_TREE/.git" &&
-	[[ "$(git rev-list --all --count)" == "4" ]] &&
-	export GIT_WORK_TREE="$PASSWORD_STORE_DIR/work" &&
-	export GIT_DIR="$GIT_WORK_TREE/.git" &&
-	[[ "$(git rev-list --all --count)" == "3" ]]
-'
-
-test_done
diff --git a/tests/t0600-git-multirepo-tests.sh b/tests/t0600-git-multirepo-tests.sh
new file mode 100755
index 0000000..46d9c46
--- /dev/null
+++ b/tests/t0600-git-multirepo-tests.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+
+test_description='Test multiple git repos'
+cd "$(dirname "$0")"
+. ./setup.sh
+
+test_expect_success 'Test init multiple repos' '
+	"$PASS" init -p personal $KEY1 &&
+	"$PASS" git init personal &&
+	"$PASS" init -p work $KEY2 &&
+	"$PASS" git init work &&
+	[[ -d $PASSWORD_STORE_DIR/work/.git ]] &&
+	[[ -d $PASSWORD_STORE_DIR/personal/.git ]]
+'
+
+test_expect_success 'Test alter passwords across multiple repos' '
+	"$PASS" generate personal/cred1 50 &&
+	"$PASS" generate work/sub/dir/cred2 50 &&
+	"$PASS" rm personal/cred1 &&
+	[[ ! -e $PASSWORD_STORE_DIR/personal/cred1.gpg ]] &&
+	[[ -e $PASSWORD_STORE_DIR/work/sub/dir/cred2.gpg ]] &&
+	export GIT_WORK_TREE="$PASSWORD_STORE_DIR/personal" &&
+	export GIT_DIR="$GIT_WORK_TREE/.git" &&
+	[[ "$(git rev-list --all --count)" == "4" ]] &&
+	export GIT_WORK_TREE="$PASSWORD_STORE_DIR/work" &&
+	export GIT_DIR="$GIT_WORK_TREE/.git" &&
+	[[ "$(git rev-list --all --count)" == "3" ]]
+'
+
+test_done
diff --git a/tests/t0700-git-submodule-tests.sh b/tests/t0700-git-submodule-tests.sh
index 3881455..35eab55 100755
--- a/tests/t0700-git-submodule-tests.sh
+++ b/tests/t0700-git-submodule-tests.sh
@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-test_description='Test multiple git repos'
+test_description='Test git submodules'
 cd "$(dirname "$0")"
 . ./setup.sh
 

From 0c840499f2c98427576e52a9d32d9e778e5d813d Mon Sep 17 00:00:00 2001
From: "Lance R. Vick" <[email protected]>
Date: Fri, 25 Nov 2016 20:17:39 -0800
Subject: [PATCH 7/7] wrap init to not expect git to be already initialized

---
 src/password-store.sh | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/password-store.sh b/src/password-store.sh
index a228ae3..b1b7097 100755
--- a/src/password-store.sh
+++ b/src/password-store.sh
@@ -301,11 +301,15 @@ cmd_init() {
 		printf "%s\n" "$@" > "$gpg_id"
 		local id_print="$(printf "%s, " "$@")"
 		echo "Password store initialized for ${id_print%, }${id_path:+ ($id_path)}"
-		git_add_file "$gpg_id" "Set GPG id to ${id_print%, }${id_path:+ ($id_path)}."
+		if [[ -e $GIT_DIR ]]; then
+			git_add_file "$gpg_id" "Set GPG id to ${id_print%, }${id_path:+ ($id_path)}."
+		fi
 	fi
 
 	reencrypt_path "$PREFIX/$id_path"
-	git_add_file "$PREFIX/$id_path" "Reencrypt password store using new GPG id ${id_print%, }${id_path:+ ($id_path)}."
+	if [[ -e $GIT_DIR ]]; then
+		git_add_file "$PREFIX/$id_path" "Reencrypt password store using new GPG id ${id_print%, }${id_path:+ ($id_path)}."
+	fi
 }
 
 cmd_show() {
@@ -573,7 +577,6 @@ cmd_git() {
 	if [[ $1 == "init" ]]; then
 		if [[ $2 ]]; then
 			local dir="$GIT_WORK_TREE/${@: -1}"
-			echo "init at $dir"
 			if [[ -d "$dir" ]]; then
 				export GIT_WORK_TREE="$dir"
 				export GIT_DIR="$GIT_WORK_TREE/.git"
_______________________________________________
Password-Store mailing list
[email protected]
https://lists.zx2c4.com/mailman/listinfo/password-store

Reply via email to