Skip to content

Commit

Permalink
Improve credential handling (#432)
Browse files Browse the repository at this point in the history
* refresh credentials during fetch calls

* don't create dupe keys

* Update src/config/setup-test-container.mjs

Co-authored-by: echo-bravo-yahoo <[email protected]>

* logger component

* refactor credentials

* refactor credentials

* separate concerns of account and database creds into separate classes

* revert create database changes

* get rid of authnz middleware

* further separate concerns

* fix directory for creds. add some env var options

* fix lint

* skip busted test

* unsafe accessor fix

* try test fix

* fix tests

* don't set a default role in the args. resolve it in credentials. mock homedir

* readme and remove .fauna dir

* don't use exit just throw an error

---------

Co-authored-by: echo-bravo-yahoo <[email protected]>
  • Loading branch information
mwilde345 and echo-bravo-yahoo authored Nov 27, 2024
1 parent 33271d3 commit ecc0524
Show file tree
Hide file tree
Showing 22 changed files with 839 additions and 630 deletions.
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
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,
})
.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

0 comments on commit ecc0524

Please sign in to comment.