Skip to content

Added Elasticsearch proxy example #1398

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

Merged
merged 24 commits into from
Feb 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ npm install @elastic/elasticsearch@<major>
#### Browser

WARNING: There is no official support for the browser environment. It exposes your Elasticsearch instance to everyone, which could lead to security issues.
We recommend that you write a lightweight proxy that uses this client instead.
We recommend that you write a lightweight proxy that uses this client instead, you can see a proxy example [here](./docs/examples/proxy).

## Documentation

Expand Down
51 changes: 51 additions & 0 deletions docs/examples/proxy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# coverage output
coverage.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules
jspm_packages

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history

# mac files
.DS_Store

# vim swap files
*.swp

#Jetbrains editor folder
.idea

.vercel
65 changes: 65 additions & 0 deletions docs/examples/proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Elasticsearch proxy example

This folder contains an example of how to build a lightweight proxy
between your frontend code and Elasticsearch if you don't
have a more sophisticated backend in place yet.

> **IMPORTANT:** This is not a production ready code and it is only for demonstration purposes,
> we make no guarantees on it's security and stability.

This project is designed to be deployed on [Vercel](https://vercel.com/), a cloud platform
for static sites and Serverless Functions. You can use other functions providers,
such as [Google Cloud functions](https://cloud.google.com/functions).

## Project structure

The project comes with four endpoints:

- `/api/search`: runs a search, requires `'read'` permission
- `/api/autocomplete`: runs an autocomplete suggestion, requires `'read'` permission
- `/api/index`: indexes or updates a document, requires `'write'` permission
- `/api/delete`: deletes a document, requires `'write'` permission

Inside `utils/authorize.js` you can find the authorization logic for the endpoints.
In each endpoint you should configure the `INDEX` variable.

## How to use

Create an account on Vercel, then create a deployment on Elastic Cloud. If you
don't have an account on Elastic Cloud, you can create one with a free 14-day trial
of the [Elasticsearch Service](https://www.elastic.co/elasticsearch/service).

### Configure Elasticsearch

Once you have created a deployment on Elastic Cloud copy the generated Cloud Id and the credentials.
Then open `utils/prepare-elasticsearch.js` and fill your credentials. The script generates
an [Api Key](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html)
that you can use for authenticating your request. Based on the configuration of the Api Key, you will be able
to perform different operation on the specified indices or index pattern.

### Configure Vercel

Install the [Vercel CLI](https://vercel.com/docs/cli) to bootstrap the project,
or read the [quickstart](https://vercel.com/docs) documentation.

If you are using the CLI, bootstrap the project by running `vercel`. Test the project locally
with `vercel dev`, and deploy it with `vercel deploy`.
Configure the `ELASTIC_CLOUD_ID` [environment varible](https://vercel.com/docs/environment-variables) as well.
The Api Key is passed from the frontend app via a `Authorization` header as `Bearer` token and is
used to authorize the API calls to the endpoints as well.
Additional configuration, such as CORS, can be added to [`vercel.json`](https://vercel.com/docs/configuration).

## Authentication

If you are using Elasticsearch only for search purposes, such as a search box, you can create
an Api Key with `read` permissions and store it in your frontend app. Then you can send it
via `Authorization` header to the proxy and run your searches.

If you need to ingest data as well, it's more secure to have a strong authentication in your application.
For such cases, use an external authentication service, such as [Auth0](https://auth0.com/)
or [Magic Link](https://magic.link/). Then create a different Api Key with `read` and `write`
permissions for authenticated users, that will not be stored in the frontend app.

## License

This software is licensed under the [Apache 2 license](../../LICENSE).
105 changes: 105 additions & 0 deletions docs/examples/proxy/api/autocomplete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

// IMPORTANT: this is not a production ready code & purely for demonstration purposes,
// we make no guarantees on it's security and stability

// NOTE: to make this endpoint work, you should create an ApiKey with 'read' permissions

'use strict'

const { Client } = require('@elastic/elasticsearch')
const authorize = require('../utils/authorize')

const INDEX = '<index-name>'
const client = new Client({
cloud: {
id: process.env.ELASTIC_CLOUD_ID
}
})

module.exports = async (req, res) => {
const [err, token] = authorize(req)
if (err) {
res.status(401)
res.json(err)
return
}

if (typeof req.query.q !== 'string') {
res.status(400)
res.json({
error: 'Bad Request',
message: 'Missing parameter "query.q"',
statusCode: 400
})
return
}

if (req.query.q.length < 3) {
res.status(400)
res.json({
error: 'Bad Request',
message: 'The length of "query.q" should be at least 3',
statusCode: 400
})
return
}

try {
const response = await client.search({
index: INDEX,
// You could directly send from the browser
// the Elasticsearch's query DSL, but it will
// expose you to the risk that a malicious user
// could overload your cluster by crafting
// expensive queries.
body: {
_source: ['id', 'url', 'name'], // the fields you want to show in the autocompletion
size: 0,
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters-completion.html
suggest: {
suggestions: {
prefix: req.query.q,
completion: {
field: 'suggest',
size: 5
}
}
}
}
}, {
headers: {
Authorization: `ApiKey ${token}`
}
})

// It might be useful to configure http control caching headers
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
// res.setHeader('stale-while-revalidate', '30')
res.json(response.body)
} catch (err) {
res.status(err.statusCode || 500)
res.json({
error: err.name,
message: err.message,
statusCode: err.statusCode || 500
})
}
}
74 changes: 74 additions & 0 deletions docs/examples/proxy/api/delete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

// IMPORTANT: this is not a production ready code & purely for demonstration purposes,
// we make no guarantees on it's security and stability

// NOTE: to make this endpoint work, you should create an ApiKey with 'write' permissions

'use strict'

const { Client } = require('@elastic/elasticsearch')
const authorize = require('../utils/authorize')

const INDEX = '<index-name>'
const client = new Client({
cloud: {
id: process.env.ELASTIC_CLOUD_ID
}
})

module.exports = async (req, res) => {
const [err, token] = authorize(req)
if (err) {
res.status(401)
res.json(err)
return
}

if (typeof req.query.id !== 'string' && req.query.id.length === 0) {
res.status(400)
res.json({
error: 'Bad Request',
message: 'Missing document id',
statusCode: 400
})
return
}

try {
const response = await client.delete({
index: INDEX,
id: req.query.id
}, {
headers: {
Authorization: `ApiKey ${token}`
}
})

res.json(response.body)
} catch (err) {
res.status(err.statusCode || 500)
res.json({
error: err.name,
message: err.message,
statusCode: err.statusCode || 500
})
}
}
76 changes: 76 additions & 0 deletions docs/examples/proxy/api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

// IMPORTANT: this is not a production ready code & purely for demonstration purposes,
// we make no guarantees on it's security and stability

// NOTE: to make this endpoint work, you should create an ApiKey with 'write' permissions

'use strict'

const { Client } = require('@elastic/elasticsearch')
const authorize = require('../utils/authorize')

const INDEX = '<index-name>'
const client = new Client({
cloud: {
id: process.env.ELASTIC_CLOUD_ID
}
})

module.exports = async (req, res) => {
const [err, token] = authorize(req)
if (err) {
res.status(401)
res.json(err)
return
}

if (typeof req.body !== 'object') {
res.status(400)
res.json({
error: 'Bad Request',
message: 'The document should be an object',
statusCode: 400
})
return
}

try {
const response = await client.index({
index: INDEX,
id: req.query.id,
body: req.body
}, {
headers: {
Authorization: `ApiKey ${token}`
}
})

res.status(response.statusCode)
res.json(response.body)
} catch (err) {
res.status(err.statusCode || 500)
res.json({
error: err.name,
message: err.message,
statusCode: err.statusCode || 500
})
}
}
Loading