Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve credential handling #432

Merged
merged 25 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fff1863
refresh credentials during fetch calls
mwilde345 Nov 22, 2024
15096a3
Merge branch 'v3' into no-preflight
mwilde345 Nov 22, 2024
7be933c
don't create dupe keys
mwilde345 Nov 22, 2024
b25d691
Update src/config/setup-test-container.mjs
mwilde345 Nov 22, 2024
f0cf18e
logger component
mwilde345 Nov 23, 2024
c8b5174
Merge branch 'v3' into no-preflight
mwilde345 Nov 23, 2024
2868551
Merge branch 'v3' into no-preflight
mwilde345 Nov 23, 2024
d1f0dc0
refactor credentials
mwilde345 Nov 26, 2024
4f1e31d
refactor credentials
mwilde345 Nov 26, 2024
1aa6db2
separate concerns of account and database creds into separate classes
mwilde345 Nov 26, 2024
9792a2b
revert create database changes
mwilde345 Nov 26, 2024
32e1a0f
get rid of authnz middleware
mwilde345 Nov 26, 2024
6c86634
further separate concerns
mwilde345 Nov 26, 2024
fa472a5
Merge branch 'v3' into no-preflight
mwilde345 Nov 26, 2024
7ac122f
fix directory for creds. add some env var options
mwilde345 Nov 26, 2024
8ab062a
fix lint
mwilde345 Nov 26, 2024
48ba039
skip busted test
mwilde345 Nov 26, 2024
b0c6dd9
unsafe accessor fix
mwilde345 Nov 26, 2024
dc96e44
try test fix
mwilde345 Nov 26, 2024
d6f9d3a
Merge branch 'v3' into no-preflight
mwilde345 Nov 26, 2024
4d9d837
Merge branch 'v3' into no-preflight
mwilde345 Nov 26, 2024
c35346e
fix tests
mwilde345 Nov 26, 2024
ff7cbf4
don't set a default role in the args. resolve it in credentials. mock…
mwilde345 Nov 27, 2024
ca34e38
readme and remove .fauna dir
mwilde345 Nov 27, 2024
28bfeff
don't use exit just throw an error
mwilde345 Nov 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import keyCommand from "./commands/key.mjs";
import loginCommand from "./commands/login.mjs";
import schemaCommand from "./commands/schema/schema.mjs";
import shellCommand from "./commands/shell.mjs";
import { authNZMiddleware } from "./lib/auth/authNZ.mjs";
import { buildCredentials } from "./lib/auth/credentials.mjs";
import { configParser } from "./lib/config/config.mjs";
import { checkForUpdates, fixPaths, logArgv } from "./lib/middleware.mjs";

Expand Down Expand Up @@ -104,7 +104,7 @@ function buildYargs(argvInput) {
.env("FAUNA")
.config("config", configParser)
.middleware([checkForUpdates, logArgv], true)
.middleware([fixPaths, authNZMiddleware], false)
.middleware([fixPaths, buildCredentials], false)
.command("eval", "evaluate a query", evalCommand)
.command("shell", "start an interactive shell", shellCommand)
.command("login", "login via website", loginCommand)
Expand Down Expand Up @@ -149,12 +149,7 @@ function buildYargs(argvInput) {
"components to emit diagnostic logs for; this takes precedence over the 'verbosity' flag",
type: "array",
default: [],
choices: ["fetch", "error", "config", "argv"],
},
// Whether authNZ middleware should run. Better way of doing this?
authRequired: {
hidden: true,
default: false,
choices: ["fetch", "error", "config", "argv", "creds"],
},
})
.wrap(yargsInstance.terminalWidth())
Expand Down
21 changes: 0 additions & 21 deletions src/commands/database/database.mjs
Original file line number Diff line number Diff line change
@@ -1,34 +1,13 @@
//@ts-check

import { container } from "../../cli.mjs";
import { commonQueryOptions } from "../../lib/command-helpers.mjs";
import createCommand from "./create.mjs";
import deleteCommand from "./delete.mjs";
import listCommand from "./list.mjs";

function validateArgs(argv) {
const logger = container.resolve("logger");

if (argv.secret && argv.database) {
// Providing a database and secret are conflicting options. If both are provided,
// it is not clear which one to use.
throw new Error(
"Cannot use both the '--secret' and '--database' options together. Please specify only one.",
);
} else if (argv.role && argv.secret) {
// The '--role' option is not supported when using a secret. Secrets have an
// implicit role.
logger.warn(
"The '--role' option is not supported when using a secret. It will be ignored.",
);
}
return true;
}

