Skip to content

Commit

Permalink
Check for SDK compatibility against current Kuzzle version (#1495)
Browse files Browse the repository at this point in the history
## What does this PR do ?

Now Kuzzle will check the `volatile.sdkName` header to ensure that the current SDK is compatible against the current Kuzzle version.  

For Kuzzle v1 SDKs, the `sdkName` property is not present. So when the property is missing, we assume that the SDK is compatible.

For Kuzzle v2 SDKs, a compatibility matrix has been added. It works with simplified semver ranges.

Example  compat matrix:
```
{
  "js": "<7" // this version of Kuzzle is compatible with the JS SDKs prior to the 7.x.x version
}
```

Example `sdkName` volatile property: `js@7.4.2`
  • Loading branch information
Aschen committed Nov 1, 2019
1 parent 17f214f commit 5d58e5d
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 32 deletions.
1 change: 1 addition & 0 deletions doc/2/api/essentials/errors/codes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ order: 500
| api.process.connection_dropped<br/><pre>0x02020003</pre> | [BadRequestError](/core/2/api/essentials/errors/handling#badrequesterror) <pre>(400)</pre> | The request has been discarded because its linked client connection has dropped |
| api.process.controller_not_found<br/><pre>0x02020004</pre> | [NotFoundError](/core/2/api/essentials/errors/handling#notfounderror) <pre>(404)</pre> | API controller not found |
| api.process.action_not_found<br/><pre>0x02020005</pre> | [NotFoundError](/core/2/api/essentials/errors/handling#notfounderror) <pre>(404)</pre> | API controller action not found |
| api.process.incompatible_sdk_version<br/><pre>0x02020006</pre> | [BadRequestError](/core/2/api/essentials/errors/handling#badrequesterror) <pre>(400)</pre> | SDK is incompatible with the current Kuzzle version |

---

Expand Down
9 changes: 2 additions & 7 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,8 @@ services:
- "6379:6379"

elasticsearch:
image: kuzzleio/elasticsearch:7.3.0
image: kuzzleio/elasticsearch:7.4.0
ports:
- "9200:9200"
ulimits:
nofile: 65536
environment:
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- node.name=alyx
- cluster.name=kuzzle
- discovery.type=single-node
nofile: 65536
127 changes: 103 additions & 24 deletions lib/api/controllers/funnelController.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const
errorsManager = require('../../util/errors'),
{ errors: { KuzzleError } } = require('kuzzle-common-objects'),
documentEventAliases = require('../../config/documentEventAliases'),
DocumentExtractor = require('../../util/DocumentExtractor');
DocumentExtractor = require('../../util/DocumentExtractor'),
sdkCompatibility = require('../../config/sdkCompatibility');

const processError = errorsManager.wrap('api', 'process');

Expand All @@ -59,7 +60,7 @@ class CacheItem {
* @param {Kuzzle} kuzzle
*/
class FunnelController {
constructor(kuzzle) {
constructor (kuzzle) {
this.kuzzle = kuzzle;
this.overloaded = false;
this.concurrentRequests = 0;
Expand All @@ -73,9 +74,11 @@ class FunnelController {

this.lastDumpedErrors = {};
this.loadDocumentEventAliases();

this.sdkCompatibility = sdkCompatibility;
}

init() {
init () {
this.controllers.auth = new AuthController(this.kuzzle);
this.controllers.bulk = new BulkController(this.kuzzle);
this.controllers.collection = new CollectionController(this.kuzzle);
Expand All @@ -94,11 +97,11 @@ class FunnelController {
return Bluebird.all(initPromises);
}

loadPluginControllers() {
loadPluginControllers () {
this.pluginsControllers = this.kuzzle.pluginsManager.getPluginControllers();
}

loadDocumentEventAliases() {
loadDocumentEventAliases () {
this.documentEventAliases = documentEventAliases;
this.documentEventAliases.mirrorList = {};

Expand Down Expand Up @@ -132,7 +135,7 @@ class FunnelController {
* @param {Function} executeCallback - the original callback given to `execute`
* @returns {boolean}
*/
getRequestSlot(executor, request, executeCallback) {
getRequestSlot (executor, request, executeCallback) {
if (this.overloaded) {
const now = Date.now();

Expand Down Expand Up @@ -215,7 +218,7 @@ class FunnelController {
* @param {Function} callback
* @returns {Number} -1: request delayed, 0: request processing, 1: error
*/
execute(request, callback) {
execute (request, callback) {
const processNow = this.getRequestSlot('execute', request, callback);

if (request.error) {
Expand Down Expand Up @@ -266,7 +269,7 @@ class FunnelController {
* @param {Function} callback
* @returns {Number} -1: request delayed, 0: request processing, 1: error while trying to get the request slot
*/
mExecute(request, callback) {
mExecute (request, callback) {
const processNow = this.getRequestSlot('mExecute', request, callback);

if (request.error) {
Expand All @@ -291,7 +294,7 @@ class FunnelController {
*
* @param {KuzzleError|*} err
*/
handleErrorDump(err) {
handleErrorDump (err) {
const handledErrors = this.kuzzle.config.dump.handledErrors;

if (this.kuzzle.config.dump.enabled && handledErrors.enabled) {
Expand Down Expand Up @@ -343,7 +346,7 @@ class FunnelController {
* @param {Request} request
* @return {Promise<Request>}
*/
checkRights(request) {
checkRights (request) {
return this.kuzzle.repositories.token.verifyToken(request.input.jwt)
.then(userToken => {
request.context.token = userToken;
Expand Down Expand Up @@ -386,15 +389,17 @@ class FunnelController {
* @param {KuzzleRequest} request
* @return {Promise}
*/
processRequest(request) {
processRequest (request) {
const controller = this.getController(request);

this.kuzzle.statistics.startRequest(request);
this.concurrentRequests++;

let modifiedRequest = request;

return this.performDocumentAlias(request, 'before')
return Bluebird.resolve()
.then(() => this._checkSdkVersion(request))
.then(() => this.performDocumentAlias(request, 'before'))
.then(newRequest => this.kuzzle.pipe(
this.getEventName(newRequest, 'before'), newRequest))
.then(newRequest => {
Expand All @@ -405,25 +410,28 @@ class FunnelController {
.then(responseData => {
modifiedRequest.setResult(
responseData,
{status: request.status === 102 ? 200 : request.status});
{ status: request.status === 102 ? 200 : request.status });

if (!this.isNativeController(modifiedRequest)
if ( !this.isNativeController(modifiedRequest)
&& !modifiedRequest.response.raw
) {
// check if the plugin response can be serialized
try {
JSON.stringify(responseData);
} catch (e) {
}
catch (e) {
modifiedRequest.setResult(null);
errorsManager.throw('plugin', 'controller', 'unserializable_response');
}
}

return this.kuzzle.pipe(this.getEventName(request, 'after'), modifiedRequest);
})
.then(newRequest => this.performDocumentAlias(newRequest, 'after'))
.then(newRequest => this.kuzzle.pipe('request:onSuccess', newRequest))
.then(newRequest => {
this.kuzzle.statistics.completedRequest(request);

return newRequest;
})
.catch(error => this.handleProcessRequestError(
Expand All @@ -441,7 +449,7 @@ class FunnelController {
* @param {String} prefix
* @return {Promise<KuzzleRequest>}
*/
performDocumentAlias(request, prefix) {
performDocumentAlias (request, prefix) {
const { controller, action } = request.input;
const mustTrigger =
controller === 'document'
Expand Down Expand Up @@ -473,7 +481,7 @@ class FunnelController {
* @param {Request} request
* @returns {Promise}
*/
executePluginRequest(request) {
executePluginRequest (request) {
return Bluebird.resolve()
.then(() => doAction(this.getController(request), request))
.catch(e => {
Expand All @@ -482,7 +490,7 @@ class FunnelController {
});
}

handleProcessRequestError(modifiedRequest, request, error) {
handleProcessRequestError (modifiedRequest, request, error) {
let _error = error;
const eventError = this.getEventName(modifiedRequest, 'error');

Expand Down Expand Up @@ -578,7 +586,7 @@ class FunnelController {
* @return {Object} controller object
* @throws {BadRequestError} If the asked controller or action is unknown
*/
getController(request) {
getController (request) {
const {controller, action} = request.input;

let target;
Expand All @@ -587,11 +595,13 @@ class FunnelController {
if (this.controllers[controller].isAction(action)) {
target = this.controllers[controller];
}
} else if (this.pluginsControllers[controller]) {
}
else if (this.pluginsControllers[controller]) {
if (this.pluginsControllers[controller][action]) {
target = this.pluginsControllers[controller];
}
} else {
}
else {
processError.throw('controller_not_found', controller);
}

Expand All @@ -607,10 +617,53 @@ class FunnelController {
* @param {Request} request
* @return {Boolean}
*/
isNativeController(request) {
isNativeController (request) {
return Boolean(this.controllers[request.input.controller]);
}

/**
* If the request is coming from an official SDK,
* then checks the compatibility of the SDK against current Kuzzle version.
*
* @param {Request} request
*
* @throws
*/
_checkSdkVersion (request) {
const
sdkVersion = request.input.volatile && request.input.volatile.sdkVersion,
sdkName = request.input.volatile && request.input.volatile.sdkName;

// sdkVersion property is only used by Kuzzle v1 SDKs
if (sdkVersion) {
processError.throw('incompatible_sdk_version', sdkVersion, 'Kuzzle v2');
}

if (! sdkName || typeof sdkName !== 'string') {
return;
}

const
separatorIdx = sdkName.indexOf('@'),
name = sdkName.substr(0, separatorIdx),
version = sdkName.substr(separatorIdx + 1);

if (name.length === 0 || version.length === 0) {
return;
}

const requirements = this.sdkCompatibility[name];
if (! requirements) {
return;
}

if (! satisfiesMajor(version, requirements)) {
const hint = `min: ${requirements.min || 'none'}, max: ${requirements.max || 'none'}`;

processError.throw('incompatible_sdk_version', version, hint);
}
}

/**
* Populates the given request with the error and calls the callback
*
Expand Down Expand Up @@ -680,7 +733,7 @@ class FunnelController {
* @param {string} string
* @returns {string}
*/
function capitalize(string) {
function capitalize (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

Expand All @@ -695,7 +748,7 @@ function capitalize(string) {
* @param {Request} request
* @return {Promise}
*/
function doAction(controller, request) {
function doAction (controller, request) {
const ret = controller[request.input.action](request);

if (!ret || typeof ret.then !== 'function') {
Expand All @@ -710,4 +763,30 @@ function doAction(controller, request) {
return ret;
}

/**
* Very straightforward function to check only if the version satisfies
* the major version requirements
*
* @param {String} version
* @param {Object} requirements
*
* @returns {Boolean}
*/
function satisfiesMajor (version, requirements) {
let
maxRequirement = true,
minRequirement = true;

if (requirements.min) {
minRequirement = version[0] >= requirements.min.toString();
}

if (requirements.max) {
maxRequirement = version[0] <= requirements.max.toString();
}

return maxRequirement && minRequirement;
}


module.exports = FunnelController;
6 changes: 6 additions & 0 deletions lib/config/error-codes/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@
"code": 5,
"message": "Controller action \"%s\" not found.",
"class": "NotFoundError"
},
"incompatible_sdk_version": {
"description": "SDK is incompatible with the current Kuzzle version",
"code": 6,
"message": "Incompatible SDK client. Your SDK version (%s) does not match Kuzzle requirement (%s).",
"class": "BadRequestError"
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions lib/config/sdkCompatibility.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"js": { "min": 7 },
"csharp": { "min": 2 },
"cpp": { "min": 2 },
"java": { "min": 3 },
"android": { "min": 5 },
"go": { "min": 3 },
"php": { "min": 4 }
}
Loading

0 comments on commit 5d58e5d

Please sign in to comment.