Skip to content

Commit fec8c19

Browse files
committed
fix: allow federated auth to use stream connectors
1 parent d530790 commit fec8c19

11 files changed

+108
-23
lines changed

api/apiclient.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,15 @@ func Open(info *Info, opts DialOpts) (Connection, error) {
180180
// login because, when doing HTTP requests, we'll want
181181
// to use the same username and password for authenticating
182182
// those. If login fails, we discard the connection.
183-
tag: tagToString(info.Tag),
184-
password: info.Password,
185-
macaroons: info.Macaroons,
186-
nonce: info.Nonce,
187-
tlsConfig: dialResult.tlsConfig,
188-
bakeryClient: bakeryClient,
189-
modelTag: info.ModelTag,
190-
proxier: dialResult.proxier,
183+
tag: tagToString(info.Tag),
184+
password: info.Password,
185+
loginProvider: loginProvider,
186+
macaroons: info.Macaroons,
187+
nonce: info.Nonce,
188+
tlsConfig: dialResult.tlsConfig,
189+
bakeryClient: bakeryClient,
190+
modelTag: info.ModelTag,
191+
proxier: dialResult.proxier,
191192
}
192193
if !info.SkipLogin {
193194
if err := loginWithContext(dialCtx, st, loginProvider); err != nil {
@@ -367,8 +368,8 @@ func (st *state) connectStream(path string, attrs url.Values, extraHeaders http.
367368
TLSClientConfig: st.tlsConfig,
368369
}
369370
var requestHeader http.Header
370-
if st.tag != "" {
371-
requestHeader = jujuhttp.BasicAuthHeader(st.tag, st.password)
371+
if st.tag != "" || st.LoginToken() != "" {
372+
requestHeader = jujuhttp.BasicAuthHeader(st.tag, st.LoginToken())
372373
} else {
373374
requestHeader = make(http.Header)
374375
}

api/clientcredentialsloginprovider.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ type clientCredentialsLoginProvider struct {
3232
clientSecret string
3333
}
3434

35+
// Token implements LoginProvider.Token
36+
func (p *clientCredentialsLoginProvider) Token() string {
37+
return p.clientSecret
38+
}
39+
3540
// Login implements the LoginProvider.Login method.
3641
//
3742
// It authenticates as the entity using client credentials.

api/clientcredentialsloginprovider_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,16 @@ func (s *clientCredentialsLoginProviderProviderSuite) Test(c *gc.C) {
6666
return nil
6767
})
6868

69+
lp := api.NewClientCredentialsLoginProvider(clientID, clientSecret)
6970
apiState, err := api.Open(&api.Info{
7071
Addrs: info.Addrs,
7172
ControllerUUID: info.ControllerUUID,
7273
CACert: info.CACert,
7374
}, api.DialOpts{
74-
LoginProvider: api.NewClientCredentialsLoginProvider(clientID, clientSecret),
75+
LoginProvider: lp,
7576
})
7677
c.Assert(err, jc.ErrorIsNil)
78+
c.Assert(lp.Token(), gc.Equals, clientSecret)
7779

7880
defer func() { _ = apiState.Close() }()
7981
}

api/interface.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ func NewLoginResultParams(result params.LoginResult) (*LoginResultParams, error)
185185
type LoginProvider interface {
186186
// Login performs log in when connecting to the controller.
187187
Login(ctx context.Context, caller base.APICaller) (*LoginResultParams, error)
188+
// Token returns the string used for authentication. Some providers may only obtain
189+
// a token after login has completed.
190+
// This is normally used as part of basic authentication in scenarios where a client
191+
// makes use of a StreamConnector like when fetching logs using `juju debug-log`.
192+
Token() string
188193
}
189194

190195
// DialOpts holds configuration parameters that control the

api/sessiontokenloginprovider.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ type sessionTokenLoginProvider struct {
5454
updateAccountDetailsFunc func(string) error
5555
}
5656

57+
// Token implements the LoginProvider.Token method.
58+
func (p *sessionTokenLoginProvider) Token() string {
59+
return p.sessionToken
60+
}
61+
5762
// Login implements the LoginProvider.Login method.
5863
//
5964
// It authenticates as the entity using the specified session token.

api/sessiontokenloginprovider_test.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,24 +102,25 @@ func (s *sessionTokenLoginProviderProviderSuite) TestSessionTokenLogin(c *gc.C)
102102
})
103103

104104
var output bytes.Buffer
105+
lp := api.NewSessionTokenLoginProvider(
106+
"expired-token",
107+
&output,
108+
func(sessionToken string) error {
109+
obtainedSessionToken = sessionToken
110+
return nil
111+
})
105112
apiState, err := api.Open(&api.Info{
106113
Addrs: info.Addrs,
107114
ControllerUUID: info.ControllerUUID,
108115
CACert: info.CACert,
109116
}, api.DialOpts{
110-
LoginProvider: api.NewSessionTokenLoginProvider(
111-
"expired-token",
112-
&output,
113-
func(sessionToken string) error {
114-
obtainedSessionToken = sessionToken
115-
return nil
116-
},
117-
),
117+
LoginProvider: lp,
118118
})
119119
c.Assert(err, jc.ErrorIsNil)
120120

121121
c.Assert(output.String(), gc.Equals, "Please visit http://localhost:8080/test-verification and enter code 1234567 to log in.\n")
122122
c.Assert(obtainedSessionToken, gc.Equals, sessionToken)
123+
c.Assert(lp.Token(), gc.Equals, sessionToken)
123124
defer func() { _ = apiState.Close() }()
124125
}
125126

