-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add keyManager handler to model proxy
The model proxy will intercept calls to the keyManager facade and persist user keys in JIMM rather than passing these calls along to the Juju controller. This is being done in order to support the SSH proxy efforts. Ideally, in Juju 4 these methods would be done on the controller api and all this logic would move into the jujuapi package.
- Loading branch information
Showing
8 changed files
with
524 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,7 @@ | |
|
||
package rpcproxy | ||
|
||
type Message = message | ||
type ( | ||
Message = message | ||
KeyManagerFacade = keyManagerFacade | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,11 +10,15 @@ import ( | |
|
||
qt "github.com/frankban/quicktest" | ||
"github.com/gorilla/websocket" | ||
jujuparams "github.com/juju/juju/rpc/params" | ||
"github.com/juju/names/v5" | ||
"github.com/juju/utils/v3/ssh" | ||
|
||
"github.com/canonical/jimm/v3/internal/dbmodel" | ||
"github.com/canonical/jimm/v3/internal/jimm/sshkeys" | ||
"github.com/canonical/jimm/v3/internal/openfga" | ||
"github.com/canonical/jimm/v3/internal/rpcproxy" | ||
"github.com/canonical/jimm/v3/internal/testutils/jimmtest/mocks" | ||
"github.com/canonical/jimm/v3/internal/testutils/rpctest" | ||
) | ||
|
||
|
@@ -58,6 +62,7 @@ func TestProxySockets(t *testing.T) { | |
ConnectController: f, | ||
AuditLog: auditLogger, | ||
LoginService: &mockLoginService{}, | ||
SSHKeyManager: &mocks.SSHKeyManager{}, | ||
} | ||
err := rpcproxy.ProxySockets(ctx, proxyHelpers) | ||
c.Check(err, qt.IsNil) | ||
|
@@ -117,6 +122,7 @@ func TestProxySocketsControllerConnectionFails(t *testing.T) { | |
ConnectController: f, | ||
AuditLog: auditLogger, | ||
LoginService: &mockLoginService{}, | ||
SSHKeyManager: &mocks.SSHKeyManager{}, | ||
} | ||
err := rpcproxy.ProxySockets(ctx, proxyHelpers) | ||
c.Check(err, qt.IsNil) | ||
|
@@ -176,6 +182,7 @@ func TestCancelProxySockets(t *testing.T) { | |
ConnectController: f, | ||
AuditLog: auditLogger, | ||
LoginService: &mockLoginService{}, | ||
SSHKeyManager: &mocks.SSHKeyManager{}, | ||
} | ||
err := rpcproxy.ProxySockets(ctx, proxyHelpers) | ||
c.Check(err, qt.ErrorMatches, "Context cancelled") | ||
|
@@ -217,6 +224,7 @@ func TestProxySocketsAuditLogs(t *testing.T) { | |
ConnectController: f, | ||
AuditLog: auditLogger, | ||
LoginService: &mockLoginService{}, | ||
SSHKeyManager: &mocks.SSHKeyManager{}, | ||
} | ||
err := rpcproxy.ProxySockets(ctx, proxyHelpers) | ||
c.Check(err, qt.IsNil) | ||
|
@@ -272,3 +280,139 @@ func TestProxySocketsAuditLogs(t *testing.T) { | |
c.Assert(auditLogs, qt.DeepEquals, expectedEvents) | ||
|
||
} | ||
|
||
func TestProxySocketsSSHKeys(t *testing.T) { | ||
c := qt.New(t) | ||
|
||
ctx := context.Background() | ||
sshFacadeChan := make(chan (string), 1) | ||
|
||
srvController := rpctest.NewServer(rpctest.Echo) | ||
|
||
errChan := make(chan error) | ||
srvJIMM := rpctest.NewServer(func(connClient *websocket.Conn) error { | ||
defer connClient.Close() | ||
testTokenGen := testTokenGenerator{} | ||
connectControllerF := func(context.Context) (rpcproxy.WebsocketConnectionWithMetadata, error) { | ||
connController := srvController.Dialer.DialWebsocket(c, srvController.URL) | ||
return rpcproxy.WebsocketConnectionWithMetadata{ | ||
Conn: connController, | ||
ModelName: "TestModelName", | ||
}, nil | ||
} | ||
proxyHelpers := rpcproxy.ProxyHelpers{ | ||
ConnClient: connClient, | ||
TokenGen: &testTokenGen, | ||
ConnectController: connectControllerF, | ||
AuditLog: func(ale *dbmodel.AuditLogEntry) {}, | ||
LoginService: &mockLoginService{ | ||
email: "[email protected]", | ||
}, | ||
SSHKeyManager: &mocks.SSHKeyManager{ | ||
AddUserPublicKey_: func(ctx context.Context, user *openfga.User, publicKey sshkeys.PublicKey) error { | ||
sshFacadeChan <- "add-keys" | ||
return nil | ||
}, | ||
ListUserPublicKeys_: func(ctx context.Context, user *openfga.User) ([]sshkeys.PublicKey, error) { | ||
sshFacadeChan <- "list-keys" | ||
return nil, nil | ||
}, | ||
RemoveUserKeyByComment_: func(ctx context.Context, user *openfga.User, comment string) error { | ||
sshFacadeChan <- "remove-keys-comment" | ||
return nil | ||
}, | ||
RemoveUserKeyByFingerprint_: func(ctx context.Context, user *openfga.User, fingerprint string) error { | ||
sshFacadeChan <- "remove-keys-fingerprint" | ||
return nil | ||
}, | ||
}, | ||
} | ||
err := rpcproxy.ProxySockets(ctx, proxyHelpers) | ||
c.Check(err, qt.IsNil) | ||
errChan <- err | ||
return err | ||
}) | ||
|
||
defer srvController.Close() | ||
defer srvJIMM.Close() | ||
ws := srvJIMM.Dialer.DialWebsocket(c, srvJIMM.URL) | ||
defer ws.Close() | ||
|
||
// Perform login | ||
p := json.RawMessage(`{"Key":"TestVal"}`) | ||
msg := rpcproxy.Message{RequestID: 1, Type: "Admin", Request: "LoginWithSessionToken", Params: p} // #nosec G115 accept integer conversion | ||
err := ws.WriteJSON(&msg) | ||
c.Assert(err, qt.IsNil) | ||
resp := rpcproxy.Message{} | ||
err = ws.ReadJSON(&resp) | ||
c.Assert(err, qt.IsNil) | ||
c.Assert(resp.Error, qt.Equals, "") | ||
|
||
// Run sub-tests for all SSH Key methods | ||
tests := []struct { | ||
name string | ||
request string | ||
params []byte | ||
expectedChanResult string | ||
expectedErr string | ||
}{ | ||
{ | ||
name: "Add key method", | ||
request: "AddKeys", | ||
expectedChanResult: "add-keys", | ||
params: mustMarshal(jujuparams.ModifyUserSSHKeys{Keys: []string{"type key comment"}}), | ||
}, | ||
{ | ||
name: "List keys method", | ||
request: "ListKeys", | ||
expectedChanResult: "list-keys", | ||
params: mustMarshal(jujuparams.ListSSHKeys{Mode: ssh.Fingerprints}), | ||
}, | ||
{ | ||
name: "Delete keys by comment", | ||
request: "DeleteKeys", | ||
expectedChanResult: "remove-keys-comment", | ||
params: mustMarshal(jujuparams.ModifyUserSSHKeys{Keys: []string{"comment"}}), | ||
}, | ||
{ | ||
name: "Delete keys by fingerprint", | ||
request: "DeleteKeys", | ||
expectedChanResult: "remove-keys-fingerprint", | ||
params: mustMarshal(jujuparams.ModifyUserSSHKeys{Keys: []string{"79:fc:60:93:ec:ce:42:fe:15:61:f2:fb:d6:22:43:6e"}}), | ||
}, | ||
} | ||
|
||
for i, test := range tests { | ||
c.Run(test.name, func(c *qt.C) { | ||
msg := rpcproxy.Message{RequestID: uint64(i + 1), Type: "KeyManager", Request: test.request, Params: test.params} // #nosec G115 accept integer conversion | ||
err := ws.WriteJSON(&msg) | ||
c.Assert(err, qt.IsNil) | ||
|
||
resp := rpcproxy.Message{} | ||
err = ws.ReadJSON(&resp) | ||
c.Assert(err, qt.IsNil) | ||
if test.expectedErr == "" { | ||
c.Assert(resp.Error, qt.Equals, "") | ||
} else { | ||
c.Assert(err, qt.Matches, test.expectedErr) | ||
} | ||
|
||
select { | ||
case res := <-sshFacadeChan: | ||
c.Assert(res, qt.Equals, test.expectedChanResult) | ||
case <-time.After(100 * time.Millisecond): | ||
c.Error("Expected SSH method was not called") | ||
} | ||
}) | ||
} | ||
ws.Close() | ||
<-errChan // Ensure go routines are cleaned up | ||
} | ||
|
||
func mustMarshal(data any) []byte { | ||
out, err := json.Marshal(data) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return out | ||
} |
Oops, something went wrong.