forked from canonical/candid
/
agent.go
189 lines (170 loc) · 6.56 KB
/
agent.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package discharger
import (
"net/http"
"time"
"golang.org/x/net/context"
"gopkg.in/CanonicalLtd/candidclient.v1"
"gopkg.in/CanonicalLtd/candidclient.v1/params"
errgo "gopkg.in/errgo.v1"
"gopkg.in/httprequest.v1"
"gopkg.in/macaroon-bakery.v2/bakery"
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
"gopkg.in/macaroon-bakery.v2/bakery/identchecker"
"gopkg.in/macaroon-bakery.v2/httpbakery"
"gopkg.in/macaroon-bakery.v2/httpbakery/agent"
"github.com/CanonicalLtd/candid/internal/auth"
"github.com/CanonicalLtd/candid/store"
)
const (
// agentMacaroonDuration is the length of time for which an agent
// identity macaroon is valid. This is shorter than for users as
// an agent can authenticate without interaction.
agentMacaroonDuration = 30 * time.Minute
// agentLoginMacaroonDuration is the lifetime of the intermediate
// macaroon used in the agent login process.
agentLoginMacaroonDuration = 10 * time.Second
)
func loginOp(user string) bakery.Op {
return bakery.Op{
Entity: "agent-" + user,
Action: "login",
}
}
// agentLoginRequest is the expected GET request to the agent-login
// endpoint. Note: this is compatible with the parameters used for the
// agent login request in the httpbakery/agent package.
type agentLoginRequest struct {
httprequest.Route `httprequest:"GET /login/agent"`
DischargeID string `httprequest:"did,form"`
Username string `httprequest:"username,form"`
PublicKey *bakery.PublicKey `httprequest:"public-key,form"`
}
type agentMacaroonResponse struct {
Macaroon *bakery.Macaroon `json:"macaroon"`
}
// agentURL returns the URL path for the agent-login endpoint for the
// candid service at the given location.
func agentURL(location string, dischargeID string) string {
p := location + "/login/agent"
if dischargeID != "" {
p += "?did=" + dischargeID
}
return p
}
// AgentLogin is the endpoint used to acquire an agent macaroon
// as part of a discharge request.
func (h *handler) AgentLogin(p httprequest.Params, req *agentLoginRequest) (*agentMacaroonResponse, error) {
if req.Username == "" {
return nil, errgo.WithCausef(nil, params.ErrBadRequest, "username not specified")
}
if req.PublicKey == nil {
return nil, errgo.WithCausef(nil, params.ErrBadRequest, "public-key not specified")
}
m, err := h.agentMacaroon(p.Context, httpbakery.RequestVersion(p.Request), identchecker.LoginOp, req.Username, req.PublicKey)
if err != nil {
return nil, errgo.Mask(err)
}
return &agentMacaroonResponse{Macaroon: m}, nil
}
// agentMacaroon creates a new macaroon containing a local third-party
// caveat addressed to the specified agent.
func (h *handler) agentMacaroon(ctx context.Context, vers bakery.Version, op bakery.Op, user string, key *bakery.PublicKey) (*bakery.Macaroon, error) {
m, err := h.params.Oven.NewMacaroon(
ctx,
vers,
[]checkers.Caveat{
checkers.TimeBeforeCaveat(time.Now().Add(agentLoginMacaroonDuration)),
candidclient.UserDeclaration(user),
bakery.LocalThirdPartyCaveat(key, vers),
auth.UserHasPublicKeyCaveat(params.Username(user), key),
},
op,
)
return m, errgo.Mask(err)
}
// legacyAgentLoginRequest is the expected GET request to the agent-login
// endpoint.
type legacyAgentLoginRequest struct {
httprequest.Route `httprequest:"GET /login/legacy-agent"`
DischargeID string `httprequest:"did,form"`
}
// LegacyAgentLogin is the endpoint used when performing agent login
// using the legacy agent-login cookie based protocols.
func (h *handler) LegacyAgentLogin(p httprequest.Params, req *legacyAgentLoginRequest) (interface{}, error) {
user, key, err := agent.LoginCookie(p.Request)
if err != nil {
if errgo.Cause(err) == agent.ErrNoAgentLoginCookie {
return nil, errgo.WithCausef(err, params.ErrBadRequest, "")
}
return nil, errgo.Mask(err)
}
resp, err := h.legacyAgentLogin(p.Context, p.Request, req.DischargeID, user, key)
if err != nil {
return nil, errgo.Mask(err, errgo.Any)
}
return resp, nil
}
// legacyAgentLoginPostRequest is the expected request to the agent-login
// endpoint when using the POST protocol.
type legacyAgentLoginPostRequest struct {
httprequest.Route `httprequest:"POST /login/legacy-agent"`
DischargeID string `httprequest:"did,form"`
AgentLogin params.AgentLogin `httprequest:",body"`
}
// LegacyAgentLoginPost is the endpoint used when performing an agent login
// using the POST protocol.
func (h *handler) LegacyAgentLoginPost(p httprequest.Params, req *legacyAgentLoginPostRequest) (*agent.LegacyAgentResponse, error) {
resp, err := h.legacyAgentLogin(p.Context, p.Request, req.DischargeID, string(req.AgentLogin.Username), req.AgentLogin.PublicKey)
if err != nil {
return nil, errgo.Mask(err, errgo.Any)
}
return resp, nil
}
// legacyAgentLogin handles the common parts of the legacy agent login protocols.
func (h *handler) legacyAgentLogin(ctx context.Context, req *http.Request, dischargeID string, user string, key *bakery.PublicKey) (*agent.LegacyAgentResponse, error) {
loginOp := loginOp(user)
vers := httpbakery.RequestVersion(req)
ctx = httpbakery.ContextWithRequest(ctx, req)
ctx = auth.ContextWithDischargeID(ctx, dischargeID)
_, err := h.params.Authorizer.Auth(ctx, httpbakery.RequestMacaroons(req), loginOp)
if err == nil {
dt, err := h.params.dischargeTokenCreator.DischargeToken(ctx, &store.Identity{
Username: user,
})
if err != nil {
return nil, errgo.Mask(err)
}
h.params.place.Done(ctx, dischargeID, &loginInfo{
DischargeToken: dt,
})
return &agent.LegacyAgentResponse{
AgentLogin: true,
}, nil
}
// TODO fail harder if the error isn't because of a verification error?
// Verification has failed. The bakery checker will want us to
// discharge a macaroon to prove identity, but we're already
// part of the discharge process so we can't do that here.
// Instead, mint a very short term macaroon containing
// the local third party caveat that will allow access if discharged.
m, err := h.agentMacaroon(ctx, vers, loginOp, user, key)
if err != nil {
return nil, errgo.Notef(err, "cannot create macaroon")
}
return nil, httpbakery.NewDischargeRequiredError(httpbakery.DischargeRequiredErrorParams{
Macaroon: m,
Request: req,
CookieNameSuffix: "agent-login",
})
}
// legacyAgentURL returns the URL path for the legacy agent login endpoint
// for the candid service at the given location.
func legacyAgentURL(location string, dischargeID string) string {
p := location + "/login/legacy-agent"
if dischargeID != "" {
p += "?did=" + dischargeID
}
return p
}