Skip to content

Commit

Permalink
WD-11707 - feat: OIDC ws login (#1776)
Browse files Browse the repository at this point in the history
* feat: log in to controllers and models using OIDC
  • Loading branch information
huwshimi authored Jun 17, 2024
1 parent 9787efd commit 7132462
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 30 deletions.
11 changes: 10 additions & 1 deletion HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,16 @@ You can now configure your local dashboard by setting the endpoint in
controllerAPIEndpoint: "wss://[container.ip]:17070/api",
```

You may also need to configure your dashboard to work with a local controller:
When deployed by a charm the controller relation will provide the value for
`identityProviderURL`. The actual value isn't used by the dashboard at this
time, but rather the existence of the value informs the dashboard that Candid is
available, so in `config.local.js` you just need to set the URL to any truthy value:

```shell
identityProviderURL: "/candid",
```

You also need to configure your dashboard to work with a local controller:

```shell
isJuju: true,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
]
},
"dependencies": {
"@canonical/jujulib": "6.0.0",
"@canonical/jujulib": "7.0.0",
"@canonical/macaroon-bakery": "1.3.2",
"@canonical/react-components": "0.52.0",
"@reduxjs/toolkit": "2.2.4",
Expand Down
41 changes: 33 additions & 8 deletions src/juju/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,33 @@ describe("Juju API", () => {
},
AuthMethod.CANDID,
);
expect(juju.login).toHaveBeenCalledWith({}, CLIENT_VERSION);
expect(juju.login).toHaveBeenCalledWith(undefined, CLIENT_VERSION);
});

it("connects and logs in when using OIDC", async () => {
const conn = { facades: {}, info: {} };
const juju = {
login: vi.fn().mockReturnValue(conn),
} as unknown as Client;
const connectSpy = vi
.spyOn(jujuLib, "connect")
.mockImplementation(async () => juju);
const response = await loginWithBakery(
"wss://example.com/api",
undefined,
AuthMethod.OIDC,
);
expect(response).toStrictEqual({
conn,
juju,
// This would be a number, but we're using mocked timers.
intervalId: expect.any(Object),
});
expect(connectSpy).toHaveBeenCalledWith(
"wss://example.com/api",
expect.objectContaining({ oidcEnabled: true }),
);
expect(juju.login).toHaveBeenCalledWith(undefined, CLIENT_VERSION);
});

