Merge lp:~jameinel/juju-core/login-returns-env-tag into lp:~go-bot/juju-core/trunk
- login-returns-env-tag
- Merge into trunk
Status: | Work in progress |
---|---|
Proposed branch: | lp:~jameinel/juju-core/login-returns-env-tag |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
851 lines (+402/-41) 14 files modified
environs/configstore/disk.go (+5/-2) environs/configstore/interface.go (+4/-0) environs/configstore/interface_test.go (+6/-4) juju/api.go (+42/-14) juju/apiconn_test.go (+139/-3) juju/mock_test.go (+5/-0) state/api/apiclient.go (+14/-1) state/api/apiclient_test.go (+22/-0) state/api/params/params.go (+6/-4) state/api/state.go (+6/-4) state/api/state_test.go (+48/-0) state/apiserver/admin.go (+21/-1) state/apiserver/common/errors.go (+2/-0) state/apiserver/login_test.go (+82/-8) |
To merge this branch: | bzr merge lp:~jameinel/juju-core/login-returns-env-tag |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+221021@code.launchpad.net |
Commit message
Description of the change
various: change API.Login() to handle EnvironTag
This changes a bit of Login handling to allow us to pass in an
EnvironTag which will get validated against the running environment. We
also return EnvironTag as part of the Login response, since otherwise
the existing client doesn't have a good way of finding out that
information.
It then goes around and updates a bunch of places so that we can store
the EnvironTag in our .jenv and read it back out (and pass it in).
I went with a new error message (invalid environment requested), rather
than just ErrPerm, partially because I have already validated the Login
credentials. (Since one user is intended to have access to multiple
environments, it made sense to layer it that way). It does still return
the CodeUnauthorized.
It is still valid to pass in an EnvironTag of "". Eventually we may
change how Login works under that circumstance (for Multi Environment
support), but for now this gets us closer to where we want to be.
If we get to the point that Bootstrap actually stashes our api
information it could also stash the environment tag. However, in the
short term if it just connects to the API like everything else, it will
get the environment UUID cached, which will let us be a bit more
protected from accidentally connecting to the wrong environment.
This might have implications for CI, as I believe they try to re-use
things like CACerts so that they can use the same .jenv files across
mulitple bootstraps. If so, this will start causing the client to get
rejected because the actual environment has changed. Though I think that
is the behavior that we decided we wanted.
William Reade (fwereade) wrote : | # |
This looks good. In the absence of completed API versioning I'd actually
be happy to land this as is -- but I'd appreciate your thoughts on
consolidating the configstore and agentconfig bits...
https:/
File environs/
https:/
environs/
I think we want to *store* the uuid, not the tag -- the tags really are
just for the API, and I would prefer that we not let them leak into
users' worldviews. Tags coming back from the API are fine and good, but
no further.
https:/
File environs/
https:/
environs/
"environ-
this choice of "uuid" makes me unreasonably happy :)
https:/
File juju/api.go (right):
https:/
juju/api.go:358: EnvironTag: apiInfo.EnvironTag,
I won't keep saying it, but this is the place to just extract the uuid
and store it.
https:/
juju/api.go:387: _ = newEnvironTag
?
https:/
File state/apiserver
https:/
state/apiserver
call?
Heh. You could, inside state, slip in and delete the document; but
Idon't think there's a way to do it here. (Stop the mongo? ;p)
https:/
File state/apiserver
https:/
state/apiserver
I guess we have enough tests that it accepts a missing environ tag
already?
I'm wondering whether we should be fixing this stuff for the agents as
well. It's less pressing, but still.
(FWIW, I feel that all this code to connect and store changes etc etc
really should be exactly the same on agent and client -- the only
interesting difference is the extra stuff that agents need to do to
change their passwords, and the client will need the same functionality
once we expose pass-word-changing there.)
- 2801. By John A Meinel
-
clean up the code so that we only store EnvironUUID and just talk about EnvironTag in the api calls
- 2802. By John A Meinel
-
clarify in the comments that we are explicitly testing the case where EnvironTag is empty.
John A Meinel (jameinel) wrote : | # |
Please take a look.
https:/
File environs/
https:/
environs/
On 2014/05/27 07:27:57, fwereade wrote:
> I think we want to *store* the uuid, not the tag -- the tags really
are just for
> the API, and I would prefer that we not let them leak into users'
worldviews.
> Tags coming back from the API are fine and good, but no further.
Fine with me. I wasn't sure if bare UUIDs were better to store than
tags. I realized that in the API things are tags, and in State they are
IDs, but wasn't sure about user level stuff.
Done.
I ended up sticking with the api.State object has an EnvironTag()
method, and api.Info thinks of tags, but configstore.
EnvironUUID.
https:/
File environs/
https:/
environs/
"environ-
On 2014/05/27 07:27:57, fwereade wrote:
> this choice of "uuid" makes me unreasonably happy :)
:)
https:/
File juju/api.go (right):
https:/
juju/api.go:358: EnvironTag: apiInfo.EnvironTag,
On 2014/05/27 07:27:57, fwereade wrote:
> I won't keep saying it, but this is the place to just extract the uuid
and store
> it.
Done.
https:/
juju/api.go:358: EnvironTag: apiInfo.EnvironTag,
On 2014/05/27 07:27:57, fwereade wrote:
> I won't keep saying it, but this is the place to just extract the uuid
and store
> it.
Done.
https:/
juju/api.go:387: _ = newEnvironTag
On 2014/05/27 07:27:57, fwereade wrote:
> ?
temporary workaround for golang not allowing unused vars while
incrementally building code. removed.
https:/
File state/apiserver
https:/
state/apiserver
call?
On 2014/05/27 07:27:58, fwereade wrote:
> Heh. You could, inside state, slip in and delete the document; but
Idon't think
> there's a way to do it here. (Stop the mongo? ;p)
Yeah, it is just one of those things where you "have" to propagate
errors, but I really don't like touching the stuff without actually
checking what it does.
Should this actually be returning some other error for the API? Is it
sane to just blat out whatever internal failure occured?
We don't really have concrete decisions around this sort of thing (that
I can tell).
https:/
File state/apiserver
https:/
John A Meinel (jameinel) wrote : | # |
Please take a look.
Andrew Wilkins (axwalk) wrote : | # |
LGTM
https:/
File juju/api.go (right):
https:/
juju/api.go:271: environUUID := ""
s/environUUID/
https:/
File juju/apiconn_
https:/
juju/apiconn_
which the API does
I think this is testing something that is currently impossible. I
suppose it doesn't hurt to be cautious of bugs introduced in the server
though.
https:/
File state/api/
https:/
state/api/
env.Tag())
An explicit test for Login with envtag=="" would be good here.
https:/
File state/apiserver
https:/
state/apiserver
Perhaps add a comment here, saying why we allow an unspecified
EnvironTag, lest someone thinks it's a bug and breaks it?
https:/
File state/apiserver
https:/
state/apiserver
func (s *loginSuite) TestLoginAccept
...
}
?
John A Meinel (jameinel) wrote : | # |
Thanks for your review. I've superseded this request with this one:
https:/
However, hopefully I've managed to address/respond to all of the
comments you've made here.
https:/
File juju/api.go (right):
https:/
juju/api.go:271: environUUID := ""
On 2014/05/29 03:59:25, axw wrote:
> s/environUUID/
Done.
https:/
File juju/apiconn_
https:/
juju/apiconn_
which the API does
On 2014/05/29 03:59:25, axw wrote:
> I think this is testing something that is currently impossible. I
suppose it
> doesn't hurt to be cautious of bugs introduced in the server though.
It actually happens in the test suite. Specifically our test suite has a
lot of places where we have no machine-0 (and thus no API host ports).
https:/
File state/api/
https:/
state/api/
env.Tag())
On 2014/05/29 03:59:25, axw wrote:
> An explicit test for Login with envtag=="" would be good here.
I believe I have that test in the follow up.
https:/
File state/apiserver
https:/
state/apiserver
On 2014/05/29 03:59:25, axw wrote:
> Perhaps add a comment here, saying why we allow an unspecified
EnvironTag, lest
> someone thinks it's a bug and breaks it?
This code is now moved, because the EnvironUUID is in the URL instead of
as a parameter to Login.
However, I added what is hopefully a good comment to
"validateEnviro
https:/
File state/apiserver
https:/
state/apiserver
On 2014/05/29 03:59:25, axw wrote:
> func (s *loginSuite) TestLoginAccept
> ...
> }
> ?
If you look, "TestLoginRepor
EnvironTag:"" which is the test you're looking for. Would you prefer a
different name?
Andrew Wilkins (axwalk) wrote : | # |
https:/
File state/apiserver
https:/
state/apiserver
On 2014/06/01 09:53:31, jameinel wrote:
> On 2014/05/29 03:59:25, axw wrote:
> > func (s *loginSuite) TestLoginAccept
> > ...
> > }
> >
> > ?
> If you look, "TestLoginRepor
EnvironTag:"" which
> is the test you're looking for. Would you prefer a different name?
Sorry, don't know how I missed that. That's fine.
John A Meinel (jameinel) wrote : | # |
np, William missed it as well. The naming probably leads reviews to think
about "what about this case" and there isn't an obviously named test doing
it.
On Sun, Jun 1, 2014 at 4:15 PM, Andrew Wilkins <<email address hidden>
> wrote:
>
>
> https:/
> File state/apiserver
>
>
> https:/
> state/apiserver
> On 2014/06/01 09:53:31, jameinel wrote:
> > On 2014/05/29 03:59:25, axw wrote:
> > > func (s *loginSuite) TestLoginAccept
> > > ...
> > > }
> > >
> > > ?
>
> > If you look, "TestLoginRepor
> EnvironTag:"" which
> > is the test you're looking for. Would you prefer a different name?
>
>
> Sorry, don't know how I missed that. That's fine.
>
> https:/
>
> --
>
> https:/
> You are the owner of lp:~jameinel/juju-core/login-returns-env-tag.
>
Unmerged revisions
- 2802. By John A Meinel
-
clarify in the comments that we are explicitly testing the case where EnvironTag is empty.
- 2801. By John A Meinel
-
clean up the code so that we only store EnvironUUID and just talk about EnvironTag in the api calls
- 2800. By John A Meinel
-
Fix another edge case.
Our test suite doesn't always have APIHostPorts available, because we don't
always have a machine-0. In those cases, we can get an EnvironTag but not
have any addresses to connect to. We don't want to end up setting the
connection addresses to the empty list, and make it hard to actually
connect to the environment.
Add a test, move where we set addrs to only set it when they have changed. - 2799. By John A Meinel
-
Fixup the APIServer tests now that Login takes the extra argument
- 2798. By John A Meinel
-
Test that Open and Login will pass the EnvironTag when requested.
- 2797. By John A Meinel
-
update another environs/
configstore test for the new tag - 2796. By John A Meinel
-
Thread the EnvironTag code through the testing mocks at least.
The 'juju' test suite is happy. - 2795. By John A Meinel
-
Start working on threading EnvironTag through all the apis, not quite working yet.
- 2794. By John A Meinel
-
rename Environ to EnvironTag.
- 2793. By John A Meinel
-
Admin.Login accepts environ tags, and will give you a valid error when it doesn't know about the UUID you are requesting.
Preview Diff
1 | === modified file 'environs/configstore/disk.go' |
2 | --- environs/configstore/disk.go 2014-05-13 04:50:10 +0000 |
3 | +++ environs/configstore/disk.go 2014-05-27 09:53:17 +0000 |
4 | @@ -32,6 +32,7 @@ |
5 | type EnvironInfoData struct { |
6 | User string |
7 | Password string |
8 | + EnvironUUID string `json:"environ-uuid,omitempty" yaml:"environ-uuid,omitempty"` |
9 | StateServers []string `json:"state-servers" yaml:"state-servers"` |
10 | CACert string `json:"ca-cert" yaml:"ca-cert"` |
11 | Config map[string]interface{} `json:"bootstrap-config,omitempty" yaml:"bootstrap-config,omitempty"` |
12 | @@ -138,8 +139,9 @@ |
13 | // APIEndpoint implements EnvironInfo.APIEndpoint. |
14 | func (info *environInfo) APIEndpoint() APIEndpoint { |
15 | return APIEndpoint{ |
16 | - Addresses: info.EnvInfo.StateServers, |
17 | - CACert: info.EnvInfo.CACert, |
18 | + Addresses: info.EnvInfo.StateServers, |
19 | + CACert: info.EnvInfo.CACert, |
20 | + EnvironUUID: info.EnvInfo.EnvironUUID, |
21 | } |
22 | } |
23 | |
24 | @@ -155,6 +157,7 @@ |
25 | func (info *environInfo) SetAPIEndpoint(endpoint APIEndpoint) { |
26 | info.EnvInfo.StateServers = endpoint.Addresses |
27 | info.EnvInfo.CACert = endpoint.CACert |
28 | + info.EnvInfo.EnvironUUID = endpoint.EnvironUUID |
29 | } |
30 | |
31 | // SetAPICredentials implements EnvironInfo.SetAPICredentials. |
32 | |
33 | === modified file 'environs/configstore/interface.go' |
34 | --- environs/configstore/interface.go 2014-05-22 00:48:08 +0000 |
35 | +++ environs/configstore/interface.go 2014-05-27 09:53:17 +0000 |
36 | @@ -19,6 +19,10 @@ |
37 | // CACert holds the CA certificate that |
38 | // signed the API server's key. |
39 | CACert string |
40 | + |
41 | + // EnvironUUID holds the UUID for the environment we are connecting to. |
42 | + // This may be empty if the environment has not been bootstrapped. |
43 | + EnvironUUID string |
44 | } |
45 | |
46 | // APICredentials hold credentials for connecting to an API endpoint. |
47 | |
48 | === modified file 'environs/configstore/interface_test.go' |
49 | --- environs/configstore/interface_test.go 2014-05-20 04:27:02 +0000 |
50 | +++ environs/configstore/interface_test.go 2014-05-27 09:53:17 +0000 |
51 | @@ -46,8 +46,9 @@ |
52 | c.Assert(err, gc.IsNil) |
53 | |
54 | expectEndpoint := configstore.APIEndpoint{ |
55 | - Addresses: []string{"example.com"}, |
56 | - CACert: "a cert", |
57 | + Addresses: []string{"example.com"}, |
58 | + CACert: "a cert", |
59 | + EnvironUUID: "dead-beef", |
60 | } |
61 | info.SetAPIEndpoint(expectEndpoint) |
62 | c.Assert(info.APIEndpoint(), gc.DeepEquals, expectEndpoint) |
63 | @@ -75,8 +76,9 @@ |
64 | info.SetAPICredentials(expectCreds) |
65 | |
66 | expectEndpoint := configstore.APIEndpoint{ |
67 | - Addresses: []string{"example.com"}, |
68 | - CACert: "a cert", |
69 | + Addresses: []string{"example.invalid"}, |
70 | + CACert: "a cert", |
71 | + EnvironUUID: "dead-beef", |
72 | } |
73 | info.SetAPIEndpoint(expectEndpoint) |
74 | |
75 | |
76 | === modified file 'juju/api.go' |
77 | --- juju/api.go 2014-05-13 04:30:48 +0000 |
78 | +++ juju/api.go 2014-05-27 09:53:17 +0000 |
79 | @@ -32,6 +32,7 @@ |
80 | type apiState interface { |
81 | Close() error |
82 | APIHostPorts() [][]instance.HostPort |
83 | + EnvironTag() string |
84 | } |
85 | |
86 | type apiOpenFunc func(*api.Info, api.DialOpts) (apiState, error) |
87 | @@ -212,7 +213,7 @@ |
88 | |
89 | st := val0.(apiState) |
90 | // Even though we are about to update API addresses based on |
91 | - // APIHostPorts in cacheChangedAPIAddresses, we first cache the |
92 | + // APIHostPorts in cacheChangedAPIInfo, we first cache the |
93 | // addresses based on the provider lookup. This is because older API |
94 | // servers didn't return their HostPort information on Login, and we |
95 | // still want to cache our connection information to them. |
96 | @@ -231,7 +232,7 @@ |
97 | } |
98 | } |
99 | // Update API addresses if they've changed. Error is non-fatal. |
100 | - if localerr := cacheChangedAPIAddresses(info, st); localerr != nil { |
101 | + if localerr := cacheChangedAPIInfo(info, st); localerr != nil { |
102 | logger.Warningf("cannot failed to cache API addresses: %v", localerr) |
103 | } |
104 | return st, nil |
105 | @@ -267,11 +268,16 @@ |
106 | return nil, &infoConnectError{fmt.Errorf("no cached addresses")} |
107 | } |
108 | logger.Infof("connecting to API addresses: %v", endpoint.Addresses) |
109 | + environUUID := "" |
110 | + if endpoint.EnvironUUID != "" { |
111 | + environUUID = names.EnvironTag(endpoint.EnvironUUID) |
112 | + } |
113 | apiInfo := &api.Info{ |
114 | - Addrs: endpoint.Addresses, |
115 | - CACert: endpoint.CACert, |
116 | - Tag: names.UserTag(info.APICredentials().User), |
117 | - Password: info.APICredentials().Password, |
118 | + Addrs: endpoint.Addresses, |
119 | + CACert: endpoint.CACert, |
120 | + Tag: names.UserTag(info.APICredentials().User), |
121 | + Password: info.APICredentials().Password, |
122 | + EnvironTag: environUUID, |
123 | } |
124 | st, err := apiOpen(apiInfo, api.DefaultDialOpts()) |
125 | if err != nil { |
126 | @@ -344,9 +350,18 @@ |
127 | // with the provided apiInfo, assuming we've just successfully |
128 | // connected to the API server. |
129 | func cacheAPIInfo(info configstore.EnvironInfo, apiInfo *api.Info) error { |
130 | + environUUID := "" |
131 | + if apiInfo.EnvironTag != "" { |
132 | + var err error |
133 | + _, environUUID, err = names.ParseTag(apiInfo.Tag, names.EnvironTagKind) |
134 | + if err != nil { |
135 | + return fmt.Errorf("invalid API environment tag: %v", err) |
136 | + } |
137 | + } |
138 | info.SetAPIEndpoint(configstore.APIEndpoint{ |
139 | - Addresses: apiInfo.Addrs, |
140 | - CACert: string(apiInfo.CACert), |
141 | + Addresses: apiInfo.Addrs, |
142 | + CACert: string(apiInfo.CACert), |
143 | + EnvironUUID: environUUID, |
144 | }) |
145 | _, username, err := names.ParseTag(apiInfo.Tag, names.UserTagKind) |
146 | if err != nil { |
147 | @@ -359,9 +374,10 @@ |
148 | return info.Write() |
149 | } |
150 | |
151 | -// cacheChangedAPIAddresses updates the local environment settings (.jenv file) |
152 | -// with the provided API server addresses if they have changed. |
153 | -func cacheChangedAPIAddresses(info configstore.EnvironInfo, st apiState) error { |
154 | +// cacheChangedAPIInfo updates the local environment settings (.jenv file) |
155 | +// with the provided API server addresses if they have changed. It will also |
156 | +// save the environment tag if it is available. |
157 | +func cacheChangedAPIInfo(info configstore.EnvironInfo, st apiState) error { |
158 | var addrs []string |
159 | for _, serverHostPorts := range st.APIHostPorts() { |
160 | for _, hostPort := range serverHostPorts { |
161 | @@ -373,11 +389,23 @@ |
162 | } |
163 | } |
164 | endpoint := info.APIEndpoint() |
165 | - if len(addrs) == 0 || !addrsChanged(endpoint.Addresses, addrs) { |
166 | + newEnvironTag := st.EnvironTag() |
167 | + changed := false |
168 | + if newEnvironTag != "" { |
169 | + _, environUUID, err := names.ParseTag(newEnvironTag, names.EnvironTagKind) |
170 | + if err == nil && endpoint.EnvironUUID != environUUID { |
171 | + changed = true |
172 | + endpoint.EnvironUUID = environUUID |
173 | + } |
174 | + } |
175 | + if len(addrs) != 0 && addrsChanged(endpoint.Addresses, addrs) { |
176 | + logger.Debugf("API addresses changed from %q to %q", endpoint.Addresses, addrs) |
177 | + changed = true |
178 | + endpoint.Addresses = addrs |
179 | + } |
180 | + if !changed { |
181 | return nil |
182 | } |
183 | - logger.Debugf("API addresses changed from %q to %q", endpoint.Addresses, addrs) |
184 | - endpoint.Addresses = addrs |
185 | info.SetAPIEndpoint(endpoint) |
186 | if err := info.Write(); err != nil { |
187 | return err |
188 | |
189 | === modified file 'juju/apiconn_test.go' |
190 | --- juju/apiconn_test.go 2014-05-16 01:33:13 +0000 |
191 | +++ juju/apiconn_test.go 2014-05-27 09:53:17 +0000 |
192 | @@ -123,11 +123,13 @@ |
193 | apiHostPorts: [][]instance.HostPort{ |
194 | instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3", instance.NetworkUnknown)}, 1234), |
195 | }, |
196 | + environTag: "environment-fake-uuid", |
197 | } |
198 | apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
199 | c.Check(apiInfo.Tag, gc.Equals, "user-foo") |
200 | c.Check(string(apiInfo.CACert), gc.Equals, "certificated") |
201 | c.Check(apiInfo.Password, gc.Equals, "foopass") |
202 | + c.Check(apiInfo.EnvironTag, gc.Equals, "environment-fake-uuid") |
203 | c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) |
204 | called++ |
205 | return expectState, nil |
206 | @@ -143,7 +145,9 @@ |
207 | c.Assert(mockStore.written, jc.IsTrue) |
208 | info, err := store.ReadInfo("noconfig") |
209 | c.Assert(err, gc.IsNil) |
210 | - c.Assert(info.APIEndpoint().Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"}) |
211 | + ep := info.APIEndpoint() |
212 | + c.Assert(ep.Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"}) |
213 | + c.Check(ep.EnvironUUID, gc.Equals, "fake-uuid") |
214 | mockStore.written = false |
215 | |
216 | // If APIHostPorts haven't changed, then the store won't be updated. |
217 | @@ -185,6 +189,8 @@ |
218 | c.Check(apiInfo.Tag, gc.Equals, "user-admin") |
219 | c.Check(string(apiInfo.CACert), gc.Not(gc.Equals), "") |
220 | c.Check(apiInfo.Password, gc.Equals, "adminpass") |
221 | + // EnvironTag wasn't in regular Config |
222 | + c.Check(apiInfo.EnvironTag, gc.Equals, "") |
223 | c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) |
224 | called++ |
225 | return expectState, nil |
226 | @@ -202,6 +208,9 @@ |
227 | c.Assert(ep.Addresses, gc.HasLen, 1) |
228 | c.Check(ep.Addresses[0], gc.Matches, `127\.0\.0\.1:\d+`) |
229 | c.Check(ep.CACert, gc.Not(gc.Equals), "") |
230 | + // Old servers won't hand back EnvironTag, so it should stay empty in |
231 | + // the cache |
232 | + c.Check(ep.EnvironUUID, gc.Equals, "") |
233 | creds := info.APICredentials() |
234 | c.Check(creds.User, gc.Equals, "admin") |
235 | c.Check(creds.Password, gc.Equals, "adminpass") |
236 | @@ -227,6 +236,128 @@ |
237 | c.Assert(st, gc.IsNil) |
238 | } |
239 | |
240 | +func (s *NewAPIClientSuite) TestWithInfoNoEnvironTag(c *gc.C) { |
241 | + store := newConfigStore("noconfig", &environInfo{ |
242 | + creds: configstore.APICredentials{ |
243 | + User: "foo", |
244 | + Password: "foopass", |
245 | + }, |
246 | + endpoint: configstore.APIEndpoint{ |
247 | + Addresses: []string{"foo.invalid"}, |
248 | + CACert: "certificated", |
249 | + }, |
250 | + }) |
251 | + |
252 | + called := 0 |
253 | + expectState := &mockAPIState{ |
254 | + apiHostPorts: [][]instance.HostPort{ |
255 | + instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3", instance.NetworkUnknown)}, 1234), |
256 | + }, |
257 | + environTag: "environment-fake-uuid", |
258 | + } |
259 | + apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
260 | + c.Check(apiInfo.Tag, gc.Equals, "user-foo") |
261 | + c.Check(string(apiInfo.CACert), gc.Equals, "certificated") |
262 | + c.Check(apiInfo.Password, gc.Equals, "foopass") |
263 | + c.Check(apiInfo.EnvironTag, gc.Equals, "") |
264 | + c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) |
265 | + called++ |
266 | + return expectState, nil |
267 | + } |
268 | + |
269 | + // Give NewAPIFromStore a store interface that can report when the |
270 | + // config was written to, to check if the cache is updated. |
271 | + mockStore := &storageWithWriteNotify{store: store} |
272 | + st, err := juju.NewAPIFromStore("noconfig", mockStore, apiOpen) |
273 | + c.Assert(err, gc.IsNil) |
274 | + c.Assert(st, gc.Equals, expectState) |
275 | + c.Assert(called, gc.Equals, 1) |
276 | + c.Assert(mockStore.written, jc.IsTrue) |
277 | + info, err := store.ReadInfo("noconfig") |
278 | + c.Assert(err, gc.IsNil) |
279 | + c.Assert(info.APIEndpoint().Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"}) |
280 | + c.Check(info.APIEndpoint().EnvironUUID, gc.Equals, "fake-uuid") |
281 | +} |
282 | + |
283 | +func (s *NewAPIClientSuite) TestWithInfoNoAPIHostports(c *gc.C) { |
284 | + // The local cache doesn't have an EnvironTag, which the API does |
285 | + // return. However, the API doesn't have apiHostPorts, we don't want to |
286 | + // override the local cache with bad endpoints. |
287 | + store := newConfigStore("noconfig", &environInfo{ |
288 | + creds: configstore.APICredentials{ |
289 | + User: "foo", |
290 | + Password: "foopass", |
291 | + }, |
292 | + endpoint: configstore.APIEndpoint{ |
293 | + Addresses: []string{"foo.invalid"}, |
294 | + CACert: "certificated", |
295 | + }, |
296 | + }) |
297 | + |
298 | + called := 0 |
299 | + expectState := &mockAPIState{ |
300 | + apiHostPorts: [][]instance.HostPort{}, |
301 | + environTag: "environment-fake-uuid", |
302 | + } |
303 | + apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
304 | + c.Check(apiInfo.Tag, gc.Equals, "user-foo") |
305 | + c.Check(string(apiInfo.CACert), gc.Equals, "certificated") |
306 | + c.Check(apiInfo.Password, gc.Equals, "foopass") |
307 | + c.Check(apiInfo.EnvironTag, gc.Equals, "") |
308 | + c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) |
309 | + called++ |
310 | + return expectState, nil |
311 | + } |
312 | + |
313 | + mockStore := &storageWithWriteNotify{store: store} |
314 | + st, err := juju.NewAPIFromStore("noconfig", mockStore, apiOpen) |
315 | + c.Assert(err, gc.IsNil) |
316 | + c.Assert(st, gc.Equals, expectState) |
317 | + c.Assert(called, gc.Equals, 1) |
318 | + c.Assert(mockStore.written, jc.IsTrue) |
319 | + info, err := store.ReadInfo("noconfig") |
320 | + c.Assert(err, gc.IsNil) |
321 | + ep := info.APIEndpoint() |
322 | + // We should have cached the environ tag, but not disturbed the |
323 | + // Addresses |
324 | + c.Check(ep.Addresses, gc.HasLen, 1) |
325 | + c.Check(ep.Addresses[0], gc.Matches, `foo\.invalid`) |
326 | + c.Check(ep.EnvironUUID, gc.Equals, "fake-uuid") |
327 | +} |
328 | + |
329 | +func (s *NewAPIClientSuite) TestNoEnvironTagDoesntOverwriteCached(c *gc.C) { |
330 | + store := newConfigStore("noconfig", dummyStoreInfo) |
331 | + called := 0 |
332 | + // State returns a new set of APIHostPorts but not a new EnvironTag. We |
333 | + // shouldn't override the cached value with environ tag of "". |
334 | + expectState := &mockAPIState{ |
335 | + apiHostPorts: [][]instance.HostPort{ |
336 | + instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3", instance.NetworkUnknown)}, 1234), |
337 | + }, |
338 | + } |
339 | + apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
340 | + c.Check(apiInfo.Tag, gc.Equals, "user-foo") |
341 | + c.Check(string(apiInfo.CACert), gc.Equals, "certificated") |
342 | + c.Check(apiInfo.Password, gc.Equals, "foopass") |
343 | + c.Check(apiInfo.EnvironTag, gc.Equals, "environment-fake-uuid") |
344 | + c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) |
345 | + called++ |
346 | + return expectState, nil |
347 | + } |
348 | + |
349 | + mockStore := &storageWithWriteNotify{store: store} |
350 | + st, err := juju.NewAPIFromStore("noconfig", mockStore, apiOpen) |
351 | + c.Assert(err, gc.IsNil) |
352 | + c.Assert(st, gc.Equals, expectState) |
353 | + c.Assert(called, gc.Equals, 1) |
354 | + c.Assert(mockStore.written, jc.IsTrue) |
355 | + info, err := store.ReadInfo("noconfig") |
356 | + c.Assert(err, gc.IsNil) |
357 | + ep := info.APIEndpoint() |
358 | + c.Assert(ep.Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"}) |
359 | + c.Check(ep.EnvironUUID, gc.Equals, "fake-uuid") |
360 | +} |
361 | + |
362 | func (s *NewAPIClientSuite) TestWithInfoAPIOpenError(c *gc.C) { |
363 | store := newConfigStore("noconfig", &environInfo{ |
364 | endpoint: configstore.APIEndpoint{ |
365 | @@ -584,8 +715,9 @@ |
366 | Password: "foopass", |
367 | }, |
368 | endpoint: configstore.APIEndpoint{ |
369 | - Addresses: []string{"foo.invalid"}, |
370 | - CACert: "certificated", |
371 | + Addresses: []string{"foo.invalid"}, |
372 | + CACert: "certificated", |
373 | + EnvironUUID: "fake-uuid", |
374 | }, |
375 | } |
376 | |
377 | @@ -631,12 +763,15 @@ |
378 | apiHostPorts: [][]instance.HostPort{ |
379 | instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3", instance.NetworkUnknown)}, 1234), |
380 | }, |
381 | + environTag: "environment-fake-uuid", |
382 | } |
383 | apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
384 | c.Check(apiInfo.Tag, gc.Equals, "user-admin") |
385 | c.Check(string(apiInfo.CACert), gc.Equals, coretesting.CACert) |
386 | c.Check(apiInfo.Password, gc.Equals, coretesting.DefaultMongoPassword) |
387 | c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) |
388 | + // we didn't know about it when connecting |
389 | + c.Check(apiInfo.EnvironTag, gc.Equals, "") |
390 | called++ |
391 | return expectState, nil |
392 | } |
393 | @@ -644,6 +779,7 @@ |
394 | c.Assert(err, gc.IsNil) |
395 | c.Assert(called, gc.Equals, 1) |
396 | c.Check(endpoint.Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"}) |
397 | + c.Check(endpoint.EnvironUUID, gc.Equals, "fake-uuid") |
398 | } |
399 | |
400 | func (s *APIEndpointForEnvSuite) TestAPIEndpointRefresh(c *gc.C) { |
401 | |
402 | === modified file 'juju/mock_test.go' |
403 | --- juju/mock_test.go 2014-03-31 12:24:52 +0000 |
404 | +++ juju/mock_test.go 2014-05-27 09:53:17 +0000 |
405 | @@ -10,6 +10,7 @@ |
406 | close func(juju.APIState) error |
407 | |
408 | apiHostPorts [][]instance.HostPort |
409 | + environTag string |
410 | } |
411 | |
412 | func (s *mockAPIState) Close() error { |
413 | @@ -23,6 +24,10 @@ |
414 | return s.apiHostPorts |
415 | } |
416 | |
417 | +func (s *mockAPIState) EnvironTag() string { |
418 | + return s.environTag |
419 | +} |
420 | + |
421 | func panicAPIOpen(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
422 | panic("api.Open called unexpectedly") |
423 | } |
424 | |
425 | === modified file 'state/api/apiclient.go' |
426 | --- state/api/apiclient.go 2014-04-30 23:18:40 +0000 |
427 | +++ state/api/apiclient.go 2014-05-27 09:53:17 +0000 |
428 | @@ -35,6 +35,9 @@ |
429 | // addr is the address used to connect to the API server. |
430 | addr string |
431 | |
432 | + // environTag holds the environment tag once we're connected |
433 | + environTag string |
434 | + |
435 | // hostPorts is the API server addresses returned from Login, |
436 | // which the client may cache and use for failover. |
437 | hostPorts [][]instance.HostPort |
438 | @@ -80,6 +83,10 @@ |
439 | // Nonce holds the nonce used when provisioning the machine. Used |
440 | // only by the machine agent. |
441 | Nonce string `yaml:",omitempty"` |
442 | + |
443 | + // Environ holds the environ tag for the environment we are trying to |
444 | + // connect to. |
445 | + EnvironTag string |
446 | } |
447 | |
448 | // DialOpts holds configuration parameters that control the |
449 | @@ -155,7 +162,7 @@ |
450 | certPool: pool, |
451 | } |
452 | if info.Tag != "" || info.Password != "" { |
453 | - if err := st.Login(info.Tag, info.Password, info.Nonce); err != nil { |
454 | + if err := st.Login(info.Tag, info.Password, info.Nonce, info.EnvironTag); err != nil { |
455 | conn.Close() |
456 | return nil, err |
457 | } |
458 | @@ -261,6 +268,12 @@ |
459 | return s.addr |
460 | } |
461 | |
462 | +// EnvironTag returns the Environment Tag describing the environment we are |
463 | +// connected to. |
464 | +func (s *State) EnvironTag() string { |
465 | + return s.environTag |
466 | +} |
467 | + |
468 | // APIHostPorts returns addresses that may be used to connect |
469 | // to the API server, including the address used to connect. |
470 | // |
471 | |
472 | === modified file 'state/api/apiclient_test.go' |
473 | --- state/api/apiclient_test.go 2014-04-02 09:43:39 +0000 |
474 | +++ state/api/apiclient_test.go 2014-05-27 09:53:17 +0000 |
475 | @@ -11,6 +11,7 @@ |
476 | |
477 | jujutesting "launchpad.net/juju-core/juju/testing" |
478 | "launchpad.net/juju-core/state/api" |
479 | + "launchpad.net/juju-core/state/api/params" |
480 | "launchpad.net/juju-core/utils/parallel" |
481 | ) |
482 | |
483 | @@ -79,6 +80,27 @@ |
484 | c.Assert(err, gc.ErrorMatches, `timed out connecting to "wss://.*/"`) |
485 | } |
486 | |
487 | +func (s *apiclientSuite) TestOpenPassesEnvironTag(c *gc.C) { |
488 | + info := s.APIInfo(c) |
489 | + environTag := info.EnvironTag |
490 | + env, err := s.State.Environment() |
491 | + c.Assert(err, gc.IsNil) |
492 | + c.ExpectFailure("JujuConnSuite.APIInfo() uses APIConn which doesn't know about EnvironTag") |
493 | + c.Check(environTag, gc.Equals, env.Tag()) |
494 | + c.Assert(environTag, gc.Not(gc.Equals), "") |
495 | + // We start by ensuring we have an invalid tag, and Open should fail. |
496 | + info.EnvironTag = "environ-bad-tag" |
497 | + _, err = api.Open(info, api.DialOpts{}) |
498 | + c.Check(err, gc.ErrorMatches, "invalid environment requested") |
499 | + c.Check(params.ErrCode(err), gc.Equals, params.CodeUnauthorized) |
500 | + c.Assert(err, gc.IsNil) |
501 | + // Now set it to the right tag, and we should succeed. |
502 | + info.EnvironTag = env.Tag() |
503 | + st, err := api.Open(info, api.DialOpts{}) |
504 | + c.Assert(err, gc.IsNil) |
505 | + st.Close() |
506 | +} |
507 | + |
508 | func (s *apiclientSuite) TestDialWebsocketStopped(c *gc.C) { |
509 | stopped := make(chan struct{}) |
510 | f := api.NewWebsocketDialer(nil, api.DialOpts{}) |
511 | |
512 | === modified file 'state/api/params/params.go' |
513 | --- state/api/params/params.go 2014-05-27 08:30:44 +0000 |
514 | +++ state/api/params/params.go 2014-05-27 09:53:17 +0000 |
515 | @@ -293,9 +293,10 @@ |
516 | |
517 | // Creds holds credentials for identifying an entity. |
518 | type Creds struct { |
519 | - AuthTag string |
520 | - Password string |
521 | - Nonce string |
522 | + AuthTag string |
523 | + Password string |
524 | + Nonce string |
525 | + EnvironTag string `json:",omitempty"` |
526 | } |
527 | |
528 | // GetAnnotationsResults holds annotations associated with an entity. |
529 | @@ -719,7 +720,8 @@ |
530 | |
531 | // LoginResult holds the result of a Login call. |
532 | type LoginResult struct { |
533 | - Servers [][]instance.HostPort |
534 | + Servers [][]instance.HostPort |
535 | + EnvironTag string |
536 | } |
537 | |
538 | // EnsureAvailability contains arguments for |
539 | |
540 | === modified file 'state/api/state.go' |
541 | --- state/api/state.go 2014-04-16 12:55:56 +0000 |
542 | +++ state/api/state.go 2014-05-27 09:53:17 +0000 |
543 | @@ -27,12 +27,13 @@ |
544 | // Subsequent requests on the state will act as that entity. This |
545 | // method is usually called automatically by Open. The machine nonce |
546 | // should be empty unless logging in as a machine agent. |
547 | -func (st *State) Login(tag, password, nonce string) error { |
548 | +func (st *State) Login(tag, password, nonce, environTag string) error { |
549 | var result params.LoginResult |
550 | err := st.Call("Admin", "", "Login", ¶ms.Creds{ |
551 | - AuthTag: tag, |
552 | - Password: password, |
553 | - Nonce: nonce, |
554 | + AuthTag: tag, |
555 | + Password: password, |
556 | + Nonce: nonce, |
557 | + EnvironTag: environTag, |
558 | }, &result) |
559 | if err == nil { |
560 | st.authTag = tag |
561 | @@ -42,6 +43,7 @@ |
562 | return err |
563 | } |
564 | st.hostPorts = hostPorts |
565 | + st.environTag = result.EnvironTag |
566 | } |
567 | return err |
568 | } |
569 | |
570 | === modified file 'state/api/state_test.go' |
571 | --- state/api/state_test.go 2014-05-20 04:27:02 +0000 |
572 | +++ state/api/state_test.go 2014-05-27 09:53:17 +0000 |
573 | @@ -11,6 +11,7 @@ |
574 | "launchpad.net/juju-core/instance" |
575 | jujutesting "launchpad.net/juju-core/juju/testing" |
576 | "launchpad.net/juju-core/state/api" |
577 | + "launchpad.net/juju-core/state/api/params" |
578 | coretesting "launchpad.net/juju-core/testing" |
579 | ) |
580 | |
581 | @@ -54,10 +55,56 @@ |
582 | s.State.SetAPIHostPorts([][]instance.HostPort{badServer}) |
583 | apistate, err := api.Open(info, api.DialOpts{}) |
584 | c.Assert(err, gc.IsNil) |
585 | + defer apistate.Close() |
586 | hostports := apistate.APIHostPorts() |
587 | c.Check(hostports, gc.DeepEquals, [][]instance.HostPort{serverhostports, badServer}) |
588 | } |
589 | |
590 | +func (s *stateSuite) TestLoginSetsEnvironTag(c *gc.C) { |
591 | + env, err := s.State.Environment() |
592 | + c.Assert(err, gc.IsNil) |
593 | + info := s.APIInfo(c) |
594 | + tag := info.Tag |
595 | + password := info.Password |
596 | + info.Tag = "" |
597 | + info.Password = "" |
598 | + apistate, err := api.Open(info, api.DialOpts{}) |
599 | + c.Assert(err, gc.IsNil) |
600 | + defer apistate.Close() |
601 | + // We haven't called Login yet, so the EnvironTag shouldn't be set. |
602 | + c.Check(apistate.EnvironTag(), gc.Equals, "") |
603 | + err = apistate.Login(tag, password, "", "") |
604 | + c.Assert(err, gc.IsNil) |
605 | + // Now that we've logged in, EnvironTag should be updated correctly. |
606 | + c.Check(apistate.EnvironTag(), gc.Equals, env.Tag()) |
607 | +} |
608 | + |
609 | +func (s *stateSuite) TestLoginPassesEnvironTag(c *gc.C) { |
610 | + env, err := s.State.Environment() |
611 | + c.Assert(err, gc.IsNil) |
612 | + info := s.APIInfo(c) |
613 | + tag := info.Tag |
614 | + password := info.Password |
615 | + info.Tag = "" |
616 | + info.Password = "" |
617 | + apistate, err := api.Open(info, api.DialOpts{}) |
618 | + c.Assert(err, gc.IsNil) |
619 | + defer apistate.Close() |
620 | + // Not set yet. |
621 | + c.Check(apistate.EnvironTag(), gc.Equals, "") |
622 | + // We intentionally pass in an invalid environ tag, and notice that |
623 | + // Login is refused. We then pass in the right tag, to make sure we |
624 | + // have the right information |
625 | + err = apistate.Login(tag, password, "", "environment-not-uuid") |
626 | + c.Check(err, gc.ErrorMatches, "invalid environment requested") |
627 | + c.Check(params.ErrCode(err), gc.Equals, params.CodeUnauthorized) |
628 | + c.Check(apistate.EnvironTag(), gc.Equals, "") |
629 | + err = apistate.Login(tag, password, "", env.Tag()) |
630 | + c.Assert(err, gc.IsNil) |
631 | + // Now that we've logged in, EnvironTag should be updated correctly. |
632 | + c.Check(apistate.EnvironTag(), gc.Equals, env.Tag()) |
633 | +} |
634 | + |
635 | func (s *stateSuite) TestAPIHostPortsMovesConnectedValueFirst(c *gc.C) { |
636 | hostportslist := s.APIState.APIHostPorts() |
637 | c.Check(hostportslist, gc.HasLen, 1) |
638 | @@ -91,6 +138,7 @@ |
639 | s.State.SetAPIHostPorts(current) |
640 | apistate, err := api.Open(info, api.DialOpts{}) |
641 | c.Assert(err, gc.IsNil) |
642 | + defer apistate.Close() |
643 | hostports := apistate.APIHostPorts() |
644 | // We should have rotate the server we connected to as the first item, |
645 | // and the address of that server as the first address |
646 | |
647 | === modified file 'state/apiserver/admin.go' |
648 | --- state/apiserver/admin.go 2014-05-20 08:02:01 +0000 |
649 | +++ state/apiserver/admin.go 2014-05-27 09:53:17 +0000 |
650 | @@ -105,8 +105,16 @@ |
651 | } |
652 | logger.Debugf("hostPorts: %v", hostPorts) |
653 | |
654 | + environ, err := a.root.srv.state.Environment() |
655 | + if err != nil { |
656 | + return params.LoginResult{}, err |
657 | + } |
658 | + |
659 | a.root.rpcConn.Serve(newRoot, serverError) |
660 | - return params.LoginResult{hostPorts}, nil |
661 | + return params.LoginResult{ |
662 | + Servers: hostPorts, |
663 | + EnvironTag: environ.Tag(), |
664 | + }, nil |
665 | } |
666 | |
667 | var doCheckCreds = checkCreds |
668 | @@ -127,6 +135,18 @@ |
669 | if err != nil || !entity.PasswordValid(c.Password) { |
670 | return nil, common.ErrBadCreds |
671 | } |
672 | + if c.EnvironTag != "" { |
673 | + env, err := st.Environment() |
674 | + if err != nil { |
675 | + // TODO: There is no test for this case, how do we even |
676 | + // trigger an error for the Environment() call? |
677 | + logger.Warningf("got error asking for state.Environment(): %v", err) |
678 | + return nil, err |
679 | + } |
680 | + if env.Tag() != c.EnvironTag { |
681 | + return nil, common.ErrInvalidEnviron |
682 | + } |
683 | + } |
684 | // Check if a machine agent is logging in with the right Nonce |
685 | if err := checkForValidMachineAgent(entity, c); err != nil { |
686 | return nil, err |
687 | |
688 | === modified file 'state/apiserver/common/errors.go' |
689 | --- state/apiserver/common/errors.go 2014-05-13 04:30:48 +0000 |
690 | +++ state/apiserver/common/errors.go 2014-05-27 09:53:17 +0000 |
691 | @@ -47,6 +47,7 @@ |
692 | var ( |
693 | ErrBadId = stderrors.New("id not found") |
694 | ErrBadCreds = stderrors.New("invalid entity name or password") |
695 | + ErrInvalidEnviron = stderrors.New("invalid environment requested") |
696 | ErrPerm = stderrors.New("permission denied") |
697 | ErrNotLoggedIn = stderrors.New("not logged in") |
698 | ErrUnknownWatcher = stderrors.New("unknown watcher id") |
699 | @@ -63,6 +64,7 @@ |
700 | state.ErrUnitHasSubordinates: params.CodeUnitHasSubordinates, |
701 | ErrBadId: params.CodeNotFound, |
702 | ErrBadCreds: params.CodeUnauthorized, |
703 | + ErrInvalidEnviron: params.CodeUnauthorized, |
704 | ErrPerm: params.CodeUnauthorized, |
705 | ErrNotLoggedIn: params.CodeUnauthorized, |
706 | ErrUnknownWatcher: params.CodeNotFound, |
707 | |
708 | === modified file 'state/apiserver/login_test.go' |
709 | --- state/apiserver/login_test.go 2014-04-17 13:14:49 +0000 |
710 | +++ state/apiserver/login_test.go 2014-05-27 09:53:17 +0000 |
711 | @@ -33,6 +33,7 @@ |
712 | tag string |
713 | password string |
714 | err string |
715 | + envtag string |
716 | code string |
717 | }{{ |
718 | tag: "user-admin", |
719 | @@ -48,6 +49,12 @@ |
720 | tag: "bar", |
721 | password: "password", |
722 | err: `"bar" is not a valid tag`, |
723 | +}, { |
724 | + tag: "user-admin", |
725 | + password: "dummy-secret", |
726 | + envtag: "environment-bad-uuid", |
727 | + err: "invalid environment requested", |
728 | + code: params.CodeUnauthorized, |
729 | }} |
730 | |
731 | func (s *loginSuite) setupServer(c *gc.C) (*api.Info, func()) { |
732 | @@ -59,11 +66,14 @@ |
733 | "", "", |
734 | ) |
735 | c.Assert(err, gc.IsNil) |
736 | + env, err := s.State.Environment() |
737 | + c.Assert(err, gc.IsNil) |
738 | info := &api.Info{ |
739 | - Tag: "", |
740 | - Password: "", |
741 | - Addrs: []string{srv.Addr()}, |
742 | - CACert: coretesting.CACert, |
743 | + Tag: "", |
744 | + Password: "", |
745 | + EnvironTag: env.Tag(), |
746 | + Addrs: []string{srv.Addr()}, |
747 | + CACert: coretesting.CACert, |
748 | } |
749 | return info, func() { |
750 | err := srv.Stop() |
751 | @@ -111,7 +121,7 @@ |
752 | c.Assert(err, gc.ErrorMatches, `unknown object type "Machiner"`) |
753 | |
754 | // Since these are user login tests, the nonce is empty. |
755 | - err = st.Login(t.tag, t.password, "") |
756 | + err = st.Login(t.tag, t.password, "", t.envtag) |
757 | c.Assert(err, gc.ErrorMatches, t.err) |
758 | c.Assert(params.ErrCode(err), gc.Equals, t.code) |
759 | |
760 | @@ -139,7 +149,7 @@ |
761 | c.Assert(err, gc.ErrorMatches, `unknown object type "Client"`) |
762 | |
763 | // Since these are user login tests, the nonce is empty. |
764 | - err = st.Login("user-inactive", "password", "") |
765 | + err = st.Login("user-inactive", "password", "", info.EnvironTag) |
766 | c.Assert(err, gc.ErrorMatches, "invalid entity name or password") |
767 | |
768 | _, err = st.Client().Status([]string{}) |
769 | @@ -177,12 +187,12 @@ |
770 | |
771 | c.Assert(tw.Log, jc.LogMatches, []string{ |
772 | `<- \[[0-9A-F]+\] <unknown> {"RequestId":1,"Type":"Admin","Request":"Login","Params":` + |
773 | - `{"AuthTag":"machine-0","Password":"[^"]*","Nonce":"fake_nonce"}` + |
774 | + `{"AuthTag":"machine-0","Password":"[^"]*","Nonce":"fake_nonce","EnvironTag":"environ[^"]*"}` + |
775 | `}`, |
776 | // Now that we are logged in, we see the entity's tag |
777 | // [0-9.umns] is to handle timestamps that are ns, us, ms, or s |
778 | // long, though we expect it to be in the 'ms' range. |
779 | - `-> \[[0-9A-F]+\] machine-0 [0-9.]+[umn]?s {"RequestId":1,"Response":{"Servers":\[\]}} Admin\[""\].Login`, |
780 | + `-> \[[0-9A-F]+\] machine-0 [0-9.]+[umn]?s {"RequestId":1,"Response":.*} Admin\[""\].Login`, |
781 | `<- \[[0-9A-F]+\] machine-0 {"RequestId":2,"Type":"Machiner","Request":"Life","Params":{"Entities":\[{"Tag":"machine-0"}\]}}`, |
782 | `-> \[[0-9A-F]+\] machine-0 [0-9.umns]+ {"RequestId":2,"Response":{"Results":\[{"Life":"alive","Error":null}\]}} Machiner\[""\]\.Life`, |
783 | }) |
784 | @@ -461,3 +471,67 @@ |
785 | c.Check(err, gc.IsNil) |
786 | } |
787 | } |
788 | + |
789 | +func (s *loginSuite) TestLoginReportsEnvironTag(c *gc.C) { |
790 | + info, cleanup := s.setupServer(c) |
791 | + defer cleanup() |
792 | + // If we call api.Open without giving a username and password, then it |
793 | + // won't call Login, so we can call it ourselves. |
794 | + // We Login without passing an EnvironTag, to show that it still lets |
795 | + // us in, and that we can find out the real EnvironTag from the |
796 | + // response. |
797 | + st, err := api.Open(info, fastDialOpts) |
798 | + c.Assert(err, gc.IsNil) |
799 | + var result params.LoginResult |
800 | + creds := ¶ms.Creds{ |
801 | + AuthTag: "user-admin", |
802 | + Password: "dummy-secret", |
803 | + EnvironTag: "", |
804 | + } |
805 | + err = st.Call("Admin", "", "Login", creds, &result) |
806 | + c.Assert(err, gc.IsNil) |
807 | + env, err := s.State.Environment() |
808 | + c.Assert(err, gc.IsNil) |
809 | + c.Assert(result.EnvironTag, gc.Equals, env.Tag()) |
810 | +} |
811 | + |
812 | +func (s *loginSuite) TestLoginAcceptsEnvironTag(c *gc.C) { |
813 | + info, cleanup := s.setupServer(c) |
814 | + defer cleanup() |
815 | + // We open the API without logging in, and then do a direct login |
816 | + // passing in the EnvironTag. The server should let us in, and we |
817 | + // should be able to check from the response that we are, indeed, in |
818 | + // the right environment. |
819 | + st, err := api.Open(info, fastDialOpts) |
820 | + c.Assert(err, gc.IsNil) |
821 | + env, err := s.State.Environment() |
822 | + c.Assert(err, gc.IsNil) |
823 | + var result params.LoginResult |
824 | + creds := ¶ms.Creds{ |
825 | + AuthTag: "user-admin", |
826 | + Password: "dummy-secret", |
827 | + EnvironTag: env.Tag(), |
828 | + } |
829 | + err = st.Call("Admin", "", "Login", creds, &result) |
830 | + c.Assert(err, gc.IsNil) |
831 | + c.Assert(result.EnvironTag, gc.Equals, env.Tag()) |
832 | +} |
833 | + |
834 | +func (s *loginSuite) TestLoginRefusesBadEnvironTag(c *gc.C) { |
835 | + info, cleanup := s.setupServer(c) |
836 | + defer cleanup() |
837 | + // This time, we pass in an environment tag that doesn't actually match |
838 | + // the environment. The login should be rejected with an appropriate |
839 | + // error response. |
840 | + st, err := api.Open(info, fastDialOpts) |
841 | + c.Assert(err, gc.IsNil) |
842 | + var result params.LoginResult |
843 | + creds := ¶ms.Creds{ |
844 | + AuthTag: "user-admin", |
845 | + Password: "dummy-secret", |
846 | + EnvironTag: "environment-not-my-uuid", |
847 | + } |
848 | + err = st.Call("Admin", "", "Login", creds, &result) |
849 | + c.Assert(err, gc.ErrorMatches, "invalid environment requested") |
850 | + c.Assert(params.ErrCode(err), gc.Equals, params.CodeUnauthorized) |
851 | +} |
Reviewers: mp+221021_ code.launchpad. net,
Message:
Please take a look.
Description:
various: change API.Login() to handle EnvironTag
This changes a bit of Login handling to allow us to pass in an
EnvironTag which will get validated against the running environment. We
also return EnvironTag as part of the Login response, since otherwise
the existing client doesn't have a good way of finding out that
information.
It then goes around and updates a bunch of places so that we can store
the EnvironTag in our .jenv and read it back out (and pass it in).
I went with a new error message (invalid environment requested), rather
than just ErrPerm, partially because I have already validated the Login
credentials. (Since one user is intended to have access to multiple
environments, it made sense to layer it that way). It does still return
the CodeUnauthorized.
It is still valid to pass in an EnvironTag of "". Eventually we may
change how Login works under that circumstance (for Multi Environment
support), but for now this gets us closer to where we want to be.
If we get to the point that Bootstrap actually stashes our api
information it could also stash the environment tag. However, in the
short term if it just connects to the API like everything else, it will
get the environment UUID cached, which will let us be a bit more
protected from accidentally connecting to the wrong environment.
This might have implications for CI, as I believe they try to re-use
things like CACerts so that they can use the same .jenv files across
mulitple bootstraps. If so, this will start causing the client to get
rejected because the actual environment has changed. Though I think that
is the behavior that we decided we wanted.
https:/ /code.launchpad .net/~jameinel/ juju-core/ login-returns- env-tag/ +merge/ 221021
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/101760046/
Affected files (+387, -40 lines): configstore/ disk.go configstore/ interface. go configstore/ interface_ test.go test.go apiclient. go apiclient_ test.go params/ params. go state_test. go /admin. go /common/ errors. go /login_ test.go
A [revision details]
M environs/
M environs/
M environs/
M juju/api.go
M juju/apiconn_
M juju/mock_test.go
M state/api/
M state/api/
M state/api/
M state/api/state.go
M state/api/
M state/apiserver
M state/apiserver
M state/apiserver