diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..fd311f2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/shunit2"] + path = tests/shunit2 + url = https://github.com/kward/shunit2.git diff --git a/Dockerfile b/Dockerfile index e23cb29..cf06cbf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ RUN apt-get update && \ rm -f /etc/ssh/ssh_host_*key* COPY sshd_config /etc/ssh/sshd_config +COPY create-sftp-user /usr/local/bin/ COPY entrypoint / EXPOSE 22 diff --git a/create-sftp-user b/create-sftp-user new file mode 100755 index 0000000..f50d3d5 --- /dev/null +++ b/create-sftp-user @@ -0,0 +1,105 @@ +#!/bin/bash +set -Eeo pipefail + +# shellcheck disable=2154 +trap 's=$?; echo "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR + +# Extended regular expression (ERE) for arguments +reUser='[A-Za-z0-9._][A-Za-z0-9._-]{0,31}' # POSIX.1-2008 +rePass='[^:]{0,255}' +reUid='[[:digit:]]*' +reGid='[[:digit:]]*' +reDir='[^:]*' +#reArgs="^($reUser)(:$rePass)(:e)?(:$reUid)?(:$reGid)?(:$reDir)?$" + +function log() { + echo "[$0] $*" +} + +function validateArg() { + name="$1" + val="$2" + re="$3" + + if [[ "$val" =~ ^$re$ ]]; then + return 0 + else + log "ERROR: Invalid $name \"$val\", do not match required regex pattern: $re" + return 1 + fi +} + +log "Parsing user data: \"$1\"" +IFS=':' read -ra args <<< "$1" + +skipIndex=0 +chpasswdOptions="" +useraddOptions=(--no-user-group) + +user="${args[0]}"; validateArg "username" "$user" "$reUser" || return 1 +pass="${args[1]}"; validateArg "password" "$pass" "$rePass" || return 1 + +if [ "${args[2]}" == "e" ]; then + chpasswdOptions="-e" + skipIndex=1 +fi + +uid="${args[$((skipIndex+2))]}"; validateArg "UID" "$uid" "$reUid" || return 1 +gid="${args[$((skipIndex+3))]}"; validateArg "GID" "$gid" "$reGid" || return 1 +dir="${args[$((skipIndex+4))]}"; validateArg "dirs" "$dir" "$reDir" || return 1 + +if getent passwd "$user" > /dev/null; then + log "WARNING: User \"$user\" already exists. Skipping." + return 0 +fi + +if [ -n "$uid" ]; then + useraddOptions+=(--non-unique --uid "$uid") +fi + +if [ -n "$gid" ]; then + if ! getent group "$gid" > /dev/null; then + groupadd --gid "$gid" "group_$gid" + fi + + useraddOptions+=(--gid "$gid") +fi + +useradd "${useraddOptions[@]}" "$user" +mkdir -p "/home/$user" +chown root:root "/home/$user" +chmod 755 "/home/$user" + +# Retrieving user id to use it in chown commands instead of the user name +# to avoid problems on alpine when the user name contains a '.' +uid="$(id -u "$user")" + +if [ -n "$pass" ]; then + echo "$user:$pass" | chpasswd $chpasswdOptions +else + usermod -p "*" "$user" # disabled password +fi + +# Add SSH keys to authorized_keys with valid permissions +if [ -d "/home/$user/.ssh/keys" ]; then + for publickey in "/home/$user/.ssh/keys"/*; do + cat "$publickey" >> "/home/$user/.ssh/authorized_keys" + done + chown "$uid" "/home/$user/.ssh/authorized_keys" + chmod 600 "/home/$user/.ssh/authorized_keys" +fi + +# Make sure dirs exists +if [ -n "$dir" ]; then + IFS=',' read -ra dirArgs <<< "$dir" + for dirPath in "${dirArgs[@]}"; do + dirPath="/home/$user/$dirPath" + if [ ! -d "$dirPath" ]; then + log "Creating directory: $dirPath" + mkdir -p "$dirPath" + chown -R "$uid:users" "$dirPath" + else + log "Directory already exists: $dirPath" + fi + done +fi diff --git a/entrypoint b/entrypoint index 5762a11..8730452 100755 --- a/entrypoint +++ b/entrypoint @@ -4,113 +4,16 @@ set -Eeo pipefail # shellcheck disable=2154 trap 's=$?; echo "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR +reArgsMaybe="^[^:[:space:]]+:.*$" # Smallest indication of attempt to use argument +reArgSkip='^([[:blank:]]*#.*|[[:blank:]]*)$' # comment or empty line + # Paths userConfPath="/etc/sftp/users.conf" userConfPathLegacy="/etc/sftp-users.conf" userConfFinalPath="/var/run/sftp/users.conf" -# Extended regular expression (ERE) for arguments -reUser='[A-Za-z0-9._][A-Za-z0-9._-]{0,31}' # POSIX.1-2008 -rePass='[^:]{0,255}' -reUid='[[:digit:]]*' -reGid='[[:digit:]]*' -reDir='[^:]*' -#reArgs="^($reUser)(:$rePass)(:e)?(:$reUid)?(:$reGid)?(:$reDir)?$" -reArgsMaybe="^[^:[:space:]]+:.*$" # Smallest indication of attempt to use argument -reArgSkip='^([[:blank:]]*#.*|[[:blank:]]*)$' # comment or empty line - function log() { - echo "[entrypoint] $*" -} - -function validateArg() { - name="$1" - val="$2" - re="$3" - - if [[ "$val" =~ ^$re$ ]]; then - return 0 - else - log "ERROR: Invalid $name \"$val\", do not match required regex pattern: $re" - return 1 - fi -} - -function createUser() { - log "Parsing user data: \"$1\"" - IFS=':' read -ra args <<< "$1" - - skipIndex=0 - chpasswdOptions="" - useraddOptions=(--no-user-group) - - user="${args[0]}"; validateArg "username" "$user" "$reUser" || return 1 - pass="${args[1]}"; validateArg "password" "$pass" "$rePass" || return 1 - - if [ "${args[2]}" == "e" ]; then - chpasswdOptions="-e" - skipIndex=1 - fi - - uid="${args[$((skipIndex+2))]}"; validateArg "UID" "$uid" "$reUid" || return 1 - gid="${args[$((skipIndex+3))]}"; validateArg "GID" "$gid" "$reGid" || return 1 - dir="${args[$((skipIndex+4))]}"; validateArg "dirs" "$dir" "$reDir" || return 1 - - if getent passwd "$user" > /dev/null; then - log "WARNING: User \"$user\" already exists. Skipping." - return 0 - fi - - if [ -n "$uid" ]; then - useraddOptions+=(--non-unique --uid "$uid") - fi - - if [ -n "$gid" ]; then - if ! getent group "$gid" > /dev/null; then - groupadd --gid "$gid" "group_$gid" - fi - - useraddOptions+=(--gid "$gid") - fi - - useradd "${useraddOptions[@]}" "$user" - mkdir -p "/home/$user" - chown root:root "/home/$user" - chmod 755 "/home/$user" - - # Retrieving user id to use it in chown commands instead of the user name - # to avoid problems on alpine when the user name contains a '.' - uid="$(id -u "$user")" - - if [ -n "$pass" ]; then - echo "$user:$pass" | chpasswd $chpasswdOptions - else - usermod -p "*" "$user" # disabled password - fi - - # Add SSH keys to authorized_keys with valid permissions - if [ -d "/home/$user/.ssh/keys" ]; then - for publickey in "/home/$user/.ssh/keys"/*; do - cat "$publickey" >> "/home/$user/.ssh/authorized_keys" - done - chown "$uid" "/home/$user/.ssh/authorized_keys" - chmod 600 "/home/$user/.ssh/authorized_keys" - fi - - # Make sure dirs exists - if [ -n "$dir" ]; then - IFS=',' read -ra dirArgs <<< "$dir" - for dirPath in "${dirArgs[@]}"; do - dirPath="/home/$user/$dirPath" - if [ ! -d "$dirPath" ]; then - log "Creating directory: $dirPath" - mkdir -p "$dirPath" - chown -R "$uid:users" "$dirPath" - else - log "Directory already exists: $dirPath" - fi - done - fi + echo "[$0] $*" >&2 } # Allow running other programs, e.g. bash @@ -130,8 +33,8 @@ fi if [ ! -f "$userConfFinalPath" ]; then mkdir -p "$(dirname $userConfFinalPath)" - # Append mounted config to final config if [ -f "$userConfPath" ]; then + # Append mounted config to final config grep -v -E "$reArgSkip" < "$userConfPath" > "$userConfFinalPath" fi @@ -154,7 +57,7 @@ if [ ! -f "$userConfFinalPath" ]; then if [ -f "$userConfFinalPath" ] && [ "$(wc -l < "$userConfFinalPath")" -gt 0 ]; then # Import users from final conf file while IFS= read -r user || [[ -n "$user" ]]; do - createUser "$user" + create-sftp-user "$user" done < "$userConfFinalPath" elif $startSshd; then log "FATAL: No users provided!" diff --git a/tests/bashunit.bash b/tests/bashunit.bash deleted file mode 100644 index 139deef..0000000 --- a/tests/bashunit.bash +++ /dev/null @@ -1,198 +0,0 @@ - -#!/usr/bin/env bash - -######################################################################## -# GLOBALS -######################################################################## - -verbose=2 - -bashunit_passed=0 -bashunit_failed=0 -bashunit_skipped=0 - -######################################################################## -# ASSERT FUNCTIONS -######################################################################## - -# Assert that a given expression evaluates to true. -# -# $1: Expression -assert() { - if test $* ; then _passed ; else _failed "$*" true ; fi -} - -# Assert that a given output string is equal to an expected string. -# -# $1: Output -# $2: Expected -assertEqual() { - echo $1 | grep -E "^$2$" > /dev/null - if [ $? -eq 0 ] ; then _passed ; else _failed "$1" "$2" ; fi -} - -# Assert that a given output string is not equal to an expected string. -# -# $1: Output -# $2: Expected -assertNotEqual() { - echo $1 | grep -E "^$2$" > /dev/null - if [ $? -ne 0 ] ; then _passed ; else _failed "$1" "$2" ; fi -} - -# Assert that a given output string starts with an expected string. -# -# $1: Output -# $2: Expected -assertStartsWith() { - echo $1 | grep -E "^$2" > /dev/null - if [ $? -eq 0 ] ; then _passed ; else _failed "$1" "$2" ; fi -} - -# Assert that the last command's return code is equal to an expected integer. -# -# $1: Output -# $2: Expected -# $?: Provided -assertReturn() { - local code=$? - if [ $code -eq $2 ] ; then _passed ; else _failed "$code" "$2" ; fi -} - -# Assert that the last command's return code is not equal to an expected integer. -# -# $1: Output -# $2: Expected -# $?: Provided -assertNotReturn() { - local code=$? - if [ $code -ne $2 ] ; then _passed ; else _failed "$code" "$2" ; fi -} - -# Assert that a given integer is greater than an expected integer. -# -# $1: Output -# $2: Expected -assertGreaterThan() { - if [ $1 -gt $2 ] ; then _passed ; else _failed "$1" "$2" ; fi -} - -# Assert that a given integer is greater than or equal to an expected integer. -# -# $1: Output -# $2: Expected -assertAtLeast() { - if [ $1 -ge $2 ] ; then _passed ; else _failed "$1" "$2" ; fi -} - -# Assert that a given integer is less than an expected integer. -# -# $1: Output -# $2: Expected -assertLessThan() { - if [ $1 -lt $2 ] ; then _passed ; else _failed "$1" "$2" ; fi -} - -# Assert that a given integer is less than or equal to an expected integer. -# -# $1: Output -# $2: Expected -assertAtMost() { - if [ $1 -le $2 ] ; then _passed ; else _failed "$1" "$2" ; fi -} - -# Skip the current test case. -# -skip() { - _skipped -} - -_failed() { - bashunit_failed=$((bashunit_failed+1)) - - local tc=${FUNCNAME[2]} - local line=${BASH_LINENO[1]} - if [ $verbose -ge 2 ] ; then - echo -e "\033[37;1m$tc\033[0m:$line:\033[31mFailed\033[0m" - fi - if [ $verbose -eq 3 ] ; then - echo -e "\033[31mExpected\033[0m: $2" - echo -e "\033[31mProvided\033[0m: $1" - fi -} - -_passed() { - bashunit_passed=$((bashunit_passed+1)) - - local tc=${FUNCNAME[2]} - local line=${BASH_LINENO[1]} - if [ $verbose -ge 2 ] ; then - echo -e "\033[37;1m$tc\033[0m:$line:\033[32mPassed\033[0m" - fi -} - -_skipped() { - bashunit_skipped=$((bashunit_skipped+1)) - - local tc=${FUNCNAME[2]} - local line=${BASH_LINENO[1]} - if [ $verbose -ge 2 ] ; then - echo -e "\033[37;1m$tc\033[0m:$line:\033[33mSkipped\033[0m" - fi -} - -######################################################################## -# RUN -######################################################################## - -usage() { - echo "Usage: [options...]" - echo - echo "Options:" - echo " -v, --verbose Print exptected and provided values" - echo " -s, --summary Only print summary omitting individual test results" - echo " -q, --quiet Do not print anything to standard output" - echo " -h, --help Show usage screen" -} - -runTests() { - local test_pattern="test[a-zA-Z0-9_]\+" - local testcases=$(grep "^ *\(function \)*$test_pattern *\\(\\)" $0 | \ - grep -o $test_pattern) - - if [ ! "${testcases[*]}" ] ; then - usage - exit 0 - fi - - if [ "$(type -t "beforeTest")" == "function" ]; then - beforeTest - fi - - for tc in $testcases ; do $tc ; done - - if [ "$(type -t "afterTest")" == "function" ]; then - afterTest - fi - - if [ $verbose -ge 1 ] ; then - echo "Done. $bashunit_passed passed." \ - "$bashunit_failed failed." \ - "$bashunit_skipped skipped." - fi - exit $bashunit_failed -} - -# Arguments -while [ $# -gt 0 ]; do - arg=$1; shift - case $arg in - "-v"|"--verbose") verbose=3;; - "-s"|"--summary") verbose=1;; - "-q"|"--quiet") verbose=0;; - "-h"|"--help") usage; exit 0;; - *) shift;; - esac -done - -runTests diff --git a/tests/files/users.conf b/tests/files/users.conf new file mode 100644 index 0000000..fe3b489 --- /dev/null +++ b/tests/files/users.conf @@ -0,0 +1,14 @@ +# comment + # whitespace before and after comment + +# empty lines + + + +user-from-conf: +test::::dir1,dir2 + +# next line has only whitespace (spaces and tabs) + + +user.with.dot diff --git a/tests/id_rsa b/tests/id_rsa deleted file mode 100644 index 647d58c..0000000 --- a/tests/id_rsa +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAxww0MPWvZohlgxFyxvZjEezrld7n5pIJuYbZ7bmZ8UcNbFH8 -kpoHmmOWQ7x8ObhDLpcWEv8ifoS8RYLI97VMCJcEc/ckcq82ZvoAROF4bG/jYXWj -EHRZzEp93FMPFBzvBZkXeBS4ksd9sfqRfNyZzUeu/t380oNsCpr8+TnaEEhC5+E6 -UbjreH0CPttkD0aRjrszer1hkGs2Py80VZSnidZGrFRNafxigjcLRNp4ZJNweFc7 -mcuLIfBapJXIJGuj2XAxrt86pBQoESfaETg0m3DWl4Z1w6wZZ26WnIiVOHdiXcj3 -U0tdZwCixPihH5yYYyy6fE11kMw9sQdhx+4BHQIDAQABAoIBAFGxrpIRpCW/AXrj -5GnIoiyvQpnGXQODGL6unC83p/khIl883x8EXO5+xSOT7qB6AgjTNdoiIPQwYl1d -KkKQhF5aLRezbaAsTXXCUe3zZEuNOJO9hmmwd1KjmDifVmb44Rk5FirQxlhnzC0K -HEBVAkMAktBEKAn2qpdHuWBI4Dkh1hWIpChtkq4h1brsgaYurkfRnJOv1i/8kIWa -QlpzxZk2m5i1TpyfRsxuqt3migcrUSJsCmMwkFylDNKYQVS4HRZAXZndq3o51ZF5 -AzwgMjCty9G1eQGFx4k8CzDvahhTKHds73RHFTEGqWZiJNayuzb0Yz0wvaFlHnWl -35E11bkCgYEA5ALPklJ36jLNJiCzReH7SlAczIN9Rh1kRReHDD4St5cxA/nCynhI -HET2g8QBnonup1V3WPsDmLTga0hPhXhrI3oSm7jhBc2HOqNaJ/z6jYgGbE6pP05u -PgCi8gubX59733FJINmy5XOMzRaVTCfO2lh3zB4Ioj2t4EQyWTaQSw8CgYEA33s6 -aa+OtrwFmB42KQBPv0E1Yg++jpyrEysrvr7+hSI/8wFJvMqz6xCjsEolgU5/BIIr -xQJhqGgtBn2/HRGYqwa00vJJwxYwzPZHK2CiGL5n9HamVBEXeQAp16V6ftPE7lAi -MmbhEpbZZpVwCRsi8XofJNS9+YHhUk0si1O8oRMCgYEAn4uf90Ehi40Uo9NJ3mJs -VemM3UY8yG0UlowKAXUF39U5hRClTsuvmahf3n+uqmLVzd0t7+Nk9tvKFQe6LSi/ -v0lR8AkD2+2e7FcVZNnN8G74H51DLHsTBOupGTkp9VVBdm5sv0HVvlyGb5OX0Hwi -cAJrgTaaz/vcyQqvOGHHwd0CgYEAo679FKVqEPtb2ZPfNV6uCjX3pJBFkOy8/Hg1 -PStk/hwc3J6H5IhPCQ+R5LAaEkBtFd9FsbFR1+gdelClpuPZfwKVdJ/TWNkq+yQy -8ll/wEHNoCc7If22xIBTJUhllPkEl0wIEAR8O4JTTyiK+5BtopJAt1g+oL35S6+M -vauiUBMCgYAHum82lFQDVvLGEj79dpuj0cFfAwWZaPhxjjpQKp4pyCR6mU2O0uO8 -FtGG1swVG8H/sW7mcFeamZqjCHFSwcKqp5Ij6Wr2xrBU7R2VqIPAsLKHWZzM0G11 -X391kZk9mXwucy8D0eM8lE/suWmdFK4vdtP+y62q4AFru0HslSCAnA== ------END RSA PRIVATE KEY----- diff --git a/tests/id_rsa.pub b/tests/id_rsa.pub deleted file mode 100644 index 1363fbd..0000000 --- a/tests/id_rsa.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHDDQw9a9miGWDEXLG9mMR7OuV3ufmkgm5htntuZnxRw1sUfySmgeaY5ZDvHw5uEMulxYS/yJ+hLxFgsj3tUwIlwRz9yRyrzZm+gBE4Xhsb+NhdaMQdFnMSn3cUw8UHO8FmRd4FLiSx32x+pF83JnNR67+3fzSg2wKmvz5OdoQSELn4TpRuOt4fQI+22QPRpGOuzN6vWGQazY/LzRVlKeJ1kasVE1p/GKCNwtE2nhkk3B4VzuZy4sh8Fqklcgka6PZcDGu3zqkFCgRJ9oRODSbcNaXhnXDrBlnbpaciJU4d2JdyPdTS11nAKLE+KEfnJhjLLp8TXWQzD2xB2HH7gEd test diff --git a/tests/run b/tests/run index 4e40de1..79cd3a5 100755 --- a/tests/run +++ b/tests/run @@ -1,87 +1,100 @@ #!/bin/bash -# See: https://github.com/djui/bashunit +# See: https://github.com/kward/shunit2 -skipAllTests=false +if [ $UID != 0 ] && ! groups | grep -qw docker; then + echo "Run with sudo/root or add user $USER to group 'docker'" + exit 1 +fi -scriptDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -buildDir="$scriptDir/.." -tmpDir="/tmp/atmoz_sftp_test" +argBuild=${1:-"build"} +argOutput=${2:-"quiet"} +argCleanup=${3:-"cleanup"} +testDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +buildDir="$testDir/.." +imageName="atmoz/sftp_test" +buildOptions=(--tag "$imageName") -sudo="sudo" -cache="--no-cache" - -build=${1:-"build"} -output=${2:-"quiet"} -cleanup=${3:-"cleanup"} -sftpImageName="atmoz/sftp_test" -sftpContainerName="atmoz_sftp_test" - -if [ "$output" == "quiet" ]; then +if [ "$argOutput" == "quiet" ]; then redirect="/dev/null" else redirect="/dev/stdout" fi -buildOptions="--tag $sftpImageName" +if [ ! -f "$testDir/shunit2/shunit2" ]; then + echo "Could not find shunit2 in $testDir/shunit2." + echo "Run 'git submodules init && git submodules update'" + exit 2 +fi + +# clear argument list (or shunit2 will try to use them) +set -- +############################################################################## +## Helper functions ############################################################################## -function beforeTest() { - if [ "$build" == "build" ]; then - buildOptions="$buildOptions $cache --pull=true" +function oneTimeSetUp() { + if [ "$argBuild" == "build" ]; then + buildOptions+=("--no-cache" "--pull=true") fi - $sudo docker build $buildOptions "$buildDir" - if [ $? -gt 0 ]; then + # Build image + if ! docker build "${buildOptions[@]}" "$buildDir"; then echo "Build failed" exit 1 fi - # Private key can not be read by others - chmod go-rw "$scriptDir/id_rsa" - - rm -rf "$tmpDir" # clean state - mkdir "$tmpDir" - - echo "test::$(id -u):$(id -g):dir1,dir2" >> "$tmpDir/users" - echo "" >> "$tmpDir/users" # empty line - echo "# comments are allowed" >> "$tmpDir/users" - echo " " >> "$tmpDir/users" # only whitespace - echo " # with whitespace in front" >> "$tmpDir/users" - echo "user.with.dot::$(id -u):$(id -g)" >> "$tmpDir/users" - $sudo docker run \ - -v "$tmpDir/users:/etc/sftp/users.conf:ro" \ - -v "$scriptDir/id_rsa.pub":/home/test/.ssh/keys/id_rsa.pub:ro \ - -v "$scriptDir/id_rsa.pub":/home/user-from-env/.ssh/keys/id_rsa.pub:ro \ - -v "$scriptDir/id_rsa.pub":/home/user.with.dot/.ssh/keys/id_rsa.pub:ro \ - -v "$tmpDir":/home/test/share \ - --name "$sftpContainerName" \ - --expose 22 \ - -e "SFTP_USERS=user-from-env::$(id -u):$(id -g) user-from-env-2::$(id -u):$(id -g)" \ - -d "$sftpImageName" \ - > "$redirect" - - waitForServer $sftpContainerName -} - -function afterTest() { - if [ "$output" != "quiet" ]; then - echo "Docker logs:" - $sudo docker logs "$sftpContainerName" + # Generate temporary ssh keys for testing + if [ ! -f "/tmp/atmoz_sftp_test_rsa" ]; then + ssh-keygen -t rsa -f "/tmp/atmoz_sftp_test_rsa" -N '' > "$redirect" 2>&1 fi - if [ "$cleanup" == "cleanup" ]; then - $sudo docker rm -fv "$sftpContainerName" > "$redirect" - rm -rf "$tmpDir" + # Private key can not be read by others (sshd will complain) + chmod go-rw "/tmp/atmoz_sftp_test_rsa" +} + +function oneTimeTearDown() { + if [ "$argCleanup" == "cleanup" ]; then + docker image rm "$imageName" > "$redirect" 2>&1 + fi +} + +function setUp() { + # shellcheck disable=SC2154 + containerName="atmoz_sftp_${_shunit_test_}" + containerTmpDir="$(mktemp -d "/tmp/${containerName}_XXXX")" + export containerName containerTmpDir + + retireContainer "$containerName" # clean up leftover container +} + +function tearDown() { + retireContainer "$containerName" + + if [ "$argCleanup" == "cleanup" ] && [ -d "$containerTmpDir" ]; then + rm -rf "$containerTmpDir" + fi +} + +function retireContainer() { + if [ "$(docker ps -qaf name="$1")" ]; then + if [ "$argOutput" != "quiet" ]; then + echo "Docker log for $1:" + docker logs "$1" + fi + + if [ "$argCleanup" == "cleanup" ]; then + docker rm -fv "$1" > "$redirect" 2>&1 + fi fi } function getSftpIp() { - $sudo docker inspect -f {{.NetworkSettings.IPAddress}} "$1" + docker inspect -f "{{.NetworkSettings.IPAddress}}" "$1" } function runSftpCommands() { - ip="$(getSftpIp $1)" + ip="$(getSftpIp "$1")" user="$2" shift 2 @@ -91,10 +104,10 @@ function runSftpCommands() { done echo "$commands" | sftp \ - -i "$scriptDir/id_rsa" \ + -i "/tmp/atmoz_sftp_test_rsa" \ -oStrictHostKeyChecking=no \ -oUserKnownHostsFile=/dev/null \ - -b - $user@$ip \ + -b - "$user@$ip" \ > "$redirect" 2>&1 status=$? @@ -106,11 +119,11 @@ function waitForServer() { containerName="$1" echo -n "Waiting for $containerName to open port 22 ..." - for i in {1..30}; do + for _ in {1..30}; do sleep 1 - ip="$(getSftpIp $containerName)" + ip="$(getSftpIp "$containerName")" echo -n "." - if [ -n "$ip" ] && nc -z $ip 22; then + if [ -n "$ip" ] && nc -z "$ip" 22; then echo " OPEN" return 0; fi @@ -120,174 +133,177 @@ function waitForServer() { return 1 } +############################################################################## +## Tests ############################################################################## -function testContainerIsRunning() { - $skipAllTests && skip && return 0 - - ps="$($sudo docker ps -q -f name="$sftpContainerName")" - assertNotEqual "$ps" "" - - if [ -z "$ps" ]; then - skipAllTests=true - fi +function testSmallestUserConfig() { + docker run --name "$containerName" \ + --entrypoint="/bin/sh" \ + "$imageName" \ + -c "create-sftp-user u: && id u" \ + > "$redirect" 2>&1 + assertTrue "user created" $? } -function testLoginUsingSshKey() { - $skipAllTests && skip && return 0 - - runSftpCommands "$sftpContainerName" "test" "exit" - assertReturn $? 0 +function testCreateUserWithDot() { + docker run --name "$containerName" \ + --entrypoint="/bin/sh" \ + "$imageName" \ + -c "create-sftp-user user.with.dot: && id user.with.dot" \ + > "$redirect" 2>&1 + assertTrue "user created" $? } -function testUserWithDotLogin() { - $skipAllTests && skip && return 0 +function testUserCustomUidAndGid() { + id="$(docker run --name "$containerName" \ + --entrypoint="/bin/sh" \ + "$imageName" \ + -c "create-sftp-user u::1234:4321: > /dev/null && id u" )" - runSftpCommands "$sftpContainerName" "user.with.dot" "exit" - assertReturn $? 0 + echo "$id" | grep -q 'uid=1234(' + assertTrue "custom UID" $? + + echo "$id" | grep -q 'gid=4321(' + assertTrue "custom GID" $? + + # Here we also check group name + assertEquals "uid=1234(u) gid=4321(group_4321) groups=4321(group_4321)" "$id" } -function testLoginUsingUserFromEnv() { - $skipAllTests && skip && return 0 - - runSftpCommands "$sftpContainerName" "user-from-env" "exit" - assertReturn $? 0 +function testCommandPassthrough() { + docker run --name "$containerName" \ + "$imageName" test 1 -eq 1 \ + > "$redirect" 2>&1 + assertTrue "command passthrough" $? } -function testWritePermission() { - $skipAllTests && skip && return 0 +function testUsersConf() { + docker run --name "$containerName" -d \ + -v "$testDir/files/users.conf:/etc/sftp/users.conf:ro" \ + "$imageName" \ + > "$redirect" 2>&1 - runSftpCommands "$sftpContainerName" "test" \ - "cd share" \ + waitForServer "$containerName" + assertTrue "waitForServer" $? + + docker exec "$containerName" id user-from-conf > /dev/null + assertTrue "user-from-conf" $? + + docker exec "$containerName" id test > /dev/null + assertTrue "test" $? + + docker exec "$containerName" id user.with.dot > /dev/null + assertTrue "user.with.dot" $? + + docker exec "$containerName" test -d /home/test/dir1 -a -d /home/test/dir2 + assertTrue "dirs exists" $? +} + +function testLegacyUsersConf() { + docker run --name "$containerName" -d \ + -v "$testDir/files/users.conf:/etc/sftp-users.conf:ro" \ + "$imageName" \ + > "$redirect" 2>&1 + + waitForServer "$containerName" + assertTrue "waitForServer" $? + + docker exec "$containerName" id user-from-conf > /dev/null + assertTrue "user-from-conf" $? +} + +function testCreateUsersUsingEnv() { + docker run --name "$containerName" -d \ + -e "SFTP_USERS=user-from-env: user-from-env-2:" \ + "$imageName" \ + > "$redirect" 2>&1 + + waitForServer "$containerName" + assertTrue "waitForServer" $? + + docker exec "$containerName" id user-from-env > /dev/null + assertTrue "user-from-env" $? + + docker exec "$containerName" id user-from-env-2 > /dev/null + assertTrue "user-from-env-2" $? +} + +function testCreateUsersUsingCombo() { + docker run --name "$containerName" -d \ + -v "$testDir/files/users.conf:/etc/sftp-users.conf:ro" \ + -e "SFTP_USERS=user-from-env:" \ + "$imageName" \ + user-from-cmd: \ + > "$redirect" 2>&1 + + waitForServer "$containerName" + assertTrue "waitForServer" $? + + docker exec "$containerName" id user-from-conf > /dev/null + assertTrue "user-from-conf" $? + + docker exec "$containerName" id user-from-env > /dev/null + assertTrue "user-from-env" $? + + docker exec "$containerName" id user-from-cmd > /dev/null + assertTrue "user-from-cmd" $? +} + +function testWriteAccessToAutocreatedDirs() { + docker run --name "$containerName" -d \ + -v "/tmp/atmoz_sftp_test_rsa.pub":/home/test/.ssh/keys/id_rsa.pub:ro \ + "$imageName" test::::dir1,dir2 \ + > "$redirect" 2>&1 + + waitForServer "$containerName" + assertTrue "waitForServer" $? + + runSftpCommands "$containerName" "test" \ + "cd dir1" \ + "mkdir test" \ + "cd ../dir2" \ "mkdir test" \ "exit" - test -d "$tmpDir/test" - assertReturn $? 0 + assertTrue "runSftpCommands" $? + + docker exec "$containerName" test -d /home/test/dir1/test -a -d /home/test/dir2/test + assertTrue "dirs exists" $? } -function testDir() { - $skipAllTests && skip && return 0 - - runSftpCommands "$sftpContainerName" "test" \ - "cd dir1" \ - "mkdir test-dir1" \ - "get -rf test-dir1 $tmpDir/" \ - "cd ../dir2" \ - "mkdir test-dir2" \ - "get -rf test-dir2 $tmpDir/" \ - "exit" - test -d "$tmpDir/test-dir1" - assertReturn $? 0 - test -d "$tmpDir/test-dir2" - assertReturn $? 0 -} - -# Smallest user config possible -function testMinimalContainerStart() { - $skipAllTests && skip && return 0 - - tmpContainerName="$sftpContainerName""_minimal" - - $sudo docker run \ - --name "$tmpContainerName" \ - -d "$sftpImageName" \ - m: \ - > "$redirect" - - waitForServer $tmpContainerName - - ps="$($sudo docker ps -q -f name="$tmpContainerName")" - assertNotEqual "$ps" "" - - if [ -z "$ps" ]; then - skipAllTests=true - fi - - if [ "$output" != "quiet" ]; then - $sudo docker logs "$tmpContainerName" - fi - - if [ "$cleanup" == "cleanup" ]; then - $sudo docker rm -fv "$tmpContainerName" > "$redirect" - fi -} - -function testLegacyConfigPath() { - $skipAllTests && skip && return 0 - - tmpContainerName="$sftpContainerName""_legacy" - - echo "test::$(id -u):$(id -g)" >> "$tmpDir/legacy_users" - $sudo docker run \ - -v "$tmpDir/legacy_users:/etc/sftp-users.conf:ro" \ - --name "$tmpContainerName" \ - --expose 22 \ - -d "$sftpImageName" \ - > "$redirect" - - waitForServer $tmpContainerName - - ps="$($sudo docker ps -q -f name="$tmpContainerName")" - assertNotEqual "$ps" "" - - if [ "$output" != "quiet" ]; then - $sudo docker logs "$tmpContainerName" - fi - - if [ "$cleanup" == "cleanup" ]; then - $sudo docker rm -fv "$tmpContainerName" > "$redirect" - fi -} - - -# Bind-mount folder using script in /etc/sftp.d/ -function testCustomContainerStart() { - $skipAllTests && skip && return 0 - - tmpContainerName="$sftpContainerName""_custom" - - mkdir -p "$tmpDir/custom/bindmount" +function testBindmountDirScript() { + mkdir -p "$containerTmpDir/custom/bindmount" echo "mkdir -p /home/custom/bindmount && \ - chown custom /home/custom/bindmount && \ + chown custom /custom /home/custom/bindmount && \ mount --bind /custom /home/custom/bindmount" \ - > "$tmpDir/mount.sh" - chmod +x "$tmpDir/mount.sh" + > "$containerTmpDir/mount.sh" + chmod +x "$containerTmpDir/mount.sh" - $sudo docker run \ + docker run --name "$containerName" -d \ --privileged=true \ - --name "$tmpContainerName" \ - -v "$scriptDir/id_rsa.pub":/home/custom/.ssh/keys/id_rsa.pub:ro \ - -v "$tmpDir/custom/bindmount":/custom \ - -v "$tmpDir/mount.sh":/etc/sftp.d/mount.sh \ - --expose 22 \ - -d "$sftpImageName" \ - custom:123 \ - > "$redirect" + -v "/tmp/atmoz_sftp_test_rsa.pub":/home/custom/.ssh/keys/id_rsa.pub:ro \ + -v "$containerTmpDir/custom/bindmount":/custom \ + -v "$containerTmpDir/mount.sh":/etc/sftp.d/mount.sh \ + "$imageName" custom:123 \ + > "$redirect" 2>&1 - waitForServer $tmpContainerName + waitForServer "$containerName" + assertTrue "waitForServer" $? - ps="$($sudo docker ps -q -f name="$tmpContainerName")" - assertNotEqual "$ps" "" - - runSftpCommands "$tmpContainerName" "custom" \ + runSftpCommands "$containerName" "custom" \ "cd bindmount" \ "mkdir test" \ "exit" + assertTrue "runSftpCommands" $? - test -d "$tmpDir/custom/bindmount/test" - assertReturn $? 0 - - if [ "$output" != "quiet" ]; then - $sudo docker logs "$tmpContainerName" - fi - - if [ "$cleanup" == "cleanup" ]; then - $sudo docker rm -fv "$tmpContainerName" > "$redirect" - fi + docker exec "$containerName" test -d /home/custom/bindmount/test + assertTrue "directory exist" $? } +############################################################################## +## Run ############################################################################## -# Run tests -source "$scriptDir/bashunit.bash" +# shellcheck disable=SC1090 +source "$testDir/shunit2/shunit2" # Nothing happens after this