Skip to main content
Version: 2.x

🛠 Middleware

caution

The following documentation refers to version 3.x.x of the service.

Middleware is a backend service responsible for serving micro-lc static and configuration files, while applying some useful manipulation logic before returning their content. This logic is also distributed through an SDK to ease the process of building custom configurations servers.

Getting started​

An easy way to deploy locally Middleware is using Docker Compose to spin up a full-stack application which serves micro-lc through a reverse proxy.

To set thing up just copy the following files, all in the same directory taking care to keep the same naming you see below.

Middleware application setup
version: '3'

services:
reverse-proxy:
depends_on:
middleware:
condition: service_started
image: nginx:${NGINX_VERSION:-1.24.0}-alpine
networks:
- internal
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
ports:
- 8080:8080

middleware:
image: microlc/middleware:${MIDDLEWARE_VERSION:-latest}
environment:
- LOG_LEVEL=debug
- HTTP_PORT=3000
- MICROSERVICE_GATEWAY_SERVICE_NAME=microservice-gateway
- USERID_HEADER_KEY=miauserid
- GROUPS_HEADER_KEY=miausergroups
- CLIENTTYPE_HEADER_KEY=client-type
- BACKOFFICE_HEADER_KEY=isbackoffice
- USER_PROPERTIES_HEADER_KEY=miauserproperties
volumes:
- ./index.html:/usr/static/public/index.html
- ./config.json:/usr/static/configurations/config.json
networks:
- internal

networks:
internal:

Now run docker compose up -d inside the directory, and you will have micro-lc hosted on http://localhost:8080/. To drop the environment, run docker compose down in the same directory.

Usage​

Middleware is available as a Docker image on Dockerhub.

Environment variables​

Middleware it built using Mia-Platform's custom-plugin-lib, hence it needs the environment variables outlined in the library documentation.

On top of those, Middleware accepts the following environment variables:

NameTypeDefaultDescription
ACL_CONTEXT_BUILDER_PATHstring/usr/src/app/config/acl-context-builder.jsAbsolute path of the ACL context builder file
LANGUAGES_DIRECTORY_PATHstring/usr/static/languagesAbsolute path of the directory containing language files
PUBLIC_DIRECTORY_PATHstring/usr/static/publicAbsolute path of the directory containing static files to be served
RESOURCES_DIRECTORY_PATHstring/usr/static/configurationsAbsolute path of the directory containing configuration resources to be served
SERVICE_CONFIG_PATHstring/usr/src/app/config/config.jsonAbsolute path of the service configuration file

Service configuration​

The service accepts a JSON configuration file containing information on the content types and on the headers to apply to file responses.

The service will use the SERVICE_CONFIG_PATH environment variable to locate the configuration file, which should contain an object with the following structure:

interface Config {
contentTypeMap?: Record<string, string | string[]>
publicHeadersMap?: Record<string, Record<string, string | (string | string[])[]>>
}

Content Type​

By default, Middleware returns a file with the following content types (depending on the file extension).

ExtensionContent-Type header
.cjsapplication/javascript
.csstext/css
.htmltext/html
.jsapplication/javascript
.jsonapplication/json
.mjsapplication/javascript
.yamltext/yaml
.ymltext/yam

Any extension not listed will trigger a default Content-Type equal to text/plain.

These settings can be overridden using with the contentTypeMap property of the service configuration, which should be a map linking extensions to Content-Type header signatures. Keys must be either a single extension or a comma separated list of extensions while values must be strings or array of strings which will be joint with a semicolon as separator.

For example, the following contentTypeMap

{
".mjs,.js": ["text/javascript", "charset=utf8"],
".xml": "application/xml"
}

which will force Middleware to return on .mjs and .js the equivalent text/javascript; charset=utf8 Content-Type header and application/xml on XML files.

caution

Any extension listed in the CONTENT_TYPE_MAP will override its previous default value allowing even to change Content-Type for JSON and YAML files which might create problems on micro-lc web component configuration dump.

Headers​

The publicHeadersMap property of the configuration allows you to specify additional headers to include in static files responses (i.e., files exposed under /public).

The property should be a map linking file pathnames to headers definitions. Keys must be valid pathname strings (e.g., /public/index.html), while values must be maps linking HTTP header names to valid values. Values can be directly a string, an array of string (which will be joined with a comma), or an array of arrays of string (which will be joined with a semicolon at inner lever and with a comma at external level).

tip

Content-Type headers specified here will win over the ones defined in contentTypeMap configuration.

tip

Middleware applies nonce injection on additional headers.

For example, the following publicHeadersMap

{
"/public/index.html": {
"content-security-policy": [
[
"script-src 'nonce-**CSP_NONCE**'",
"style-src 'self'"
]
],
"link": "</micro-lc-configurations/config.json>; rel=preload; as=fetch"
}
}

