From 73df1bb91f7c19be0159e257d35a001b863a4e80 Mon Sep 17 00:00:00 2001 From: Siddharth Sheladiya Date: Thu, 6 Mar 2025 22:22:22 -0500 Subject: [PATCH 1/9] fix: rebase with master --- .../graph-explorer-proxy-server/package.json | 3 +- .../src/node-server.ts | 109 +++++++++++++++--- .../src/connector/fetchDatabaseRequest.ts | 1 + .../CreateConnection/CreateConnection.tsx | 22 ++++ packages/shared/src/types/index.ts | 4 + pnpm-lock.yaml | 51 ++++++++ 6 files changed, 172 insertions(+), 18 deletions(-) diff --git a/packages/graph-explorer-proxy-server/package.json b/packages/graph-explorer-proxy-server/package.json index 6cee6e8f2..0c9bdc5e5 100644 --- a/packages/graph-explorer-proxy-server/package.json +++ b/packages/graph-explorer-proxy-server/package.json @@ -14,6 +14,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-sdk/credential-providers": "^3.758.0", + "@aws-sdk/client-sts": "^3.758.0", "@graph-explorer/shared": "workspace:*", "aws4": "^1.13.2", "body-parser": "^1.20.3", @@ -43,4 +44,4 @@ "tsx": "^4.19.3", "vitest": "^3.0.8" } -} +} \ No newline at end of file diff --git a/packages/graph-explorer-proxy-server/src/node-server.ts b/packages/graph-explorer-proxy-server/src/node-server.ts index 6b8055f2b..952fddc44 100644 --- a/packages/graph-explorer-proxy-server/src/node-server.ts +++ b/packages/graph-explorer-proxy-server/src/node-server.ts @@ -14,17 +14,28 @@ import { clientRoot, proxyServerRoot } from "./paths.js"; import { errorHandlingMiddleware, handleError } from "./error-handler.js"; import { BooleanStringSchema, env } from "./env.js"; import { pipeline } from "stream"; +import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; const app = express(); const DEFAULT_SERVICE_TYPE = "neptune-db"; +interface AwsCredentials { + accessKeyId: string; + secretAccessKey: string; + sessionToken?: string; + expiration?: Date; +} + +const credentialCache: { [roleArn: string]: AwsCredentials } = {}; + interface DbQueryIncomingHttpHeaders extends IncomingHttpHeaders { queryid?: string; "graph-db-connection-url"?: string; "aws-neptune-region"?: string; "service-type"?: string; "db-query-logging-enabled"?: string; + "aws-assume-role-arn"?: string; } interface LoggerIncomingHttpHeaders extends IncomingHttpHeaders { @@ -34,8 +45,15 @@ interface LoggerIncomingHttpHeaders extends IncomingHttpHeaders { app.use(requestLoggingMiddleware()); + +// Function to check if the credentials are valid. +function areCredentialsValid(creds: AwsCredentials): boolean { + return creds.expiration ? new Date(creds.expiration).getTime() - Date.now() > 5 * 60 * 1000 : true; +} + + // Function to get IAM headers for AWS4 signing process. -async function getIAMHeaders(options: string | aws4.Request) { +async function getIAMHeaders(options: string | aws4.Request, region: string | undefined, awsAssumeRoleArn: string | undefined) { const credentialProvider = fromNodeProviderChain(); const creds = await credentialProvider(); if (creds === undefined) { @@ -44,13 +62,54 @@ async function getIAMHeaders(options: string | aws4.Request) { ); } - const headers = aws4.sign(options, { + if (awsAssumeRoleArn !== undefined && awsAssumeRoleArn !== "") { + if (credentialCache[awsAssumeRoleArn] && areCredentialsValid(credentialCache[awsAssumeRoleArn])) { + return aws4.sign(options, { + accessKeyId: credentialCache[awsAssumeRoleArn].accessKeyId, + secretAccessKey: credentialCache[awsAssumeRoleArn].secretAccessKey, + ...(credentialCache[awsAssumeRoleArn].sessionToken && { sessionToken: credentialCache[awsAssumeRoleArn].sessionToken }), + }); + } + + try { + const command = new AssumeRoleCommand({ + RoleArn: awsAssumeRoleArn, + RoleSessionName: "GraphExplorerProxyServer", + }); + const stsClient = new STSClient({ region: region }); + const { Credentials } = await stsClient.send(command); + + if (!Credentials || !Credentials.AccessKeyId || !Credentials.SecretAccessKey || !Credentials.SessionToken) { + throw new Error("Failed to assume role, no credentials returned"); + } + + proxyLogger.debug("Assumed role successfully using the provided role ARN %s, it will expire at: %s", awsAssumeRoleArn, Credentials.Expiration); + credentialCache[awsAssumeRoleArn] = { + accessKeyId: Credentials.AccessKeyId, + secretAccessKey: Credentials.SecretAccessKey, + sessionToken: Credentials.SessionToken, + expiration: Credentials.Expiration, + }; + + return aws4.sign(options, { + accessKeyId: Credentials?.AccessKeyId, + secretAccessKey: Credentials?.SecretAccessKey, + ...(Credentials?.SessionToken && { sessionToken: Credentials?.SessionToken }), + }); + } + catch (error) { + proxyLogger.error("IAM is enabled but credentials cannot be assumed using the provided role ARN: %s, Error: %s", awsAssumeRoleArn, error); + throw new Error( + "IAM is enabled but credentials cannot be assumed using the provided role ARN: %s, Error: %s" + awsAssumeRoleArn + error + ); + } + } + + return aws4.sign(options, { accessKeyId: creds.accessKeyId, secretAccessKey: creds.secretAccessKey, ...(creds.sessionToken && { sessionToken: creds.sessionToken }), }); - - return headers; } // Function to retry fetch requests with exponential backoff. @@ -60,6 +119,7 @@ const retryFetch = async ( isIamEnabled: boolean, region: string | undefined, serviceType: string, + awsAssumeRoleArn: string | undefined, retryDelay = 10000, refetchMaxRetries = 1 ) => { @@ -73,7 +133,7 @@ const retryFetch = async ( region, method: options.method, body: options.body ?? undefined, - }); + }, region, awsAssumeRoleArn); options = { host: url.hostname, @@ -83,7 +143,7 @@ const retryFetch = async ( region, method: options.method, body: options.body ?? undefined, - headers: data.headers, + headers: data?.headers, }; } options = { @@ -94,7 +154,6 @@ const retryFetch = async ( method: options.method, body: options.body ?? undefined, headers: options.headers, - compress: false, // prevent automatic decompression }; try { @@ -130,7 +189,8 @@ async function fetchData( options: RequestInit, isIamEnabled: boolean, region: string | undefined, - serviceType: string + serviceType: string, + awsAssumeRoleArn: string | undefined ) { try { const response = await retryFetch( @@ -138,7 +198,8 @@ async function fetchData( options, isIamEnabled, region, - serviceType + serviceType, + awsAssumeRoleArn ); // Set the headers from the fetch response to the client response @@ -201,6 +262,7 @@ app.post("/sparql", (req, res, next) => { const serviceType = isIamEnabled ? (headers["service-type"] ?? DEFAULT_SERVICE_TYPE) : ""; + const awsAssumeRoleArn = isIamEnabled ? headers["aws-assume-role-arn"] : ""; /// Function to cancel long running queries if the client disappears before completion async function cancelQuery() { @@ -221,7 +283,8 @@ app.post("/sparql", (req, res, next) => { }, isIamEnabled, region, - serviceType + serviceType, + awsAssumeRoleArn ); } catch (err) { // Not really an error @@ -275,7 +338,8 @@ app.post("/sparql", (req, res, next) => { requestOptions, isIamEnabled, region, - serviceType + serviceType, + awsAssumeRoleArn ); }); @@ -293,6 +357,7 @@ app.post("/gremlin", (req, res, next) => { const serviceType = isIamEnabled ? (headers["service-type"] ?? DEFAULT_SERVICE_TYPE) : ""; + const awsAssumeRoleArn = isIamEnabled ? headers["aws-assume-role-arn"] : ""; // Validate the input before making any external calls. const queryString = req.body.query; @@ -320,7 +385,8 @@ app.post("/gremlin", (req, res, next) => { { method: "GET" }, isIamEnabled, region, - serviceType + serviceType, + awsAssumeRoleArn ); } catch (err) { // Not really an error @@ -360,7 +426,8 @@ app.post("/gremlin", (req, res, next) => { requestOptions, isIamEnabled, region, - serviceType + serviceType, + awsAssumeRoleArn ); }); @@ -398,6 +465,7 @@ app.post("/openCypher", (req, res, next) => { const serviceType = isIamEnabled ? (headers["service-type"] ?? DEFAULT_SERVICE_TYPE) : ""; + const awsAssumeRoleArn = isIamEnabled ? headers["aws-assume-role-arn"] : ""; return fetchData( res, @@ -406,7 +474,8 @@ app.post("/openCypher", (req, res, next) => { requestOptions, isIamEnabled, region, - serviceType + serviceType, + awsAssumeRoleArn ); }); @@ -424,6 +493,7 @@ app.get("/summary", (req, res, next) => { }; const region = isIamEnabled ? headers["aws-neptune-region"] : ""; + const awsAssumeRoleArn = isIamEnabled ? headers["aws-assume-role-arn"] : ""; fetchData( res, @@ -432,7 +502,8 @@ app.get("/summary", (req, res, next) => { requestOptions, isIamEnabled, region, - serviceType + serviceType, + awsAssumeRoleArn ); }); @@ -450,6 +521,7 @@ app.get("/pg/statistics/summary", (req, res, next) => { }; const region = isIamEnabled ? headers["aws-neptune-region"] : ""; + const awsAssumeRoleArn = isIamEnabled ? headers["aws-assume-role-arn"] : ""; fetchData( res, @@ -458,7 +530,8 @@ app.get("/pg/statistics/summary", (req, res, next) => { requestOptions, isIamEnabled, region, - serviceType + serviceType, + awsAssumeRoleArn ); }); @@ -476,6 +549,7 @@ app.get("/rdf/statistics/summary", (req, res, next) => { }; const region = isIamEnabled ? headers["aws-neptune-region"] : ""; + const awsAssumeRoleArn = isIamEnabled ? headers["aws-assume-role-arn"] : ""; fetchData( res, @@ -484,7 +558,8 @@ app.get("/rdf/statistics/summary", (req, res, next) => { requestOptions, isIamEnabled, region, - serviceType + serviceType, + awsAssumeRoleArn ); }); diff --git a/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts b/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts index 8f421822f..bb9a4614f 100644 --- a/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts +++ b/packages/graph-explorer/src/connector/fetchDatabaseRequest.ts @@ -71,6 +71,7 @@ function getAuthHeaders( if (connection?.awsAuthEnabled) { headers["aws-neptune-region"] = connection.awsRegion || ""; headers["service-type"] = connection.serviceType || DEFAULT_SERVICE_TYPE; + headers["aws-assume-role-arn"] = connection.awsAssumeRoleArn || ""; } return { ...headers, ...typeHeaders }; diff --git a/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx b/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx index 91152ba32..153bebdcd 100644 --- a/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx +++ b/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx @@ -38,6 +38,7 @@ type ConnectionForm = { awsAuthEnabled?: boolean; serviceType?: NeptuneServiceType; awsRegion?: string; + awsAssumeRoleArn?: string; fetchTimeoutEnabled: boolean; fetchTimeoutMs?: number; nodeExpansionLimitEnabled: boolean; @@ -67,6 +68,7 @@ function mapToConnection(data: Required): ConnectionConfig { awsAuthEnabled: data.awsAuthEnabled, serviceType: data.serviceType, awsRegion: data.awsRegion, + awsAssumeRoleArn: data.awsAssumeRoleArn, fetchTimeoutMs: data.fetchTimeoutEnabled ? data.fetchTimeoutMs : undefined, nodeExpansionLimit: data.nodeExpansionLimitEnabled ? data.nodeExpansionLimit @@ -159,6 +161,7 @@ const CreateConnection = ({ awsAuthEnabled: initialData?.awsAuthEnabled || false, serviceType: initialData?.serviceType || "neptune-db", awsRegion: initialData?.awsRegion || "", + awsAssumeRoleArn: initialData?.awsAssumeRoleArn || "", fetchTimeoutEnabled: initialData?.fetchTimeoutEnabled || false, fetchTimeoutMs: initialData?.fetchTimeoutMs, nodeExpansionLimitEnabled: initialData?.nodeExpansionLimitEnabled || false, @@ -332,6 +335,25 @@ const CreateConnection = ({ onChange={onFormChange("serviceType")} /> +
+ + AWS Assume Role ARN + + ARN of the role that the proxy-server should assume to sign requests. This is only required if + the connector is running outside of the AWS account that + hosts the Neptune resources. + +
+ } + value={form.awsAssumeRoleArn} + onChange={onFormChange("awsAssumeRoleArn")} + placeholder="arn:aws:iam::aws-account-no:role/role-name" + errorMessage="Invalid ARN" + /> + )} diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 178b8a467..fcfa346fa 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -39,6 +39,10 @@ export type ConnectionConfig = { * It is needed to sign requests. */ awsRegion?: string; + /** + * ARN of the role that the proxy-server should assume to sign requests. + */ + awsAssumeRoleArn?: string; /** * Number of milliseconds before aborting a request. * By default, undefined. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59bd8c27b..75abab362 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -428,6 +428,9 @@ importers: packages/graph-explorer-proxy-server: dependencies: + '@aws-sdk/client-sts': + specifier: ^3.758.0 + version: 3.758.0 '@aws-sdk/credential-providers': specifier: ^3.758.0 version: 3.758.0 @@ -570,6 +573,10 @@ packages: resolution: {integrity: sha512-BoGO6IIWrLyLxQG6txJw6RT2urmbtlwfggapNCrNPyYjlXpzTSJhBYjndg7TpDATFd0SXL0zm8y/tXsUXNkdYQ==} engines: {node: '>=18.0.0'} + '@aws-sdk/client-sts@3.758.0': + resolution: {integrity: sha512-ue9hbzjWNQmmyoSeWDRPwnYddsD3BVao5mSFA1kXFNVqWPEenjpkZ1xAlBVzHMMNoEz7LvGI+onXIHntNyiOLQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/core@3.758.0': resolution: {integrity: sha512-0RswbdR9jt/XKemaLNuxi2gGr4xGlHyGxkTdhSQzCyUe9A9OPCoLl3rIESRguQEech+oJnbHk/wuiwHqTuP9sg==} engines: {node: '>=18.0.0'} @@ -5962,6 +5969,50 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/client-sts@3.758.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.758.0 + '@aws-sdk/credential-provider-node': 3.758.0 + '@aws-sdk/middleware-host-header': 3.734.0 + '@aws-sdk/middleware-logger': 3.734.0 + '@aws-sdk/middleware-recursion-detection': 3.734.0 + '@aws-sdk/middleware-user-agent': 3.758.0 + '@aws-sdk/region-config-resolver': 3.734.0 + '@aws-sdk/types': 3.734.0 + '@aws-sdk/util-endpoints': 3.743.0 + '@aws-sdk/util-user-agent-browser': 3.734.0 + '@aws-sdk/util-user-agent-node': 3.758.0 + '@smithy/config-resolver': 4.0.1 + '@smithy/core': 3.1.5 + '@smithy/fetch-http-handler': 5.0.1 + '@smithy/hash-node': 4.0.1 + '@smithy/invalid-dependency': 4.0.1 + '@smithy/middleware-content-length': 4.0.1 + '@smithy/middleware-endpoint': 4.0.6 + '@smithy/middleware-retry': 4.0.7 + '@smithy/middleware-serde': 4.0.2 + '@smithy/middleware-stack': 4.0.1 + '@smithy/node-config-provider': 4.0.1 + '@smithy/node-http-handler': 4.0.3 + '@smithy/protocol-http': 5.0.1 + '@smithy/smithy-client': 4.1.6 + '@smithy/types': 4.1.0 + '@smithy/url-parser': 4.0.1 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.7 + '@smithy/util-defaults-mode-node': 4.0.7 + '@smithy/util-endpoints': 3.0.1 + '@smithy/util-middleware': 4.0.1 + '@smithy/util-retry': 4.0.1 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/core@3.758.0': dependencies: '@aws-sdk/types': 3.734.0 From f74cde3108f521e4d133ec95169a5932e09172f3 Mon Sep 17 00:00:00 2001 From: Siddharth Sheladiya Date: Tue, 4 Mar 2025 12:37:32 -0500 Subject: [PATCH 2/9] fix: access data headers directly --- packages/graph-explorer-proxy-server/src/node-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graph-explorer-proxy-server/src/node-server.ts b/packages/graph-explorer-proxy-server/src/node-server.ts index 952fddc44..c338644fa 100644 --- a/packages/graph-explorer-proxy-server/src/node-server.ts +++ b/packages/graph-explorer-proxy-server/src/node-server.ts @@ -143,7 +143,7 @@ const retryFetch = async ( region, method: options.method, body: options.body ?? undefined, - headers: data?.headers, + headers: data.headers, }; } options = { From 6a2045cd59eb27b444104cd3dca0fa470d724a48 Mon Sep 17 00:00:00 2001 From: Siddharth Sheladiya Date: Tue, 4 Mar 2025 12:45:22 -0500 Subject: [PATCH 3/9] fix: update formating for recent changes --- .../src/node-server.ts | 69 +++++++++++++------ .../CreateConnection/CreateConnection.tsx | 11 +-- packages/shared/src/types/index.ts | 4 +- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/packages/graph-explorer-proxy-server/src/node-server.ts b/packages/graph-explorer-proxy-server/src/node-server.ts index c338644fa..8e90050d3 100644 --- a/packages/graph-explorer-proxy-server/src/node-server.ts +++ b/packages/graph-explorer-proxy-server/src/node-server.ts @@ -45,15 +45,19 @@ interface LoggerIncomingHttpHeaders extends IncomingHttpHeaders { app.use(requestLoggingMiddleware()); - // Function to check if the credentials are valid. function areCredentialsValid(creds: AwsCredentials): boolean { - return creds.expiration ? new Date(creds.expiration).getTime() - Date.now() > 5 * 60 * 1000 : true; + return creds.expiration + ? new Date(creds.expiration).getTime() - Date.now() > 5 * 60 * 1000 + : true; } - // Function to get IAM headers for AWS4 signing process. -async function getIAMHeaders(options: string | aws4.Request, region: string | undefined, awsAssumeRoleArn: string | undefined) { +async function getIAMHeaders( + options: string | aws4.Request, + region: string | undefined, + awsAssumeRoleArn: string | undefined +) { const credentialProvider = fromNodeProviderChain(); const creds = await credentialProvider(); if (creds === undefined) { @@ -63,11 +67,16 @@ async function getIAMHeaders(options: string | aws4.Request, region: string | un } if (awsAssumeRoleArn !== undefined && awsAssumeRoleArn !== "") { - if (credentialCache[awsAssumeRoleArn] && areCredentialsValid(credentialCache[awsAssumeRoleArn])) { + if ( + credentialCache[awsAssumeRoleArn] && + areCredentialsValid(credentialCache[awsAssumeRoleArn]) + ) { return aws4.sign(options, { accessKeyId: credentialCache[awsAssumeRoleArn].accessKeyId, secretAccessKey: credentialCache[awsAssumeRoleArn].secretAccessKey, - ...(credentialCache[awsAssumeRoleArn].sessionToken && { sessionToken: credentialCache[awsAssumeRoleArn].sessionToken }), + ...(credentialCache[awsAssumeRoleArn].sessionToken && { + sessionToken: credentialCache[awsAssumeRoleArn].sessionToken, + }), }); } @@ -79,11 +88,20 @@ async function getIAMHeaders(options: string | aws4.Request, region: string | un const stsClient = new STSClient({ region: region }); const { Credentials } = await stsClient.send(command); - if (!Credentials || !Credentials.AccessKeyId || !Credentials.SecretAccessKey || !Credentials.SessionToken) { + if ( + !Credentials || + !Credentials.AccessKeyId || + !Credentials.SecretAccessKey || + !Credentials.SessionToken + ) { throw new Error("Failed to assume role, no credentials returned"); } - proxyLogger.debug("Assumed role successfully using the provided role ARN %s, it will expire at: %s", awsAssumeRoleArn, Credentials.Expiration); + proxyLogger.debug( + "Assumed role successfully using the provided role ARN %s, it will expire at: %s", + awsAssumeRoleArn, + Credentials.Expiration + ); credentialCache[awsAssumeRoleArn] = { accessKeyId: Credentials.AccessKeyId, secretAccessKey: Credentials.SecretAccessKey, @@ -94,13 +112,18 @@ async function getIAMHeaders(options: string | aws4.Request, region: string | un return aws4.sign(options, { accessKeyId: Credentials?.AccessKeyId, secretAccessKey: Credentials?.SecretAccessKey, - ...(Credentials?.SessionToken && { sessionToken: Credentials?.SessionToken }), + ...(Credentials?.SessionToken && { + sessionToken: Credentials?.SessionToken, + }), }); - } - catch (error) { - proxyLogger.error("IAM is enabled but credentials cannot be assumed using the provided role ARN: %s, Error: %s", awsAssumeRoleArn, error); + } catch (error) { + proxyLogger.error( + "IAM is enabled but credentials cannot be assumed using the provided role ARN: %s, Error: %s", + awsAssumeRoleArn, + error + ); throw new Error( - "IAM is enabled but credentials cannot be assumed using the provided role ARN: %s, Error: %s" + awsAssumeRoleArn + error + "IAM is enabled but credentials cannot be assumed using the provided role ARN" ); } } @@ -125,15 +148,19 @@ const retryFetch = async ( ) => { for (let i = 0; i < refetchMaxRetries; i++) { if (isIamEnabled) { - const data = await getIAMHeaders({ - host: url.hostname, - port: url.port, - path: url.pathname + url.search, - service: serviceType, + const data = await getIAMHeaders( + { + host: url.hostname, + port: url.port, + path: url.pathname + url.search, + service: serviceType, + region, + method: options.method, + body: options.body ?? undefined, + }, region, - method: options.method, - body: options.body ?? undefined, - }, region, awsAssumeRoleArn); + awsAssumeRoleArn + ); options = { host: url.hostname, diff --git a/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx b/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx index 153bebdcd..2c4d60327 100644 --- a/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx +++ b/packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx @@ -339,12 +339,15 @@ const CreateConnection = ({ +
AWS Assume Role ARN - ARN of the role that the proxy-server should assume to sign requests. This is only required if - the connector is running outside of the AWS account that - hosts the Neptune resources. + ARN of the role that the proxy-server should assume to + sign requests. This is only required if the connector is + running outside of the AWS account that hosts the Neptune + resources.
} diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index fcfa346fa..c335b15a0 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -40,8 +40,8 @@ export type ConnectionConfig = { */ awsRegion?: string; /** - * ARN of the role that the proxy-server should assume to sign requests. - */ + * ARN of the role that the proxy-server should assume to sign requests. + */ awsAssumeRoleArn?: string; /** * Number of milliseconds before aborting a request. From 5b96d96ecf824f2026e37f8eb91bfefd1ac7177c Mon Sep 17 00:00:00 2001 From: Siddharth Sheladiya Date: Tue, 4 Mar 2025 13:05:25 -0500 Subject: [PATCH 4/9] chore: update changelog to reflect latest changes --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index 7d68b56c9..ad390d833 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,8 @@ ## Upcoming +- **Improved** accessing neptune using assume role + ([#813](https://github.com/aws/graph-explorer/pull/818)) - **Fixed** issue with long node titles or descriptions pushing the "add to graph" button off the screen ([#824](https://github.com/aws/graph-explorer/pull/824)) From 04899a6364586bc5f4edf68e8e9dad37c3da91d2 Mon Sep 17 00:00:00 2001 From: Siddharth Sheladiya Date: Tue, 4 Mar 2025 13:11:00 -0500 Subject: [PATCH 5/9] fix: revert unintended changes --- packages/graph-explorer-proxy-server/src/node-server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/graph-explorer-proxy-server/src/node-server.ts b/packages/graph-explorer-proxy-server/src/node-server.ts index 8e90050d3..26a5bf3d1 100644 --- a/packages/graph-explorer-proxy-server/src/node-server.ts +++ b/packages/graph-explorer-proxy-server/src/node-server.ts @@ -181,6 +181,7 @@ const retryFetch = async ( method: options.method, body: options.body ?? undefined, headers: options.headers, + compress: false, // prevent automatic decompression }; try { From 0fdb2f650d6e880c4a5d5ce5ccdfc24e9302a27c Mon Sep 17 00:00:00 2001 From: Siddharth Sheladiya Date: Thu, 6 Mar 2025 22:35:36 -0500 Subject: [PATCH 6/9] fix: formatting in package.json --- packages/graph-explorer-proxy-server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graph-explorer-proxy-server/package.json b/packages/graph-explorer-proxy-server/package.json index 0c9bdc5e5..9dffbf8d2 100644 --- a/packages/graph-explorer-proxy-server/package.json +++ b/packages/graph-explorer-proxy-server/package.json @@ -44,4 +44,4 @@ "tsx": "^4.19.3", "vitest": "^3.0.8" } -} \ No newline at end of file +} From 61e426d942ced6c2fecff2b283ee98d558a1aff0 Mon Sep 17 00:00:00 2001 From: Siddharth Sheladiya Date: Fri, 7 Mar 2025 10:33:17 -0500 Subject: [PATCH 7/9] fix: PR number in changelog.md --- Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index ad390d833..f5ade3351 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,7 +3,7 @@ ## Upcoming - **Improved** accessing neptune using assume role - ([#813](https://github.com/aws/graph-explorer/pull/818)) + ([#818](https://github.com/aws/graph-explorer/pull/818)) - **Fixed** issue with long node titles or descriptions pushing the "add to graph" button off the screen ([#824](https://github.com/aws/graph-explorer/pull/824)) From dfc34dc75fc80886c10f1951c2dc8299735ea69d Mon Sep 17 00:00:00 2001 From: Siddharth Sheladiya Date: Wed, 12 Mar 2025 10:34:37 -0400 Subject: [PATCH 8/9] fix: separate getCredentials and signing to better handle errors at multiple levels --- .../src/node-server.ts | 65 ++++++++++--------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/graph-explorer-proxy-server/src/node-server.ts b/packages/graph-explorer-proxy-server/src/node-server.ts index 26a5bf3d1..f881174b8 100644 --- a/packages/graph-explorer-proxy-server/src/node-server.ts +++ b/packages/graph-explorer-proxy-server/src/node-server.ts @@ -52,34 +52,17 @@ function areCredentialsValid(creds: AwsCredentials): boolean { : true; } -// Function to get IAM headers for AWS4 signing process. -async function getIAMHeaders( - options: string | aws4.Request, - region: string | undefined, - awsAssumeRoleArn: string | undefined +async function getCredentials( + awsAssumeRoleArn: string | undefined, + region: string | undefined ) { - const credentialProvider = fromNodeProviderChain(); - const creds = await credentialProvider(); - if (creds === undefined) { - throw new Error( - "IAM is enabled but credentials cannot be found on the credential provider chain." - ); - } - if (awsAssumeRoleArn !== undefined && awsAssumeRoleArn !== "") { if ( credentialCache[awsAssumeRoleArn] && areCredentialsValid(credentialCache[awsAssumeRoleArn]) ) { - return aws4.sign(options, { - accessKeyId: credentialCache[awsAssumeRoleArn].accessKeyId, - secretAccessKey: credentialCache[awsAssumeRoleArn].secretAccessKey, - ...(credentialCache[awsAssumeRoleArn].sessionToken && { - sessionToken: credentialCache[awsAssumeRoleArn].sessionToken, - }), - }); + return credentialCache[awsAssumeRoleArn]; } - try { const command = new AssumeRoleCommand({ RoleArn: awsAssumeRoleArn, @@ -109,25 +92,45 @@ async function getIAMHeaders( expiration: Credentials.Expiration, }; - return aws4.sign(options, { - accessKeyId: Credentials?.AccessKeyId, - secretAccessKey: Credentials?.SecretAccessKey, - ...(Credentials?.SessionToken && { - sessionToken: Credentials?.SessionToken, - }), - }); + return credentialCache[awsAssumeRoleArn]; } catch (error) { proxyLogger.error( "IAM is enabled but credentials cannot be assumed using the provided role ARN: %s, Error: %s", awsAssumeRoleArn, error ); - throw new Error( - "IAM is enabled but credentials cannot be assumed using the provided role ARN" - ); + return undefined; } } + const credentialProvider = fromNodeProviderChain(); + const creds = await credentialProvider(); + if (creds === undefined) { + proxyLogger.error( + "IAM is enabled but credentials cannot be found on the credential provider chain." + ); + return undefined; + } + + return { + accessKeyId: creds.accessKeyId, + secretAccessKey: creds.secretAccessKey, + sessionToken: creds.sessionToken, + }; +} + +// Function to get IAM headers for AWS4 signing process. +async function getIAMHeaders( + options: string | aws4.Request, + region: string | undefined, + awsAssumeRoleArn: string | undefined +) { + const creds = await getCredentials(awsAssumeRoleArn, region); + if (!creds) { + throw new Error( + "IAM is enabled but credentials cannot be found or assumed." + ); + } return aws4.sign(options, { accessKeyId: creds.accessKeyId, secretAccessKey: creds.secretAccessKey, From 90fedc119660f41c18c387f111570d1dec3776df Mon Sep 17 00:00:00 2001 From: Siddharth Sheladiya Date: Thu, 13 Mar 2025 18:28:54 -0400 Subject: [PATCH 9/9] fix: refresh connection when assume role arn is changed --- packages/graph-explorer/src/core/connector.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/graph-explorer/src/core/connector.ts b/packages/graph-explorer/src/core/connector.ts index fb19de244..3904c611f 100644 --- a/packages/graph-explorer/src/core/connector.ts +++ b/packages/graph-explorer/src/core/connector.ts @@ -40,6 +40,7 @@ const activeConnectionSelector = equalSelector({ "graphDbUrl", "awsAuthEnabled", "awsRegion", + "awsAssumeRoleArn", "fetchTimeoutMs", "nodeExpansionLimit", ] as (keyof ConnectionConfig)[];