Merge branch '15881-ldap'
[arvados.git] / lib / controller / localdb / login_ldap_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package localdb
6
7 import (
8         "context"
9         "encoding/json"
10         "net"
11         "net/http"
12
13         "git.arvados.org/arvados.git/lib/config"
14         "git.arvados.org/arvados.git/lib/controller/railsproxy"
15         "git.arvados.org/arvados.git/sdk/go/arvados"
16         "git.arvados.org/arvados.git/sdk/go/arvadostest"
17         "git.arvados.org/arvados.git/sdk/go/auth"
18         "git.arvados.org/arvados.git/sdk/go/ctxlog"
19         "github.com/bradleypeabody/godap"
20         check "gopkg.in/check.v1"
21 )
22
23 var _ = check.Suite(&LDAPSuite{})
24
25 type LDAPSuite struct {
26         cluster *arvados.Cluster
27         ctrl    *ldapLoginController
28         ldap    *godap.LDAPServer // fake ldap server that accepts auth goodusername/goodpassword
29 }
30
31 func (s *LDAPSuite) TearDownSuite(c *check.C) {
32         // Undo any changes/additions to the user database so they
33         // don't affect subsequent tests.
34         arvadostest.ResetEnv()
35         c.Check(arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil), check.IsNil)
36 }
37
38 func (s *LDAPSuite) SetUpSuite(c *check.C) {
39         cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
40         c.Assert(err, check.IsNil)
41         s.cluster, err = cfg.GetCluster("")
42         c.Assert(err, check.IsNil)
43
44         ln, err := net.Listen("tcp", "127.0.0.1:0")
45         s.ldap = &godap.LDAPServer{
46                 Listener: ln,
47                 Handlers: []godap.LDAPRequestHandler{
48                         &godap.LDAPBindFuncHandler{
49                                 LDAPBindFunc: func(binddn string, bindpw []byte) bool {
50                                         return binddn == "cn=goodusername,dc=example,dc=com" && string(bindpw) == "goodpassword"
51                                 },
52                         },
53                         &godap.LDAPSimpleSearchFuncHandler{
54                                 LDAPSimpleSearchFunc: func(req *godap.LDAPSimpleSearchRequest) []*godap.LDAPSimpleSearchResultEntry {
55                                         if req.FilterAttr != "uid" || req.BaseDN != "dc=example,dc=com" {
56                                                 return []*godap.LDAPSimpleSearchResultEntry{}
57                                         }
58                                         return []*godap.LDAPSimpleSearchResultEntry{
59                                                 &godap.LDAPSimpleSearchResultEntry{
60                                                         DN: "cn=" + req.FilterValue + "," + req.BaseDN,
61                                                         Attrs: map[string]interface{}{
62                                                                 "SN":   req.FilterValue,
63                                                                 "CN":   req.FilterValue,
64                                                                 "uid":  req.FilterValue,
65                                                                 "mail": req.FilterValue + "@example.com",
66                                                         },
67                                                 },
68                                         }
69                                 },
70                         },
71                 },
72         }
73         go func() {
74                 ctxlog.TestLogger(c).Print(s.ldap.Serve())
75         }()
76
77         s.cluster.Login.LDAP.Enable = true
78         err = json.Unmarshal([]byte(`"ldap://`+ln.Addr().String()+`"`), &s.cluster.Login.LDAP.URL)
79         s.cluster.Login.LDAP.StartTLS = false
80         s.cluster.Login.LDAP.SearchBindUser = "cn=goodusername,dc=example,dc=com"
81         s.cluster.Login.LDAP.SearchBindPassword = "goodpassword"
82         s.cluster.Login.LDAP.SearchBase = "dc=example,dc=com"
83         c.Assert(err, check.IsNil)
84         s.ctrl = &ldapLoginController{
85                 Cluster:    s.cluster,
86                 RailsProxy: railsproxy.NewConn(s.cluster),
87         }
88 }
89
90 func (s *LDAPSuite) TestLoginSuccess(c *check.C) {
91         resp, err := s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
92                 Username: "goodusername",
93                 Password: "goodpassword",
94         })
95         c.Check(err, check.IsNil)
96         c.Check(resp.APIToken, check.Not(check.Equals), "")
97         c.Check(resp.UUID, check.Matches, `zzzzz-gj3su-.*`)
98         c.Check(resp.Scopes, check.DeepEquals, []string{"all"})
99
100         ctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{"v2/" + resp.UUID + "/" + resp.APIToken}})
101         user, err := railsproxy.NewConn(s.cluster).UserGetCurrent(ctx, arvados.GetOptions{})
102         c.Check(err, check.IsNil)
103         c.Check(user.Email, check.Equals, "goodusername@example.com")
104         c.Check(user.Username, check.Equals, "goodusername")
105 }
106
107 func (s *LDAPSuite) TestLoginFailure(c *check.C) {
108         // search returns no results
109         s.cluster.Login.LDAP.SearchBase = "dc=example,dc=invalid"
110         resp, err := s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
111                 Username: "goodusername",
112                 Password: "goodpassword",
113         })
114         c.Check(err, check.ErrorMatches, `LDAP: Authentication failure \(with username "goodusername" and password\)`)
115         hs, ok := err.(interface{ HTTPStatus() int })
116         if c.Check(ok, check.Equals, true) {
117                 c.Check(hs.HTTPStatus(), check.Equals, http.StatusUnauthorized)
118         }
119         c.Check(resp.APIToken, check.Equals, "")
120
121         // search returns result, but auth fails
122         s.cluster.Login.LDAP.SearchBase = "dc=example,dc=com"
123         resp, err = s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
124                 Username: "badusername",
125                 Password: "badpassword",
126         })
127         c.Check(err, check.ErrorMatches, `LDAP: Authentication failure \(with username "badusername" and password\)`)
128         hs, ok = err.(interface{ HTTPStatus() int })
129         if c.Check(ok, check.Equals, true) {
130                 c.Check(hs.HTTPStatus(), check.Equals, http.StatusUnauthorized)
131         }
132         c.Check(resp.APIToken, check.Equals, "")
133 }