causes a request to /public/index.html to have the following headers:

content-security-policy: script-src 'nonce-KMxEW8oQKCm12+XocTYib9u6++'; style-src 'self'
link: </micro-lc-configurations/config.json>; rel=preload; as=fetch

Serving from file system​

Configuration files and regular files served by Middleware are loaded from file system.

Static files are sourced from the directory specified with the PUBLIC_DIRECTORY_PATH environment variable and are exposed under /public prefix. Configuration files are searched in the directory specified with the RESOURCES_DIRECTORY_PATH environment variable and are exposed under /configurations prefix.

tip

Calling /public, /public/ or a non-existing file under public (e.g., /public/unknown-file.js) will result in Middleware responding with the root index.html file, if existing.

For example, given a file system with the following structure:

├── public
| └── index.html
| └── assets
| └── style.css
|
├── configurations
└── config.json
└── lib
└── index.js

Middleware will expose the following routes:

GET - /public (redirecting to /public/index.html)
GET - /public/ (redirecting to /public/index.html)
GET - /public/index.html
GET - /public/assets/style.css

GET - /configurations/config.json
GET - /configurations/lib/index.js

Static files manipulation​

If a required static file is an HTML resource (i.e., a file exposed under /public ending with .html), before returning the file, Middleware applies some parsing logics to its content.

Nonce injection​

Middleware allows you to inject a server-generated nonce in HTML files. In particular, it will find any occurrence of the of the expression **CSP_NONCE** and replace it with the actual value.

For example, the following HTML file

<!DOCTYPE html>
<html lang="en">

<head>
<link
rel="stylesheet"
nonce="**CSP_NONCE**"
href="./assets/style.css" />
</head>

</html>

will be serve as

<!DOCTYPE html>
<html lang="en">

<head>
<link
rel="stylesheet"
nonce="KMxEW8oQKCm12+XocTYib9u6++"
href="./assets/style.css" />
</head>

</html>

Configuration files manipulation​

If a required configuration file is a JSON or YAML resource (i.e., a file exposed under /configurations ending with .json, .yaml or .yml), before returning the file, Middleware applies some parsing logics to its content.

ACL application​

Middleware allows you to implement access control limit on served files, removing sections of configurations based on certain properties of the caller. These properties can be extracted in a default or a custom way.

ACL expressions can be specified anywhere in configuration using the special key aclExpression having as value a stringified boolean expression based on caller's properties.

tip

You can use any combination of properties and JavaScript operators in you ACL expressions.

For example, the following expressions are all valid:

  • groups.admin && permissions.api.users.get
  • !groups.developer
  • api.users.get || api.users.post
  • (admin && !permissions.api.users.post) || permissions.api.users.count.get
  • (groups.admin === true && api.users.post === true)

Middleware evaluates each ACL expression against caller's properties and, if the expression results in a falsy value, it removes from the configuration the whole object which the expression is a property of. It then proceeds to remove any aclExpression key left over to not leak server-side logic into the client.

Default extraction​

Middleware considers caller's groups and permissions as properties.

Caller's groups are extracted from request headers, particularly from the header the key of which is specified through GROUPS_HEADER_KEY environment variable. The value of the header should be a comma-separated list of groups (e.g., "admin,user").

Caller's permissions are extracted from request headers too. Middleware takes the header the key of which is specified through USER_PROPERTIES_HEADER_KEY environment variable and expects a stringified JSON object containing a comma-separated list of permissions under the key permissions (e.g., "{\"permissions\":"api.users.get,api.users.post"}").

info

In this scenario groups and permissions are respectively placed within groups and permissions objects. Thus, ACL expressions should be defined as groups.<group-name> and permissions.<permissions-name>.

Example​

Let's consider the following configuration file served under GET - /middleware/config.json.

{
"content": {
"tag": "div",
"properties": {
"aclExpression": "groups.admin",
"adminName": "John Doe"
},
"content": [
{
"aclExpression": "groups.superadmin || permissions.api.users.get",
"tag": "button"
}
]
}
}

The response of the following request

curl 'https://*********/middleware/config.json' \
-H 'user-groups: user' \
-H 'user-properties: { "permissions": "api.users.get" }'

will be

{
"content": {
"tag": "div",
"content": [
{
"tag": "button"
}
]
}
}
Custom extraction​

Middleware considers the result of a custom function as caller's properties.

Caller's properties are extracted by a user-defined custom function that Middleware reads from the path specified through ACL_CONTEXT_BUILDER_PATH environment variable. The function must return an array of strings and has only one input argument which is a JSON object with the following type:

type AclContextBuilderInput = {
body?: unknown
headers: Record<string, string | string[] | undefined>
method: string
pathParams: unknown
queryParams: unknown
url: string
}