api/state.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ type state struct {
9898
macaroons []macaroon.Slice
9999
nonce string
100100

101+
// loginProvider holds the provider used for login.
102+
loginProvider LoginProvider
103+
101104
// serverRootAddress holds the cached API server address and port used
102105
// to login.
103106
serverRootAddress string
@@ -193,6 +196,12 @@ func (st *state) AuthTag() names.Tag {
193196
return st.authTag
194197
}
195198

199+
// LoginToken returns a token appropriate for basic auth as determined by the
200+
// underlying login provider.
201+
func (st *state) LoginToken() string {
202+
return st.loginProvider.Token()
203+
}
204+
196205
// ControllerAccess returns the access level of authorized user to the model.
197206
func (st *state) ControllerAccess() string {
198207
return st.controllerAccess

api/userpass_login_provider_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2024 Canonical Ltd.
2+
// Licensed under the AGPLv3, see LICENCE file for details.
3+
4+
package api_test
5+
6+
import (
7+
"github.com/juju/names/v5"
8+
jc "github.com/juju/testing/checkers"
9+
gc "gopkg.in/check.v1"
10+
11+
"github.com/juju/juju/api"
12+
jujutesting "github.com/juju/juju/juju/testing"
13+
)
14+
15+
type userPassLoginProviderSuite struct {
16+
jujutesting.JujuConnSuite
17+
}
18+
19+
var _ = gc.Suite(&userPassLoginProviderSuite{})
20+
21+
func (s *userPassLoginProviderSuite) Test(c *gc.C) {
22+
info := s.APIInfo(c)
23+
24+
username := names.NewUserTag("admin")
25+
password := jujutesting.AdminSecret
26+
27+
lp := api.NewUserpassLoginProvider(username, password, "", nil, nil, nil)
28+
apiState, err := api.Open(&api.Info{
29+
Addrs: info.Addrs,
30+
ControllerUUID: info.ControllerUUID,
31+
CACert: info.CACert,
32+
}, api.DialOpts{
33+
LoginProvider: lp,
34+
})
35+
c.Assert(err, jc.ErrorIsNil)
36+
defer apiState.Close()
37+
c.Assert(lp.Token(), gc.Equals, password)
38+
}

api/userpassloginprovider.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ type userpassLoginProvider struct {
5757
cookieURL *url.URL
5858
}
5959

60+
// Token implements the LoginProvider.Token method.
61+
func (p *userpassLoginProvider) Token() string {
62+
return p.password
63+
}
64+
6065
// Login implements the LoginProvider.Login method.
6166
//
6267
// It authenticates as the entity with the given name and password

cmd/internal/loginprovider/tryinorderloginprovider.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,14 @@ func NewTryInOrderLoginProvider(logger loggo.Logger, providers ...api.LoginProvi
2727
}
2828

2929
type tryInOrderLoginProviders struct {
30-
providers []api.LoginProvider
31-
logger loggo.Logger
30+
providers []api.LoginProvider
31+
logger loggo.Logger
32+
loginToken string
33+
}
34+
35+
// Token implements the LoginProvider.Token method.
36+
func (p *tryInOrderLoginProviders) Token() string {
37+
return p.loginToken
3238
}
3339

3440
// Login implements the LoginProvider.Login method.
@@ -40,6 +46,7 @@ func (p *tryInOrderLoginProviders) Login(ctx context.Context, caller base.APICal
4046
p.logger.Debugf("login error using provider %d - %s", i, err.Error())
4147
} else {
4248
p.logger.Debugf("successful login using provider %d", i)
49+
p.loginToken = provider.Token()
4350
return result, nil
4451
}
4552
lastError = err

cmd/internal/loginprovider/tryinorderloginprovider_test.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,27 @@ var _ = gc.Suite(&tryInOrderLoginProviderSuite{})
2323
func (s *tryInOrderLoginProviderSuite) Test(c *gc.C) {
2424
p1 := &mockLoginProvider{err: errors.New("provider 1 error")}
2525
p2 := &mockLoginProvider{err: errors.New("provider 2 error")}
26-
p3 := &mockLoginProvider{}
26+
p3 := &mockLoginProvider{token: "successful-login-token"}
2727

2828
logger := loggo.GetLogger("juju.cmd.loginprovider")
2929
lp := loginprovider.NewTryInOrderLoginProvider(logger, p1, p2)
3030
_, err := lp.Login(context.Background(), nil)
3131
c.Assert(err, gc.ErrorMatches, "provider 2 error")
3232

3333
lp = loginprovider.NewTryInOrderLoginProvider(logger, p1, p2, p3)
34+
c.Assert(lp.Token(), gc.Equals, "")
3435
_, err = lp.Login(context.Background(), nil)
3536
c.Assert(err, jc.ErrorIsNil)
37+
c.Assert(lp.Token(), gc.Equals, "successful-login-token")
3638
}
3739

3840
type mockLoginProvider struct {
39-
err error
41+
err error
42+
token string
43+
}
44+
45+
func (p *mockLoginProvider) Token() string {
46+
return p.token
4047
}
4148

4249
func (p *mockLoginProvider) Login(ctx context.Context, caller base.APICaller) (*api.LoginResultParams, error) {

0 commit comments

Comments
 (0)