1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
22 "git.arvados.org/arvados.git/sdk/go/arvados"
23 check "gopkg.in/check.v1"
26 var _ = check.Suite(&LoginDockerSuite{})
28 // LoginDockerSuite is an integration test of controller's different Login
29 // methods. Each test creates a different Login configuration and runs
30 // controller in a Docker container with it. It runs other Docker containers
31 // for supporting services.
32 type LoginDockerSuite struct {
41 func (s *LoginDockerSuite) setUpDockerNetwork() (string, error) {
42 netName := "arvados-net-" + path.Base(path.Dir(s.tmpdir))
43 cmd := exec.Command("docker", "network", "create", netName)
44 cmd.Stderr = os.Stderr
45 if err := cmd.Run(); err != nil {
51 // Run cmd and read stdout looking for an IP address on a line by itself.
52 // Return the last one found.
53 func (s *LoginDockerSuite) ipFromCmd(cmd *exec.Cmd) (string, error) {
54 cmd.Stderr = os.Stderr
55 out, err := cmd.Output()
59 lines := bytes.Split(out, []byte{'\n'})
61 for _, line := range lines {
62 if ip := net.ParseIP(string(line)); ip != nil {
63 return ip.String(), nil
66 return "", fmt.Errorf("no IP address found in the output of %v", cmd)
69 // SetUpSuite creates a Docker network, starts an openldap server in it, and
70 // creates user account fixtures in LDAP.
71 // We used to use the LDAP server for multiple tests. We don't currently, but
72 // there are pros and cons to starting it here vs. in each individaul test, so
73 // it's staying here for now.
74 func (s *LoginDockerSuite) SetUpSuite(c *check.C) {
75 s.localdbSuite.SetUpSuite(c)
78 s.netName, err = s.setUpDockerNetwork()
79 c.Assert(err, check.IsNil)
80 s.netAddr, err = s.ipFromCmd(exec.Command("docker", "network", "inspect",
81 "--format", "{{(index .IPAM.Config 0).Gateway}}", s.netName))
82 c.Assert(err, check.IsNil)
83 setup := exec.Command("login_docker_test/setup_suite.sh", s.netName, s.tmpdir)
84 setup.Stderr = os.Stderr
86 c.Assert(err, check.IsNil)
89 // TearDownSuite stops all containers running on the Docker network we set up,
90 // then deletes the network itself.
91 func (s *LoginDockerSuite) TearDownSuite(c *check.C) {
93 cmd := exec.Command("login_docker_test/teardown_suite.sh", s.netName)
94 cmd.Stderr = os.Stderr
96 c.Check(err, check.IsNil)
98 s.localdbSuite.TearDownSuite(c)
101 // Create a test cluster configuration in the test temporary directory.
102 // Update it to use the current PostgreSQL and RailsAPI proxies.
103 func (s *LoginDockerSuite) setUpConfig(c *check.C) {
104 src, err := os.Open(os.Getenv("ARVADOS_CONFIG"))
105 c.Assert(err, check.IsNil)
107 dst, err := os.Create(path.Join(s.tmpdir, "arvados.yml"))
108 c.Assert(err, check.IsNil)
109 _, err = io.Copy(dst, src)
110 closeErr := dst.Close()
111 c.Assert(err, check.IsNil)
112 c.Assert(closeErr, check.IsNil)
114 pgconn := map[string]interface{}{
116 "port": s.pgProxy.Port(),
118 err = s.updateConfig(".Clusters.zzzzz.PostgreSQL.Connection |= (. * $arg)", pgconn)
119 c.Assert(err, check.IsNil)
120 intVal := make(map[string]string)
121 intURLs := make(map[string]interface{})
122 railsURL := "https://" + net.JoinHostPort(s.netAddr, s.railsProxy.Port())
123 intURLs[railsURL] = intVal
124 err = s.updateConfig(".Clusters.zzzzz.Services.RailsAPI.InternalURLs = $arg", intURLs)
125 c.Assert(err, check.IsNil)
126 intURLs = make(map[string]interface{})
127 intURLs["http://0.0.0.0:80"] = intVal
128 err = s.updateConfig(".Clusters.zzzzz.Services.Controller.InternalURLs = $arg", intURLs)
129 c.Assert(err, check.IsNil)
132 // Update the test cluster configuration with the given yq expression.
133 // The expression can use `$arg` to refer to the object passed in as `arg`.
134 func (s *LoginDockerSuite) updateConfig(expr string, arg map[string]interface{}) error {
135 jsonArg, err := json.Marshal(arg)
139 cmd := exec.Command("yq", "-yi",
140 "--argjson", "arg", string(jsonArg),
141 expr, path.Join(s.tmpdir, "arvados.yml"))
142 cmd.Stderr = os.Stderr
146 // Update the test cluster configuration to use the named login method.
147 func (s *LoginDockerSuite) enableLogin(key string) error {
148 login := make(map[string]interface{})
149 login["Test"] = map[string]bool{"Enable": false}
150 login[key] = map[string]bool{"Enable": true}
151 return s.updateConfig(".Clusters.zzzzz.Login |= (. * $arg)", login)
154 // SetUpTest does all the common preparation for a controller test container:
155 // it creates TCP proxies for PostgreSQL and RailsAPI on the test host, then
156 // writes a new Arvados cluster configuration pointed at those for servers to
158 func (s *LoginDockerSuite) SetUpTest(c *check.C) {
159 s.localdbSuite.SetUpTest(c)
160 s.pgProxy = newPgProxy(c, s.cluster, s.netAddr)
161 s.railsProxy = newInternalProxy(c, s.cluster.Services.RailsAPI, s.netAddr)
165 // TearDownTest looks for the `controller.cid` file created when we start the
166 // test container. If found, it stops that container and deletes the file.
167 // Then it closes the TCP proxies created by SetUpTest.
168 func (s *LoginDockerSuite) TearDownTest(c *check.C) {
169 cidPath := path.Join(s.tmpdir, "controller.cid")
170 if cid, err := os.ReadFile(cidPath); err == nil {
171 cmd := exec.Command("docker", "stop", strings.TrimSpace(string(cid)))
172 cmd.Stderr = os.Stderr
174 c.Check(err, check.IsNil)
176 if err := os.Remove(cidPath); err != nil {
177 c.Check(os.IsNotExist(err), check.Equals, true)
181 s.localdbSuite.TearDownTest(c)
184 func (s *LoginDockerSuite) startController(args ...string) (*url.URL, error) {
185 args = append([]string{s.netName, s.tmpdir}, args...)
186 cmd := exec.Command("login_docker_test/start_controller_container.sh", args...)
187 ip, err := s.ipFromCmd(cmd)
197 func (s *LoginDockerSuite) parseResponse(resp *http.Response, body any) error {
198 defer resp.Body.Close()
199 respBody, err := io.ReadAll(resp.Body)
203 if resp.StatusCode < 400 {
204 return json.Unmarshal(respBody, body)
209 err = json.Unmarshal(respBody, &errResp)
211 return fmt.Errorf("%s with malformed JSON response: %w", resp.Status, err)
212 } else if len(errResp.Errors) == 0 {
213 return fmt.Errorf("%s with no Errors in response", resp.Status)
215 return fmt.Errorf("%s: %s", resp.Status, strings.Join(errResp.Errors, ":"))
219 func (s *LoginDockerSuite) authenticate(server *url.URL, username, password string) (*arvados.APIClientAuthorization, error) {
220 reqURL := server.JoinPath("/arvados/v1/users/authenticate").String()
221 reqValues := url.Values{
222 "username": {username},
223 "password": {password},
225 resp, err := http.PostForm(reqURL, reqValues)
229 token := &arvados.APIClientAuthorization{}
230 err = s.parseResponse(resp, token)
234 func (s *LoginDockerSuite) getCurrentUser(server *url.URL, token string) (*arvados.User, error) {
235 reqURL := server.JoinPath("/arvados/v1/users/current").String()
236 req, err := http.NewRequest("GET", reqURL, nil)
240 req.Header.Add("Authorization", "Bearer "+token)
241 resp, err := http.DefaultClient.Do(req)
245 user := &arvados.User{}
246 err = s.parseResponse(resp, user)
250 func (s *LoginDockerSuite) TestLoginPAM(c *check.C) {
251 err := s.enableLogin("PAM")
252 c.Assert(err, check.IsNil)
253 setupPath, err := filepath.Abs("login_docker_test/setup_pam_test.sh")
254 c.Assert(err, check.IsNil)
255 arvURL, err := s.startController("-v", setupPath+":/setup.sh:ro")
256 c.Assert(err, check.IsNil)
258 _, err = s.authenticate(arvURL, "foo-bar", "nosecret")
259 c.Check(err, check.ErrorMatches,
260 `401 Unauthorized: PAM: Authentication failure \(with username "foo-bar" and password\)`)
262 _, err = s.authenticate(arvURL, "expired", "secret")
263 c.Check(err, check.ErrorMatches,
264 `401 Unauthorized: PAM: Authentication failure; "Your account has expired; please contact your system administrator\."`)
266 aca, err := s.authenticate(arvURL, "foo-bar", "secret")
267 if c.Check(err, check.IsNil) {
268 user, err := s.getCurrentUser(arvURL, aca.TokenV2())
269 if c.Check(err, check.IsNil) {
270 // Check PAMDefaultEmailDomain was propagated as expected
271 c.Check(user.Email, check.Equals, "foo-bar@example.com")
276 func (s *LoginDockerSuite) TestLoginLDAPBuiltin(c *check.C) {
277 err := s.enableLogin("LDAP")
278 c.Assert(err, check.IsNil)
279 arvURL, err := s.startController()
280 c.Assert(err, check.IsNil)
282 _, err = s.authenticate(arvURL, "foo-bar", "nosecret")
283 c.Check(err, check.ErrorMatches,
284 `401 Unauthorized: LDAP: Authentication failure \(with username "foo-bar" and password\)`)
286 aca, err := s.authenticate(arvURL, "foo-bar", "secret")
287 if c.Check(err, check.IsNil) {
288 user, err := s.getCurrentUser(arvURL, aca.TokenV2())
289 if c.Check(err, check.IsNil) {
290 // User fields come from LDAP attributes
291 c.Check(user.FirstName, check.Equals, "Foo")
292 c.Check(user.LastName, check.Equals, "Bar")
293 // "-" character removed by RailsAPI
294 c.Check(user.Username, check.Equals, "foobar")
295 c.Check(user.Email, check.Equals, "foo-bar-baz@example.com")