~juju-qa/ubuntu/xenial/juju/xenial-2.0-beta3

« back to all changes in this revision

Viewing changes to src/github.com/juju/idmclient/idmtest/idmtest.go

  • Committer: Martin Packman
  • Date: 2016-03-30 19:31:08 UTC
  • mfrom: (1.1.41)
  • Revision ID: martin.packman@canonical.com-20160330193108-h9iz3ak334uk0z5r
Merge new upstream source 2.0~beta3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2015 Canonical Ltd.
 
2
// Licensed under the LGPLv3, see LICENCE file for details.
 
3
 
 
4
// Package idmtest holds a mock implementation of the identity manager
 
5
// suitable for testing.
 
6
package idmtest
 
7
 
 
8
import (
 
9
        "fmt"
 
10
        "net/http"
 
11
        "net/http/httptest"
 
12
        "net/url"
 
13
        "sync"
 
14
 
 
15
        "github.com/juju/httprequest"
 
16
        "github.com/julienschmidt/httprouter"
 
17
        "gopkg.in/errgo.v1"
 
18
        "gopkg.in/macaroon-bakery.v1/bakery"
 
19
        "gopkg.in/macaroon-bakery.v1/bakery/checkers"
 
20
        "gopkg.in/macaroon-bakery.v1/httpbakery"
 
21
        "gopkg.in/macaroon-bakery.v1/httpbakery/agent"
 
22
 
 
23
        "github.com/juju/idmclient/params"
 
24
)
 
25
 
 
26
// Server represents a mock identity server.
 
27
// It currently serves only the discharge and groups endpoints.
 
28
type Server struct {
 
29
        // URL holds the URL of the mock identity server.
 
30
        // The discharger endpoint is located at URL/v1/discharge.
 
31
        URL *url.URL
 
32
 
 
33
        // PublicKey holds the public key of the mock identity server.
 
34
        PublicKey *bakery.PublicKey
 
35
 
 
36
        router *httprouter.Router
 
37
        srv    *httptest.Server
 
38
        bakery *bakery.Service
 
39
 
 
40
        // mu guards the fields below it.
 
41
        mu          sync.Mutex
 
42
        users       map[string]*user
 
43
        defaultUser string
 
44
        waits       []chan struct{}
 
45
}
 
46
 
 
47
type user struct {
 
48
        groups []string
 
49
        key    *bakery.KeyPair
 
50
}
 
51
 
 
52
// NewServer runs a mock identity server. It can discharge
 
53
// macaroons and return information on user group membership.
 
54
// The returned server should be closed after use.
 
55
func NewServer() *Server {
 
56
        srv := &Server{
 
57
                users: make(map[string]*user),
 
58
        }
 
59
        bsvc, err := bakery.NewService(bakery.NewServiceParams{
 
60
                Locator: srv,
 
61
        })
 
62
        if err != nil {
 
63
                panic(err)
 
64
        }
 
65
        srv.bakery = bsvc
 
66
        srv.PublicKey = bsvc.PublicKey()
 
67
        errorMapper := httprequest.ErrorMapper(errToResp)
 
68
        h := &handler{
 
69
                srv: srv,
 
70
        }
 
71
        router := httprouter.New()
 
72
        for _, route := range errorMapper.Handlers(func(httprequest.Params) (*handler, error) {
 
73
                return h, nil
 
74
        }) {
 
75
                router.Handle(route.Method, route.Path, route.Handle)
 
76
        }
 
77
        mux := http.NewServeMux()
 
78
        httpbakery.AddDischargeHandler(mux, "/", srv.bakery, srv.check)
 
79
        router.Handler("POST", "/v1/discharger/*rest", http.StripPrefix("/v1/discharger", mux))
 
80
        router.Handler("GET", "/v1/discharger/*rest", http.StripPrefix("/v1/discharger", mux))
 
81
        router.Handler("POST", "/discharge", mux)
 
82
        router.Handler("GET", "/publickey", mux)
 
83
 
 
84
        srv.srv = httptest.NewServer(router)
 
85
        srv.URL, err = url.Parse(srv.srv.URL)
 
86
        if err != nil {
 
87
                panic(err)
 
88
        }
 
89
        return srv
 
90
}
 
91
 
 
92
func errToResp(err error) (int, interface{}) {
 
93
        // Allow bakery errors to be returned as the bakery would
 
94
        // like them, so that httpbakery.Client.Do will work.
 
95
        if err, ok := errgo.Cause(err).(*httpbakery.Error); ok {
 
96
                return httpbakery.ErrorToResponse(err)
 
97
        }
 
98
        errorBody := errorResponseBody(err)
 
99
        status := http.StatusInternalServerError
 
100
        switch errorBody.Code {
 
101
        case params.ErrNotFound:
 
102
                status = http.StatusNotFound
 
103
        case params.ErrForbidden, params.ErrAlreadyExists:
 
104
                status = http.StatusForbidden
 
105
        case params.ErrBadRequest:
 
106
                status = http.StatusBadRequest
 
107
        case params.ErrUnauthorized, params.ErrNoAdminCredsProvided:
 
108
                status = http.StatusUnauthorized
 
109
        case params.ErrMethodNotAllowed:
 
110
                status = http.StatusMethodNotAllowed
 
111
        case params.ErrServiceUnavailable:
 
112
                status = http.StatusServiceUnavailable
 
113
        }
 
114
        return status, errorBody
 
115
}
 
