@politico/lambda
v1.0.8
Published
A utility library to simplify setting up lambda functions
Downloads
46
Maintainers
Keywords
Readme
@politico/lambda
This library is meant to make it easier to spin up multi-use Lambda microservices integrated in an API Gateway. While it can be desireable to break Lambda functions down into their smallest possible forms, it's often more convenient to create Lambdas that do multiple things. For example, you might want to build a Lambda that reads from and writes to a database. Your project might even have several types of records and it's simplest to have one Lambda that handles CRUD operations for all of them. If you're used to something like Express, you may think about solving this problem by routing requests to different handler functions, but integrating Express in a Lambda environment can be a hassle. This library seeks to provide an Express-like routing experience while staying true to the simplicity of a Lambda environment.
Routing
In a simple case, setting up a Lambda router might look something like this:
import { createRouter, log, ok } from '@politico/lambda';
const router = createRouter({ log });
router.get('/hello/world', {
handler: () => ok({
response: {
message: 'Hello world!',
},
}),
});
export const handler = (event) => router.handleLambdaApiGateway(event);
Here we're:
- creating a router that's set up to log requests to stdout,
- registering a route handler to respond to
GET
requests made to the/hello/world
endpoint, and - exporting a function called
handler
that can operate as the entrypoint for a Lambda function.
With that, you should be able to create your Lambda, connect it as an
integration for an API Gateway, and ping your endpoint. Note that setting up
logging is optional. If you don't pass in a log
object then logging will be
disabled, or you can pass in your own custom logger as long as it has
a logLevel
function that accepts a log level and additional arguments to log.
Adding Auth
This library supports token-based authentication out of the box, relying on
an external service acting as the token authority. To configure your router
with authentication, you can provide auth
options like this:
import { createRouter, log } from '@politico/lambda';
const router = createRouter({
logger: log,
auth: {
appName: 'my-app-name',
secret: 'ssssshhhhh',
signinUrl: 'https://auth.com/signin',
roles: [
'user',
'admin',
],
},
});
This then lets you configure routes with minimum roles set, like so:
router.get('/hello/world', {
handler: myHandler,
minimumRole: 'admin',
});
Now only users with the admin
role can access this endpoint!
By how does the router know which roles a user has, or even who the user is?
That's where the external token service comes in. The router will look for
a user's token either as a bearer token in an Authorization
header or as
a newsroomAuthToken
cookie. If it finds a token, it tries to parse it as
a JWT using the provided secret. If the token is expired, it tries to refresh
it by sending it to the token service at the provided signinUrl
. If the token
can be neither verified nor refreshed, authentication fails.
The router assumes that the payload of the token will contain information about
the user as well as a set of permissions keyed by app name. The router will
attempt to look up app-specific permissions using the provided appName
as the
key of the permissions object. A user may have multiple permissions (or
"roles") for the same app.
Once a user's roles are established for the specified app, the router can check
their authorization when attempting to access specific endpoints. This is where
the router's roles
configuration comes into play. This array of roles
specifies two things: the set of roles supported by this app, and the relative
"level" of each role. A user will be able to access an endpoint with
a configured minimumRole
if either (1) they have the exact specified role, or
(2) they have at least one role of a strictly greater level than the level of
the configured minimum role for that endpoint.
There are two ways you can specify the roles supported by a router. First, you can be fully explicit and specify each role's name and level:
import { createRouter, log } from '@politico/lambda';
const router = createRouter({
logger: log,
auth: {
appName: 'my-app-name',
secret: 'ssssshhhhh',
signinUrl: 'https://auth.com/signin',
roles: [
{ name: 'user-type-1', level: 0 },
{ name: 'user-type-2', level: 0 },
{ name: 'admin', level: 1 },
],
},
});
This can be helpful for more complex role schemes. Notice that in the above
example, both roles user-type-1
and user-type-2
have the level of 0 while
admin
has a level of 1. This means that if an endpoint has a minimum role of
user-type-1
, users with only the role user-type-2
won't be able to access
it, and vice versa. Users with the admin
role will be able to access both
because the level of admin
is greater than the levels of the user roles.
Often, an app's role scheme is more simply hierarchical and there are several increasing levels of access. As a convenience, you can specify roles as a simple array of names in this case:
import { createRouter, log } from '@politico/lambda';
const router = createRouter({
logger: log,
auth: {
appName: 'my-app-name',
secret: 'ssssshhhhh',
signinUrl: 'https://auth.com/signin',
roles: [
'user',
'admin',
],
},
});
Here, the level of each role is assumed to be its index in the array of roles,
so user
has a level of 0 and admin
has a role of 1.
Integrating with API Gateway
A common and convenient way to deploy Lambda microservices is as integrations behind an API Gateway that handles routing to multiple services. When using path-based routing within a Lambda microservice, this can create some complications, because whatever path prefix the API Gateway uses to route the request to the appropriate Lambda is included in the request event that gets passed to the Lambda.
The router supports removing a path prefix for its internal routing purposes
automatically. To configure this, set the servicePathPrefix
option:
import { createRouter, log, ok } from '@politico/lambda';
const router = createRouter({
log,
servicePathPrefix: '/service/my-service',
});
Now, any request that comes in will get /service/my-service
stripped off of
the start of the request path. Note that the router does not enforce that
requests match this prefix; it simply removes the prefix if it's there.
Other Utilities
This library is built to make creating Lambda microservices easier, so it includes a few additional helpers.
- loadEnvValue helps load values from the environment that will be type-safe as strings
- ok and error help format Lambda handler responses
- runDevServer runs an Express server that mimics an API Gateway integration for local development (note that this shouldn't be used in production)