Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update storage trigger samples #1271

Merged
merged 4 commits into from
May 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 74 additions & 71 deletions functions/composer-storage-trigger/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const FormData = require('form-data');
* @param {!Object} event The Cloud Functions event.
* @returns {Promise}
*/
exports.triggerDag = function triggerDag(event) {
exports.triggerDag = async event => {
// Fill in your Composer environment information here.

// The project that holds your function
Expand All @@ -50,23 +50,27 @@ exports.triggerDag = function triggerDag(event) {
const BODY = {conf: JSON.stringify(event.data)};

// Make the request
return authorizeIap(CLIENT_ID, PROJECT_ID, USER_AGENT).then(iap => {
try {
const iap = await authorizeIap(CLIENT_ID, PROJECT_ID, USER_AGENT);

return makeIapPostRequest(
WEBSERVER_URL,
BODY,
iap.idToken,
USER_AGENT,
iap.jwt
);
});
} catch (err) {
ace-n marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(err);
}
};

/**
* @param {string} clientId The client id associated with the Composer webserver application.
* @param {string} projectId The id for the project containing the Cloud Function.
* @param {string} userAgent The user agent string which will be provided with the webserver request.
*/
function authorizeIap(clientId, projectId, userAgent) {
const authorizeIap = async (clientId, projectId, userAgent) => {
const SERVICE_ACCOUNT = `${projectId}@appspot.gserviceaccount.com`;
const JWT_HEADER = Buffer.from(
JSON.stringify({alg: 'RS256', typ: 'JWT'})
Expand All @@ -76,89 +80,88 @@ function authorizeIap(clientId, projectId, userAgent) {
let jwtClaimset = '';

// Obtain an Oauth2 access token for the appspot service account
return fetch(
const res = await fetch(
`http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/${SERVICE_ACCOUNT}/token`,
{
headers: {'User-Agent': userAgent, 'Metadata-Flavor': 'Google'},
}
)
.then(res => res.json())
.then(tokenResponse => {
if (tokenResponse.error) {
return Promise.reject(tokenResponse.error);
}
const accessToken = tokenResponse.access_token;
const iat = Math.floor(new Date().getTime() / 1000);
const claims = {
iss: SERVICE_ACCOUNT,
aud: 'https://www.googleapis.com/oauth2/v4/token',
iat: iat,
exp: iat + 60,
target_audience: clientId,
};
jwtClaimset = Buffer.from(JSON.stringify(claims)).toString('base64');
const toSign = [JWT_HEADER, jwtClaimset].join('.');

return fetch(
`https://iam.googleapis.com/v1/projects/${projectId}/serviceAccounts/${SERVICE_ACCOUNT}:signBlob`,
{
method: 'POST',
body: JSON.stringify({
bytesToSign: Buffer.from(toSign).toString('base64'),
}),
headers: {
'User-Agent': userAgent,
Authorization: `Bearer ${accessToken}`,
},
}
);
})
.then(res => res.json())
.then(body => {
if (body.error) {
return Promise.reject(body.error);
}
// Request service account signature on header and claimset
const jwtSignature = body.signature;
jwt = [JWT_HEADER, jwtClaimset, jwtSignature].join('.');
const form = new FormData();
form.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
form.append('assertion', jwt);
return fetch('https://www.googleapis.com/oauth2/v4/token', {
method: 'POST',
body: form,
});
})
.then(res => res.json())
.then(body => {
if (body.error) {
return Promise.reject(body.error);
}
return {
jwt: jwt,
idToken: body.id_token,
};
});
}
);
const tokenResponse = res.json();
if (tokenResponse.error) {
return Promise.reject(tokenResponse.error);
}

const accessToken = tokenResponse.access_token;
const iat = Math.floor(new Date().getTime() / 1000);
const claims = {
iss: SERVICE_ACCOUNT,
aud: 'https://www.googleapis.com/oauth2/v4/token',
iat: iat,
exp: iat + 60,
target_audience: clientId,
};
jwtClaimset = Buffer.from(JSON.stringify(claims)).toString('base64');
const toSign = [JWT_HEADER, jwtClaimset].join('.');

const blob = await fetch(
`https://iam.googleapis.com/v1/projects/${projectId}/serviceAccounts/${SERVICE_ACCOUNT}:signBlob`,
{
method: 'POST',
body: JSON.stringify({
bytesToSign: Buffer.from(toSign).toString('base64'),
}),
headers: {
'User-Agent': userAgent,
Authorization: `Bearer ${accessToken}`,
},
}
);
const blobJson = blob.json();
if (blobJson.error) {
return Promise.reject(blobJson.error);
}

// Request service account signature on header and claimset
const jwtSignature = blobJson.signature;
jwt = [JWT_HEADER, jwtClaimset, jwtSignature].join('.');
const form = new FormData();
form.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
form.append('assertion', jwt);

const token = await fetch('https://www.googleapis.com/oauth2/v4/token', {
method: 'POST',
body: form,
});
const tokenJson = token.json();
if (tokenJson.error) {
return Promise.reject(tokenJson.error);
}

return {
jwt: jwt,
idToken: tokenJson.id_token,
};
};

/**
* @param {string} url The url that the post request targets.
* @param {string} body The body of the post request.
* @param {string} idToken Bearer token used to authorize the iap request.
* @param {string} userAgent The user agent to identify the requester.
*/
function makeIapPostRequest(url, body, idToken, userAgent) {
return fetch(url, {
const makeIapPostRequest = async (url, body, idToken, userAgent) => {
const res = await fetch(url, {
method: 'POST',
headers: {
'User-Agent': userAgent,
Authorization: `Bearer ${idToken}`,
},
body: JSON.stringify(body),
}).then(res => {
if (!res.ok) {
return res.text().then(body => Promise.reject(body));
}
});
}

if (!res.ok) {
const err = await res.text();
throw new Error(err);
}
};
// [END composer_trigger]
7 changes: 4 additions & 3 deletions functions/composer-storage-trigger/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@ it('Handles error in JSON body', async () => {
const expectedMsg = 'Something bad happened.';
const bodyJson = {error: expectedMsg};
const body = {
json: sinon.stub().resolves(bodyJson),
json: sinon.stub().returns(bodyJson),
};
const sample = getSample(sinon.stub().resolves(body));

try {
await sample.program.triggerDag(event);
assert.fail('No error thrown');
} catch (err) {
assert.strictEqual(new RegExp(/Something bad happened/).test(err), true);
assert.deepStrictEqual(err, new Error('Something bad happened.'));
}
});

Expand Down Expand Up @@ -85,6 +86,6 @@ it('Handles error in IAP response.', async () => {
try {
await sample.program.triggerDag(event);
} catch (err) {
assert.strictEqual(err, expectedMsg);
assert.deepStrictEqual(err, new Error(expectedMsg));
}
});