116
 
 
117
// errorResponse returns an appropriate error response for the provided error.
 
118
func errorResponseBody(err error) *params.Error {
 
119
        errResp := &params.Error{
 
120
                Message: err.Error(),
 
121
        }
 
122
        cause := errgo.Cause(err)
 
123
        if coder, ok := cause.(errorCoder); ok {
 
124
                errResp.Code = coder.ErrorCode()
 
125
        } else if errgo.Cause(err) == httprequest.ErrUnmarshal {
 
126
                errResp.Code = params.ErrBadRequest
 
127
        }
 
128
        return errResp
 
129
}
 
130
 
 
131
type errorCoder interface {
 
132
        ErrorCode() params.ErrorCode
 
133
}
 
134
 
 
135
// Close shuts down the server.
 
136
func (srv *Server) Close() {
 
137
        srv.srv.Close()
 
138
}
 
139
 
 
140
// PublicKeyForLocation implements bakery.PublicKeyLocator
 
141
// by returning the server's public key for all locations.
 
142
func (srv *Server) PublicKeyForLocation(loc string) (*bakery.PublicKey, error) {
 
143
        return srv.PublicKey, nil
 
144
}
 
145
 
 
146
// UserPublicKey returns the key for the given user.
 
147
// It panics if the user has not been added.
 
148
func (srv *Server) UserPublicKey(username string) *bakery.KeyPair {
 
149
        u := srv.user(username)
 
150
        if u == nil {
 
151
                panic("no user found")
 
152
        }
 
153
        return u.key
 
154
}
 
155
 
 
156
// Client returns a bakery client that will discharge as the given user.
 
157
// If the user does not exist, it is added with no groups.
 
158
func (srv *Server) Client(username string) *httpbakery.Client {
 
159
        c := httpbakery.NewClient()
 
160
        u := srv.user(username)
 
161
        if u == nil {
 
162
                srv.AddUser(username)
 
163
                u = srv.user(username)
 
164
        }
 
165
        c.Key = u.key
 
166
        agent.SetUpAuth(c, srv.URL, username)
 
167
        return c
 
168
}
 
169
 
 
170
// SetDefaultUser configures the server so that it will discharge for
 
171
// the given user if no agent-login cookie is found. The user does not
 
172
// need to have been added. Note that this will bypass the
 
173
// VisitURL logic.
 
174
//
 
175
// If the name is empty, there will be no default user.
 
176
func (srv *Server) SetDefaultUser(name string) {
 
177
        srv.mu.Lock()
 
178
        defer srv.mu.Unlock()
 
179
        srv.defaultUser = name
 
180
}
 
181
 
 
182
// AddUser adds a new user that's in the given set of groups.
 
183
func (srv *Server) AddUser(name string, groups ...string) {
 
184
        key, err := bakery.GenerateKey()
 
185
        if err != nil {
 
186
                panic(err)
 
187
        }
 
188
        srv.mu.Lock()
 
189
        defer srv.mu.Unlock()
 
190
        srv.users[name] = &user{
 
191
                groups: groups,
 
192
                key:    key,
 
193
        }
 
194
}
 
195
 
 
196
func (srv *Server) user(name string) *user {
 
197
        srv.mu.Lock()
 
198
        defer srv.mu.Unlock()
 
199
        return srv.users[name]
 
200
}
 
201
 
 
202
func (srv *Server) check(req *http.Request, cavId, cav string) ([]checkers.Caveat, error) {
 
203
        if cav != "is-authenticated-user" {
 
204
                return nil, errgo.Newf("unknown third party caveat %q", cav)
 
205
        }
 
206
 
 
207
        // First check if we have a login cookie so that we can avoid
 
208
        // going through the VisitURL logic when an explicit default user
 
209
        // has been set.
 
210
        username, key, err := agent.LoginCookie(req)
 
211
        if errgo.Cause(err) == agent.ErrNoAgentLoginCookie {
 
212
                srv.mu.Lock()
 
213
                defer srv.mu.Unlock()
 
214
                if srv.defaultUser != "" {
 
215
                        return []checkers.Caveat{
 
216
                                checkers.DeclaredCaveat("username", srv.defaultUser),
 
217
                        }, nil
 
218
                }
 
219
        }
 
220
        if err != nil {
 
221
                return nil, errgo.Notef(err, "bad agent-login cookie in request")
 
222
        }
 
223
        srv.mu.Lock()
 
224
        defer srv.mu.Unlock()
 
225
 
 
226
        waitId := len(srv.waits)
 
227
        srv.waits = append(srv.waits, make(chan struct{}, 1))
 
228
        // Return a visit URL so that the client code is forced through that
 
229
        // path, testing that its client correctly visits the URL and that
 
230
        // any agent-login cookie has been appropriately set.
 
231
        return nil, &httpbakery.Error{
 
232
                Code: httpbakery.ErrInteractionRequired,
 
233
                Info: &httpbakery.ErrorInfo{
 
234
                        VisitURL: fmt.Sprintf("%s/v1/login/%d", srv.URL, waitId),
 
235
                        WaitURL:  fmt.Sprintf("%s/v1/wait/%d?username=%s&caveat-id=%s&pubkey=%v", srv.URL, waitId, username, url.QueryEscape(cavId), url.QueryEscape(key.String())),
 
236
                },
 
237
        }
 
238
}
 
