Skip to content

Commit f8632d5

Browse files
committed
Initial commit
0 parents  commit f8632d5

14 files changed

+375
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
coverage

.npmignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
tests
2+
examples
3+
coverage

index.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
3+
const getObjectPath = require('./lib/utils').getObjectPath;
4+
const executeLambdaCallback = require('./lib/lambda-callback').executeLambdaCallback;
5+
const authorizerValidationCallback = require('./lib/authorizer-callback').authorizerValidationCallback;
6+
const authorizerCheckCallback = require('./lib/authorizer-callback').authorizerCheckCallback;
7+
const decorateLambdaReqCallback = require('./lib/decorators-callback').decorateLambdaReqCallback;
8+
const decorateAddCORSCallback = require('./lib/decorators-callback').decorateAddCORSCallback;
9+
10+
module.exports = ({ html, authorizers }) => (slsConf, slsHandlers) => {
11+
for (let funcId in slsConf.functions) {
12+
const funcConf = slsConf.functions[funcId];
13+
let func = getObjectPath(funcConf.handler, slsHandlers);
14+
const events = (funcConf.events || []).map(e => e.http).filter(e => !!e);
15+
for (let e of events) {
16+
const authSource = getObjectPath(['authorizer', 'identitySource'], e);
17+
const authValidatorExp = getObjectPath(['authorizer', 'identityValidationExpression'], e);
18+
const authorizerFunction = (
19+
getObjectPath(getObjectPath(['authorizer', 'arn'], e) || '', authorizers) ||
20+
getObjectPath(getObjectPath(['authorizer', 'name'], e) || '', slsHandlers)
21+
);
22+
html(
23+
e.method.toLowerCase(),
24+
`/${e.path.replace(/\{(.+?)\}/g, ':$1')}`,
25+
[
26+
decorateLambdaReqCallback(),
27+
...(authorizerFunction ? [
28+
authorizerValidationCallback(
29+
authSource,
30+
authValidatorExp ? new RegExp(authValidatorExp) : null,
31+
{
32+
regionId: 'express',
33+
accountId: 'serverlessify',
34+
apiId: `${slsConf.service}-${funcId}`,
35+
}
36+
),
37+
authorizerCheckCallback(authorizerFunction)
38+
] : []),
39+
...(e.cors ? [decorateAddCORSCallback()] : []),
40+
executeLambdaCallback(func),
41+
]
42+
)
43+
}
44+
}
45+
};

lib/authorizer-callbacks.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use strict';
2+
3+
const getObjectPath = require('./utils').getObjectPath;
4+
5+
module.exports.authorizerValidationCallback = function(
6+
identitySource,
7+
identityValidationRegExp,
8+
{
9+
regionId,
10+
accountId,
11+
apiId,
12+
}
13+
) {
14+
return function(req, res, next) {
15+
const sources = {
16+
method: {
17+
request: {
18+
header: req.headers,
19+
},
20+
},
21+
};
22+
const source = getObjectPath(
23+
(identitySource || 'method.request.header.Auth').toLowerCase(),
24+
sources
25+
);
26+
if (identityValidationRegExp && identityValidationRegExp.exec(source) === null) {
27+
res.sendStatus(403);
28+
return;
29+
}
30+
const method = req.method.toUpperCase();
31+
const resourcePath = req.path;
32+
req.lambda = (req.lambda || {});
33+
req.lambda.authorizer = {
34+
event: {
35+
type: 'TOKEN',
36+
authorizationToken: source,
37+
methodArn: `arn:serverlessify:execute-api:${regionId}:${accountId}:${apiId}/${method}${resourcePath}`,
38+
},
39+
};
40+
next();
41+
};
42+
};
43+
44+
// event format
45+
// {
46+
// "type":"TOKEN",
47+
// "authorizationToken":"<caller-supplied-token>",
48+
// "methodArn":"arn:aws:execute-api:<regionId>:<accountId>:<apiId>/<stage>/<method>/<resourcePath>"
49+
// }
50+
// awsAuthDocument format
51+
// {
52+
// // The principal user identification associated with the token send by the client.
53+
// "principalId": "xxxxxxxx",
54+
// "policyDocument": {
55+
// "Version": "2012-10-17",
56+
// "Statement": [
57+
// {
58+
// "Action": "execute-api:Invoke",
59+
// "Effect": "Allow|Deny",
60+
// "Resource": "arn:aws:execute-api:<regionId>:<accountId>:<appId>/<stage>/<httpVerb>/[<resource>/<httpVerb>/[...]]"
61+
// }
62+
// ]
63+
// }
64+
// }
65+
module.exports.authorizerCheckCallback = function(
66+
authorizer,
67+
setCacheEntry,
68+
getCacheEntry
69+
) {
70+
return function(req, res, next) {
71+
if (!authorizer) {
72+
return next();
73+
}
74+
75+
function authorizerCallback(err, awsAuthDocument) {
76+
// TODO addCacheEntry(req.lambda.authorizer.event.authorizationToken, awsAuthDocument).then
77+
if (err) {
78+
res.status(500).send(err);
79+
} else {
80+
// TODO consider other statement values
81+
const effect = getObjectPath(
82+
['policyDocument', 'Statement', 0, 'Effect'],
83+
awsAuthDocument
84+
);
85+
if (effect === 'Allow') {
86+
next();
87+
} else {
88+
res.sendStatus(401);
89+
}
90+
}
91+
}
92+
93+
const cacheKey = req.lambda.authorizer.event.authorizationToken;
94+
getCacheEntry(
95+
cacheKey,
96+
function(err, cachedAuthorization) {
97+
if (cachedAuthorization) {
98+
authorizerCallback(err, cachedAuthorization);
99+
} else {
100+
authorizer(
101+
req.lambda.authorizer.event,
102+
req.lambda.context,
103+
function(innerErr, awsAuthDocument) {
104+
setCacheEntry({
105+
key: cacheKey,
106+
value: awsAuthDocument,
107+
}, function() {
108+
authorizerCallback(innerErr, awsAuthDocument);
109+
});
110+
}
111+
);
112+
}
113+
}
114+
);
115+
};
116+
};

