git/t/lib-credential.sh

# Shell library for testing credential handling including helpers. See t0302
# for an example of testing a specific helper.

# Try a set of credential helpers; the expected stdin,
# stdout and stderr should be provided on stdin,
# separated by "--".
check() {
	credential_opts=
	credential_cmd=$1
	shift
	for arg in "$@"; do
		credential_opts="$credential_opts -c credential.helper='$arg'"
	done
	read_chunk >stdin &&
	read_chunk >expect-stdout &&
	read_chunk >expect-stderr &&
	if ! eval "git $credential_opts credential $credential_cmd <stdin >stdout 2>stderr"; then
		echo "git credential failed with code $?" &&
		cat stderr &&
		false
	fi &&
	test_cmp expect-stdout stdout &&
	test_cmp expect-stderr stderr
}

read_chunk() {
	while read line; do
		case "$line" in
		--) break ;;
		*) echo "$line" ;;
		esac
	done
}

# Clear any residual data from previous tests. We only
# need this when testing third-party helpers which read and
# write outside of our trash-directory sandbox.
#
# Don't bother checking for success here, as it is
# outside the scope of tests and represents a best effort to
# clean up after ourselves.
helper_test_clean() {
	reject $1 https example.com store-user
	reject $1 https example.com user1
	reject $1 https example.com user2
	reject $1 https example.com user-expiry
	reject $1 https example.com user-expiry-overwrite
	reject $1 https example.com user4
	reject $1 https example.com user-distinct-pass
	reject $1 https example.com user-overwrite
	reject $1 https example.com user-erase1
	reject $1 https example.com user-erase2
	reject $1 https victim.example.com user
	reject $1 http path.tld user
	reject $1 https timeout.tld user
	reject $1 https sso.tld
}

reject() {
	(
		echo protocol=$2
		echo host=$3
		echo username=$4
	) | git -c credential.helper=$1 credential reject
}

