Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ notifications:
node_js:
- '8'
- '6'
before_install:
- if [[ `npm -v` != 3* ]]; then npm i -g npm@latest; fi
before_script:
- npm prune
branches:
Expand Down
64 changes: 0 additions & 64 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,70 +4,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [3.0.0] (2017-12-05)

See full release notes at https://github.com/dadi/api/releases/tag/v3.0.0

### Added

#### Data Connectors

API Version 3.0 supports multiple data connectors. In previous versions API used MongoDB as a backend; this is now configurable. API Data Connectors are available as NPM packages. To add one to your API installation, run the associated NPM install command:

`$ npm install @dadi/api-mongodb --save`

Each data connector has it's own configuration requirements, but API must also be configured to use the data connectors you select. Modify your API configuration as follows:

```json
{
"datastore": "@dadi/api-mongodb", // the NPM package name for the data connector to use for the content layer
"auth": {
"tokenUrl": "/token",
"tokenTtl": 1800,
"clientCollection": "clientStore",
"tokenCollection": "tokenStore",
"datastore": "@dadi/api-mongodb", // the NPM package name for the data connector to use for the authentication layer
"database": "test"
}
}
```

In addition, the data connector itself normally requires it's own configuration file. For example the MongoDB data connector requires a file using the following naming convention `mongodb.<environment>.json`. These configuration files should be placed the `config` directory of the API application.

#### Connection Recovery

API is now capable of recovering from database connection failures. When API is started with no available database service it will keep retrying until a successful connection can be made, then it runs the normal boot process.

In addition, if the database connection is lost during normal operation of API, any requests made while the connection is offline will result in a HTTP 503 returned to the client.

The maximum number of connection retries can be configured in the main configuration file by adding the following block:

```json
"databaseConnection": {
"maxRetries": 5 // default 10
}
```

### Changed

