19973: Merge branch 'main'
[arvados.git] / lib / controller / localdb / login_ldap_docker_test.sh
1 #!/bin/bash
2
3 # Copyright (C) The Arvados Authors. All rights reserved.
4 #
5 # SPDX-License-Identifier: AGPL-3.0
6
7 # This script demonstrates using LDAP for Arvados user authentication.
8 #
9 # It configures arvados controller in a docker container, optionally
10 # with pam_ldap(5) configured to authenticate against an OpenLDAP
11 # server in a second docker container.
12 #
13 # After adding a "foo" user entry, it uses curl to check that the
14 # Arvados controller's login endpoint accepts the "foo" account
15 # username/password and rejects invalid credentials.
16 #
17 # It is intended to be run inside .../build/run-tests.sh (in
18 # interactive mode: "test lib/controller/localdb -tags=docker
19 # -check.f=LDAP -check.vv"). It assumes ARVADOS_TEST_API_HOST points
20 # to a RailsAPI server and the desired version of arvados-server is
21 # installed in $GOPATH/bin.
22
23 set -e -o pipefail
24
25 debug=/dev/null
26 if [[ -n ${ARVADOS_DEBUG} ]]; then
27     debug=/dev/stderr
28     set -x
29 fi
30
31 case "${config_method}" in
32     pam | ldap)
33         ;;
34     *)
35         echo >&2 "\$config_method env var must be 'pam' or 'ldap'"
36         exit 1
37         ;;
38 esac
39
40 hostname="$(hostname)"
41 tmpdir="$(mktemp -d)"
42 cleanup() {
43     trap - ERR
44     rm -r ${tmpdir}
45     for h in ${ldapctr} ${ctrlctr}; do
46         if [[ -n ${h} ]]; then
47             docker kill ${h}
48         fi
49     done
50 }
51 trap cleanup ERR
52
53 if [[ -z "$(docker image ls -q osixia/openldap:1.3.0)" ]]; then
54     echo >&2 "Pulling docker image for ldap server"
55     docker pull osixia/openldap:1.3.0
56 fi
57
58 ldapctr=ldap-${RANDOM}
59 echo >&2 "Starting ldap server in docker container ${ldapctr}"
60 docker run --rm --detach \
61        -p 389 -p 636 \
62        --name=${ldapctr} \
63        osixia/openldap:1.3.0
64 docker logs --follow ${ldapctr} 2>$debug >$debug &
65 ldaphostports=$(docker port ${ldapctr} 389/tcp)
66 ldapport=${ldaphostports##*:}
67 ldapurl="ldap://${hostname}:${ldapport}"
68 passwordhash="$(docker exec -i ${ldapctr} slappasswd -s "secret")"
69
70 # These are the default admin credentials for osixia/openldap:1.3.0
71 adminuser=admin
72 adminpassword=admin
73
74 cat >"${tmpdir}/zzzzz.yml" <<EOF
75 Clusters:
76   zzzzz:
77     PostgreSQL:
78       Connection:
79         client_encoding: utf8
80         host: ${hostname}
81         port: "${pgport}"
82         dbname: arvados_test
83         user: arvados
84         password: insecure_arvados_test
85     ManagementToken: e687950a23c3a9bceec28c6223a06c79
86     SystemRootToken: systemusertesttoken1234567890aoeuidhtnsqjkxbmwvzpy
87     API:
88       RequestTimeout: 30s
89     TLS:
90       Insecure: true
91     Collections:
92       BlobSigningKey: zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc
93       TrustAllContent: true
94       ForwardSlashNameSubstitution: /
95     Services:
96       RailsAPI:
97         InternalURLs:
98           "https://${hostname}:${ARVADOS_TEST_API_HOST##*:}/": {}
99       Controller:
100         ExternalURL: http://0.0.0.0:9999/
101         InternalURLs:
102           "http://0.0.0.0:9999/": {}
103     SystemLogs:
104       LogLevel: debug
105 EOF
106 case "${config_method}" in
107     pam)
108         setup_pam_ldap="apt update && DEBIAN_FRONTEND=noninteractive apt install -y ldap-utils libpam-ldap && pam-auth-update --package /usr/share/pam-configs/ldap"
109         cat >>"${tmpdir}/zzzzz.yml" <<EOF
110     Login:
111       PAM:
112         Enable: true
113         # Without this specific DefaultEmailDomain, inserted users
114         # would prevent subsequent database/reset from working (see
115         # database_controller.rb).
116         DefaultEmailDomain: example.com
117 EOF
118         ;;
119     ldap)
120         setup_pam_ldap=""
121         cat >>"${tmpdir}/zzzzz.yml" <<EOF
122     Login:
123       LDAP:
124         Enable: true
125         URL: ${ldapurl}
126         StartTLS: false
127         SearchBase: dc=example,dc=org
128         SearchBindUser: cn=admin,dc=example,dc=org
129         SearchBindPassword: admin
130 EOF
131             ;;
132 esac
133
134 cat >&2 "${tmpdir}/zzzzz.yml"
135
136 cat >"${tmpdir}/pam_ldap.conf" <<EOF
137 base dc=example,dc=org
138 ldap_version 3
139 uri ${ldapurl}
140 pam_password crypt
141 binddn cn=${adminuser},dc=example,dc=org
142 bindpw ${adminpassword}
143 EOF
144
145 cat >"${tmpdir}/add_example_user.ldif" <<EOF
146 dn: cn=bar,dc=example,dc=org
147 objectClass: posixGroup
148 objectClass: top
149 cn: bar
150 gidNumber: 11111
151 description: "Example group 'bar'"
152
153 dn: uid=foo-bar,dc=example,dc=org
154 uid: foo-bar
155 cn: "Foo Bar"
156 givenName: Foo
157 sn: Bar
158 mail: foo-bar-baz@example.com
159 objectClass: inetOrgPerson
160 objectClass: posixAccount
161 objectClass: top
162 objectClass: shadowAccount
163 shadowMax: -1
164 shadowMin: 1
165 shadowWarning: 7
166 shadowLastChange: 10701
167 loginShell: /bin/bash
168 uidNumber: 11111
169 gidNumber: 11111
170 homeDirectory: /home/foo-bar
171 userPassword: ${passwordhash}
172
173 dn: uid=expired,dc=example,dc=org
174 uid: expired
175 cn: "Exp Ired"
176 givenName: Exp
177 sn: Ired
178 mail: expired@example.com
179 objectClass: inetOrgPerson
180 objectClass: posixAccount
181 objectClass: top
182 objectClass: shadowAccount
183 shadowMax: 180
184 shadowMin: 1
185 shadowWarning: 7
186 shadowLastChange: 10701
187 loginShell: /bin/bash
188 uidNumber: 11112
189 gidNumber: 11111
190 homeDirectory: /home/expired
191 userPassword: ${passwordhash}
192 EOF
193
194 echo >&2 "Adding example user entry user=foo-bar pass=secret (retrying until server comes up)"
195 docker run --rm --entrypoint= \
196        -v "${tmpdir}/add_example_user.ldif":/add_example_user.ldif:ro \
197        osixia/openldap:1.3.0 \
198        bash -c "for f in \$(seq 1 5); do if ldapadd -H '${ldapurl}' -D 'cn=${adminuser},dc=example,dc=org' -w '${adminpassword}' -f /add_example_user.ldif; then exit 0; else sleep 2; fi; done; echo 'failed to add user entry'; exit 1"
199
200 echo >&2 "Building arvados controller binary to run in container"
201 go build -o "${tmpdir}" ../../../cmd/arvados-server
202
203 ctrlctr=ctrl-${RANDOM}
204 echo >&2 "Starting arvados controller in docker container ${ctrlctr}"
205 docker run --detach --rm --name=${ctrlctr} \
206        -p 9999 \
207        -v "${tmpdir}/pam_ldap.conf":/etc/pam_ldap.conf:ro \
208        -v "${tmpdir}/arvados-server":/bin/arvados-server:ro \
209        -v "${tmpdir}/zzzzz.yml":/etc/arvados/config.yml:ro \
210        -v $(realpath "${PWD}/../../.."):/arvados:ro \
211        debian:10 \
212        bash -c "${setup_pam_ldap:-true} && arvados-server controller"
213 docker logs --follow ${ctrlctr} 2>$debug >$debug &
214 ctrlhostports=$(docker port ${ctrlctr} 9999/tcp)
215 ctrlport=${ctrlhostports##*:}
216
217 echo >&2 "Waiting for arvados controller to come up..."
218 for f in $(seq 1 20); do
219     if curl -s "http://0.0.0.0:${ctrlport}/arvados/v1/config" >/dev/null; then
220         break
221     else
222         sleep 1
223     fi
224     echo -n >&2 .
225 done
226 echo >&2
227 echo >&2 "Arvados controller is up at http://0.0.0.0:${ctrlport}"
228
229 check_contains() {
230     resp="${1}"
231     str="${2}"
232     if ! echo "${resp}" | fgrep -q "${str}"; then
233         echo >&2 "${resp}"
234         echo >&2 "FAIL: expected in response, but not found: ${str@Q}"
235         return 1
236     fi
237 }
238
239 set +x
240
241 echo >&2 "Testing authentication failure"
242 resp="$(set -x; curl -s --include -d username=foo-bar -d password=nosecret "http://0.0.0.0:${ctrlport}/arvados/v1/users/authenticate" | tee $debug)"
243 check_contains "${resp}" "HTTP/1.1 401"
244 if [[ "${config_method}" = ldap ]]; then
245     check_contains "${resp}" '{"errors":["LDAP: Authentication failure (with username \"foo-bar\" and password)"]}'
246 else
247     check_contains "${resp}" '{"errors":["PAM: Authentication failure (with username \"foo-bar\" and password)"]}'
248 fi
249
250 if [[ "${config_method}" = pam ]]; then
251     echo >&2 "Testing expired credentials"
252     resp="$(set -x; curl -s --include -d username=expired -d password=secret "http://0.0.0.0:${ctrlport}/arvados/v1/users/authenticate" | tee $debug)"
253     check_contains "${resp}" "HTTP/1.1 401"
254     check_contains "${resp}" '{"errors":["PAM: Authentication failure; \"You are required to change your LDAP password immediately.\""]}'
255 fi
256
257 echo >&2 "Testing authentication success"
258 resp="$(set -x; curl -s --include -d username=foo-bar -d password=secret "http://0.0.0.0:${ctrlport}/arvados/v1/users/authenticate" | tee $debug)"
259 check_contains "${resp}" "HTTP/1.1 200"
260 check_contains "${resp}" '"api_token":"'
261 check_contains "${resp}" '"scopes":["all"]'
262 check_contains "${resp}" '"uuid":"zzzzz-gj3su-'
263
264 secret="${resp##*api_token\":\"}"
265 secret="${secret%%\"*}"
266 uuid="${resp##*uuid\":\"}"
267 uuid="${uuid%%\"*}"
268 token="v2/$uuid/$secret"
269 echo >&2 "New token is ${token}"
270
271 resp="$(set -x; curl -s --include -H "Authorization: Bearer ${token}" "http://0.0.0.0:${ctrlport}/arvados/v1/users/current" | tee $debug)"
272 check_contains "${resp}" "HTTP/1.1 200"
273 if [[ "${config_method}" = ldap ]]; then
274     # user fields come from LDAP attributes
275     check_contains "${resp}" '"first_name":"Foo"'
276     check_contains "${resp}" '"last_name":"Bar"'
277     check_contains "${resp}" '"username":"foobar"' # "-" removed by rails api
278     check_contains "${resp}" '"email":"foo-bar-baz@example.com"'
279 else
280     # PAMDefaultEmailDomain
281     check_contains "${resp}" '"email":"foo-bar@example.com"'
282 fi
283
284 cleanup