This repository was archived by the owner on Jun 9, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 18
feat: fetch secrets from OneGraph as-needed #107
Merged
ascorbic
merged 5 commits into
netlify:feat/secrets-handling
from
dwwoelfel:feat/secrets-handling-dynamic
Aug 4, 2021
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
f822157
Make secrets lookup just-in-time and dynamic
d0dd0bf
fix: fix ci issues
393a711
feat: update TypeScript definitions and auth metadata
e5f8842
feat: add warning if Autlify has not been set up
dwwoelfel 6c0d8e3
feat: remove withContext and implement review feedback
dwwoelfel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,32 @@ | ||
import { Context } from '../function/context' | ||
import { Handler } from '../function/handler' | ||
import * as services from './services.json' | ||
|
||
export type Services = typeof services | ||
|
||
export type ServiceKey = keyof Services | ||
|
||
export type ServiceTokens<T extends ServiceKey> = Services[T]['tokens'] | ||
export type ScopeInfo = { | ||
category: string | null | ||
scope: string | ||
display: string | ||
isDefault: boolean | ||
isRequired: boolean | ||
description: string | null | ||
title: string | null | ||
} | ||
|
||
export type NetlifySecrets = { | ||
[K in ServiceKey]?: ServiceTokens<K> | ||
export type Scope = { | ||
scope: string | ||
scopeInfo: ScopeInfo | null | ||
} | ||
|
||
export interface ContextWithSecrets extends Context { | ||
secrets: NetlifySecrets | ||
export type Service = { | ||
friendlyServiceName: string | ||
service: string | ||
isLoggedIn: boolean | ||
bearerToken: string | null | ||
grantedScopes: Array<Scope> | null | ||
} | ||
|
||
export type HandlerWithSecrets = Handler<ContextWithSecrets> | ||
export type NetlifySecrets = { | ||
gitHub?: Service | null | ||
spotify?: Service | null | ||
salesforce?: Service | null | ||
stripe?: Service | null | ||
} | ||
|
||
export declare const getSecrets: () => NetlifySecrets | ||
|
||
export declare const withSecrets: <C extends Context>(handler: HandlerWithSecrets) => Handler<C> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,140 @@ | ||
const { Buffer } = require('buffer') | ||
const https = require('https') | ||
const process = require('process') | ||
|
||
const services = require('./services.json') | ||
const { ONEGRAPH_AUTHLIFY_APP_ID } = require('./consts') | ||
|
||
const getSecrets = () => | ||
Object.entries(services).reduce((secrets, [serviceName, service]) => { | ||
const serviceSecrets = [] | ||
// This is so if there are no secrets we don't add an empty object | ||
Object.entries(service.tokens).forEach(([tokenName, token]) => { | ||
if (token in process.env) { | ||
serviceSecrets.push([tokenName, process.env[token]]) | ||
const camelize = function (text) { | ||
const safe = text.replace(/[-_\s.]+(.)?/g, (_, sub) => (sub ? sub.toUpperCase() : '')) | ||
return safe.slice(0, 1).toLowerCase() + safe.slice(1) | ||
} | ||
|
||
// The services will be camelized versions of the OneGraph service enums | ||
// unless overridden by the serviceNormalizeOverrides object | ||
const serviceNormalizeOverrides = { | ||
// Keys are the OneGraph service enums, values are the desired `secret.<service>` names | ||
GITHUB: 'gitHub', | ||
} | ||
|
||
const oneGraphRequest = function (secretToken, requestBody) { | ||
// eslint-disable-next-line node/no-unsupported-features/node-builtins | ||
const requestBodyBuffer = Buffer.from(new TextEncoder().encode(requestBody)) | ||
return new Promise((resolve, reject) => { | ||
const port = 443 | ||
|
||
const options = { | ||
host: 'serve.onegraph.com', | ||
path: `/graphql?app_id=${ONEGRAPH_AUTHLIFY_APP_ID}`, | ||
port, | ||
method: 'POST', | ||
headers: { | ||
Authorization: `Bearer ${secretToken}`, | ||
'Content-Type': 'application/json', | ||
Accept: 'application/json', | ||
'Content-Length': Buffer.byteLength(requestBodyBuffer), | ||
}, | ||
} | ||
|
||
const req = https.request(options, (res) => { | ||
if (res.statusCode !== 200) { | ||
return reject(new Error(res.statusCode)) | ||
} | ||
|
||
let body = [] | ||
|
||
res.on('data', (chunk) => { | ||
body.push(chunk) | ||
}) | ||
|
||
res.on('end', () => { | ||
const data = Buffer.concat(body).toString() | ||
try { | ||
body = JSON.parse(data) | ||
} catch (error) { | ||
reject(error) | ||
} | ||
resolve(body) | ||
}) | ||
}) | ||
if (serviceSecrets.length !== 0) { | ||
// No Object.fromEntries in node < 12 | ||
return { | ||
...secrets, | ||
[serviceName]: serviceSecrets.reduce((acc, [tokenName, token]) => ({ ...acc, [tokenName]: token }), {}), | ||
|
||
req.on('error', (error) => { | ||
reject(error.message) | ||
}) | ||
|
||
req.write(requestBodyBuffer) | ||
|
||
req.end() | ||
}) | ||
} | ||
|
||
const formatSecrets = (result) => { | ||
const services = | ||
result.data && result.data.me && result.data.me.serviceMetadata && result.data.me.serviceMetadata.loggedInServices | ||
|
||
if (services) { | ||
const newSecrets = services.reduce((acc, service) => { | ||
const normalized = serviceNormalizeOverrides[service.service] || camelize(service.friendlyServiceName) | ||
// eslint-disable-next-line no-param-reassign | ||
acc[normalized] = service | ||
return acc | ||
}, {}) | ||
|
||
return newSecrets | ||
} | ||
|
||
return {} | ||
} | ||
|
||
// Note: We may want to have configurable "sets" of secrets, | ||
// e.g. "dev" and "prod" | ||
const getSecrets = async () => { | ||
const secretToken = process.env.ONEGRAPH_AUTHLIFY_TOKEN | ||
|
||
if (!secretToken) { | ||
console.warn( | ||
'getSecrets is not set up. Visit Netlify Labs to enable it or trigger a new deploy if it has been enabled.', | ||
) | ||
return {} | ||
} | ||
|
||
// We select for more than we typically need here | ||
// in order to allow for some metaprogramming for | ||
// consumers downstream. Also, the data is typically | ||
// static and shouldn't add any measurable overhead. | ||
const doc = `query FindLoggedInServicesQuery { | ||
me { | ||
serviceMetadata { | ||
loggedInServices { | ||
friendlyServiceName | ||
service | ||
isLoggedIn | ||
bearerToken | ||
grantedScopes { | ||
scope | ||
scopeInfo { | ||
category | ||
scope | ||
display | ||
isDefault | ||
isRequired | ||
description | ||
title | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return secrets | ||
}, {}) | ||
}` | ||
|
||
const body = JSON.stringify({ query: doc }) | ||
|
||
const result = await oneGraphRequest(secretToken, body) | ||
|
||
const newSecrets = formatSecrets(result) | ||
|
||
// eslint-disable-next-line promise/prefer-await-to-callbacks | ||
const withSecrets = (handler) => (event, context, callback) => { | ||
const secrets = getSecrets() | ||
return handler(event, { ...context, secrets }, callback) | ||
return newSecrets | ||
} | ||
|
||
module.exports = { | ||
getSecrets, | ||
withSecrets, | ||
} |
This file was deleted.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think optional chaining is what I miss most when not transpiling 😁