Skip to content

Commit

Permalink
Add improved secret management (#301)
Browse files Browse the repository at this point in the history
* Add secret class to manage scoped secrets

* Update config to store a Secret instead of a string

* Update all the things to use Secret

* Add some eval tests for scoped secret handling
  • Loading branch information
macmv authored Oct 12, 2023
1 parent 70f81ed commit ceffdc5
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 91 deletions.
3 changes: 2 additions & 1 deletion src/commands/cloud-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { hostname } from "os";
import { Command } from "@oclif/core";
import { underline, blue } from "chalk";
import fetch from "node-fetch";
import { Secret } from "../lib/secret";

const DEFAULT_NAME = "cloud";
const DB = process.env.FAUNA_URL ?? "https://db.fauna.com";
Expand Down Expand Up @@ -48,7 +49,7 @@ export default class CloudLoginCommand extends Command {
for (const region of Object.values(regions)) {
config.rootConfig.endpoints[region.endpointName(base)] = new Endpoint({
url: DB,
secret: region.secret,
secret: Secret.parse(region.secret),
});
}

Expand Down
25 changes: 22 additions & 3 deletions src/commands/endpoint/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Flags, Args, Command, ux } from "@oclif/core";
import { input, confirm } from "@inquirer/prompts";
import { Endpoint, ShellConfig, getRootConfigPath } from "../../lib/config";
import FaunaClient from "../../lib/fauna-client";
import { Secret } from "../../lib/secret";

export default class AddEndpointCommand extends Command {
static args = {
Expand Down Expand Up @@ -83,12 +84,30 @@ export default class AddEndpointCommand extends Command {
},
}));

const secret =
flags?.secret ?? (await input({ message: "Database Secret" }));
let secret: Secret;
if (flags?.secret === undefined) {
const v = await input({
message: "Database Secret",
validate: (secret) => {
try {
Secret.parse(secret);
return true;
} catch (e: any) {
return e.message;
}
},
});
secret = Secret.parse(v);
} else {
secret = Secret.parse(flags.secret);
}

ux.action.start("Checking secret");