it("handles login errors", async () => {
Expand Down Expand Up @@ -243,7 +269,7 @@ describe("Juju API", () => {
user: "eggman",
password: "123",
},
generateConnectionOptions(false),
generateConnectionOptions(AuthMethod.LOCAL, false),
AuthMethod.LOCAL,
);
expect(response).toStrictEqual(juju);
Expand All @@ -262,7 +288,7 @@ describe("Juju API", () => {
user: "eggman",
password: "123",
},
generateConnectionOptions(false),
generateConnectionOptions(AuthMethod.LOCAL, false),
AuthMethod.LOCAL,
);
vi.advanceTimersByTime(LOGIN_TIMEOUT);
Expand All @@ -283,7 +309,7 @@ describe("Juju API", () => {
user: "eggman",
password: "123",
},
generateConnectionOptions(false),
generateConnectionOptions(AuthMethod.LOCAL, false),
AuthMethod.LOCAL,
);
await expect(response).rejects.toMatchObject(new Error("Uh oh!"));
Expand Down Expand Up @@ -336,9 +362,8 @@ describe("Juju API", () => {
await fetchModelStatus("abc123", "wss://example.com/api", () => state);
expect(connectAndLoginSpy).toHaveBeenCalledWith(
expect.any(String),
// An empty object is passed when using an external provider.
{},
expect.any(Object),
undefined,
CLIENT_VERSION,
);
});
Expand Down Expand Up @@ -1295,11 +1320,11 @@ describe("Juju API", () => {
const response = await connectAndLoginToModel("abc123", state);
expect(connectAndLogin).toHaveBeenCalledWith(
"wss://example.com/model/abc123/api",
expect.any(Object),
{
username: credentials.user,
password: credentials.password,
},
expect.any(Object),
CLIENT_VERSION,
);
expect(response).toMatchObject(conn);
Expand All @@ -1326,11 +1351,11 @@ describe("Juju API", () => {
);
expect(connectAndLogin).toHaveBeenCalledWith(
"wss://example.com/model/abc123/api",
expect.any(Object),
{
username: credentials.user,
password: credentials.password,
},
expect.any(Object),
CLIENT_VERSION,
);
expect(response).toMatchObject(conn);
Expand Down
22 changes: 10 additions & 12 deletions src/juju/api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import type {
ConnectOptions,
Credentials,
Client as JujuClient,
} from "@canonical/jujulib";
import type { ConnectOptions, Client as JujuClient } from "@canonical/jujulib";
import { connect, connectAndLogin } from "@canonical/jujulib";
import Action from "@canonical/jujulib/dist/api/facades/action";
import AllWatcher from "@canonical/jujulib/dist/api/facades/all-watcher";
Expand Down Expand Up @@ -80,6 +76,7 @@ export const CLIENT_VERSION = "3.0.0";
@returns The configuration options.
*/
export function generateConnectionOptions(
authMethod?: AuthMethod,
usePinger = false,
onClose: ConnectOptions["closeCallback"] = () => null,
) {
Expand All @@ -105,6 +102,7 @@ export function generateConnectionOptions(
closeCallback: onClose,
debug: false,
facades,
oidcEnabled: authMethod === AuthMethod.OIDC,
wsclass: WebSocket,
};
}
Expand All @@ -113,14 +111,12 @@ function determineLoginParams(
credentials: Credential | null | undefined,
authMethod?: AuthMethod,
) {
let loginParams: Credentials = {};
if (credentials && authMethod === AuthMethod.LOCAL) {
loginParams = {
return {
username: credentials.user,
password: credentials.password,
};
}
return loginParams;
}

function startPingerLoop(conn: ConnectionWithFacades) {
Expand Down Expand Up @@ -160,7 +156,9 @@ export async function loginWithBakery(
) {
const juju: JujuClient = await connect(
wsControllerURL,
generateConnectionOptions(true, (e) => console.log("controller closed", e)),
generateConnectionOptions(authMethod, true, (e) =>
console.log("controller closed", e),
),
);
const loginParams = determineLoginParams(credentials, authMethod);
let conn: ConnectionWithFacades | null | undefined = null;
Expand Down Expand Up @@ -200,8 +198,8 @@ export async function connectAndLoginWithTimeout(
const loginParams = determineLoginParams(credentials, authMethod);
const juju: Promise<LoginResponse> = connectAndLogin(
modelURL,
loginParams,
options,
loginParams,
CLIENT_VERSION,
);
return Promise.race([timeout, juju]);
Expand Down Expand Up @@ -233,7 +231,7 @@ export async function fetchModelStatus(
const { conn, logout } = await connectAndLoginWithTimeout(
modelURL,
controllerCredentials,
generateConnectionOptions(false),
generateConnectionOptions(config?.authMethod, false),
config?.authMethod,
);
if (isLoggedIn(getState(), wsControllerURL)) {
Expand Down Expand Up @@ -527,7 +525,7 @@ export async function connectToModel(
const response = await connectAndLoginWithTimeout(
modelURL,
credentials,
generateConnectionOptions(true),
generateConnectionOptions(authMethod, true),
authMethod,
);
return response.conn;
Expand Down
6 changes: 3 additions & 3 deletions src/juju/jimm/JIMMV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class JIMMV3 {
const req = {
type: "JIMM",
request: "DisableControllerUUIDMasking",
version: 3,
version: this.version,
params: {},
};
this._transport.write(req, resolve, reject);
Expand All @@ -86,7 +86,7 @@ class JIMMV3 {
const req = {
type: "JIMM",
request: "FindAuditEvents",
version: 3,
version: this.version,
params: params,
};
this._transport.write(req, resolve, reject);
Expand All @@ -98,7 +98,7 @@ class JIMMV3 {
const req = {
type: "JIMM",
request: "ListControllers",
version: 3,
version: this.version,
params: {},
};
this._transport.write(req, resolve, reject);
Expand Down
45 changes: 45 additions & 0 deletions src/juju/jimm/JIMMV4.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,49 @@ describe("JIMMV4", () => {
expect.any(Function),
);
});

it("disableControllerUUIDMasking", async () => {
const jimm = new JIMMV4(transport, connectionInfo);
void jimm.disableControllerUUIDMasking();
expect(transport.write).toHaveBeenCalledWith(
{
type: "JIMM",
request: "DisableControllerUUIDMasking",
version: 4,
params: {},
},
expect.any(Function),
expect.any(Function),
);
});

it("findAuditEvents", async () => {
const jimm = new JIMMV4(transport, connectionInfo);
void jimm.findAuditEvents({ "user-tag": "user-eggman@external" });
expect(transport.write).toHaveBeenCalledWith(
{
type: "JIMM",
request: "FindAuditEvents",
version: 4,
params: { "user-tag": "user-eggman@external" },
},
expect.any(Function),
expect.any(Function),
);
});

it("listControllers", async () => {
const jimm = new JIMMV4(transport, connectionInfo);
void jimm.listControllers();
expect(transport.write).toHaveBeenCalledWith(
{
type: "JIMM",
request: "ListControllers",
version: 4,
params: {},
},
expect.any(Function),
expect.any(Function),
);
});
});
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -625,15 +625,15 @@ __metadata:
languageName: node
linkType: hard

"@canonical/jujulib@npm:6.0.0":
version: 6.0.0
resolution: "@canonical/jujulib@npm:6.0.0"
"@canonical/jujulib@npm:7.0.0":
version: 7.0.0
resolution: "@canonical/jujulib@npm:7.0.0"
dependencies:
"@canonical/macaroon-bakery": "npm:1.3.2"
btoa: "npm:1.2.1"
websocket: "npm:1.0.34"
xhr2: "npm:0.2.1"
checksum: 10c0/3d25504d136fc5e103e3547b252b7ac51ce1df4b9d9afac3b1c2df7233fad1903e2b46b82e6d2a02d730d6ef4f195e82f5581140aacc06dc287450951e9f129a
checksum: 10c0/1ddfb5338ea6b4446ce3dd64b39a1d53bd5a736035f2478193879510c623f76faa225e6971b759ad5c9dc7e1412dc5f74aea0b16ef2a03869d1a48a4198c644d
languageName: node
linkType: hard

Expand Down Expand Up @@ -7513,7 +7513,7 @@ __metadata:
resolution: "juju-dashboard@workspace:."
dependencies:
"@babel/plugin-proposal-private-property-in-object": "npm:7.21.11"
"@canonical/jujulib": "npm:6.0.0"
"@canonical/jujulib": "npm:7.0.0"
"@canonical/macaroon-bakery": "npm:1.3.2"
"@canonical/react-components": "npm:0.52.0"
"@reduxjs/toolkit": "npm:2.2.4"
Expand Down

0 comments on commit 7132462

Please sign in to comment.