lib/decorators-callbacks.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
module.exports.decorateLambdaReqCallback = function(getPrincipalId) {
4+
return function(req, res, next) {
5+
req.lambda = {
6+
event: {
7+
method: req.method,
8+
headers: req.headers,
9+
body: req.body,
10+
path: req.params,
11+
query: req.query,
12+
principalId: getPrincipalId && getPrincipalId(req),
13+
// stageVariables, // TODO define stageVariables
14+
},
15+
context: {},
16+
};
17+
next();
18+
};
19+
};
20+
21+
module.exports.decorateAddCORSCallback = function() {
22+
return function(req, res, next) {
23+
res.header('Access-Control-Allow-Origin', '*');
24+
res.header('Access-Control-Allow-Methods', 'GET,PUT,HEAD,PATCH,POST,DELETE,OPTIONS');
25+
res.header('Access-Control-Allow-Headers', 'Authorization,Content-Type,x-amz-date,x-amz-security-token');
26+
next();
27+
};
28+
};

lib/lambda-callbacks.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use strict';
2+
3+
module.exports.executeLambdaCallback = function(handlerFunction) {
4+
return function(req, res) {
5+
handlerFunction(
6+
req.lambda.event,
7+
req.lambda.context,
8+
function(err, resp) {
9+
if (err) {
10+
res.status(500).send(err);
11+
} else {
12+
res.status(200).send(resp);
13+
}
14+
}
15+
);
16+
};
17+
};

lib/utils.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
3+
module.exports.getObjectPath = function(path, obj) {
4+
let res = obj;
5+
const ps = Array.isArray(path) ? path : path.split('.');
6+
for (let p of ps) {
7+
if (!res) {
8+
break;
9+
}
10+
res = res[p];
11+
}
12+
return res;
13+
};

package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "serverlessify",
3+
"version": "1.0.0",
4+
"description": "Utility to simulate AWS Lambda event sources and wirte a server for Serverless functions",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "istanbul cover _mocha tests/all.test.js -- -R spec --recursive"
8+
},
9+
"keywords": [
10+
"seerverless",
11+
"aws",
12+
"lambda",
13+
"backend",
14+
"express"
15+
],
16+
"author": "Nicola Peduzzi <[email protected]> (http://nikso.net)",
17+
"license": "MIT",
18+
"devDependencies": {
19+
"chai": "3.5.0",
20+
"istanbul": "0.4.5",
21+
"mocha": "3.0.2",
22+
"sinon": "1.17.5",
23+
"sinon-chai": "2.8.0"
24+
}
25+
}

tests/all.test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
describe('serverlessify', () => {
2+
require('./utils.test');
3+
require('./decorators-callbacks.test');
4+
require('./authorizer-callbacks.test');
5+
require('./lambda-callbacks.test');
6+
require('./index.test');
7+
});

tests/authorizer-callbacks.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const expect = require('chai').expect;
2+
const sinon = require('sinon');
3+
const subject = require('../lib/authorizer-callbacks');
4+
require('chai').use(require('sinon-chai'));
5+
6+
describe('utils', () => {
7+
it('should define callbacks', () => {
8+
expect(subject.authorizerValidationCallback).to.be.a('function');
9+
expect(subject.authorizerCheckCallback).to.be.a('function');
10+
});
11+
});
12+

0 commit comments

Comments
 (0)