1
// Copyright 2015 Canonical Ltd.
2
// Licensed under the LGPLv3, see LICENCE file for details.
4
// Package idmtest holds a mock implementation of the identity manager
5
// suitable for testing.
15
"github.com/juju/httprequest"
16
"github.com/julienschmidt/httprouter"
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"
23
"github.com/juju/idmclient/params"
26
// Server represents a mock identity server.
27
// It currently serves only the discharge and groups endpoints.
29
// URL holds the URL of the mock identity server.
30
// The discharger endpoint is located at URL/v1/discharge.
33
// PublicKey holds the public key of the mock identity server.
34
PublicKey *bakery.PublicKey
36
router *httprouter.Router
38
bakery *bakery.Service
40
// mu guards the fields below it.
42
users map[string]*user
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 {
57
users: make(map[string]*user),
59
bsvc, err := bakery.NewService(bakery.NewServiceParams{
66
srv.PublicKey = bsvc.PublicKey()
67
errorMapper := httprequest.ErrorMapper(errToResp)
71
router := httprouter.New()
72
for _, route := range errorMapper.Handlers(func(httprequest.Params) (*handler, error) {
75
router.Handle(route.Method, route.Path, route.Handle)
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)
84
srv.srv = httptest.NewServer(router)
85
srv.URL, err = url.Parse(srv.srv.URL)
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)
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
114
return status, errorBody
117
// errorResponse returns an appropriate error response for the provided error.
118
func errorResponseBody(err error) *params.Error {
119
errResp := ¶ms.Error{
120
Message: err.Error(),
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
131
type errorCoder interface {
132
ErrorCode() params.ErrorCode
135
// Close shuts down the server.
136
func (srv *Server) Close() {
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
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)
151
panic("no user found")
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)
162
srv.AddUser(username)
163
u = srv.user(username)
166
agent.SetUpAuth(c, srv.URL, username)
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
175
// If the name is empty, there will be no default user.
176
func (srv *Server) SetDefaultUser(name string) {
178
defer srv.mu.Unlock()
179
srv.defaultUser = name
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()
189
defer srv.mu.Unlock()
190
srv.users[name] = &user{
196
func (srv *Server) user(name string) *user {
198
defer srv.mu.Unlock()
199
return srv.users[name]
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)
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
210
username, key, err := agent.LoginCookie(req)
211
if errgo.Cause(err) == agent.ErrNoAgentLoginCookie {
213
defer srv.mu.Unlock()
214
if srv.defaultUser != "" {
215
return []checkers.Caveat{
216
checkers.DeclaredCaveat("username", srv.defaultUser),
221
return nil, errgo.Notef(err, "bad agent-login cookie in request")
224
defer srv.mu.Unlock()
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())),
240
type handler struct {
244
type groupsRequest struct {
245
httprequest.Route `httprequest:"GET /v1/u/:User/groups"`
246
User string `httprequest:",path"`
249
func (h *handler) GetGroups(p httprequest.Params, req *groupsRequest) ([]string, error) {
250
if err := h.checkRequest(p.Request); err != nil {
253
if u := h.srv.user(req.User); u != nil {
256
return nil, params.ErrNotFound
259
func (h *handler) checkRequest(req *http.Request) error {
260
_, err := httpbakery.CheckRequest(h.srv.bakery, req, nil, checkers.New())
264
_, ok := errgo.Cause(err).(*bakery.VerificationError)
268
m, err := h.srv.bakery.NewMacaroon("", nil, []checkers.Caveat{{
269
Location: h.srv.URL.String() + "/v1/discharger",
270
Condition: "is-authenticated-user",
275
return httpbakery.NewDischargeRequiredErrorForRequest(m, "", err, req)
278
type loginRequest struct {
279
httprequest.Route `httprequest:"GET /v1/login/:WaitID"`
280
WaitID int `httprequest:",path"`
283
// TODO export VisitURLResponse from the bakery.
284
type visitURLResponse struct {
285
AgentLogin bool `json:"agent_login"`
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) {
294
defer h.srv.mu.Unlock()
296
case h.srv.waits[req.WaitID] <- struct{}{}:
299
return &visitURLResponse{
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"`
312
func (h *handler) Wait(req *waitRequest) (*httpbakery.WaitResponse, error) {
314
c := h.srv.waits[req.WaitID]
317
u := h.srv.user(req.Username)
319
return nil, errgo.Newf("user not found")
321
if *req.PublicKey != u.key.Public {
322
return nil, errgo.Newf("public key mismatch")
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),
330
m, err := h.srv.bakery.Discharge(bakery.ThirdPartyCheckerFunc(checker), req.CaveatID)
332
return nil, errgo.NoteMask(err, "cannot discharge", errgo.Any)
334
return &httpbakery.WaitResponse{