Skip to content

Instantly share code, notes, and snippets.

@asciimike
Created September 11, 2018 21:04
Show Gist options
  • Save asciimike/b79a9a1038037dd046b0ff627fe9e286 to your computer and use it in GitHub Desktop.
Save asciimike/b79a9a1038037dd046b0ff627fe9e286 to your computer and use it in GitHub Desktop.
Example of a "serverless" system
function logger(string) {
console.log("Logging [" + Date.now() + "]: " + string);
}
{
"/users/:userId": {
"GET": {
"condition": false,
"action": "logger('getting a userId')"
},
"PUT": {
"condition": "userId == req.auth.sub && data.name != null",
"action": "logger('putting a userId')"
}
}
}
var bodyParser = require('body-parser');
var jwt_decode = require('jwt-decode');
var express = require('express');
var app = express();
var utils = require('./utils.js');
var RULES_FILE = 'rules.json';
var ACTIONS_FILE = 'actions.js';
// Authentication middleware
var authenticate = function(req, res, next) {
// Parse JWT if one exists
var token = req.query.token || ''
var auth;
if (token) {
auth = jwt_decode(token);
}
// Validate the JWT, left as an exercise for the reader
// Check that it's not expired
// Check the signature (cert from the issuer)
// If malformed or incorrect, send 401
if (!auth) {
return res.status(401).jsonp({ error: 'Unauthenticated' });
}
req.auth = auth;
next();
};
app.use(authenticate);
// Admin credential checks
function admin(req, res, next) {
if (req.auth.isAdmin != true) {
return res.status(401).jsonp({ error: 'Operation requires admin credentials' });
}
next();
}
// Handle action uploads
app.put('/.actions', admin, bodyParser.text({ type: 'text/javascript' }), function(req, res) {
utils.putFile(ACTIONS_FILE, new Buffer(req.body)).then(function() {
return res.status(200).jsonp("ok");
}).catch(function(error) {
// Handle bad action deploys
return res.status(500).jsonp({ error: error });
});
});
app.use(bodyParser.json());
// Handle rules uploads
app.put('/.rules', admin, function(req, res) {
utils.putFile(RULES_FILE, JSON.stringify(req.body)).then(function() {
return res.status(200).jsonp("ok");
}).catch(function(error) {
// Handle bad rules deploys
return res.status(500).jsonp({ error: error });
});
});
// Authorization middleware
var authorize = function(req, res, next) {
// Fetch the rules
utils.getFile(RULES_FILE).then(function(rulesData) {
var rules = JSON.parse(rulesData);
// Add captured variables to the environment
var env = req.params;
env.req = req;
env.data = req.body;
// Find the correct condition, pass it along
var method = req.method || '';
var path = req.route.path || '';
var condition = rules[path][method]['condition'] || false;
req.rule_condition = condition;
req.rule_env = env;
// Find the correct action, pass it along
var action = rules[path][method]['action'] || '';
req.rule_action = action;
// Eval the proper rule
var result = utils.pureEval(condition, env);
// If not allowed, send 403
if (!result) {
return res.status(403).jsonp({ error: 'Unauthorized' });
}
next();
}).catch(function(error) {
// Handle bad rules fetches
return res.status(500).jsonp({ error: error });
});
}
// Action middleware
function act(req, res, next) {
// Fetch the action code
utils.getFile(ACTIONS_FILE).then(function(actions){
// Construct our action
var action = actions + ";" + req.rule_action + ";"
// Eval the appropriate action in our environment
utils.pureEval(action, req.rule_env);
// Return 200, we're done!
return res.status(200).jsonp("ok");
}).catch(function(error) {
// Handle bad action fetches
return res.status(500).jsonp({ error: error });
});
}
// Provided app logic
// Get a user profile
app.get('/users/:userId', authorize, function(req, res, next) {
var profileFile = "users/" + req.params.userId + ".json";
utils.getFile(profileFile).then(function(userProfile) {
res.send(userProfile);
next();
}).catch(function(error) {
// Handle bad profile fetches
return res.status(500).jsonp({ error: error });
});
}, act);
// Set a user profile
app.put('/users/:userId', authorize, function(req, res, next) {
var profileFile = "users/" + req.params.userId + ".json";
utils.putFile(profileFile, JSON.stringify(req.body)).then(function() {
next();
}).catch(function(error) {
// Handle bad profile puts
return res.status(500).jsonp({ error: error });
});
}, act);
// App server
var server = app.listen(process.env.PORT || '8080', function () {
console.log('App listening on port %s', server.address().port);
});
# test getting a user profile
curl http://localhost:8080/users/mike\?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJtaWtlIiwiZXhwIjoxMzkzMjg2ODkzLCJpYXQiOjEzOTMyNjg4OTN9.cTzfsadm223EmbhjyLLl_6k3EywTii3E0eM8OiL3pqs
# test putting a user profile
curl \
-X PUT \
-d '{"name": "mike"}' \
-H "content-type: application/json" \
http://localhost:8080/users/mike\?token\=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJtaWtlIiwiZXhwIjoxMzkzMjg2ODkzLCJpYXQiOjEzOTMyNjg4OTN9.cTzfsadm223EmbhjyLLl_6k3EywTii3E0eM8OiL3pqs
# deploy rules
curl \
-X PUT \
-d @rules.json \
-H "content-type: application/json" \
http://localhost:8080/.rules\?token\=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc0FkbWluIjp0cnVlLCJzdWIiOiJtaWtlIiwiZXhwIjoxMzkzMjg2ODkzLCJpYXQiOjEzOTMyNjg4OTN9.uhnypJQyvZgX-37c0vKFP3b-KRSTnX24pso4_AMV394
# deploy actions
curl \
-X PUT \
-d @actions.js \
-H "content-type: text/javascript" \
http://localhost:8080/.actions\?token\=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc0FkbWluIjp0cnVlLCJzdWIiOiJtaWtlIiwiZXhwIjoxMzkzMjg2ODkzLCJpYXQiOjEzOTMyNjg4OTN9.uhnypJQyvZgX-37c0vKFP3b-KRSTnX24pso4_AMV394
var gcloud = require('google-cloud');
var storage = gcloud.storage({
projectId: '<your-project-id>',
});
var BUCKET_NAME = '<your-project-id>.appspot.com';
// Fetch file from GCS
function getFile(filename) {
return new Promise(function(resolve, reject) {
storage.bucket(BUCKET_NAME).file(filename).download(function(error, data) {
if (error) {
reject (error);
}
resolve(data);
});
});
}
// Upload file to GCS
function putFile(filename, data) {
return new Promise(function(resolve, reject) {
var uploadStream = storage.bucket(BUCKET_NAME).file(filename).createWriteStream();
uploadStream.on('error', function(error) {
reject(error);
}).on('finish', function() {
resolve();
});
uploadStream.write(data);
uploadStream.end();
});
}
// Eval an expression with additional context
function pureEval(expr, ctx) {
with (ctx) {
return eval(expr);
}
}
module.exports = {
pureEval: pureEval,
getFile: getFile,
putFile: putFile,
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment