Skip to content

Commit

Permalink
feat: Improve client auth options (#115)
Browse files Browse the repository at this point in the history
## What does this PR do?
Allows authorisation via the `CoreDomainClient` using JWT, headers
(public users) or admin secret
 
 ## Next steps...
Update all references in `planx-new`, try to instantiate one client
per-request in Express
  • Loading branch information
DafyddLlyr authored Sep 19, 2023
1 parent 0ff911c commit 03db128
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 57 deletions.
4 changes: 2 additions & 2 deletions src/export/oneApp/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { X2jOptionsOptional, XMLParser, XMLValidator } from "fast-xml-parser";
import get from "lodash.get";

import { graphQLClient } from "../../requests/graphql";
import { getGraphQLClient } from "../../requests/graphql";
import { generateOneAppXML } from "./index";
import { mockSession } from "./mocks/session";
import { OneAppPayload } from "./model";
Expand Down Expand Up @@ -33,7 +33,7 @@ const parseOptions: X2jOptionsOptional = {

const parser = new XMLParser(parseOptions);

const client = graphQLClient({ url: process.env.HASURA_GRAPHQL_URL! });
const client = getGraphQLClient({ url: process.env.HASURA_GRAPHQL_URL! });

describe("generateOneAppXML", () => {
test("includes template doc files when the flow has document templates", async () => {
Expand Down
77 changes: 59 additions & 18 deletions src/requests/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,66 @@
import { GraphQLClient } from "graphql-request";

export function graphQLClient({
url,
secret,
authorizedSession,
}: {
url: string;
secret?: string;
authorizedSession?: {
interface JWTAuth {
jwt: string;
}

interface SessionAuth {
session: {
email: string;
sessionId: string;
};
}

interface AdminAuth {
adminSecret: string;
}

export type Auth = JWTAuth | SessionAuth | AdminAuth;

/**
* Client which queries using user's JWT for role-bases access to Hasura
* Should be used for all requests made my authorised users
*/
const getRoleBasedClient = (url: string, auth: JWTAuth) =>
new GraphQLClient(url, { headers: { authorization: `Bearer ${auth.jwt}` } });

/**
* Session based client which allows non-authorised users access to their session
* Used for Save & Return
*/
const getSessionClient = (url: string, auth: SessionAuth) =>
new GraphQLClient(url, {
headers: {
"x-hasura-lowcal-session-id": auth.session.sessionId,
"x-hasura-lowcal-email": auth.session.email,
},
});

/**
* Admin client with full access to Hasura
* Should be used with caution - only for testing / scripting
* @deprecated Consider moving to an "api" role in Hasura
*/
const getAdminClient = (url: string, auth: AdminAuth) =>
new GraphQLClient(url, {
headers: { "x-hasura-admin-secret": auth.adminSecret },
});

export function getGraphQLClient({
url,
auth,
}: {
url: string;
auth?: Auth;
}): GraphQLClient {
let headers = {};
if (secret) {
headers = { "x-hasura-admin-secret": secret };
} else if (authorizedSession) {
headers = {
"x-hasura-lowcal-session-id": authorizedSession.sessionId,
"x-hasura-lowcal-email": authorizedSession.email,
};
}
return new GraphQLClient(url, { headers });
// Return public client by default
if (!auth) return new GraphQLClient(url);

if ("jwt" in auth) return getRoleBasedClient(url, auth);
if ("session" in auth) return getSessionClient(url, auth);
if ("adminSecret" in auth) return getAdminClient(url, auth);

throw Error(
"Unable to instantiate GraphQL client - insufficient auth details provided",
);
}
28 changes: 14 additions & 14 deletions src/requests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,35 @@ describe("CoreDomainClient", () => {
expect(core).toBeInstanceOf(CoreDomainClient);
});

test("instantiating a client with a secret", () => {
const core = new CoreDomainClient({ hasuraSecret: "shhh..." });
test("instantiating a client with an admin secret", () => {
const auth = { adminSecret: "shhh..." };
const core = new CoreDomainClient({ auth });
expect(core).toBeInstanceOf(CoreDomainClient);
});

test("instantiating a client with a URL and a secret", () => {
test("instantiating a client with a URL and an admin secret", () => {
const auth = { adminSecret: "shhh..." };
const core = new CoreDomainClient({
hasuraSecret: "shhh...",
targetURL: "https://example.com",
auth,
});
expect(core).toBeInstanceOf(CoreDomainClient);
});

test("a public client can authorize a session", () => {
const core = new CoreDomainClient();
core.authorizeSession({ email: "[email protected]", sessionId: "1234" });
test("instantiating a client with session details", () => {
const auth = { session: { email: "[email protected]", sessionId: "1234" } };
const core = new CoreDomainClient({ auth });
expect(core.client.requestConfig.headers).toEqual({
"x-hasura-lowcal-session-id": "1234",
"x-hasura-lowcal-email": "[email protected]",
});
});

test("an admin client cannot authorize a session", () => {
const core = new CoreDomainClient({
hasuraSecret: "shhh...",
targetURL: "https://example.com",
test("instantiating a client with a JSON web token", () => {
const auth = { jwt: "ABC123" };
const core = new CoreDomainClient({ auth });
expect(core.client.requestConfig.headers).toEqual({
authorization: "Bearer ABC123",
});
expect(() =>
core.authorizeSession({ email: "[email protected]", sessionId: "1234" }),
).toThrow();
});
});
28 changes: 5 additions & 23 deletions src/requests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "./document-templates";
import { ExportClient } from "./export";
import { createFlow, FlowClient, publishFlow } from "./flow";
import { graphQLClient } from "./graphql";
import { Auth, getGraphQLClient } from "./graphql";
import { createPaymentRequest, PaymentRequestClient } from "./payment-request";
import { formatRawProjectTypes } from "./project-types";
import {
Expand All @@ -22,11 +22,10 @@ import {
import { createTeam, TeamClient } from "./team";
import { createUser, UserClient } from "./user";

const defaultURL = process.env.HASURA_GRAPHQL_URL;
const defaultURL = process.env.HASURA_GRAPHQL_URL!;

export class CoreDomainClient {
client!: GraphQLClient;
protected type: "admin" | "public";
protected url: string;

// client namespaces
Expand All @@ -38,14 +37,10 @@ export class CoreDomainClient {
paymentRequest!: PaymentRequestClient;
export!: ExportClient;

constructor(args?: {
hasuraSecret?: string | undefined;
targetURL?: string | undefined;
}) {
const url: string = args?.targetURL ? args?.targetURL : defaultURL!;
constructor(args?: { targetURL?: string | undefined; auth?: Auth }) {
const url = args?.targetURL || defaultURL;
this.url = url;
this.type = args?.hasuraSecret ? "admin" : "public";
const client = graphQLClient({ url, secret: args?.hasuraSecret });
const client = getGraphQLClient({ url, auth: args?.auth });
this.setClient(client);
}

Expand All @@ -61,19 +56,6 @@ export class CoreDomainClient {
this.export = new ExportClient(this.client);
}

authorizeSession(sessionDetails: { email: string; sessionId: string }) {
if (this.type === "admin") {
throw new Error(
"authorizing a session with an admin client is not allowed",
);
}
const client = graphQLClient({
url: this.url,
authorizedSession: sessionDetails,
});
this.setClient(client);
}

// TODO: refactor below into client namespaces (e.g. SessionClient)
// namspacing prevents this class from growing too unwieldy while still allowing for
// a simple interface for callers.
Expand Down

0 comments on commit 03db128

Please sign in to comment.