239
 
 
240
type handler struct {
 
241
        srv *Server
 
242
}
 
243
 
 
244
type groupsRequest struct {
 
245
        httprequest.Route `httprequest:"GET /v1/u/:User/groups"`
 
246
        User              string `httprequest:",path"`
 
247
}
 
248
 
 
249
func (h *handler) GetGroups(p httprequest.Params, req *groupsRequest) ([]string, error) {
 
250
        if err := h.checkRequest(p.Request); err != nil {
 
251
                return nil, err
 
252
        }
 
253
        if u := h.srv.user(req.User); u != nil {
 
254
                return u.groups, nil
 
255
        }
 
256
        return nil, params.ErrNotFound
 
257
}
 
258
 
 
259
func (h *handler) checkRequest(req *http.Request) error {
 
260
        _, err := httpbakery.CheckRequest(h.srv.bakery, req, nil, checkers.New())
 
261
        if err == nil {
 
262
                return nil
 
263
        }
 
264
        _, ok := errgo.Cause(err).(*bakery.VerificationError)
 
265
        if !ok {
 
266
                return err
 
267
        }
 
268
        m, err := h.srv.bakery.NewMacaroon("", nil, []checkers.Caveat{{
 
269
                Location:  h.srv.URL.String() + "/v1/discharger",
 
270
                Condition: "is-authenticated-user",
 
271
        }})
 
272
        if err != nil {
 
273
                panic(err)
 
274
        }
 
275
        return httpbakery.NewDischargeRequiredErrorForRequest(m, "", err, req)
 
276
}
 
277
 
 
278
type loginRequest struct {
 
279
        httprequest.Route `httprequest:"GET /v1/login/:WaitID"`
 
280
        WaitID            int `httprequest:",path"`
 
281
}
 
282
 
 
283
// TODO export VisitURLResponse from the bakery.
 
284
type visitURLResponse struct {
 
285
        AgentLogin bool `json:"agent_login"`
 
286
}
 
287
 
 
288
// serveLogin provides the /login endpoint. When /login is called it should
 
289
// be provided with a test id. /login also supports some additional parameters:
 
290
//     a = if set to "true" an agent URL will be added to the json response.
 
291
//     i = if set to "true" a plaintext response will be sent to simulate interaction.
 
292
func (h *handler) Login(p httprequest.Params, req *loginRequest) (*visitURLResponse, error) {
 
293
        h.srv.mu.Lock()
 
294
        defer h.srv.mu.Unlock()
 
295
        select {
 
296
        case h.srv.waits[req.WaitID] <- struct{}{}:
 
297
        default:
 
298
        }
 
299
        return &visitURLResponse{
 
300
                AgentLogin: true,
 
301
        }, nil
 
302
}
 
303
 
 
304
type waitRequest struct {
 
305
        httprequest.Route `httprequest:"GET /v1/wait/:WaitID"`
 
306
        WaitID            int               `httprequest:",path"`
 
307
        Username          string            `httprequest:"username,form"`
 
308
        CaveatID          string            `httprequest:"caveat-id,form"`
 
309
        PublicKey         *bakery.PublicKey `httprequest:"pubkey,form"`
 
310
}
 
311
 
 
312
func (h *handler) Wait(req *waitRequest) (*httpbakery.WaitResponse, error) {
 
313
        h.srv.mu.Lock()
 
314
        c := h.srv.waits[req.WaitID]
 
315
        h.srv.mu.Unlock()
 
316
        <-c
 
317
        u := h.srv.user(req.Username)
 
318
        if u == nil {
 
319
                return nil, errgo.Newf("user not found")
 
320
        }
 
321
        if *req.PublicKey != u.key.Public {
 
322
                return nil, errgo.Newf("public key mismatch")
 
323
        }
 
324
        checker := func(cavId, cav string) ([]checkers.Caveat, error) {
 
325
                return []checkers.Caveat{
 
326
                        checkers.DeclaredCaveat("username", req.Username),
 
327
                        bakery.LocalThirdPartyCaveat(&u.key.Public),
 
328
                }, nil
 
329
        }
 
330
        m, err := h.srv.bakery.Discharge(bakery.ThirdPartyCheckerFunc(checker), req.CaveatID)
 
331
        if err != nil {
 
332
                return nil, errgo.NoteMask(err, "cannot discharge", errgo.Any)
 
333
        }
 
334
        return &httpbakery.WaitResponse{
 
335
                Macaroon: m,
 
336
        }, nil
 
337
}