const client = new FaunaClient({ secret, endpoint: url });
const client = new FaunaClient({
secret: secret.buildSecret(),
endpoint: url,
});
try {
const res = await client.query(`0`);
if (res.status !== 200) {
Expand Down
27 changes: 10 additions & 17 deletions src/lib/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const ini = require("ini");

import { ProjectConfig, Stack } from "./project-config";
import { RootConfig, Endpoint } from "./root-config";
import { Secret } from "../secret";

export { RootConfig, ProjectConfig, Endpoint, Stack };

Expand Down Expand Up @@ -118,12 +119,8 @@ export type ShellOpts = {
};

export type EndpointConfig = {
secret: string;
secret: Secret;
url: string;
/** this is currently just used for messaging purposes, the secret
* in this config will already contain the database path if needed.
*/
database?: string;
name?: string;
graphqlHost: string;
graphqlPort: number;
Expand Down Expand Up @@ -247,7 +244,7 @@ export class ShellConfig {
// No `~/.fauna-shell` was found, so `--secret` is required to make an endpoint. If `--secret` wasn't passed, `validate` should fail.
if (secretFlag !== undefined) {
this.endpoint = new Endpoint({
secret: secretFlag,
secret: Secret.parse(secretFlag),
url: urlFlag,
graphqlHost: this.flags.strOpt("graphqlHost"),
graphqlPort: this.flags.numberOpt("graphqlPort"),
Expand All @@ -260,7 +257,9 @@ export class ShellConfig {
}

// override endpoint with values from flags.
this.endpoint.secret = secretFlag ?? this.endpoint.secret;
if (secretFlag !== undefined) {
this.endpoint.secret = Secret.parse(secretFlag);
}
this.endpoint.url = urlFlag ?? this.endpoint.url;
this.endpoint.graphqlHost =
this.flags.strOpt("graphqlHost") ?? this.endpoint.graphqlHost;
Expand All @@ -278,21 +277,15 @@ export class ShellConfig {
}
};

lookupEndpoint = (opts: {
scope?: string;
role?: string;
}): EndpointConfig => {
lookupEndpoint = (opts: { scope?: string }): EndpointConfig => {
this.validate();

let database = this.stack?.database ?? "";
let database = this.stack?.database.split("/") ?? [];
if (opts.scope !== undefined) {
if (this.stack !== undefined) {
database += "/";
}
database += opts.scope;
database.push(...opts.scope.split("/"));
}

return this.endpoint!.makeScopedEndpoint(database, opts.role);
return this.endpoint!.makeScopedEndpoint(database);
};

configErrors(): string[] {
Expand Down
23 changes: 8 additions & 15 deletions src/lib/config/root-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Config, EndpointConfig, InvalidConfigError } from ".";
import fs from "fs";
import { Secret } from "../secret";
const ini = require("ini");

// Represents `~/.fauna-shell`
Expand Down Expand Up @@ -113,7 +114,7 @@ export class RootConfig {
*/
export class Endpoint {
name?: string;
secret: string;
secret: Secret;
url: string;

graphqlHost: string;
Expand All @@ -126,7 +127,7 @@ export class Endpoint {
} else {
return new Endpoint({
name: name,
secret: secOpt,
secret: Secret.parse(secOpt),
url: Endpoint.getURLFromConfig(config),

graphqlHost: config.strOpt("graphqlHost"),
Expand All @@ -137,7 +138,7 @@ export class Endpoint {

constructor(opts: {
name?: string;
secret: string;
secret: Secret;
url?: string;
graphqlHost?: string;
graphqlPort?: number;
Expand All @@ -150,26 +151,18 @@ export class Endpoint {
this.graphqlPort = opts.graphqlPort ?? 443;
}

makeScopedEndpoint(databaseScope?: string, role?: string): EndpointConfig {
let appendedRoleStr = "";
if (role) {
appendedRoleStr = `:${role}`;
} else if (databaseScope) {
appendedRoleStr = ":admin";
makeScopedEndpoint(databaseScope?: string[]): EndpointConfig {
const secret = this.secret.clone();
if (databaseScope !== undefined) {
secret.databaseScope.push(...databaseScope);
}

const secret =
this.secret +
(databaseScope ? `:${databaseScope}` : "") +
appendedRoleStr;

return {
secret,
name: this.name,
url: this.url,
graphqlHost: this.graphqlHost,
graphqlPort: this.graphqlPort,
database: databaseScope,
};
}

Expand Down
46 changes: 18 additions & 28 deletions src/lib/fauna-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ class FaunaCommand extends Command {
try {
connectionOptions = this.shellConfig.lookupEndpoint({
scope: dbScope,
role,
});

const { hostname, port, protocol } = new URL(connectionOptions.url);
Expand All @@ -71,7 +70,7 @@ class FaunaCommand extends Command {
domain: hostname,
port,
scheme: protocol?.replace(/:$/, ""),
secret: connectionOptions.secret,
secret: connectionOptions.secret.buildSecret({ role }),

// Force http1. See getClient.
fetch: fetch,
Expand Down Expand Up @@ -103,17 +102,15 @@ class FaunaCommand extends Command {
let connectedMessage;
if (connectionOptions.name !== undefined) {
connectedMessage = `Connected to endpoint: ${connectionOptions.name}`;
if (
connectionOptions.database !== undefined &&
connectionOptions.database !== ""
) {
connectedMessage += ` database: ${connectionOptions.database}`;
if (connectionOptions.secret.databaseScope.length > 0) {
connectedMessage += ` database: ${connectionOptions.secret.databaseScope.join(
"/"
)}`;
}
} else if (
connectionOptions.database !== undefined &&
connectionOptions.database !== ""
) {
connectedMessage = `Connected to database: ${connectionOptions.database}`;
} else if (connectionOptions.secret.databaseScope.length > 0) {
connectedMessage = `Connected to database: ${connectionOptions.secret.databaseScope.join(
"/"
)}`;
}
if (connectedMessage !== undefined) {
this.log(connectedMessage);
Expand All @@ -125,18 +122,15 @@ class FaunaCommand extends Command {
// construct v4 client
let connectionOptions;
try {
connectionOptions = this.shellConfig.lookupEndpoint({
scope: dbScope,
role,
});
connectionOptions = this.shellConfig.lookupEndpoint({ scope: dbScope });

const { hostname, port, protocol } = new URL(connectionOptions.url);

const client = new Client({
domain: hostname,
port,
scheme: protocol?.replace(/:$/, ""),
secret: connectionOptions.secret,
secret: connectionOptions.secret.buildSecret({ role }),

// Force http1. Fixes tests I guess? I spent a solid 30 minutes
// debugging the whole `nock` thing in our tests, only to realize this
Expand Down Expand Up @@ -164,13 +158,10 @@ class FaunaCommand extends Command {
// construct v10 client
let connectionOptions;
try {
connectionOptions = this.shellConfig.lookupEndpoint({
scope: dbScope,
role,
});
connectionOptions = this.shellConfig.lookupEndpoint({ scope: dbScope });
const client = new FaunaClient({
endpoint: connectionOptions.url,
secret: connectionOptions.secret,
secret: connectionOptions.secret.buildSecret({ role }),
time: this.flags.timeout
? parseInt(this.flags.timeout, 10)
: undefined,
Expand Down Expand Up @@ -199,14 +190,11 @@ class FaunaCommand extends Command {
const { hostname, port, protocol } = new URL(connectionOptions.url);

for (let i = 0; i < path.length; i++) {
const secret =
connectionOptions.secret + ":" + path.slice(0, i).join("/") + ":admin";

const client = new Client({
domain: hostname,
port,
scheme: protocol?.replace(/:$/, ""),
secret,
secret: connectionOptions.secret.buildSecret(),

// See getClient.
fetch: fetch,
Expand All @@ -219,15 +207,17 @@ class FaunaCommand extends Command {
await client.close();

if (!exists) {
const fullPath = [...connectionOptions.secret.databaseScope, ...path.slice(0, i + 1)];
this.error(
`Database '${path.slice(0, i + 1).join("/")}' doesn't exist`
`Database '${fullPath.join("/")}' doesn't exist`
);
}

connectionOptions.secret.appendScope(path[i]);
}

return this.getClient({
dbScope: scope,
role: "admin",
version,
});
}
Expand Down
5 changes: 4 additions & 1 deletion src/lib/stack-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ export class StackFactory {

promptDatabasePath = async (endpoint: Endpoint): Promise<string> => {
const { url, secret } = endpoint;
const client = new FaunaClient({ endpoint: url, secret });
const client = new FaunaClient({
endpoint: url,
secret: secret.buildSecret(),
});

const res = await client.query("0");
if (res.status !== 200) {
Expand Down
13 changes: 7 additions & 6 deletions test/commands/endpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AddEndpointCommand from "../../src/commands/endpoint/add";
import ListEndpointCommand from "../../src/commands/endpoint/list";
import RemoveEndpointCommand from "../../src/commands/endpoint/remove";
import { Config } from "@oclif/core";
import { Secret } from "../../src/lib/secret";

const rootConfigPath = getRootConfigPath();

Expand Down Expand Up @@ -54,7 +55,7 @@ describe("endpoint:add", () => {
endpoints: {
"my-endpoint": {
url: "http://bar.baz",
secret: "fn3333",
secret: Secret.parse("fn3333"),
name: "my-endpoint",
// These graphql bits are only saved if they differ from the
// default.
Expand All @@ -64,7 +65,7 @@ describe("endpoint:add", () => {
foobar: {
url: "http://foo.baz",
name: undefined,
secret: "fn1234",
secret: Secret.parse("fn1234"),
graphqlHost: "graphql.fauna.com",
graphqlPort: 443,
},
Expand Down Expand Up @@ -108,7 +109,7 @@ describe("endpoint:add", () => {
endpoints: {
"my-endpoint": {
url: "http://bar.baz",
secret: "fn3333",
secret: Secret.parse("fn3333"),
name: "my-endpoint",
// These graphql bits are only saved if they differ from the
// default.
Expand All @@ -117,7 +118,7 @@ describe("endpoint:add", () => {
},
foobar: {
url: "http://foo.baz",
secret: "fn1234",
secret: Secret.parse("fn1234"),
name: undefined,
graphqlHost: "graphql.fauna.com",
graphqlPort: 443,
Expand Down Expand Up @@ -185,7 +186,7 @@ describe("endpoint:remove", () => {
endpoints: {
"my-endpoint": {
url: "http://bar.baz",
secret: "fn3333",
secret: Secret.parse("fn3333"),
name: "my-endpoint",
// These graphql bits are only saved if they differ from the
// default.
Expand Down Expand Up @@ -225,7 +226,7 @@ describe("endpoint:remove", () => {
endpoints: {
"other-endpoint": {
url: "http://bar.baz",
secret: "fn3333",
secret: Secret.parse("fn3333"),
name: "other-endpoint",
// These graphql bits are only saved if they differ from the
// default.
Expand Down
Loading

0 comments on commit ceffdc5

Please sign in to comment.