For example,

{
"body": {
"foo": "bar"
},
"headers": {
"header-1": "value",
},
"method": "POST",
"pathParams": {
"*": "/file.json"
},
"queryParams": {
"foo": "bar"
},
"url": "/configurations/file.json"
}
info

In this scenario properties are placed at root level. Thus, ACL expressions should be simply defined as <property-name>.

caution

The user-defined custom function cannot access the default global object.

Example​

Let's consider the following configuration file served under GET - /middleware/config.json.

{
"content": {
"tag": "div",
"properties": {
"aclExpression": "property-1",
"adminName": "John Doe"
},
"content": [
{
"aclExpression": "prop.resource.get || api.users.get",
"tag": "button"
}
]
}
}

Then let's consider the following custom ACL context builder.

export default ({ headers, method, pathParams, queryParams, url }) => {
const property = headers['my-custom-header']
return [property]
}

The response of the following request

curl 'https://*********/middleware/config.json' \
-H 'my-custom-header: prop.resource.get'

will be

{
"content": {
"tag": "div",
"content": [
{
"tag": "button"
}
]
}
}

Language translation​

Middleware implements a Content Negotiation mechanism in order to serve configuration files translated according to the most suitable language representation for the client.

In order to provide this feature, you have to add JSON language configuration files in the directory defined by the environment variable LANGUAGES_DIRECTORY_PATH: one for each language you want to support. The name of these files should reflect the language-tag format as formally defined in BCP 47 followed by the JSON extension (e.g. en-US.json). The translation values defined in these language files are resolved by the lodash get function.

Example​

Let's consider the following configuration file:

{
"property": "value",
"textProperty": "main.text"
}

language configuration files can adhere to one of the following structures and produce the same translated JSON:

en-US.json
{
"main.text": "translated!"
}
en-US.json
{
"main": {
"text": "translated!"
}
}

The resulting JSON is:

{
"property": "value",
"textProperty": "translated!"
}

References resolution​

In order to avoid writing repeating values multiple times in your configurations, Middleware supports references resolutions following JSON schema specification. In particular, if you need to repeat the same value in various places of your configuration, you can define it once in the dedicated top-level key definitions, and then references it wherever you like using the keyword $ref (e.g., { "dataSchema": { "$ref": "#/definitions/dataSchema" }}).

Middleware will resolve references in files and will remove the key definitions (if any) before serving them. Keep in mind that Middleware will throw if some references cannot be resolved.

Example​

Let's consider the following configuration file served under GET - /middleware/config.json.

{
"definitions": {
"clientKey": "some-client-key"
},
"content": {
"tag": "div",
"properties": {
"clientKey": {
"$ref": "#/definitions/clientKey"
}
},
"content": [
{
"tag": "button",
"properties": {
"clientKey": {
"$ref": "#/definitions/clientKey"
}
}
}
]
}
}

The response of the following request

curl 'https://*********/middleware/config.json'

will be

{
"definitions": {
"clientKey": "some-client-key"
},
"content": {
"tag": "div",
"properties": {
"clientKey": "some-client-key"
},
"content": [
{
"tag": "button",
"properties": {
"clientKey": "some-client-key"
}
}
]
}
}

Parsing configurations​

The service exposes a utility route that accepts a configuration as body and responds with a parsed version (i.e., it applies all the manipulation logic usually applied to a served configuration file). The routes specifics are the following.

  • Method: POST
  • Path: /configurations/parse
  • Body: the valid JSON object that has to be parsed
  • Response: a JSON object with manipulations applied

SDK​

The logic under ACL application and references resolution is conveniently shipped in a standalone SDK to ease the implementation of alternative backend solutions.

Usage​

You can install the SDK from NPM

npm install @micro-lc/middleware

and import it in your files

const middlewareSdk = require('@micro-lc/middleware/sdk')

Methods​

evaluateAcl(json, userGroups, userPermissions)​

const result = resolveReferences(json, userGroups, userPermissions)

This method evaluates aclExpression keys in input JSON. It does not modify the input object.

Parameters

  • json: string | number | boolean | object | unknown[] | null
    Input JSON with ACL rules to be evaluated.
  • userGroups: string[]
    List of caller's groups.
  • userPermissions: string[]
    List of caller's permissions.

Return value

  • Promise<string | number | boolean | object | unknown[] | null>
    JSON with ACL rules evaluated.

resolveReferences(json)​

const result = await resolveReferences(json)

This method resolves the references in a JSON object. It does not modify the input object.

The method throws if a reference cannot be found.

Parameters

  • json: string | number | boolean | object | unknown[] | null
    Input JSON with references to be resolved.

Return value

  • Promise<string | number | boolean | object | unknown[] | null>
    JSON with references resolved.