From b0974a209ffe9d3d6eccbe42b30240ada9f941dd Mon Sep 17 00:00:00 2001 From: Neil Macneale V Date: Thu, 12 Oct 2023 11:25:19 -0700 Subject: [PATCH 1/4] Add secret class to manage scoped secrets --- src/lib/secret.ts | 51 +++++++++++++++++++++++++++++++ test/lib/secret.test.ts | 68 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/lib/secret.ts create mode 100644 test/lib/secret.test.ts diff --git a/src/lib/secret.ts b/src/lib/secret.ts new file mode 100644 index 00000000..201dd63d --- /dev/null +++ b/src/lib/secret.ts @@ -0,0 +1,51 @@ +export class Secret { + // A fauna key, like `fn1234`. + key: string; + // A database scope, like `["foo", "bar"]` + databaseScope: string[]; + + constructor(opts: { key: string; databaseScope?: string[] }) { + this.key = opts.key; + this.databaseScope = opts.databaseScope ?? []; + } + + static parse(key: string) { + if (key.length === 0) { + throw new Error("Secret cannot be empty"); + } + if (key.includes(":")) { + throw new Error("Secret cannot be scoped"); + } + return new Secret({ key }); + } + + buildSecret(opts?: { role?: string }): string { + let secret = this.key; + if (this.databaseScope.length > 0) { + secret += `:${this.databaseScope.join("/")}`; + } + if (opts?.role !== undefined || this.databaseScope.length > 0) { + const role = opts?.role ?? "admin"; + secret += ["admin", "client", "server", "server-readonly"].includes(role) + ? `:${role}` + : `:@role/${role}`; + } + return secret; + } + + /** + * Parses the given scope, appends it to the `Secret`, and returns the new + * secret. This mutates `this`. + */ + appendScope(scope: string) { + this.databaseScope.push(...scope.split("/")); + return this; + } + + clone(): Secret { + return new Secret({ + key: this.key, + databaseScope: [...this.databaseScope], + }); + } +} diff --git a/test/lib/secret.test.ts b/test/lib/secret.test.ts new file mode 100644 index 00000000..34cda3a8 --- /dev/null +++ b/test/lib/secret.test.ts @@ -0,0 +1,68 @@ +import { expect } from "chai"; +import { Secret } from "../../src/lib/secret"; + +describe("secret", () => { + it("appends paths scoped secrets", () => { + const secret = Secret.parse("fn1234"); + expect(secret.databaseScope).to.eql([]); + + secret.appendScope("foo"); + expect(secret.databaseScope).to.eql(["foo"]); + + secret.appendScope("bar"); + expect(secret.databaseScope).to.eql(["foo", "bar"]); + }); + + it("parses database paths from appendScope", () => { + const secret = Secret.parse("fn1234"); + expect(secret.databaseScope).to.eql([]); + + secret.appendScope("foo/bar"); + expect(secret.databaseScope).to.eql(["foo", "bar"]); + }); + + it("disallows empty secrets", () => { + expect(() => Secret.parse("")).to.throw("Secret cannot be empty"); + }); + + it("disallows scoped secrets", () => { + expect(() => Secret.parse("fn1234:foo")).to.throw( + "Secret cannot be scoped" + ); + }); + + it("builds a secret with a role", () => { + const secret = Secret.parse("fn1234"); + expect(secret.buildSecret()).to.equal("fn1234"); + expect(secret.buildSecret({ role: "admin" })).to.equal("fn1234:admin"); + expect(secret.buildSecret({ role: "server" })).to.equal("fn1234:server"); + expect(secret.buildSecret({ role: "server-readonly" })).to.equal( + "fn1234:server-readonly" + ); + expect(secret.buildSecret({ role: "client" })).to.equal("fn1234:client"); + expect(secret.buildSecret({ role: "foo" })).to.equal("fn1234:@role/foo"); + }); + + it("builds a secret with a scope", () => { + const secret = Secret.parse("fn1234"); + secret.appendScope("foo"); + expect(secret.buildSecret()).to.equal("fn1234:foo:admin"); + secret.appendScope("bar"); + expect(secret.buildSecret()).to.equal("fn1234:foo/bar:admin"); + }); + + it("builds a secret with a scope and role", () => { + const secret = Secret.parse("fn1234"); + secret.appendScope("foo/bar"); + expect(secret.buildSecret()).to.equal("fn1234:foo/bar:admin"); + expect(secret.buildSecret({ role: "admin" })).to.equal( + "fn1234:foo/bar:admin" + ); + expect(secret.buildSecret({ role: "server" })).to.equal( + "fn1234:foo/bar:server" + ); + expect(secret.buildSecret({ role: "foo" })).to.equal( + "fn1234:foo/bar:@role/foo" + ); + }); +}); From 4277b510062df67955882681809dd76ee3ff3fd3 Mon Sep 17 00:00:00 2001 From: Neil Macneale V Date: Thu, 12 Oct 2023 11:37:54 -0700 Subject: [PATCH 2/4] Update config to store a Secret instead of a string --- src/lib/config/index.ts | 27 ++++++++++----------------- src/lib/config/root-config.ts | 23 ++++++++--------------- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts index 802647ab..a1b81d55 100644 --- a/src/lib/config/index.ts +++ b/src/lib/config/index.ts @@ -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 }; @@ -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; @@ -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"), @@ -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; @@ -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[] { diff --git a/src/lib/config/root-config.ts b/src/lib/config/root-config.ts index 373235ee..5ed3a3d0 100644 --- a/src/lib/config/root-config.ts +++ b/src/lib/config/root-config.ts @@ -1,5 +1,6 @@ import { Config, EndpointConfig, InvalidConfigError } from "."; import fs from "fs"; +import { Secret } from "../secret"; const ini = require("ini"); // Represents `~/.fauna-shell` @@ -113,7 +114,7 @@ export class RootConfig { */ export class Endpoint { name?: string; - secret: string; + secret: Secret; url: string; graphqlHost: string; @@ -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"), @@ -137,7 +138,7 @@ export class Endpoint { constructor(opts: { name?: string; - secret: string; + secret: Secret; url?: string; graphqlHost?: string; graphqlPort?: number; @@ -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, }; } From e40cc88bc38f61fa7f96cb649f200302e92491a4 Mon Sep 17 00:00:00 2001 From: Neil Macneale V Date: Thu, 12 Oct 2023 11:41:06 -0700 Subject: [PATCH 3/4] Update all the things to use Secret --- src/commands/cloud-login.ts | 3 +- src/commands/endpoint/add.ts | 25 ++++++++++-- src/lib/fauna-command.js | 46 +++++++++------------- src/lib/stack-factory.ts | 5 ++- test/commands/endpoint.test.ts | 13 ++++--- test/lib/config.test.ts | 70 +++++++++++++++++++++++++--------- 6 files changed, 106 insertions(+), 56 deletions(-) diff --git a/src/commands/cloud-login.ts b/src/commands/cloud-login.ts index 71b22c24..fcce62a5 100644 --- a/src/commands/cloud-login.ts +++ b/src/commands/cloud-login.ts @@ -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"; @@ -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), }); } diff --git a/src/commands/endpoint/add.ts b/src/commands/endpoint/add.ts index c2c9c5b2..cff4be9e 100644 --- a/src/commands/endpoint/add.ts +++ b/src/commands/endpoint/add.ts @@ -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 = { @@ -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) { diff --git a/src/lib/fauna-command.js b/src/lib/fauna-command.js index 2814b6bc..1f3bf914 100644 --- a/src/lib/fauna-command.js +++ b/src/lib/fauna-command.js @@ -62,7 +62,6 @@ class FaunaCommand extends Command { try { connectionOptions = this.shellConfig.lookupEndpoint({ scope: dbScope, - role, }); const { hostname, port, protocol } = new URL(connectionOptions.url); @@ -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, @@ -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); @@ -125,10 +122,7 @@ 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); @@ -136,7 +130,7 @@ class FaunaCommand extends Command { 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 @@ -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, @@ -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, @@ -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, }); } diff --git a/src/lib/stack-factory.ts b/src/lib/stack-factory.ts index cf1c9fd4..c747b8f9 100644 --- a/src/lib/stack-factory.ts +++ b/src/lib/stack-factory.ts @@ -115,7 +115,10 @@ export class StackFactory { promptDatabasePath = async (endpoint: Endpoint): Promise => { 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) { diff --git a/test/commands/endpoint.test.ts b/test/commands/endpoint.test.ts index ff0d3c75..958a7d1e 100644 --- a/test/commands/endpoint.test.ts +++ b/test/commands/endpoint.test.ts @@ -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(); @@ -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. @@ -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, }, @@ -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. @@ -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, @@ -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. @@ -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. diff --git a/test/lib/config.test.ts b/test/lib/config.test.ts index 51cff81f..5d711be1 100644 --- a/test/lib/config.test.ts +++ b/test/lib/config.test.ts @@ -7,12 +7,10 @@ import { getRootConfigPath, } from "../../src/lib/config"; import sinon from "sinon"; +import { Secret } from "../../src/lib/secret"; -const lookupEndpoint = ( - opts: ShellOpts & { role?: string; scope?: string } -) => { +const lookupEndpoint = (opts: ShellOpts & { scope?: string }) => { return new ShellConfig(opts).lookupEndpoint({ - role: opts.role, scope: opts.scope, }); }; @@ -30,7 +28,7 @@ describe("root config", () => { }, }) ).to.deep.contain({ - secret: "fn1234", + secret: Secret.parse("fn1234"), url: "http://localhost:8443", }); }); @@ -46,7 +44,7 @@ describe("root config", () => { }, }) ).to.deep.contain({ - secret: "fn1234", + secret: Secret.parse("fn1234"), url: "https://db.fauna.com", }); }); @@ -65,7 +63,7 @@ describe("root config", () => { }, }) ).to.deep.contain({ - secret: "fn1234", + secret: Secret.parse("fn1234"), url: "http://localhost:8443", }); }); @@ -86,7 +84,7 @@ describe("root config", () => { }, }) ).to.deep.contain({ - secret: "fn5678", + secret: Secret.parse("fn5678"), url: "https://db.fauna.com", }); }); @@ -102,7 +100,7 @@ describe("root config", () => { }, }) ).to.deep.contain({ - secret: "fn1234", + secret: Secret.parse("fn1234"), url: "https://db.fauna.com", }); }); @@ -143,10 +141,48 @@ describe("root config with flags", () => { }, }) ).to.deep.contain({ - secret: "fn555", + secret: Secret.parse("fn555"), url: "https://db.fauna.com", }); }); + + it("allows overriding secret with --secret", () => { + expect( + lookupEndpoint({ + flags: { + secret: "fn555", + }, + rootConfig: { + default: "my-endpoint", + "my-endpoint": { + secret: "fn1234", + }, + }, + }) + ).to.deep.contain({ + secret: { + key: "fn555", + databaseScope: [], + }, + url: "https://db.fauna.com", + }); + }); + + it("disallows scoped secrets", () => { + expect(() => + lookupEndpoint({ + flags: { + secret: "fn555:db:@role/bar", + }, + rootConfig: { + default: "my-endpoint", + "my-endpoint": { + secret: "fn1234", + }, + }, + }) + ).to.throw("Secret cannot be scoped"); + }); }); describe("local config", () => { @@ -174,7 +210,7 @@ describe("local config", () => { }, }) ).to.deep.contain({ - secret: "fn555:foo:admin", + secret: Secret.parse("fn555").appendScope("foo"), url: "https://db.fauna.com", }); }); @@ -203,7 +239,7 @@ describe("local config", () => { }, }) ).to.deep.contain({ - secret: "fn555:my-db:admin", + secret: Secret.parse("fn555").appendScope("my-db"), url: "https://db.fauna.com", }); }); @@ -241,7 +277,7 @@ describe("local config with flags", () => { }, }) ).to.deep.contain({ - secret: "fn888:foo:admin", + secret: Secret.parse("fn888").appendScope("foo"), url: "http://localhost:10443", }); }); @@ -281,7 +317,7 @@ describe("local config with flags", () => { }, }) ).to.deep.contain({ - secret: "fn888:bar:admin", + secret: Secret.parse("fn888").appendScope("bar"), url: "http://localhost:10443", }); }); @@ -326,7 +362,7 @@ describe("local config with flags", () => { }, }) ).to.deep.contain({ - secret: "fn999:my-db-3:admin", + secret: Secret.parse("fn999").appendScope("my-db-3"), url: "http://somewhere-else:10443", }); }); @@ -345,7 +381,7 @@ describe("local config with flags", () => { scope: "my-scope", }) ).to.deep.contain({ - secret: "fn1234:my-scope:admin", + secret: Secret.parse("fn1234").appendScope("my-scope"), url: "http://localhost:8443", }); }); @@ -373,7 +409,7 @@ describe("local config with flags", () => { scope: "my-scope", }) ).to.deep.contain({ - secret: "fn1234:my-db/my-scope:admin", + secret: Secret.parse("fn1234").appendScope("my-db/my-scope"), url: "http://localhost:8443", }); }); From 4c6d798aa219d3caa0c445e4333b46dcbcd56eb5 Mon Sep 17 00:00:00 2001 From: Neil Macneale V Date: Thu, 12 Oct 2023 11:41:19 -0700 Subject: [PATCH 4/4] Add some eval tests for scoped secret handling --- test/commands/eval.test.js | 38 ++++++++++++++++++++++++++++++++++++-- test/helpers/utils.js | 19 ++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/test/commands/eval.test.js b/test/commands/eval.test.js index f2bdbcf0..d2899ce3 100644 --- a/test/commands/eval.test.js +++ b/test/commands/eval.test.js @@ -1,5 +1,5 @@ const { expect, test } = require("@oclif/test"); -const { withOpts, getEndpoint, matchFqlReq, withLegacyOpts } = require("../helpers/utils.js"); +const { withOpts, getEndpoint, evalV10, matchFqlReq, withLegacyOpts } = require("../helpers/utils.js"); const { query: q } = require("faunadb"); describe("eval", () => { @@ -115,6 +115,40 @@ describe("eval in v10", () => { .it("runs eval in json tagged format", (ctx) => { expect(JSON.parse(ctx.stdout)).to.deep.equal({ two: { "@int": "2" } }); }); + + test + .do(async () => { + // This can fail if `MyDB` already exists, but thats fine. + await evalV10("Database.create({ name: 'MyDB' })"); + }) + .stdout() + // --secret is passed by withOpts, and passing a scope with that is allowed. + .command(withOpts(["eval", "MyDB", "{ three: 3 }", "--format", "json-tagged"])) + .it("allows setting --secret and scope", (ctx) => { + expect(JSON.parse(ctx.stdout)).to.deep.equal({ three: { "@int": "3" } }); + }); + + test + .do(async () => { + // This can fail if `MyDB` already exists, but thats fine. + await evalV10("Database.create({ name: 'MyDB' })"); + }) + .stdout() + // a scoped secret is never valid. + .command([ + "eval", + "{ two: 3 }", + "--format", + "json-tagged", + "--secret", + `foo:MyDB`, + "--url", + getEndpoint() + ]) + .catch((e) => { + expect(e.message).to.equal("Secret cannot be scoped"); + }) + .it("disallows scoped secrets"); }); function mockQuery(api) { @@ -123,7 +157,7 @@ function mockQuery(api) { .post("/", matchFqlReq(q.Now())) .reply(200, { resource: new Date() }) .post("/", matchFqlReq(q.Paginate(q.Collections()))) - .reply(200, function () { + .reply(200, function() { const auth = this.req.headers.authorization[0].split(":"); return { resource: { diff --git a/test/helpers/utils.js b/test/helpers/utils.js index 254009c5..832d9630 100644 --- a/test/helpers/utils.js +++ b/test/helpers/utils.js @@ -1,3 +1,4 @@ +import fetch from "node-fetch"; const url = require("url"); const { query: q } = require("faunadb"); const env = process.env; @@ -32,13 +33,29 @@ module.exports.withOpts = (cmd) => { return cmd.concat(opts); }; -module.exports.getEndpoint = () => +const getEndpoint = () => url.format({ protocol: env.FAUNA_SCHEME, hostname: env.FAUNA_DOMAIN, port: env.FAUNA_PORT, }); +module.exports.getEndpoint = getEndpoint; + +module.exports.evalV10 = (query) => { + const endpoint = getEndpoint(); + const secret = env.FAUNA_SECRET; + return fetch(new URL("/query/1", endpoint), { + method: "POST", + headers: { + Authorization: `Bearer ${secret}`, + }, + body: JSON.stringify({ + query, + }), + }); +} + const fqlToJsonString = (fql) => JSON.stringify(q.wrap(fql)); module.exports.fqlToJsonString = fqlToJsonString;