helper_test() {
	HELPER=$1

	test_expect_success "helper ($HELPER) has no existing data" '
		check fill $HELPER <<-\EOF
		protocol=https
		host=example.com
		--
		protocol=https
		host=example.com
		username=askpass-username
		password=askpass-password
		--
		askpass: Username for '\''https://example.com'\'':
		askpass: Password for '\''https://[email protected]'\'':
		EOF
	'

	test_expect_success "helper ($HELPER) stores password" '
		check approve $HELPER <<-\EOF
		protocol=https
		host=example.com
		username=store-user
		password=store-pass
		EOF
	'

	test_expect_success "helper ($HELPER) can retrieve password" '
		check fill $HELPER <<-\EOF
		protocol=https
		host=example.com
		--
		protocol=https
		host=example.com
		username=store-user
		password=store-pass
		--
		EOF
	'

	test_expect_success "helper ($HELPER) requires matching protocol" '
		check fill $HELPER <<-\EOF
		protocol=http
		host=example.com
		--
		protocol=http
		host=example.com
		username=askpass-username
		password=askpass-password
		--
		askpass: Username for '\''http://example.com'\'':
		askpass: Password for '\''http://[email protected]'\'':
		EOF
	'

	test_expect_success "helper ($HELPER) requires matching host" '
		check fill $HELPER <<-\EOF
		protocol=https
		host=other.tld
		--
		protocol=https
		host=other.tld
		username=askpass-username
		password=askpass-password
		--
		askpass: Username for '\''https://other.tld'\'':
		askpass: Password for '\''https://[email protected]'\'':
		EOF
	'

	test_expect_success "helper ($HELPER) requires matching username" '
		check fill $HELPER <<-\EOF
		protocol=https
		host=example.com
		username=other
		--
		protocol=https
		host=example.com
		username=other
		password=askpass-password
		--
		askpass: Password for '\''https://[email protected]'\'':
		EOF
	'

	test_expect_success "helper ($HELPER) requires matching path" '
		test_config credential.usehttppath true &&
		check approve $HELPER <<-\EOF &&
		protocol=http
		host=path.tld
		path=foo.git
		username=user
		password=pass
		EOF
		check fill $HELPER <<-\EOF
		protocol=http
		host=path.tld
		path=bar.git
		--
		protocol=http
		host=path.tld
		path=bar.git
		username=askpass-username
		password=askpass-password
		--
		askpass: Username for '\''http://path.tld/bar.git'\'':
		askpass: Password for '\''http://[email protected]/bar.git'\'':
		EOF
	'

	test_expect_success "helper ($HELPER) overwrites on store" '
		check approve $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user-overwrite
		password=pass1
		EOF
		check approve $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user-overwrite
		password=pass2
		EOF
		check fill $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user-overwrite
		--
		protocol=https
		host=example.com
		username=user-overwrite
		password=pass2
		EOF
		check reject $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user-overwrite
		password=pass2
		EOF
		check fill $HELPER <<-\EOF
		protocol=https
		host=example.com
		username=user-overwrite
		--
		protocol=https
		host=example.com
		username=user-overwrite
		password=askpass-password
		--
		askpass: Password for '\''https://[email protected]'\'':
		EOF
	'

	test_expect_success "helper ($HELPER) can forget host" '
		check reject $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		EOF
		check fill $HELPER <<-\EOF
		protocol=https
		host=example.com
		--
		protocol=https
		host=example.com
		username=askpass-username
		password=askpass-password
		--
		askpass: Username for '\''https://example.com'\'':
		askpass: Password for '\''https://[email protected]'\'':
		EOF
	'

	test_expect_success "helper ($HELPER) can store multiple users" '
		check approve $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user1
		password=pass1
		EOF
		check approve $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user2
		password=pass2
		EOF
		check fill $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user1
		--
		protocol=https
		host=example.com
		username=user1
		password=pass1
		EOF
		check fill $HELPER <<-\EOF
		protocol=https
		host=example.com
		username=user2
		--
		protocol=https
		host=example.com
		username=user2
		password=pass2
		EOF
	'

	test_expect_success "helper ($HELPER) does not erase a password distinct from input" '
		check approve $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user-distinct-pass
		password=pass1
		EOF
		check reject $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user-distinct-pass
		password=pass2
		EOF
		check fill $HELPER <<-\EOF
		protocol=https
		host=example.com
		username=user-distinct-pass
		--
		protocol=https
		host=example.com
		username=user-distinct-pass
		password=pass1
		EOF
	'

	test_expect_success "helper ($HELPER) can forget user" '
		check reject $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user1
		EOF
		check fill $HELPER <<-\EOF
		protocol=https
		host=example.com
		username=user1
		--
		protocol=https
		host=example.com
		username=user1
		password=askpass-password
		--
		askpass: Password for '\''https://[email protected]'\'':
		EOF
	'

	test_expect_success "helper ($HELPER) remembers other user" '
		check fill $HELPER <<-\EOF
		protocol=https
		host=example.com
		username=user2
		--
		protocol=https
		host=example.com
		username=user2
		password=pass2
		EOF
	'

	test_expect_success "helper ($HELPER) can store empty username" '
		check approve $HELPER <<-\EOF &&
		protocol=https
		host=sso.tld
		username=
		password=
		EOF
		check fill $HELPER <<-\EOF
		protocol=https
		host=sso.tld
		--
		protocol=https
		host=sso.tld
		username=
		password=
		EOF
	'

	test_expect_success "helper ($HELPER) erases all matching credentials" '
		check approve $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user-erase1
		password=pass1
		EOF
		check approve $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user-erase2
		password=pass1
		EOF
		check reject $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		EOF
		check fill $HELPER <<-\EOF
		protocol=https
		host=example.com
		--
		protocol=https
		host=example.com
		username=askpass-username
		password=askpass-password
		--
		askpass: Username for '\''https://example.com'\'':
		askpass: Password for '\''https://[email protected]'\'':
		EOF
	'

	: ${GIT_TEST_LONG_CRED_BUFFER:=1024}
	# 23 bytes accounts for "wwwauth[]=basic realm=" plus NUL
	LONG_VALUE_LEN=$((GIT_TEST_LONG_CRED_BUFFER - 23))
	LONG_VALUE=$(perl -e 'print "a" x shift' $LONG_VALUE_LEN)

	test_expect_success "helper ($HELPER) not confused by long header" '
		check approve $HELPER <<-\EOF &&
		protocol=https
		host=victim.example.com
		username=user
		password=to-be-stolen
		EOF

		check fill $HELPER <<-EOF
		protocol=https
		host=badguy.example.com
		wwwauth[]=basic realm=${LONG_VALUE}host=victim.example.com
		--
		protocol=https
		host=badguy.example.com
		username=askpass-username
		password=askpass-password
		wwwauth[]=basic realm=${LONG_VALUE}host=victim.example.com
		--
		askpass: Username for '\''https://badguy.example.com'\'':
		askpass: Password for '\''https://[email protected]'\'':
		EOF
	'
}

helper_test_timeout() {
	HELPER="$*"

	test_expect_success "helper ($HELPER) times out" '
		check approve "$HELPER" <<-\EOF &&
		protocol=https
		host=timeout.tld
		username=user
		password=pass
		EOF
		sleep 2 &&
		check fill "$HELPER" <<-\EOF
		protocol=https
		host=timeout.tld
		--
		protocol=https
		host=timeout.tld
		username=askpass-username
		password=askpass-password
		--
		askpass: Username for '\''https://timeout.tld'\'':
		askpass: Password for '\''https://[email protected]'\'':
		EOF
	'
}

