@mimik/api-helper 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +43 -0
- package/.nycrc +4 -0
- package/README.md +130 -0
- package/index.js +347 -0
- package/lib/ajvHelpers.js +22 -0
- package/lib/baseHandlers.js +88 -0
- package/lib/common.js +80 -0
- package/lib/extract-helper.js +58 -0
- package/lib/oauthValidation-helper.js +40 -0
- package/lib/securityHandlers.js +345 -0
- package/package.json +56 -0
package/.eslintrc
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugins": [
|
|
3
|
+
"@mimik/document-env",
|
|
4
|
+
"@mimik/dependencies"
|
|
5
|
+
],
|
|
6
|
+
"env": {
|
|
7
|
+
"node": true
|
|
8
|
+
},
|
|
9
|
+
"parserOptions": {
|
|
10
|
+
"ecmaVersion": 2020
|
|
11
|
+
},
|
|
12
|
+
"extends": "airbnb",
|
|
13
|
+
"rules": {
|
|
14
|
+
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
|
|
15
|
+
"import/no-unresolved": ["error", { "amd": true, "commonjs": true, "caseSensitiveStrict": true }],
|
|
16
|
+
"brace-style": [1, "stroustrup", { "allowSingleLine": true }],
|
|
17
|
+
"no-confusing-arrow": [0], // arrow isnt confusing
|
|
18
|
+
"max-len": [1, 180, { "ignoreComments": true }],
|
|
19
|
+
"linebreak-style": 0,
|
|
20
|
+
"quotes": [1, "single"],
|
|
21
|
+
"semi": [1, "always"],
|
|
22
|
+
"no-process-env": ["error"],
|
|
23
|
+
"@mimik/document-env/validate-document-env": 2,
|
|
24
|
+
"@mimik/dependencies/case-sensitive": 2,
|
|
25
|
+
"@mimik/dependencies/no-cycles": 2,
|
|
26
|
+
"@mimik/dependencies/require-json-ext": 2
|
|
27
|
+
},
|
|
28
|
+
"settings":{
|
|
29
|
+
"react": {
|
|
30
|
+
"version": "detect"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"globals": {
|
|
34
|
+
"module": true,
|
|
35
|
+
"require": true,
|
|
36
|
+
"const": false,
|
|
37
|
+
"it": false,
|
|
38
|
+
"describe": false,
|
|
39
|
+
"before": true,
|
|
40
|
+
"after": true,
|
|
41
|
+
"JSON": true
|
|
42
|
+
}
|
|
43
|
+
}
|
package/.nycrc
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
## Functions
|
|
2
|
+
|
|
3
|
+
<dl>
|
|
4
|
+
<dt><a href="#apiSetup">apiSetup(setup, registeredOperations, securityHandlers, extraFormats, config, correlationId)</a> ⇒ <code>Promise</code></dt>
|
|
5
|
+
<dd><p>Setup the API to be use for a service</p>
|
|
6
|
+
</dd>
|
|
7
|
+
<dt><a href="#getAPIFile">getAPIFile(apiFilename, correlationId, options)</a> ⇒ <code>Promise</code></dt>
|
|
8
|
+
<dd><p>Gets the API file from swaggerhub and store it in the give PATH location.</p>
|
|
9
|
+
</dd>
|
|
10
|
+
<dt><a href="#setupServerFiles">setupServerFiles(apiFilename, controllersDirectory, buidDirectory, correlationId, options)</a> ⇒ <code>Promise</code></dt>
|
|
11
|
+
<dd><p>Setup and validates files for the server</p>
|
|
12
|
+
</dd>
|
|
13
|
+
<dt><a href="#validateSecuritySchemes">validateSecuritySchemes(apiDefinition, correlationId)</a> ⇒</dt>
|
|
14
|
+
<dd><p>Validates the known SecuritySchemes: <code>SystemSecurity</code>, <code>AdminSecurity</code>, <code>UserSecurity</code>, <code>PeerSecurity</code>.</p>
|
|
15
|
+
</dd>
|
|
16
|
+
<dt><a href="#extractProperties">extractProperties(apiDefinition, controllersDirectory, buidDirectory, correlationId)</a> ⇒</dt>
|
|
17
|
+
<dd><p>Extracts the properties from API definiton and creates a file binding the handler with the controller operations.</p>
|
|
18
|
+
</dd>
|
|
19
|
+
</dl>
|
|
20
|
+
|
|
21
|
+
<a name="apiSetup"></a>
|
|
22
|
+
|
|
23
|
+
## apiSetup(setup, registeredOperations, securityHandlers, extraFormats, config, correlationId) ⇒ <code>Promise</code>
|
|
24
|
+
Setup the API to be use for a service
|
|
25
|
+
|
|
26
|
+
**Kind**: global function
|
|
27
|
+
**Returns**: <code>Promise</code> - .
|
|
28
|
+
&fulfil {object} The API file itself.
|
|
29
|
+
**Category**: async
|
|
30
|
+
**Throws**:
|
|
31
|
+
|
|
32
|
+
- <code>Promise</code> An error is thrown if the initiatilization failed.
|
|
33
|
+
|
|
34
|
+
By default System and Admin security are automatically registered
|
|
35
|
+
|
|
36
|
+
**Requires**: <code>module:@mimik/response-helper</code>, <code>module:@mimik/sumologic-winston-logger</code>, <code>module:@mimik/swagger-helper</code>, <code>module:ajv-formats</code>, <code>module:fs</code>, <code>module:jsonwebtoken</code>, <code>module:lodash</code>
|
|
37
|
+
|
|
38
|
+
| Param | Type | Description |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| setup | <code>object</code> | Object containing the apiFilename and the exisiting securitySchemes in the API definition. |
|
|
41
|
+
| registeredOperations | <code>object</code> | List of the operation to register for the API. |
|
|
42
|
+
| securityHandlers | <code>object</code> | List of the securityHandlers to add for the service. |
|
|
43
|
+
| extraFormats | <code>object</code> | list of the formats to add for validatng properties. |
|
|
44
|
+
| config | <code>object</code> | Configuration of the service. |
|
|
45
|
+
| correlationId | <code>UUID.<string></code> | CorrelationId when logging activites. |
|
|
46
|
+
|
|
47
|
+
<a name="getAPIFile"></a>
|
|
48
|
+
|
|
49
|
+
## getAPIFile(apiFilename, correlationId, options) ⇒ <code>Promise</code>
|
|
50
|
+
Gets the API file from swaggerhub and store it in the give PATH location.
|
|
51
|
+
|
|
52
|
+
**Kind**: global function
|
|
53
|
+
**Returns**: <code>Promise</code> - .
|
|
54
|
+
&fulfil {object} The API file itself.
|
|
55
|
+
**Category**: async
|
|
56
|
+
**Throws**:
|
|
57
|
+
|
|
58
|
+
- <code>Promise</code> An error is thrown if the apiFilename resolution generates an error or the request to the API provider fails or the file connot be saved.
|
|
59
|
+
|
|
60
|
+
**Requires**: <code>module:@mimik/request-retry</code>, <code>module:@mimik/response-helper</code>, <code>module:@mimik/sumologic-winston-logger</code>, <code>module:fs</code>, <code>module:js-yaml</code>, <code>module:path</code>
|
|
61
|
+
|
|
62
|
+
| Param | Type | Description |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| apiFilename | <code>PATH.<string></code> | Name of the file where the API file will be stored. |
|
|
65
|
+
| correlationId | <code>UUID.<string></code> | CorrelationId when logging activites. |
|
|
66
|
+
| options | <code>object</code> | Options associated with the call. Use to pass `metrics` to `rpRetry` and `apiKey`` to access private API. |
|
|
67
|
+
|
|
68
|
+
<a name="setupServerFiles"></a>
|
|
69
|
+
|
|
70
|
+
## setupServerFiles(apiFilename, controllersDirectory, buidDirectory, correlationId, options) ⇒ <code>Promise</code>
|
|
71
|
+
Setup and validates files for the server
|
|
72
|
+
|
|
73
|
+
**Kind**: global function
|
|
74
|
+
**Returns**: <code>Promise</code> - .
|
|
75
|
+
&fulfil {object} The API file, the API filename, and the existing know security schemes.
|
|
76
|
+
**Category**: async
|
|
77
|
+
**Throws**:
|
|
78
|
+
|
|
79
|
+
- <code>Promise</code> An error is thrown for many reasons assocated with getAPIFile or validateSecuritySchemes or extractProperties.
|
|
80
|
+
|
|
81
|
+
**Requires**: <code>module:@mimik/request-retry</code>, <code>module:@mimik/response-helper</code>, <code>module:@mimik/sumologic-winston-logger</code>, <code>module:fs</code>, <code>module:js-yaml</code>, <code>module:path</code>
|
|
82
|
+
|
|
83
|
+
| Param | Type | Description |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| apiFilename | <code>PATH.<string></code> | Name of the file where the API file will be stored. |
|
|
86
|
+
| controllersDirectory | <code>PATH.<string></code> | Directory to find the controller files. |
|
|
87
|
+
| buidDirectory | <code>PATH.<string></code> | = Directory where the register file will be stored. |
|
|
88
|
+
| correlationId | <code>UUID.<string></code> | CorrelationId when logging activites. |
|
|
89
|
+
| options | <code>object</code> | Options associated with the call. Use to pass `metrics` to `rpRetry` and `apiKey`` to access private API. |
|
|
90
|
+
|
|
91
|
+
<a name="validateSecuritySchemes"></a>
|
|
92
|
+
|
|
93
|
+
## validateSecuritySchemes(apiDefinition, correlationId) ⇒
|
|
94
|
+
Validates the known SecuritySchemes: `SystemSecurity`, `AdminSecurity`, `UserSecurity`, `PeerSecurity`.
|
|
95
|
+
|
|
96
|
+
**Kind**: global function
|
|
97
|
+
**Returns**: An array of the know securitySchemes that are in the API definition.
|
|
98
|
+
**Category**: sync
|
|
99
|
+
**Throws**:
|
|
100
|
+
|
|
101
|
+
- An error is thrown for the first validation fails.
|
|
102
|
+
|
|
103
|
+
**Requires**: <code>module:@mimik/sumologic-winston-logger</code>, <code>module:@mimik/response-helper</code>
|
|
104
|
+
|
|
105
|
+
| Param | Type | Description |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| apiDefinition | <code>object</code> | JSON object containing the API definition. |
|
|
108
|
+
| correlationId | <code>UUID.<string></code> | CorrelationId when logging activites. |
|
|
109
|
+
|
|
110
|
+
<a name="extractProperties"></a>
|
|
111
|
+
|
|
112
|
+
## extractProperties(apiDefinition, controllersDirectory, buidDirectory, correlationId) ⇒
|
|
113
|
+
Extracts the properties from API definiton and creates a file binding the handler with the controller operations.
|
|
114
|
+
|
|
115
|
+
**Kind**: global function
|
|
116
|
+
**Returns**: null
|
|
117
|
+
**Category**: sync
|
|
118
|
+
**Throws**:
|
|
119
|
+
|
|
120
|
+
- An error is thrown for many reasons, like operationId does not exist in controllers, controller dies not exist...
|
|
121
|
+
|
|
122
|
+
**Requires**: <code>module:@mimik/response-helper</code>, <code>module:@mimik/sumologic-winston-logger</code>, <code>module:fs</code>
|
|
123
|
+
|
|
124
|
+
| Param | Type | Description |
|
|
125
|
+
| --- | --- | --- |
|
|
126
|
+
| apiDefinition | <code>object</code> | JSON object containing the API definition. |
|
|
127
|
+
| controllersDirectory | <code>PATH.<string></code> | Directory to find the controller files. |
|
|
128
|
+
| buidDirectory | <code>PATH.<string></code> | = Directory where the register file will be stored. |
|
|
129
|
+
| correlationId | <code>UUID.<string></code> | CorrelationId when logging activites. |
|
|
130
|
+
|
package/index.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
const { OpenAPIBackend } = require('openapi-backend');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const pathLib = require('path');
|
|
4
|
+
const yaml = require('js-yaml');
|
|
5
|
+
const _ = require('lodash');
|
|
6
|
+
|
|
7
|
+
const logger = require('@mimik/sumologic-winston-logger');
|
|
8
|
+
const { getRichError } = require('@mimik/response-helper');
|
|
9
|
+
const { rpRetry } = require('@mimik/request-retry');
|
|
10
|
+
|
|
11
|
+
const { saveProperties } = require('./lib/extract-helper');
|
|
12
|
+
const { validateOauth2, validateApiKey } = require('./lib/oauthValidation-helper');
|
|
13
|
+
const { ajvFormats } = require('./lib/ajvHelpers');
|
|
14
|
+
const securityLib = require('./lib/securityHandlers');
|
|
15
|
+
const baseHandlers = require('./lib/baseHandlers');
|
|
16
|
+
const {
|
|
17
|
+
LOCAL,
|
|
18
|
+
SET_ON,
|
|
19
|
+
SECURITY_ON,
|
|
20
|
+
SECURITY_OFF,
|
|
21
|
+
X_ROUTER_CONTROLLER,
|
|
22
|
+
EXTENSION,
|
|
23
|
+
ADMIN_SECURITY,
|
|
24
|
+
SYSTEM_SECURITY,
|
|
25
|
+
PEER_SECURITY,
|
|
26
|
+
USER_SECURITY,
|
|
27
|
+
API_KEY_SECURITY,
|
|
28
|
+
CLIENT_CREDENTIALS,
|
|
29
|
+
IMPLICIT,
|
|
30
|
+
POSTFIX,
|
|
31
|
+
API_PROVIDER,
|
|
32
|
+
RESOLVED,
|
|
33
|
+
} = require('./lib/common');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
*
|
|
37
|
+
* Setup the API to be use for a service
|
|
38
|
+
*
|
|
39
|
+
* @function apiSetup
|
|
40
|
+
* @category async
|
|
41
|
+
* @requires @mimik/response-helper
|
|
42
|
+
* @requires @mimik/sumologic-winston-logger
|
|
43
|
+
* @requires @mimik/swagger-helper
|
|
44
|
+
* @requires ajv-formats
|
|
45
|
+
* @requires fs
|
|
46
|
+
* @requires jsonwebtoken
|
|
47
|
+
* @requires lodash
|
|
48
|
+
* @param {object} setup - Object containing the apiFilename and the exisiting securitySchemes in the API definition.
|
|
49
|
+
* @param {object} registeredOperations - List of the operation to register for the API.
|
|
50
|
+
* @param {object} securityHandlers - List of the securityHandlers to add for the service.
|
|
51
|
+
* @param {object} extraFormats - list of the formats to add for validatng properties.
|
|
52
|
+
* @param {object} config - Configuration of the service.
|
|
53
|
+
* @param {UUID.<string>} correlationId - CorrelationId when logging activites.
|
|
54
|
+
* @return {Promise}.
|
|
55
|
+
* &fulfil {object} The API file itself.
|
|
56
|
+
* @throws {Promise} An error is thrown if the initiatilization failed.
|
|
57
|
+
*
|
|
58
|
+
* By default System and Admin security are automatically registered
|
|
59
|
+
*/
|
|
60
|
+
const apiSetup = (setup, registeredOperations, securityHandlers, extraFormats, config, correlationId) => {
|
|
61
|
+
const { apiFilename, existingSecuritySchemes } = setup;
|
|
62
|
+
const { SystemSecurity, AdminSecurity } = securityLib(config);
|
|
63
|
+
const api = new OpenAPIBackend({
|
|
64
|
+
definition: apiFilename,
|
|
65
|
+
apiRoot: config.serverSettings.basePath,
|
|
66
|
+
strict: true,
|
|
67
|
+
validate: true,
|
|
68
|
+
ajvOpts: {
|
|
69
|
+
allErrors: true,
|
|
70
|
+
verbose: true,
|
|
71
|
+
},
|
|
72
|
+
customizeAjv: ajvFormats(extraFormats),
|
|
73
|
+
handlers: baseHandlers,
|
|
74
|
+
});
|
|
75
|
+
let mode;
|
|
76
|
+
|
|
77
|
+
api.register(registeredOperations);
|
|
78
|
+
if (config && (config.nodeEnvironment.toLowerCase() !== LOCAL || config.serverSettings.securitySet === SET_ON)) {
|
|
79
|
+
mode = SECURITY_ON;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
logger.warn('security disabled: tokens will not be used and /me and /onbehalf will not work', correlationId);
|
|
83
|
+
mode = SECURITY_OFF;
|
|
84
|
+
}
|
|
85
|
+
api.registerSecurityHandler(SYSTEM_SECURITY, SystemSecurity[mode]);
|
|
86
|
+
api.registerSecurityHandler(ADMIN_SECURITY, AdminSecurity[mode]);
|
|
87
|
+
// api.registerSecurityHandler('ApiKeySecurity', ApiKeySecurity[mode]);
|
|
88
|
+
if (securityHandlers) {
|
|
89
|
+
const securityHandlerNames = Object.keys(securityHandlers);
|
|
90
|
+
|
|
91
|
+
existingSecuritySchemes.forEach((securityScheme) => {
|
|
92
|
+
if (!securityHandlerNames.includes(securityScheme)) throw getRichError('System', 'missing handler for security Scheme', { securityScheme });
|
|
93
|
+
});
|
|
94
|
+
Object.keys(securityHandlers).forEach((securityHandlerName) => {
|
|
95
|
+
api.registerSecurityHandler(securityHandlerName, securityHandlers[securityHandlerName][mode]);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
else if (existingSecuritySchemes.length !== 0) throw getRichError('System', 'missing handlers for security Scheme', { existingSecuritySchemes });
|
|
99
|
+
api.init()
|
|
100
|
+
.catch((err) => {
|
|
101
|
+
throw getRichError('System', 'could not initialize the api', { api }, err);
|
|
102
|
+
});
|
|
103
|
+
return api;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
*
|
|
108
|
+
* Gets the API file from swaggerhub and store it in the give PATH location.
|
|
109
|
+
*
|
|
110
|
+
* @function getAPIFile
|
|
111
|
+
* @category async
|
|
112
|
+
* @requires @mimik/request-retry
|
|
113
|
+
* @requires @mimik/response-helper
|
|
114
|
+
* @requires @mimik/sumologic-winston-logger
|
|
115
|
+
* @requires fs
|
|
116
|
+
* @requires js-yaml
|
|
117
|
+
* @requires path
|
|
118
|
+
* @param {PATH.<string>} apiFilename - Name of the file where the API file will be stored.
|
|
119
|
+
* @param {UUID.<string>} correlationId - CorrelationId when logging activites.
|
|
120
|
+
* @param {object} options - Options associated with the call. Use to pass `metrics` to `rpRetry` and `apiKey`` to access private API.
|
|
121
|
+
* @return {Promise}.
|
|
122
|
+
* &fulfil {object} The API file itself.
|
|
123
|
+
* @throws {Promise} An error is thrown if the apiFilename resolution generates an error or the request to the API provider fails or the file connot be saved.
|
|
124
|
+
*/
|
|
125
|
+
const getAPIFile = (apiFilename, correlationId, options) => {
|
|
126
|
+
logger.info('Getting API definition', correlationId);
|
|
127
|
+
try {
|
|
128
|
+
if (fs.existsSync(apiFilename)) {
|
|
129
|
+
logger.debug('API file already exists', { apiFilename }, correlationId);
|
|
130
|
+
let apiDefinition = fs.readFileSync(apiFilename, 'utf8');
|
|
131
|
+
|
|
132
|
+
try { apiDefinition = JSON.parse(apiDefinition); }
|
|
133
|
+
catch (errJSON) {
|
|
134
|
+
try { apiDefinition = yaml.load(apiDefinition); }
|
|
135
|
+
catch (errYaml) {
|
|
136
|
+
return Promise.reject(getRichError('System', 'wrong file format', { apiFilename }, errYaml));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return Promise.resolve(apiDefinition);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
return Promise.reject(getRichError('System', 'file system error', { apiFilename }, err));
|
|
144
|
+
}
|
|
145
|
+
let fileName;
|
|
146
|
+
let apiDirectory;
|
|
147
|
+
|
|
148
|
+
try { fileName = pathLib.basename(apiFilename); }
|
|
149
|
+
catch (err) { return Promise.reject(getRichError('System', 'file name error', { apiFilename }, err)); }
|
|
150
|
+
try { apiDirectory = pathLib.dirname(apiFilename); }
|
|
151
|
+
catch (err) { return Promise.reject(getRichError('System', 'directory name error', { apiFilename }, err)); }
|
|
152
|
+
const params = fileName.split('_');
|
|
153
|
+
|
|
154
|
+
if (params.length !== 4 || params[3] !== POSTFIX) {
|
|
155
|
+
return Promise.reject(getRichError('System', 'wrong api name', { apiFilename }));
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
if (!fs.existsSync(apiDirectory)) {
|
|
159
|
+
logger.debug('Creating directory', { apiDirectory }, correlationId);
|
|
160
|
+
fs.mkdirSync(apiDirectory);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
return Promise.reject(getRichError('System', 'file system error', { apiDirectory }, err));
|
|
165
|
+
}
|
|
166
|
+
const url = `${API_PROVIDER}/${params[0]}/${params[1]}/${params[2]}?${RESOLVED}`;
|
|
167
|
+
|
|
168
|
+
logger.debug('API file does not exist, retrieving it', { url }, correlationId);
|
|
169
|
+
const opts = {
|
|
170
|
+
method: 'GET',
|
|
171
|
+
url,
|
|
172
|
+
headers: {
|
|
173
|
+
'x-correlation-id': correlationId,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
if (options) {
|
|
177
|
+
if (options.apiKey) opts.headers = { Authorization: options.apiKey };
|
|
178
|
+
if (options.metrics) {
|
|
179
|
+
opts.metrics = options.metrics;
|
|
180
|
+
opts.metrics.url = url;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return rpRetry(opts)
|
|
184
|
+
.catch((err) => {
|
|
185
|
+
const error = err;
|
|
186
|
+
|
|
187
|
+
error.message = `${error.message} - { "apiFilename":"${apiFilename}"}`;
|
|
188
|
+
throw error;
|
|
189
|
+
})
|
|
190
|
+
.then((result) => {
|
|
191
|
+
try {
|
|
192
|
+
fs.writeFileSync(apiFilename, JSON.stringify(result, null, 2));
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
throw getRichError('System', `file system error: ${err.message}`, { apiFilename }, err);
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
*
|
|
203
|
+
* Validates the known SecuritySchemes: `SystemSecurity`, `AdminSecurity`, `UserSecurity`, `PeerSecurity`.
|
|
204
|
+
*
|
|
205
|
+
* @function validateSecuritySchemes
|
|
206
|
+
* @category sync
|
|
207
|
+
* @requires @mimik/sumologic-winston-logger
|
|
208
|
+
* @requires @mimik/response-helper
|
|
209
|
+
* @param {object} apiDefinition - JSON object containing the API definition.
|
|
210
|
+
* @param {UUID.<string>} correlationId - CorrelationId when logging activites.
|
|
211
|
+
* @return An array of the know securitySchemes that are in the API definition.
|
|
212
|
+
* @throws An error is thrown for the first validation fails.
|
|
213
|
+
*/
|
|
214
|
+
const validateSecuritySchemes = (apiDefinition, correlationId) => {
|
|
215
|
+
const existingSecuritySchemes = [];
|
|
216
|
+
|
|
217
|
+
if (apiDefinition.components?.securitySchemes) {
|
|
218
|
+
logger.info('validating known security schemes', correlationId);
|
|
219
|
+
const { securitySchemes } = apiDefinition.components;
|
|
220
|
+
|
|
221
|
+
validateOauth2(securitySchemes, ADMIN_SECURITY, CLIENT_CREDENTIALS);
|
|
222
|
+
validateOauth2(securitySchemes, SYSTEM_SECURITY, CLIENT_CREDENTIALS);
|
|
223
|
+
existingSecuritySchemes.push(validateOauth2(securitySchemes, PEER_SECURITY, CLIENT_CREDENTIALS));
|
|
224
|
+
existingSecuritySchemes.push(validateOauth2(securitySchemes, USER_SECURITY, IMPLICIT));
|
|
225
|
+
existingSecuritySchemes.push(validateApiKey(securitySchemes, API_KEY_SECURITY));
|
|
226
|
+
}
|
|
227
|
+
return existingSecuritySchemes;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
*
|
|
232
|
+
* Extracts the properties from API definiton and creates a file binding the handler with the controller operations.
|
|
233
|
+
*
|
|
234
|
+
* @function extractProperties
|
|
235
|
+
* @category sync
|
|
236
|
+
* @requires @mimik/response-helper
|
|
237
|
+
* @requires @mimik/sumologic-winston-logger
|
|
238
|
+
* @requires fs
|
|
239
|
+
* @param {object} apiDefinition - JSON object containing the API definition.
|
|
240
|
+
* @param {PATH.<string>} controllersDirectory - Directory to find the controller files.
|
|
241
|
+
* @param {PATH.<string>} buidDirectory = Directory where the register file will be stored.
|
|
242
|
+
* @param {UUID.<string>} correlationId - CorrelationId when logging activites.
|
|
243
|
+
* @return null
|
|
244
|
+
* @throws An error is thrown for many reasons, like operationId does not exist in controllers, controller dies not exist...
|
|
245
|
+
*/
|
|
246
|
+
const extractProperties = (apiDefinition, controllersDirectory, buildDirectory, correlationId) => {
|
|
247
|
+
const result = {};
|
|
248
|
+
const { paths } = apiDefinition;
|
|
249
|
+
let controllersDirectoryName;
|
|
250
|
+
|
|
251
|
+
try { controllersDirectoryName = pathLib.basename(controllersDirectory); }
|
|
252
|
+
catch (err) {
|
|
253
|
+
throw getRichError('System', 'directory name error', { controllersDirectory }, err);
|
|
254
|
+
}
|
|
255
|
+
logger.info('creating handler link file', correlationId);
|
|
256
|
+
try {
|
|
257
|
+
if (!fs.existsSync(controllersDirectory)) {
|
|
258
|
+
throw getRichError('System', 'missing directory', { controllersDirectory });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
throw getRichError('System', 'file system error', { controllersDirectory }, err);
|
|
263
|
+
}
|
|
264
|
+
if (paths) {
|
|
265
|
+
const routes = Object.keys(paths);
|
|
266
|
+
|
|
267
|
+
routes.forEach((route) => {
|
|
268
|
+
const path = paths[route];
|
|
269
|
+
const verbs = Object.keys(path);
|
|
270
|
+
|
|
271
|
+
verbs.forEach((verb) => {
|
|
272
|
+
const operation = path[verb];
|
|
273
|
+
|
|
274
|
+
if (operation.operationId) {
|
|
275
|
+
if (!operation[X_ROUTER_CONTROLLER]) {
|
|
276
|
+
throw getRichError('System', 'missing property', { property: X_ROUTER_CONTROLLER });
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const controller = operation[X_ROUTER_CONTROLLER];
|
|
280
|
+
const controllerFilename = `${controllersDirectory}/${controller}${EXTENSION}`;
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
if (!fs.existsSync(controllerFilename)) {
|
|
284
|
+
throw getRichError('System', 'missing controller file', { controllerFilename });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
throw getRichError('System', 'file system error', { controllerFilename }, err);
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const file = fs.readFileSync(controllerFilename, 'utf8');
|
|
292
|
+
if (!file.includes(`${operation.operationId},`)
|
|
293
|
+
&& !file.includes(`${operation.operationId}:`)
|
|
294
|
+
&& !file.includes(`module.exports = ${operation.operationId}`)) { // code must be linted before
|
|
295
|
+
throw getRichError('System', 'missing operationId in controller file', { controllerFilename, operationId: operation.operationId });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
throw getRichError('System', 'file system error', { controllerFilename, operationId: operation.operationId }, err);
|
|
300
|
+
}
|
|
301
|
+
if (!result[controller]) result[controller] = [operation.operationId];
|
|
302
|
+
else result[controller].push(operation.operationId);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
saveProperties(result, buildDirectory, controllersDirectoryName, correlationId);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
*
|
|
313
|
+
* Setup and validates files for the server
|
|
314
|
+
*
|
|
315
|
+
* @function setupServerFiles
|
|
316
|
+
* @category async
|
|
317
|
+
* @requires @mimik/request-retry
|
|
318
|
+
* @requires @mimik/response-helper
|
|
319
|
+
* @requires @mimik/sumologic-winston-logger
|
|
320
|
+
* @requires fs
|
|
321
|
+
* @requires js-yaml
|
|
322
|
+
* @requires path
|
|
323
|
+
* @param {PATH.<string>} apiFilename - Name of the file where the API file will be stored.
|
|
324
|
+
* @param {PATH.<string>} controllersDirectory - Directory to find the controller files.
|
|
325
|
+
* @param {PATH.<string>} buidDirectory = Directory where the register file will be stored.
|
|
326
|
+
* @param {UUID.<string>} correlationId - CorrelationId when logging activites.
|
|
327
|
+
* @param {object} options - Options associated with the call. Use to pass `metrics` to `rpRetry` and `apiKey`` to access private API.
|
|
328
|
+
* @return {Promise}.
|
|
329
|
+
* &fulfil {object} The API file, the API filename, and the existing know security schemes.
|
|
330
|
+
* @throws {Promise} An error is thrown for many reasons assocated with getAPIFile or validateSecuritySchemes or extractProperties.
|
|
331
|
+
*/
|
|
332
|
+
const setupServerFiles = (apiFilename, controllersDirectory, buildDirectory, correlationId, options) => getAPIFile(apiFilename, correlationId, options)
|
|
333
|
+
.then((apiDefinition) => {
|
|
334
|
+
const existingSecuritySchemes = _.compact(validateSecuritySchemes(apiDefinition, correlationId));
|
|
335
|
+
|
|
336
|
+
extractProperties(apiDefinition, controllersDirectory, buildDirectory, correlationId);
|
|
337
|
+
return { apiDefinition, apiFilename, existingSecuritySchemes };
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
module.exports = {
|
|
341
|
+
apiSetup,
|
|
342
|
+
securityLib,
|
|
343
|
+
getAPIFile,
|
|
344
|
+
validateSecuritySchemes,
|
|
345
|
+
extractProperties,
|
|
346
|
+
setupServerFiles,
|
|
347
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const addFormats = require('ajv-formats');
|
|
2
|
+
|
|
3
|
+
const { DEFAULT_FORMATS } = require('./common');
|
|
4
|
+
|
|
5
|
+
const ajvFormats = (origFormats) => (ajv) => {
|
|
6
|
+
const extraFormats = {};
|
|
7
|
+
const libFormats = DEFAULT_FORMATS;
|
|
8
|
+
let formats = origFormats;
|
|
9
|
+
|
|
10
|
+
if (!formats) formats = {};
|
|
11
|
+
Object.keys(formats).forEach((format) => {
|
|
12
|
+
if (!formats[format].type) libFormats.push(format);
|
|
13
|
+
else extraFormats[format] = formats[format];
|
|
14
|
+
});
|
|
15
|
+
addFormats(ajv, libFormats);
|
|
16
|
+
Object.keys(extraFormats).forEach((format) => ajv.addFormat(format, extraFormats[format]));
|
|
17
|
+
return ajv;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
ajvFormats,
|
|
22
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const { rejectRequest } = require('@mimik/swagger-helper');
|
|
2
|
+
|
|
3
|
+
const validationFail = (c, req, res) => {
|
|
4
|
+
const error = new Error('Failed schema validation');
|
|
5
|
+
|
|
6
|
+
error.statusCode = 400;
|
|
7
|
+
error.info = {
|
|
8
|
+
method: req.method,
|
|
9
|
+
path: req.url,
|
|
10
|
+
errors: c.validation.errors,
|
|
11
|
+
warnings: c.validation.warnings || [],
|
|
12
|
+
};
|
|
13
|
+
rejectRequest(error, c, res);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const notFound = (c, req, res) => {
|
|
17
|
+
const path = req.url;
|
|
18
|
+
const error = new Error(`path ${path} not defined in Swagger specification`);
|
|
19
|
+
|
|
20
|
+
error.statusCode = 404;
|
|
21
|
+
error.info = {
|
|
22
|
+
method: req.method,
|
|
23
|
+
path,
|
|
24
|
+
};
|
|
25
|
+
rejectRequest(error, c, res);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const unauthorizedHandler = (c, req, res) => {
|
|
29
|
+
let error = new Error('Unauthorized');
|
|
30
|
+
|
|
31
|
+
error.statusCode = 401;
|
|
32
|
+
const schemes = Object.keys(c.security);
|
|
33
|
+
|
|
34
|
+
delete schemes.authorized;
|
|
35
|
+
schemes.forEach((scheme) => {
|
|
36
|
+
if (c.security[scheme]?.error) error = c.security[scheme].error;
|
|
37
|
+
});
|
|
38
|
+
rejectRequest(error, c, res);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const notImplemented = (c, req, res) => {
|
|
42
|
+
const { method } = req;
|
|
43
|
+
const path = req.url;
|
|
44
|
+
const error = new Error(`${req.method} ${path} defined in Swagger specification, but no implemented`);
|
|
45
|
+
|
|
46
|
+
error.statusCode = 505;
|
|
47
|
+
error.info = {
|
|
48
|
+
method,
|
|
49
|
+
path,
|
|
50
|
+
operationId: c.operation.operationId,
|
|
51
|
+
};
|
|
52
|
+
rejectRequest(error, c, res);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const methodNotAllowed = (c, req, res) => {
|
|
56
|
+
const { method } = req;
|
|
57
|
+
const path = req.url;
|
|
58
|
+
const error = new Error(`path ${path} defined in Swagger specification, but the method ${method} is not defined`);
|
|
59
|
+
|
|
60
|
+
error.statusCode = 405;
|
|
61
|
+
error.info = {
|
|
62
|
+
method,
|
|
63
|
+
path,
|
|
64
|
+
};
|
|
65
|
+
rejectRequest(error, c, res);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
const postResponseHandler = (c, req, res) => {
|
|
70
|
+
const valid = c.api.validateResponse(c.response, c.operation);
|
|
71
|
+
console.log('----->', c.response);
|
|
72
|
+
console.log('----->', valid);
|
|
73
|
+
if (valid.errors) {
|
|
74
|
+
// response validation failed
|
|
75
|
+
return res.status(502).json({ status: 502, err: valid.errors });
|
|
76
|
+
}
|
|
77
|
+
return res.status(200).json(c.response);
|
|
78
|
+
};
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
validationFail,
|
|
83
|
+
notFound,
|
|
84
|
+
unauthorizedHandler,
|
|
85
|
+
notImplemented,
|
|
86
|
+
methodNotAllowed,
|
|
87
|
+
// postResponseHandler,
|
|
88
|
+
};
|
package/lib/common.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const X_ROUTER_CONTROLLER = 'x-swagger-router-controller';
|
|
2
|
+
const EXTENSION = '.js';
|
|
3
|
+
const REGISTER = 'register';
|
|
4
|
+
|
|
5
|
+
const OAUTH2 = 'oauth2';
|
|
6
|
+
const API_KEY_IN = 'header';
|
|
7
|
+
const API_KEY_NAME = 'apiKey';
|
|
8
|
+
|
|
9
|
+
const ADMIN_SECURITY = 'AdminSecurity';
|
|
10
|
+
const SYSTEM_SECURITY = 'SystemSecurity';
|
|
11
|
+
const PEER_SECURITY = 'PeerSecurity';
|
|
12
|
+
const USER_SECURITY = 'UserSecurity';
|
|
13
|
+
const API_KEY_SECURITY = 'ApiKeySecurity';
|
|
14
|
+
|
|
15
|
+
const CLIENT_CREDENTIALS = 'clientCredentials';
|
|
16
|
+
const IMPLICIT = 'implicit';
|
|
17
|
+
|
|
18
|
+
const POSTFIX = 'swagger.json';
|
|
19
|
+
const API_PROVIDER = 'https://api.swaggerhub.com/apis';
|
|
20
|
+
const RESOLVED = 'resolved=true';
|
|
21
|
+
|
|
22
|
+
const LOCAL = 'local';
|
|
23
|
+
const SET_ON = 'on';
|
|
24
|
+
const SECURITY_ON = 'regular';
|
|
25
|
+
const SECURITY_OFF = 'mock';
|
|
26
|
+
|
|
27
|
+
const DEFAULT_FORMATS = ['date', 'time', 'date-time', 'byte', 'uuid', 'uri', 'email', 'ipv4', 'ipv6'];
|
|
28
|
+
|
|
29
|
+
const AUTHORIZATION = 'authorization';
|
|
30
|
+
const ADMIN = 'admin';
|
|
31
|
+
const SUB_ADMIN = 'subAdmin';
|
|
32
|
+
const CLIENT = '@clients';
|
|
33
|
+
const NO_GENERIC = 'noGeneric';
|
|
34
|
+
const ON_BEHALF = 'onBehalf';
|
|
35
|
+
const CLAIMS_DEFINITION = 'Claims';
|
|
36
|
+
const SCOPES_SEPARATOR = ' ';
|
|
37
|
+
const SCOPE_CLAIMS_SEPARATOR = '::';
|
|
38
|
+
const RESOURCE_SEPARATOR = ':';
|
|
39
|
+
const CLAIMS_SEPARATOR = ',';
|
|
40
|
+
const BEARERS = ['bearer', 'Bearer'];
|
|
41
|
+
const USER = 'user';
|
|
42
|
+
const CLUSTER = 'cluster';
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
X_ROUTER_CONTROLLER,
|
|
46
|
+
EXTENSION,
|
|
47
|
+
REGISTER,
|
|
48
|
+
OAUTH2,
|
|
49
|
+
API_KEY_IN,
|
|
50
|
+
API_KEY_NAME,
|
|
51
|
+
ADMIN_SECURITY,
|
|
52
|
+
SYSTEM_SECURITY,
|
|
53
|
+
PEER_SECURITY,
|
|
54
|
+
USER_SECURITY,
|
|
55
|
+
API_KEY_SECURITY,
|
|
56
|
+
CLIENT_CREDENTIALS,
|
|
57
|
+
IMPLICIT,
|
|
58
|
+
POSTFIX,
|
|
59
|
+
API_PROVIDER,
|
|
60
|
+
RESOLVED,
|
|
61
|
+
LOCAL,
|
|
62
|
+
SET_ON,
|
|
63
|
+
SECURITY_ON,
|
|
64
|
+
SECURITY_OFF,
|
|
65
|
+
DEFAULT_FORMATS,
|
|
66
|
+
AUTHORIZATION,
|
|
67
|
+
ADMIN,
|
|
68
|
+
SUB_ADMIN,
|
|
69
|
+
CLIENT,
|
|
70
|
+
NO_GENERIC,
|
|
71
|
+
ON_BEHALF,
|
|
72
|
+
CLAIMS_DEFINITION,
|
|
73
|
+
SCOPES_SEPARATOR,
|
|
74
|
+
SCOPE_CLAIMS_SEPARATOR,
|
|
75
|
+
RESOURCE_SEPARATOR,
|
|
76
|
+
CLAIMS_SEPARATOR,
|
|
77
|
+
BEARERS,
|
|
78
|
+
USER,
|
|
79
|
+
CLUSTER,
|
|
80
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
const { getRichError } = require('@mimik/response-helper');
|
|
4
|
+
const logger = require('@mimik/sumologic-winston-logger');
|
|
5
|
+
|
|
6
|
+
const { EXTENSION, REGISTER } = require('./common');
|
|
7
|
+
|
|
8
|
+
const saveProperties = (extractResult, buildDirectory, controllersDirectoryName, correlationId) => {
|
|
9
|
+
try {
|
|
10
|
+
if (!fs.existsSync(buildDirectory)) {
|
|
11
|
+
logger.debug('creating directory', { buildDirectory }, correlationId);
|
|
12
|
+
fs.mkdirSync(buildDirectory);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
throw getRichError('System', 'file system error', { buildDirectory }, err);
|
|
17
|
+
}
|
|
18
|
+
const filename = `${buildDirectory}/${REGISTER}${EXTENSION}`;
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync(filename)) {
|
|
21
|
+
logger.debug('removing file', { filename }, correlationId);
|
|
22
|
+
fs.unlinkSync(filename);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
throw getRichError('System', 'file system error', { filename }, err);
|
|
27
|
+
}
|
|
28
|
+
const controllers = Object.keys(extractResult);
|
|
29
|
+
const operationIds = [];
|
|
30
|
+
let stringToSave = '';
|
|
31
|
+
let itemToSave;
|
|
32
|
+
|
|
33
|
+
controllers.forEach((controller) => {
|
|
34
|
+
itemToSave = '';
|
|
35
|
+
extractResult[controller].forEach((operationId) => {
|
|
36
|
+
itemToSave = `${itemToSave} ${operationId},\n`;
|
|
37
|
+
operationIds.push(operationId);
|
|
38
|
+
});
|
|
39
|
+
stringToSave = `${stringToSave}const {\n${itemToSave}} = require('../${controllersDirectoryName}/${controller}');\n`;
|
|
40
|
+
});
|
|
41
|
+
stringToSave = `${stringToSave}\nmodule.exports = {\n`;
|
|
42
|
+
itemToSave = '';
|
|
43
|
+
operationIds.forEach((operationId) => {
|
|
44
|
+
itemToSave = `${itemToSave} ${operationId},\n`;
|
|
45
|
+
});
|
|
46
|
+
stringToSave = `${stringToSave}${itemToSave}};\n`;
|
|
47
|
+
logger.info(`creating ${filename}`, { filename }, correlationId);
|
|
48
|
+
try {
|
|
49
|
+
fs.writeFileSync(filename, stringToSave);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
throw getRichError('System', `file system error: ${err.message}`, { filename }, err);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
saveProperties,
|
|
58
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { getRichError } = require('@mimik/response-helper');
|
|
2
|
+
|
|
3
|
+
const { OAUTH2, API_KEY_IN, API_KEY_NAME } = require('./common');
|
|
4
|
+
|
|
5
|
+
const validateOauth2 = (securitySchemes, securityType, flow) => {
|
|
6
|
+
if (securitySchemes) {
|
|
7
|
+
const security = securitySchemes[securityType];
|
|
8
|
+
|
|
9
|
+
if (security) {
|
|
10
|
+
if (security.type !== OAUTH2) {
|
|
11
|
+
throw getRichError('System', `auth type is not ${OAUTH2}`, { securityType, receivedAuth: security.type, expectedAuth: OAUTH2 });
|
|
12
|
+
}
|
|
13
|
+
if (!security.flows[flow]) {
|
|
14
|
+
throw getRichError('System', 'no flow type available', { securityType, flow });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const validateApiKey = (securitySchemes, securityType) => {
|
|
21
|
+
if (securitySchemes) {
|
|
22
|
+
const security = securitySchemes[securityType];
|
|
23
|
+
|
|
24
|
+
if (security) {
|
|
25
|
+
if (security.in !== API_KEY_IN) {
|
|
26
|
+
throw getRichError('System', `apikey security must be in ${API_KEY_IN}`, { securityType, receivedIn: security.in, expectedIn: API_KEY_IN });
|
|
27
|
+
}
|
|
28
|
+
if (security.name !== API_KEY_NAME) {
|
|
29
|
+
throw getRichError('System', `apikey security must be named ${API_KEY_NAME}`, { securityType, receivedName: security.name, expectedName: API_KEY_NAME });
|
|
30
|
+
}
|
|
31
|
+
return securityType;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
validateOauth2,
|
|
39
|
+
validateApiKey,
|
|
40
|
+
};
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
const _ = require('lodash');
|
|
3
|
+
|
|
4
|
+
const { TOKEN_PARAMS } = require('@mimik/swagger-helper');
|
|
5
|
+
const {
|
|
6
|
+
AUTHORIZATION,
|
|
7
|
+
ADMIN,
|
|
8
|
+
SUB_ADMIN,
|
|
9
|
+
CLIENT,
|
|
10
|
+
NO_GENERIC,
|
|
11
|
+
ON_BEHALF,
|
|
12
|
+
CLAIMS_DEFINITION,
|
|
13
|
+
SCOPES_SEPARATOR,
|
|
14
|
+
SCOPE_CLAIMS_SEPARATOR,
|
|
15
|
+
RESOURCE_SEPARATOR,
|
|
16
|
+
CLAIMS_SEPARATOR,
|
|
17
|
+
BEARERS,
|
|
18
|
+
USER,
|
|
19
|
+
CLUSTER,
|
|
20
|
+
API_KEY_NAME,
|
|
21
|
+
ADMIN_SECURITY,
|
|
22
|
+
SYSTEM_SECURITY,
|
|
23
|
+
USER_SECURITY,
|
|
24
|
+
} = require('./common');
|
|
25
|
+
|
|
26
|
+
const getScopes = (c, securityType) => {
|
|
27
|
+
let scopes = [];
|
|
28
|
+
|
|
29
|
+
c.operation.security.forEach((security) => {
|
|
30
|
+
if (security[securityType]) scopes = scopes.concat(security[securityType]);
|
|
31
|
+
});
|
|
32
|
+
return scopes;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getError = (message, statusCode) => {
|
|
36
|
+
const error = new Error(message);
|
|
37
|
+
|
|
38
|
+
error.statusCode = statusCode;
|
|
39
|
+
return error;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const checkToken = (authToken) => {
|
|
43
|
+
let token;
|
|
44
|
+
|
|
45
|
+
try { token = jwt.decode(authToken); }
|
|
46
|
+
catch (err) {
|
|
47
|
+
err.statusCode = 401;
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
if (!token) {
|
|
51
|
+
throw getError('invalid token', 401);
|
|
52
|
+
}
|
|
53
|
+
return token;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const checkHeaders = (headers) => {
|
|
57
|
+
if (!headers) {
|
|
58
|
+
throw getError('missing header', 401);
|
|
59
|
+
}
|
|
60
|
+
const authNames = Object.keys(headers).filter((key) => key.toLowerCase() === AUTHORIZATION);
|
|
61
|
+
const authNamesLength = authNames.length;
|
|
62
|
+
|
|
63
|
+
if (authNamesLength === 0) {
|
|
64
|
+
throw getError(`missing ${AUTHORIZATION} header`, 401);
|
|
65
|
+
}
|
|
66
|
+
if (authNamesLength !== 1) {
|
|
67
|
+
throw getError(`duplicated ${AUTHORIZATION} header`, 401);
|
|
68
|
+
}
|
|
69
|
+
const auth = headers[authNames[0]].split(' ');
|
|
70
|
+
|
|
71
|
+
if (!BEARERS.includes(auth[0]) || auth.length !== 2) {
|
|
72
|
+
throw getError(`authorization type incorrect: ${auth[0]}`, 401);
|
|
73
|
+
}
|
|
74
|
+
return auth[1];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const checkScopes = (tokenScopes, defScopes, definition) => {
|
|
78
|
+
if (!tokenScopes) {
|
|
79
|
+
throw new Error('no scope in authorization token');
|
|
80
|
+
}
|
|
81
|
+
let claims = [];
|
|
82
|
+
let onBehalf = false;
|
|
83
|
+
|
|
84
|
+
if (defScopes && defScopes.length !== 0) {
|
|
85
|
+
const currentScopes = tokenScopes.split(SCOPES_SEPARATOR);
|
|
86
|
+
const intersects = [];
|
|
87
|
+
let resourceIndex = 1;
|
|
88
|
+
|
|
89
|
+
currentScopes.forEach((currentScope) => {
|
|
90
|
+
const analyzedScope = currentScope.split(SCOPE_CLAIMS_SEPARATOR);
|
|
91
|
+
const analyzedResource = analyzedScope[0].split(RESOURCE_SEPARATOR);
|
|
92
|
+
|
|
93
|
+
if (analyzedResource[0] === ON_BEHALF) {
|
|
94
|
+
onBehalf = true;
|
|
95
|
+
resourceIndex = 2; // legacy handling
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (defScopes.includes(analyzedScope[0])) {
|
|
99
|
+
if (analyzedScope[1]) {
|
|
100
|
+
const includedDefinitionName = `${analyzedResource[resourceIndex]}${CLAIMS_DEFINITION}`;
|
|
101
|
+
|
|
102
|
+
if (!definition.components || !definition.components.schemas) {
|
|
103
|
+
throw getError(`missing ${includedDefinitionName} definition: no definitions`, 500);
|
|
104
|
+
}
|
|
105
|
+
const includedDefinition = definition.components.schemas[includedDefinitionName];
|
|
106
|
+
|
|
107
|
+
if (!includedDefinition) {
|
|
108
|
+
throw getError(`missing ${includedDefinitionName} definition`, 500);
|
|
109
|
+
}
|
|
110
|
+
const includedClaims = analyzedScope[1].split(CLAIMS_SEPARATOR);
|
|
111
|
+
const definitionClaims = Object.keys(includedDefinition);
|
|
112
|
+
const claimsIntersects = _.intersection(includedClaims, definitionClaims);
|
|
113
|
+
|
|
114
|
+
if (claimsIntersects.length !== includedClaims.length) {
|
|
115
|
+
throw getError(`incorrect claims included: ${_.difference(includedClaims, claimsIntersects)}`, 403);
|
|
116
|
+
}
|
|
117
|
+
claims = claims.concat(claimsIntersects);
|
|
118
|
+
}
|
|
119
|
+
intersects.push(analyzedScope[0]);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
if (intersects.length === 0) {
|
|
123
|
+
throw getError(`incorrect scopes: ${tokenScopes}`, 403);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { onBehalf, claims };
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
module.exports = (config) => {
|
|
130
|
+
const verifyTokenClientCredentials = (authToken) => {
|
|
131
|
+
const { server, generic } = config.security;
|
|
132
|
+
const options = {
|
|
133
|
+
audience: (generic.audience === NO_GENERIC) ? server.audience : generic.audience,
|
|
134
|
+
issuer: server.issuer,
|
|
135
|
+
// subject: `${config.serverSettings.id}@clients`,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
try { jwt.verify(authToken, (generic.key === NO_GENERIC) ? server.accessKey : generic.key, options); }
|
|
139
|
+
catch (err) {
|
|
140
|
+
if (generic.previousKey) { // backward compatibility
|
|
141
|
+
try { jwt.verify(authToken, generic.previousKey, options); }
|
|
142
|
+
catch (secondErr) {
|
|
143
|
+
secondErr.statusCode = 403;
|
|
144
|
+
throw secondErr;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
err.statusCode = 403;
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const verifyTokenImplicit = (authToken) => {
|
|
155
|
+
const { implicit, generic, server } = config.security;
|
|
156
|
+
const options = {
|
|
157
|
+
audience: (implicit && implicit.audience) || server.audience,
|
|
158
|
+
issuer: (implicit && implicit.issuer) || server.issuer,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
try { jwt.verify(authToken, (implicit && implicit.key) || ((generic.key === NO_GENERIC) ? server.accessKey : generic.key), options); }
|
|
162
|
+
catch (err) {
|
|
163
|
+
err.statusCode = 403;
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const AdminSecurity = {
|
|
169
|
+
regular: (c, req) => {
|
|
170
|
+
const authToken = checkHeaders(req.headers);
|
|
171
|
+
const token = checkToken(authToken);
|
|
172
|
+
const { request } = c;
|
|
173
|
+
|
|
174
|
+
if (token.subType !== ADMIN && token.subType !== SUB_ADMIN) {
|
|
175
|
+
throw getError('invalid token: wrong type', 403);
|
|
176
|
+
}
|
|
177
|
+
if (token.subType === SUB_ADMIN) {
|
|
178
|
+
if (!token.cust) {
|
|
179
|
+
throw getError('invalid token: no customer', 403);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (token.sub !== `${config.security.admin.externalId}${CLIENT}`) {
|
|
183
|
+
throw getError(`jwt subject invalid: ${token.sub}`, 403);
|
|
184
|
+
}
|
|
185
|
+
verifyTokenClientCredentials(authToken);
|
|
186
|
+
const scopeResult = checkScopes(token.scope, getScopes(c, ADMIN_SECURITY), c.api.definition);
|
|
187
|
+
|
|
188
|
+
req[TOKEN_PARAMS.claims] = scopeResult.claims;
|
|
189
|
+
request[TOKEN_PARAMS.claims] = scopeResult.claims;
|
|
190
|
+
if (token.subType) {
|
|
191
|
+
req[TOKEN_PARAMS.tokenType] = token.subType;
|
|
192
|
+
request[TOKEN_PARAMS.tokenType] = token.subType;
|
|
193
|
+
}
|
|
194
|
+
if (token.sub) {
|
|
195
|
+
req[TOKEN_PARAMS.clientId] = token.sub;
|
|
196
|
+
request[TOKEN_PARAMS.clientId] = token.sub;
|
|
197
|
+
}
|
|
198
|
+
if (token.cust) {
|
|
199
|
+
req[TOKEN_PARAMS.customer] = token.cust;
|
|
200
|
+
request[TOKEN_PARAMS.customer] = token.cust;
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
},
|
|
204
|
+
mock: (c, req) => {
|
|
205
|
+
const { request } = c;
|
|
206
|
+
|
|
207
|
+
req[TOKEN_PARAMS.claims] = ['dummyClaims'];
|
|
208
|
+
req[TOKEN_PARAMS.tokenType] = ADMIN;
|
|
209
|
+
req[TOKEN_PARAMS.clientId] = 'dummyClientId';
|
|
210
|
+
req[TOKEN_PARAMS.customer] = 'dummyCustomer';
|
|
211
|
+
request[TOKEN_PARAMS.claims] = ['dummyClaims'];
|
|
212
|
+
request[TOKEN_PARAMS.tokenType] = ADMIN;
|
|
213
|
+
request[TOKEN_PARAMS.clientId] = 'dummyClientId';
|
|
214
|
+
request[TOKEN_PARAMS.customer] = 'dummyCustomer';
|
|
215
|
+
return true;
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const SystemSecurity = {
|
|
220
|
+
regular: (c, req) => {
|
|
221
|
+
const authToken = checkHeaders(req.headers);
|
|
222
|
+
const token = checkToken(authToken);
|
|
223
|
+
const { request } = c;
|
|
224
|
+
|
|
225
|
+
if (token.subType === ADMIN || token.subType === SUB_ADMIN) {
|
|
226
|
+
throw getError('invalid token: wrong type', 403);
|
|
227
|
+
}
|
|
228
|
+
verifyTokenClientCredentials(authToken);
|
|
229
|
+
const scopeResult = checkScopes(token.scope, getScopes(c, SYSTEM_SECURITY), c.api.definition);
|
|
230
|
+
|
|
231
|
+
if (scopeResult.onBehalf) {
|
|
232
|
+
req[TOKEN_PARAMS.onBehalf] = true;
|
|
233
|
+
request[TOKEN_PARAMS.onBehalf] = true;
|
|
234
|
+
}
|
|
235
|
+
req[TOKEN_PARAMS.claims] = scopeResult.claims;
|
|
236
|
+
request[TOKEN_PARAMS.claims] = scopeResult.claims;
|
|
237
|
+
if (token.subType) {
|
|
238
|
+
req[TOKEN_PARAMS.tokenType] = token.subType;
|
|
239
|
+
request[TOKEN_PARAMS.tokenType] = token.subType;
|
|
240
|
+
}
|
|
241
|
+
if (token.sub) {
|
|
242
|
+
req[TOKEN_PARAMS.clientId] = token.sub;
|
|
243
|
+
request[TOKEN_PARAMS.clientId] = token.sub;
|
|
244
|
+
}
|
|
245
|
+
if (token.cust) {
|
|
246
|
+
req[TOKEN_PARAMS.customer] = token.cust;
|
|
247
|
+
request[TOKEN_PARAMS.customer] = token.cust;
|
|
248
|
+
}
|
|
249
|
+
if (token.type === CLUSTER) {
|
|
250
|
+
req[TOKEN_PARAMS.cluster] = true;
|
|
251
|
+
request[TOKEN_PARAMS.cluster] = true;
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
},
|
|
255
|
+
mock: (c, req) => {
|
|
256
|
+
const { request } = c;
|
|
257
|
+
|
|
258
|
+
req[TOKEN_PARAMS.claims] = ['dummyClaims'];
|
|
259
|
+
req[TOKEN_PARAMS.tokenType] = 'dummyServiceType';
|
|
260
|
+
req[TOKEN_PARAMS.clientId] = 'dummyClientId';
|
|
261
|
+
req[TOKEN_PARAMS.customer] = 'dummyCustomer';
|
|
262
|
+
request[TOKEN_PARAMS.claims] = ['dummyClaims'];
|
|
263
|
+
request[TOKEN_PARAMS.tokenType] = 'dummyServiceType';
|
|
264
|
+
request[TOKEN_PARAMS.clientId] = 'dummyClientId';
|
|
265
|
+
request[TOKEN_PARAMS.customer] = 'dummyCustomer';
|
|
266
|
+
return true;
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const UserSecurity = {
|
|
271
|
+
regular: (c, req) => {
|
|
272
|
+
const authToken = checkHeaders(req.headers);
|
|
273
|
+
const token = checkToken(authToken);
|
|
274
|
+
const { request } = c;
|
|
275
|
+
|
|
276
|
+
verifyTokenImplicit(authToken);
|
|
277
|
+
const scopeResult = checkScopes(token.scope, getScopes(c, USER_SECURITY), c.api.definition);
|
|
278
|
+
|
|
279
|
+
if (scopeResult.onBehalf) {
|
|
280
|
+
req[TOKEN_PARAMS.onBehalf] = true;
|
|
281
|
+
request[TOKEN_PARAMS.onBehalf] = true;
|
|
282
|
+
}
|
|
283
|
+
req[TOKEN_PARAMS.claims] = scopeResult.claims;
|
|
284
|
+
request[TOKEN_PARAMS.claims] = scopeResult.claims;
|
|
285
|
+
req[TOKEN_PARAMS.tokenType] = USER;
|
|
286
|
+
request[TOKEN_PARAMS.tokenType] = USER;
|
|
287
|
+
if (token.sub) {
|
|
288
|
+
req[TOKEN_PARAMS.userId] = token.sub;
|
|
289
|
+
request[TOKEN_PARAMS.userId] = token.sub;
|
|
290
|
+
}
|
|
291
|
+
if (token.azp) {
|
|
292
|
+
req[TOKEN_PARAMS.appId] = token.azp;
|
|
293
|
+
request[TOKEN_PARAMS.appId] = token.azp;
|
|
294
|
+
}
|
|
295
|
+
if (token.may_act && token.may_act.sub) {
|
|
296
|
+
req[TOKEN_PARAMS.onBehalfId] = token.sub;
|
|
297
|
+
request[TOKEN_PARAMS.onBehalfId] = token.sub;
|
|
298
|
+
req[TOKEN_PARAMS.userId] = token.may_act.sub;
|
|
299
|
+
request[TOKEN_PARAMS.userId] = token.may_act.sub;
|
|
300
|
+
}
|
|
301
|
+
return true;
|
|
302
|
+
},
|
|
303
|
+
mock: (c, req) => {
|
|
304
|
+
const { request } = c;
|
|
305
|
+
|
|
306
|
+
req[TOKEN_PARAMS.claims] = ['dummyClaims'];
|
|
307
|
+
req[TOKEN_PARAMS.userId] = 'dummyUserId';
|
|
308
|
+
req[TOKEN_PARAMS.appId] = 'dummyAppId';
|
|
309
|
+
req[TOKEN_PARAMS.tokenType] = USER;
|
|
310
|
+
request[TOKEN_PARAMS.claims] = ['dummyClaims'];
|
|
311
|
+
request[TOKEN_PARAMS.userId] = 'dummyUserId';
|
|
312
|
+
request[TOKEN_PARAMS.appId] = 'dummyAppId';
|
|
313
|
+
request[TOKEN_PARAMS.tokenType] = USER;
|
|
314
|
+
return true;
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const ApiKeySecurity = {
|
|
319
|
+
regular: (c, req) => {
|
|
320
|
+
const apiKey = req.headers ? req.headers[API_KEY_NAME.toLowerCase()] : null;
|
|
321
|
+
const { request } = c;
|
|
322
|
+
|
|
323
|
+
if (config.security.apiKeys.includes(apiKey)) {
|
|
324
|
+
req[API_KEY_NAME] = apiKey;
|
|
325
|
+
request[API_KEY_NAME] = apiKey;
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
throw getError('invalid API key', 401);
|
|
329
|
+
},
|
|
330
|
+
mock: (c, req) => {
|
|
331
|
+
const { request } = c;
|
|
332
|
+
|
|
333
|
+
req[API_KEY_NAME] = 'dummyApiKey';
|
|
334
|
+
request[API_KEY_NAME] = 'dummyApiKey';
|
|
335
|
+
return true;
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
AdminSecurity,
|
|
341
|
+
SystemSecurity,
|
|
342
|
+
UserSecurity,
|
|
343
|
+
ApiKeySecurity,
|
|
344
|
+
};
|
|
345
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mimik/api-helper",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "helper for openAPI backend and mimik service",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"lint": "eslint --ignore-path .gitignore .",
|
|
8
|
+
"docs": "jsdoc2md index.js > README.md",
|
|
9
|
+
"test": "echo \"Error: no test specified\" && exit 0",
|
|
10
|
+
"test-ci": "echo \"Error: no test specified\" && exit 0",
|
|
11
|
+
"prepublishOnly": "npm run docs && npm run lint && npm run test-ci",
|
|
12
|
+
"commit-ready": "npm run docs && npm run lint && npm run test-ci",
|
|
13
|
+
"prepare": "husky install"
|
|
14
|
+
},
|
|
15
|
+
"husky": {
|
|
16
|
+
"hooks": {
|
|
17
|
+
"pre-commit": "npm run commit-ready",
|
|
18
|
+
"pre-push": "npm run test"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mimik",
|
|
23
|
+
"microservice",
|
|
24
|
+
"openAPI"
|
|
25
|
+
],
|
|
26
|
+
"author": "mimik technology inc <support@mimik.com> (https://developer.mimik.com/)",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://bitbucket.org/mimiktech/api-helper"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@mimik/request-helper":"^1.7.8",
|
|
34
|
+
"@mimik/request-retry": "^2.1.4",
|
|
35
|
+
"@mimik/response-helper": "^2.6.3",
|
|
36
|
+
"@mimik/sumologic-winston-logger": "^1.6.15",
|
|
37
|
+
"@mimik/swagger-helper": "^3.0.5",
|
|
38
|
+
"ajv-formats": "^3.0.0-rc.0",
|
|
39
|
+
"js-yaml":"^4.1.0",
|
|
40
|
+
"jsonwebtoken": "^9.0.0",
|
|
41
|
+
"lodash": "^4.17.21",
|
|
42
|
+
"openapi-backend": "^5.9.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@mimik/eslint-plugin-dependencies": "^2.4.5",
|
|
46
|
+
"@mimik/eslint-plugin-document-env": "^1.0.5",
|
|
47
|
+
"eslint": "8.38.0",
|
|
48
|
+
"eslint-config-airbnb": "19.0.4",
|
|
49
|
+
"eslint-plugin-import": "2.27.5",
|
|
50
|
+
"eslint-plugin-jsx-a11y": "6.7.1",
|
|
51
|
+
"eslint-plugin-react": "7.32.2",
|
|
52
|
+
"eslint-plugin-react-hooks": "4.6.0",
|
|
53
|
+
"husky": "8.0.3",
|
|
54
|
+
"jsdoc-to-markdown": "8.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|