* New startup message displayed, with links to documentation
* [#141](https://github.com/dadi/api/issues/141): the internal fields will be prefixed with a special character (`_` by default) which is configurable using the configuration property `internalFieldsPrefix`
* [#180](https://github.com/dadi/api/issues/180): document properties with `null` values are not returned as part of the response
* [#251](https://github.com/dadi/api/issues/251): added a new `/hello` endpoint which returns HTTP 200 and a "Welcome to API" message
* [#263](https://github.com/dadi/api/issues/263): all delete hooks now receive a `deletedDocs` property
* [#314](https://github.com/dadi/api/issues/314): when configuration option `feedback` is `true` we now send a response body when deleting documents
* [#327](https://github.com/dadi/api/issues/327): API becomes capable of recovering from database connection failures
* [#328](https://github.com/dadi/api/issues/328): remove schema validation on settings: 'callback', 'defaultFilters', 'fieldLimiters' and 'count'. Now only requires 'cache' and 'authenticate'
* [#332](https://github.com/dadi/api/issues/332): allow POST to collection endpoints using `text/plain` content-type, which will be converted if it is valid JSON
* Configuration file validation removed, suppressing warnings on application startup
* POST/PUT/DELETE using non-existing document identifiers returns a 404:

DELETE requests throws a 404 (instead of 204) when deleting a non-existing document by ID. This applies to requests where the document ID is passed in the URL, not when in the body (e.g. DELETE /v1/db/collection/DOC-ID vs DELETE /v1/db/collection).

POST/PUT requests throw a 404 when updating a non-existing document by ID. This applies to requests where the document ID is passed in the URL, not when in the body (e.g. PUT /v1/db/collection/DOC-ID vs PUT /v1/db/collection).

Closes [#345](https://github.com/dadi/api/issues/345).

## [2.2.9] (2017-12-05)

### Changed
Expand Down
74 changes: 48 additions & 26 deletions dadi/lib/auth/tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,55 @@ module.exports.connect = () => {
connection = Connection(dbOptions, null, config.get('auth.datastore'))
}

module.exports.generate = (req, res, next) => {
debug('Generate token')

const handleInvalidCredentials = () => {
const error = new Error('Invalid Credentials')

error.statusCode = 401

res.setHeader(
'WWW-Authenticate', 'Bearer, error="invalid_credentials", error_description="Invalid credentials supplied"'
)

return next(error)
}

// Look up the credentials supplied in the request body in clientStore
const credentials = {
clientId: req.body.clientId,
secret: req.body.secret
}
module.exports.generate = function (req, res, next) {
// Look up the creds in clientStore
var _done = function (database) {
if (
typeof req.body.clientId !== 'string' ||
typeof req.body.secret !== 'string'
) {
var error = new Error('Invalid Credentials')
error.statusCode = 401
res.setHeader('WWW-Authenticate', 'Bearer, error="invalid_credentials", error_description="Invalid credentials supplied"')
return next(error)
}

// Return 401 if the clientId/secret are not plain strings.
if (
typeof credentials.clientId !== 'string' ||
typeof credentials.secret !== 'string'
) {
return handleInvalidCredentials()
database.collection(clientCollectionName).findOne({
clientId: req.body.clientId,
secret: req.body.secret
}, function (err, client) {
if (err) return next(err)

if (client) {
// Generate token
var token
getToken(function (returnedToken) {
token = returnedToken

// Save token
return tokenStore.set(token, client, function (err) {
if (err) return next(err)

var tok = {
accessToken: token,
tokenType: 'Bearer',
expiresIn: config.get('auth.tokenTtl')
}

var json = JSON.stringify(tok)
res.setHeader('Content-Type', 'application/json')
res.setHeader('Cache-Control', 'no-store')
res.setHeader('Pragma', 'no-cache')
res.end(json)
})
})
} else {
var error = new Error('Invalid Credentials')
error.statusCode = 401
res.setHeader('WWW-Authenticate', 'Bearer, error="invalid_credentials", error_description="Invalid credentials supplied"')
return next(error)
}
})
}

const connectionReady = database => {
Expand Down
56 changes: 56 additions & 0 deletions dadi/lib/cors/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const path = require('path')
const vary = require('vary')

const config = require(path.join(__dirname, '/../../../config.js'))

module.exports = function cors (server) {
server.app.use((req, res, next) => {
if (config.get('cors') !== true) {
return next()
}

const method = req.method && req.method.toLowerCase()

// Preflight requests set some sensible defaults, but mostly permit anything
// the client asks for by reflecting its headers back.
if (method === 'options') {
// Reflect the requesting `Origin` back to the client, falling back on a
// wildcard permission.
if (req.headers.origin) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
vary(res, 'Origin')
} else {
res.setHeader('Access-Control-Allow-Origin', '*')
}

// Permit all HTTP methods.
res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE')

// Reflect any requested headers.
if (req.headers['access-control-request-headers']) {
res.setHeader('Access-Control-Allow-Headers', req.headers['access-control-request-headers'])
vary(res, 'Access-Control-Request-Headers')
}

// Explicitly set the content length to 0 for buggy browsers.
res.setHeader('Content-Length', 0)

// Set the status code to 204 (No Content).
res.statusCode = 204

// Do not process further middleware.
return res.end()
}

// All other requests reflect the requesting `Origin` back to the client,
// effectively enabling CORS for all endpoints and HTTP methods.
if (req.headers.origin) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
vary(res, 'Origin')
} else {
res.setHeader('Access-Control-Allow-Origin', '*')
}

next()
})
}
6 changes: 0 additions & 6 deletions dadi/lib/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@ module.exports.sendBackJSON = function (successCode, res, next) {
return
}

if (config.get('cors') === true) {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization')
}

res.setHeader('content-type', 'application/json')
res.setHeader('content-length', Buffer.byteLength(resBody))

Expand Down
3 changes: 3 additions & 0 deletions dadi/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var auth = require(path.join(__dirname, '/auth'))
var cache = require(path.join(__dirname, '/cache'))
var Connection = require(path.join(__dirname, '/model/connection'))
var Controller = require(path.join(__dirname, '/controller'))
var cors = require(path.join(__dirname, '/cors'))
var HooksController = require(path.join(__dirname, '/controller/hooks'))
var MediaController = require(path.join(__dirname, '/controller/media'))
var dadiBoot = require('@dadi/boot')
Expand Down Expand Up @@ -213,6 +214,8 @@ Server.prototype.start = function (done) {
app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }))
app.use(bodyParser.text({ limit: '50mb' }))

cors(self)

// update configuration based on domain
var domainConfigLoaded
app.use(function (req, res, next) {
Expand Down
33 changes: 33 additions & 0 deletions dadi/lib/model/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,39 @@ function sortQueriesByNestedLevel (queries) {
return keys.reduce((r, k) => (r[k] = queries[k], r), {}) // eslint-disable-line
}

function convertApparentObjectIds (query, schema) {
_.each(Object.keys(query), function (key) {
if (/apiVersion/.test(key)) {
return
}

var fieldSettings = getSchemaOrParent(key, schema)
var type = fieldSettings ? fieldSettings.type : undefined

if (key === '$in') {
if (typeof query[key] === 'object' && _.isArray(query[key])) {
var arr = query[key]
_.each(arr, function (value, key) {
if (typeof value === 'string' && ObjectID.isValid(value) && value.match(/^[a-fA-F0-9]{24}$/)) {
arr[key] = ObjectID.createFromHexString(value)
}
})
query[key] = arr
}
} else if (typeof query[key] === 'object' && query[key] !== null) {
if (typeof type !== 'undefined' && /^Mixed|Object$/.test(type)) {
// ignore
} else if (typeof type === 'undefined' || type !== 'Reference') { // Don't convert query id when it's a Reference field
query[key] = convertApparentObjectIds(query[key], schema)
}
} else if (typeof query[key] === 'string' && !/^Mixed|Reference|Object$/.test(type) && ObjectID.isValid(query[key]) && query[key].match(/^[a-fA-F0-9]{24}$/)) {
query[key] = ObjectID.createFromHexString(query[key])
}
})

return query
}

function makeCaseInsensitive (obj, schema) {
var newObj = _.clone(obj)

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dadi/api",
"version": "3.0.0",
"version": "2.2.9",
"main": "main.js",
"scripts": {
"create-client": "cd ../../.. && node ./node_modules/@dadi/api/utils/create-client.js",
Expand Down Expand Up @@ -48,7 +48,8 @@
"streamifier": "^0.1.1",
"underscore": "1.8.3",
"underscore-contrib": "^0.3.0",
"validator": "7.0.0"
"validator": "7.0.0",
"vary": "^1.1.2"
},
"devDependencies": {
"@commitlint/cli": "~4.1.1",
Expand Down
72 changes: 0 additions & 72 deletions test/acceptance/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,6 @@ describe('Authentication', function () {
})
})

it('should not issue token if creds contain query keywords', function (done) {
var client = request('http://' + config.get('server.host') + ':' + config.get('server.port'))

client
.post(tokenRoute)
.send({
clientId: {'$ne': null},
secret: {'$ne': null}
})
.expect(401, function (err, res) {
res.headers['www-authenticate'].should.exist
res.headers['www-authenticate'].should.eql('Bearer, error="invalid_credentials", error_description="Invalid credentials supplied"')
done()
})
})

it('should allow requests containing token', function (done) {
help.getBearerToken(function (err, token) {
var client = request('http://' + config.get('server.host') + ':' + config.get('server.port'))
Expand Down Expand Up @@ -562,60 +546,4 @@ describe('Authentication', function () {
}, 300)
})
})

it('should allow an OPTIONS request for the token route', function (done) {
var oldCors = config.get('cors')
config.set('cors', true)

var _done = function (err) {
config.set('cors', oldCors)
done(err)
}

help.getBearerToken(function (err, token) {
var client = request('http://' + config.get('server.host') + ':' + config.get('server.port'))

client
.options('/token')
.set('Origin', 'http://example.com')
.set('Access-Control-Request-Method', 'GET')
.set('Access-Control-Request-Headers', 'X-Requested-With')
.expect('content-type', 'application/json')
.expect('access-control-allow-origin', '*')
.expect('Access-Control-Allow-Methods', 'POST, OPTIONS')
.expect(200)
.end(function (err, res) {
if (err) return _done(err)
_done()
})
})
})

it('should contain the correct CORS headers when cors = true', function (done) {
var oldCors = config.get('cors')
config.set('cors', true)

var _done = function (err) {
config.set('cors', oldCors)
done(err)
}

help.getBearerToken(function (err, token) {
var client = request('http://' + config.get('server.host') + ':' + config.get('server.port'))

client
.options('/vtest/testdb/test-schema')
.set('Origin', 'http://example.com')
.set('Access-Control-Request-Method', 'GET')
.set('Access-Control-Request-Headers', 'X-Requested-With')
.set('Authorization', 'Bearer ' + token)
.expect('content-type', 'application/json')
.expect('access-control-allow-origin', '*')
.expect(200)
.end(function (err, res) {
if (err) return _done(err)
_done()
})
})
})
})
Loading