helper_test_password_expiry_utc() {
	HELPER=$1

	test_expect_success "helper ($HELPER) stores password_expiry_utc" '
		check approve $HELPER <<-\EOF
		protocol=https
		host=example.com
		username=user-expiry
		password=pass
		password_expiry_utc=9999999999
		EOF
	'

	test_expect_success "helper ($HELPER) gets password_expiry_utc" '
		check fill $HELPER <<-\EOF
		protocol=https
		host=example.com
		username=user-expiry
		--
		protocol=https
		host=example.com
		username=user-expiry
		password=pass
		password_expiry_utc=9999999999
		--
		EOF
	'

	test_expect_success "helper ($HELPER) overwrites when password_expiry_utc changes" '
		check approve $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user-expiry-overwrite
		password=pass1
		password_expiry_utc=9999999998
		EOF
		check approve $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user-expiry-overwrite
		password=pass2
		password_expiry_utc=9999999999
		EOF
		check fill $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user-expiry-overwrite
		--
		protocol=https
		host=example.com
		username=user-expiry-overwrite
		password=pass2
		password_expiry_utc=9999999999
		EOF
		check reject $HELPER <<-\EOF &&
		protocol=https
		host=example.com
		username=user-expiry-overwrite
		password=pass2
		EOF
		check fill $HELPER <<-\EOF
		protocol=https
		host=example.com
		username=user-expiry-overwrite
		--
		protocol=https
		host=example.com
		username=user-expiry-overwrite
		password=askpass-password
		--
		askpass: Password for '\''https://[email protected]'\'':
		EOF
	'
}

helper_test_oauth_refresh_token() {
	HELPER=$1

	test_expect_success "helper ($HELPER) stores oauth_refresh_token" '
		check approve $HELPER <<-\EOF
		protocol=https
		host=example.com
		username=user4
		password=pass
		oauth_refresh_token=xyzzy
		EOF
	'

	test_expect_success "helper ($HELPER) gets oauth_refresh_token" '
		check fill $HELPER <<-\EOF
		protocol=https
		host=example.com
		username=user4
		--
		protocol=https
		host=example.com
		username=user4
		password=pass
		oauth_refresh_token=xyzzy
		--
		EOF
	'
}

helper_test_authtype() {
	HELPER=$1

	test_expect_success "helper ($HELPER) stores authtype and credential" '
		check approve $HELPER <<-\EOF
		capability[]=authtype
		authtype=Bearer
		credential=random-token
		protocol=https
		host=git.example.com
		EOF
	'

	test_expect_success "helper ($HELPER) gets authtype and credential" '
		check fill $HELPER <<-\EOF
		capability[]=authtype
		protocol=https
		host=git.example.com
		--
		capability[]=authtype
		authtype=Bearer
		credential=random-token
		protocol=https
		host=git.example.com
		--
		EOF
	'

	test_expect_success "helper ($HELPER) stores authtype and credential with username" '
		check approve $HELPER <<-\EOF
		capability[]=authtype
		authtype=Bearer
		credential=other-token
		protocol=https
		host=git.example.com
		username=foobar
		EOF
	'

	test_expect_success "helper ($HELPER) gets authtype and credential with username" '
		check fill $HELPER <<-\EOF
		capability[]=authtype
		protocol=https
		host=git.example.com
		username=foobar
		--
		capability[]=authtype
		authtype=Bearer
		credential=other-token
		protocol=https
		host=git.example.com
		username=foobar
		--
		EOF
	'

	test_expect_success "helper ($HELPER) does not get authtype and credential with different username" '
		check fill $HELPER <<-\EOF
		capability[]=authtype
		protocol=https
		host=git.example.com
		username=barbaz
		--
		protocol=https
		host=git.example.com
		username=barbaz
		password=askpass-password
		--
		askpass: Password for '\''https://[email protected]'\'':
		EOF
	'

	test_expect_success "helper ($HELPER) does not store ephemeral authtype and credential" '
		check approve $HELPER <<-\EOF &&
		capability[]=authtype
		authtype=Bearer
		credential=git2-token
		protocol=https
		host=git2.example.com
		ephemeral=1
		EOF

		check fill $HELPER <<-\EOF
		capability[]=authtype
		protocol=https
		host=git2.example.com
		--
		protocol=https
		host=git2.example.com
		username=askpass-username
		password=askpass-password
		--
		askpass: Username for '\''https://git2.example.com'\'':
		askpass: Password for '\''https://[email protected]'\'':
		EOF
	'

	test_expect_success "helper ($HELPER) does not store ephemeral username and password" '
		check approve $HELPER <<-\EOF &&
		capability[]=authtype
		protocol=https
		host=git2.example.com
		user=barbaz
		password=secret
		ephemeral=1
		EOF

		check fill $HELPER <<-\EOF
		capability[]=authtype
		protocol=https
		host=git2.example.com
		--
		protocol=https
		host=git2.example.com
		username=askpass-username
		password=askpass-password
		--
		askpass: Username for '\''https://git2.example.com'\'':
		askpass: Password for '\''https://[email protected]'\'':
		EOF
	'
}

write_script askpass <<\EOF
echo >&2 askpass: $*
what=$(echo $1 | cut -d" " -f1 | tr A-Z a-z | tr -cd a-z)
echo "askpass-$what"
EOF
GIT_ASKPASS="$PWD/askpass"
export GIT_ASKPASS