function buildDatabase(yargs) {
return yargs
.options(commonQueryOptions)
.check(validateArgs)
.command(listCommand)
.command(createCommand)
.command(deleteCommand)
Expand Down
35 changes: 29 additions & 6 deletions src/commands/database/list.mjs
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
//@ts-check

import { container } from "../../cli.mjs";
import { commonQueryOptions } from "../../lib/command-helpers.mjs";
import { FaunaAccountClient } from "../../lib/fauna-account-client.mjs";
import { performQuery } from "../eval.mjs";

async function listDatabases(argv) {
const profile = argv.profile;
const logger = container.resolve("logger");
const accountClient = container.resolve("accountClient");
const accountCreds = container.resolve("accountCreds");
const accountKey = accountCreds.get({ key: profile }).accountKey;
const databases = await accountClient.listDatabases(accountKey);

// query the account api
const accountClient = new FaunaAccountClient();
const databases = await accountClient.listDatabases();
logger.stdout(databases);

// query the fauna api
mwilde345 marked this conversation as resolved.
Show resolved Hide resolved
const dbClient = await container.resolve("getSimpleClient")(argv);
const result = await performQuery(dbClient, "Database.all()", undefined, {
...argv,
format: "json",
});
logger.stdout(result);

// see what credentials are being used
const credentials = container.resolve("credentials");
logger.debug(
`
Account Key: ${credentials.accountKeys.key}\n
Database Key: ${credentials.databaseKeys.key}`,
"creds",
);
}

function buildListCommand(yargs) {
return yargs.version(false).help("help", "show help");
return yargs
.options({
...commonQueryOptions,
mwilde345 marked this conversation as resolved.
Show resolved Hide resolved
})
.help("help", "show help");
}

export default {
Expand Down
1 change: 0 additions & 1 deletion src/commands/eval.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ export async function performV4Query(client, fqlQuery, outputFile, flags) {
export async function performQuery(client, fqlQuery, outputFile, argv) {
const performV4Query = container.resolve("performV4Query");
const performV10Query = container.resolve("performV10Query");

if (argv.version === "4") {
return performV4Query(client, fqlQuery, outputFile, argv);
} else {
Expand Down
33 changes: 8 additions & 25 deletions src/commands/key.mjs
Original file line number Diff line number Diff line change
@@ -1,32 +1,20 @@
//@ts-check

import { container } from "../cli.mjs";
import { getAccountKey, getDBKey } from "../lib/auth/authNZ.mjs";
import { FaunaAccountClient } from "../lib/fauna-account-client.mjs";

// TODO: this function should just spit out the secret that was created.
// consider an optional flag that will save this secret to the creds file, overwriting
// the existing secret if it exists at key/path/role
async function createKey(argv) {
const { database, profile, role, url } = argv;
const logger = container.resolve("logger");
const accountKey = await getAccountKey(profile);
// TODO: after logging in, should we list the top level databases and create db keys for them?
// depending on how many top level dbs....
// Have to list DBs on login so we know which databases are top-level and require frontdoor calls

// TODO: we should create the key with fauna unless it's a top level key
// in which case we should create it with the account client

// TODO: when using fauna to create a key at the specified database path, we should
// getDBKey(parent path).
const dbSecret = getDBKey({
accountKey,
const AccountClient = new FaunaAccountClient();
const { database, role, ttl } = argv;
const databaseKey = await AccountClient.createKey({
path: database,
role,
url,
ttl,
});
logger.stdout("got account key", accountKey);
logger.stdout("got db secret", dbSecret);
logger.stdout(
`Created key for ${database} with role ${role}\n${JSON.stringify(databaseKey)}`,
);
}

function buildKeyCommand(yargs) {
Expand All @@ -37,7 +25,6 @@ function buildKeyCommand(yargs) {
describe: "choose a method to interact with your databases",
})
.options({
// TODO: make this a common option after new authNZ is in place
url: {
type: "string",
description: "the Fauna URL to query",
Expand All @@ -46,12 +33,8 @@ function buildKeyCommand(yargs) {
role: {
alias: "r",
type: "string",
default: "admin",
describe: "The role to assign to the key",
},
authRequired: {
default: true,
},
})
.help("help", "show help")
.example([["$0 key create"]]);
Expand Down
36 changes: 15 additions & 21 deletions src/commands/login.mjs
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
//@ts-check

import { container } from "../cli.mjs";
import { commonQueryOptions } from "../lib/command-helpers.mjs";
import { FaunaAccountClient } from "../lib/fauna-account-client.mjs";

async function doLogin(argv) {
async function doLogin() {
const logger = container.resolve("logger");
const open = container.resolve("open");
const accountClient = container.resolve("accountClient");
const credentials = container.resolve("credentials");
const oAuth = container.resolve("oauthClient");
const accountCreds = container.resolve("accountCreds");
oAuth.server.on("ready", async () => {
const authCodeParams = oAuth.getOAuthParams();
const dashboardOAuthURL =
await accountClient.startOAuthRequest(authCodeParams);
await FaunaAccountClient.startOAuthRequest(authCodeParams);
open(dashboardOAuthURL);
logger.stdout(`To login, open your browser to:\n ${dashboardOAuthURL}`);
});
oAuth.server.on("auth_code_received", async () => {
try {
const tokenParams = oAuth.getTokenParams();
const accessToken = await accountClient.getToken(tokenParams);
const { accountKey, refreshToken } =
await accountClient.getSession(accessToken);
accountCreds.save({
creds: { accountKey, refreshToken },
key: argv.profile,
});
const accessToken = await FaunaAccountClient.getToken(tokenParams);
await credentials.login(accessToken);
logger.stdout(`Login Success!\n`);
} catch (err) {
logger.stderr(err);
Expand All @@ -33,17 +29,15 @@ async function doLogin(argv) {
await oAuth.start();
}

/**
* Passthrough yargs until more functionality is added to the command
* @param {*} yargs
* @returns
*/
function buildLoginCommand(yargs) {
return yargs
.options({
profile: {
type: "string",
description: "a user profile",
default: "default",
},
})
.version(false)
.help("help", "show help");
return yargs.options({
...commonQueryOptions,
});
}

export default {
Expand Down
15 changes: 7 additions & 8 deletions src/config/setup-container.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ import updateNotifier from "update-notifier";
import { parseYargs } from "../cli.mjs";
import { performV4Query, performV10Query } from "../commands/eval.mjs";
import { makeAccountRequest } from "../lib/account.mjs";
import { Credentials } from "../lib/auth/credentials.mjs";
import OAuthClient from "../lib/auth/oauth-client.mjs";
import { getSimpleClient } from "../lib/command-helpers.mjs";
import { makeFaunaRequest } from "../lib/db.mjs";
import { getV10Client,runV10Query } from "../lib/fauna.mjs";
import { FaunaAccountClient } from "../lib/fauna-account-client.mjs";
import { getV10Client, runV10Query } from "../lib/fauna.mjs";
import fetchWrapper from "../lib/fetch-wrapper.mjs";
import { AccountKey, SecretKey } from "../lib/file-util.mjs";
import buildLogger from "../lib/logger.mjs";
import {
deleteUnusedSchemaFiles,
Expand Down Expand Up @@ -67,16 +66,16 @@ export const injectables = {
performV4Query: awilix.asValue(performV4Query),
performV10Query: awilix.asValue(performV10Query),
getSimpleClient: awilix.asValue(getSimpleClient),
accountClient: awilix.asClass(FaunaAccountClient, {
lifetime: Lifetime.SCOPED,
}),
oauthClient: awilix.asClass(OAuthClient, { lifetime: Lifetime.SCOPED }),
makeAccountRequest: awilix.asValue(makeAccountRequest),
makeFaunaRequest: awilix.asValue(makeFaunaRequest),
accountCreds: awilix.asClass(AccountKey, { lifetime: Lifetime.SCOPED }),
secretCreds: awilix.asClass(SecretKey, { lifetime: Lifetime.SCOPED }),
errorHandler: awilix.asValue((error, exitCode) => exit(exitCode)),

// While we inject the class instance before this in middleware,
// we need to register it here to resolve types.
credentials: awilix.asClass(Credentials, {
lifetime: Lifetime.SINGLETON,
}),
// utilities for interacting with Fauna
runV10Query: awilix.asValue(runV10Query),
getV10Client: awilix.asValue(getV10Client),
Expand Down
12 changes: 7 additions & 5 deletions src/config/setup-test-container.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { f, InMemoryWritableStream } from "../../test/helpers.mjs";
import { parseYargs } from "../cli.mjs";
import { makeAccountRequest } from "../lib/account.mjs";
import { makeFaunaRequest } from "../lib/db.mjs";
import { AccountKey, SecretKey } from "../lib/file-util.mjs";
import buildLogger from "../lib/logger.mjs";
import { injectables, setupCommonContainer } from "./setup-container.mjs";

Expand Down Expand Up @@ -55,6 +54,7 @@ export function setupTestContainer() {
// real implementation
parseYargs: awilix.asValue(spy(parseYargs)),
fs: awilix.asValue(customfs),
homedir: awilix.asValue(stub().returns("/home/user")),
fsp: awilix.asValue({
unlink: stub(),
writeFile: stub(),
Expand All @@ -64,11 +64,13 @@ export function setupTestContainer() {
getSimpleClient: awilix.asValue(
stub().returns({ close: () => Promise.resolve() }),
),
accountClient: awilix.asFunction(stub()),
AccountClient: awilix.asValue(() => ({
startOAuthRequest: stub(),
getToken: stub(),
getSession: stub(),
})),
oauthClient: awilix.asFunction(stub()),
accountCreds: awilix.asClass(AccountKey).scoped(),
secretCreds: awilix.asClass(SecretKey).scoped(),
// in tests, let's exit by throwing
credentials: awilix.asClass(stub()).singleton(),
errorHandler: awilix.asValue((error, exitCode) => {
error.code = exitCode;
throw error;
Expand Down
1 change: 1 addition & 0 deletions src/lib/account.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ async function parseResponse(response, shouldThrow) {
}
switch (response.status) {
case 401:
// TODO: try and refresh creds and then redo the call, if not then throw.
throw new InvalidCredsError(message);
case 403:
throw new UnauthorizedError(message);
Expand Down
Loading