@backstage-community/plugin-servicenow-backend 1.0.0
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/CHANGELOG.md +12 -0
- package/README.md +33 -0
- package/dist/config/config.cjs.js +105 -0
- package/dist/config/config.cjs.js.map +1 -0
- package/dist/index.cjs.js +10 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/plugin.cjs.js +38 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/service/router.cjs.js +49 -0
- package/dist/service/router.cjs.js.map +1 -0
- package/dist/service/validator.cjs.js +147 -0
- package/dist/service/validator.cjs.js.map +1 -0
- package/dist/service-now-rest/client.cjs.js +219 -0
- package/dist/service-now-rest/client.cjs.js.map +1 -0
- package/package.json +83 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# ServiceNow Backend Plugin
|
|
2
|
+
|
|
3
|
+
This is the **backend implementation** of the ServiceNow plugin for Backstage.
|
|
4
|
+
It provides an API that securely connects to your ServiceNow instance and returns incident data for catalog entities.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
This plugin is installed via the `@backstage-community/plugin-servicenow-backend` package. To install it to your backend package, run the following command:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# From your root directory
|
|
14
|
+
yarn --cwd packages/backend add @@backstage-community/plugin-servicenow-backend
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then add the plugin to your backend in `packages/backend/src/index.ts`:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
const backend = createBackend();
|
|
21
|
+
// ...
|
|
22
|
+
backend.add(import('@backstage-community/plugin-servicenow-backend'));
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Development
|
|
26
|
+
|
|
27
|
+
This plugin backend can be started in a standalone mode from directly in this
|
|
28
|
+
package with `yarn start`. It is a limited setup that is most convenient when
|
|
29
|
+
developing the plugin backend itself.
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
Refer to [Configuration.md](../../docs/Configuration.md) for detailed setup instructions, including authentication and ServiceNow instance configuration.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var errors = require('@backstage/errors');
|
|
4
|
+
|
|
5
|
+
function readServiceNowConfig(config) {
|
|
6
|
+
const serviceNowConfig = config.getOptionalConfig("servicenow");
|
|
7
|
+
if (!serviceNowConfig) {
|
|
8
|
+
return void 0;
|
|
9
|
+
}
|
|
10
|
+
const instanceUrl = serviceNowConfig.getString("instanceUrl");
|
|
11
|
+
if (!instanceUrl) {
|
|
12
|
+
throw new errors.InputError(
|
|
13
|
+
"Missing required config value at servicenow.instanceUrl"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
let oauth = void 0;
|
|
17
|
+
const oauthConfig = serviceNowConfig.getOptionalConfig("oauth");
|
|
18
|
+
let basicAuth = void 0;
|
|
19
|
+
const basicAuthConfig = serviceNowConfig.getOptionalConfig("basicAuth");
|
|
20
|
+
if (oauthConfig && basicAuthConfig) {
|
|
21
|
+
throw new errors.InputError(
|
|
22
|
+
"Both servicenow.oauth and servicenow.basicAuth are configured. Please use only one."
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (oauthConfig) {
|
|
26
|
+
const grantType = oauthConfig.getString("grantType");
|
|
27
|
+
if (grantType !== "client_credentials" && grantType !== "password") {
|
|
28
|
+
throw new Error(`Unsupported OAuth grantType: ${grantType}`);
|
|
29
|
+
}
|
|
30
|
+
const clientId = oauthConfig.getString("clientId");
|
|
31
|
+
const clientSecret = oauthConfig.getString("clientSecret");
|
|
32
|
+
const tokenUrl = oauthConfig.getOptionalString("tokenUrl");
|
|
33
|
+
if (!clientId) {
|
|
34
|
+
throw new errors.InputError(
|
|
35
|
+
"Missing required config value at servicenow.oauth.clientId"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (!clientSecret) {
|
|
39
|
+
throw new errors.InputError(
|
|
40
|
+
"Missing required config value at servicenow.oauth.clientSecret"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (grantType === "client_credentials") {
|
|
44
|
+
oauth = {
|
|
45
|
+
grantType,
|
|
46
|
+
clientId,
|
|
47
|
+
clientSecret,
|
|
48
|
+
tokenUrl
|
|
49
|
+
};
|
|
50
|
+
} else if (grantType === "password") {
|
|
51
|
+
const username = oauthConfig.getString("username");
|
|
52
|
+
const password = oauthConfig.getString("password");
|
|
53
|
+
if (!username) {
|
|
54
|
+
throw new errors.InputError(
|
|
55
|
+
"Missing required config value at servicenow.oauth.username for 'password' grant type"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (!password) {
|
|
59
|
+
throw new errors.InputError(
|
|
60
|
+
"Missing required config value at servicenow.oauth.password for 'password' grant type"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
oauth = {
|
|
64
|
+
grantType,
|
|
65
|
+
clientId,
|
|
66
|
+
clientSecret,
|
|
67
|
+
username,
|
|
68
|
+
password,
|
|
69
|
+
tokenUrl
|
|
70
|
+
};
|
|
71
|
+
} else {
|
|
72
|
+
throw new errors.InputError(
|
|
73
|
+
`Invalid or missing 'grantType' in servicenow.oauth configuration. Must be 'client_credentials' or 'password'. Received: ${grantType}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (basicAuthConfig) {
|
|
78
|
+
const username = basicAuthConfig.getString("username");
|
|
79
|
+
const password = basicAuthConfig.getString("password");
|
|
80
|
+
if (!username) {
|
|
81
|
+
throw new errors.InputError(
|
|
82
|
+
"Missing required config value at servicenow.basicAuth.username"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (!password) {
|
|
86
|
+
throw new errors.InputError(
|
|
87
|
+
"Missing required config value at servicenow.basicAuth.password"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
basicAuth = {
|
|
91
|
+
username,
|
|
92
|
+
password
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
servicenow: {
|
|
97
|
+
instanceUrl,
|
|
98
|
+
oauth,
|
|
99
|
+
basicAuth
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
exports.readServiceNowConfig = readServiceNowConfig;
|
|
105
|
+
//# sourceMappingURL=config.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.cjs.js","sources":["../../src/config/config.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Config } from '@backstage/config';\nimport { InputError } from '@backstage/errors';\nimport type {\n BasicAuthConfig,\n OAuthConfig,\n ServiceNowConfig,\n} from '../../config';\n\n/**\n * Reads the ServiceNow configuration from the provided Config object.\n *\n * @param config - The Backstage configuration object.\n * @returns The parsed ServiceNow configuration, or undefined if not found.\n * @throws InputError if the configuration is invalid.\n * @public\n */\nexport function readServiceNowConfig(\n config: Config,\n): ServiceNowConfig | undefined {\n const serviceNowConfig = config.getOptionalConfig('servicenow');\n if (!serviceNowConfig) {\n return undefined;\n }\n\n const instanceUrl = serviceNowConfig.getString('instanceUrl');\n if (!instanceUrl) {\n throw new InputError(\n 'Missing required config value at servicenow.instanceUrl',\n );\n }\n\n let oauth: OAuthConfig | undefined = undefined;\n const oauthConfig = serviceNowConfig.getOptionalConfig('oauth');\n\n let basicAuth: BasicAuthConfig | undefined = undefined;\n const basicAuthConfig = serviceNowConfig.getOptionalConfig('basicAuth');\n\n if (oauthConfig && basicAuthConfig) {\n throw new InputError(\n 'Both servicenow.oauth and servicenow.basicAuth are configured. Please use only one.',\n );\n }\n\n if (oauthConfig) {\n const grantType = oauthConfig.getString('grantType');\n if (grantType !== 'client_credentials' && grantType !== 'password') {\n throw new Error(`Unsupported OAuth grantType: ${grantType}`);\n }\n const clientId = oauthConfig.getString('clientId');\n const clientSecret = oauthConfig.getString('clientSecret');\n const tokenUrl = oauthConfig.getOptionalString('tokenUrl');\n\n if (!clientId) {\n throw new InputError(\n 'Missing required config value at servicenow.oauth.clientId',\n );\n }\n if (!clientSecret) {\n throw new InputError(\n 'Missing required config value at servicenow.oauth.clientSecret',\n );\n }\n\n if (grantType === 'client_credentials') {\n oauth = {\n grantType,\n clientId,\n clientSecret,\n tokenUrl,\n };\n } else if (grantType === 'password') {\n const username = oauthConfig.getString('username');\n const password = oauthConfig.getString('password');\n if (!username) {\n throw new InputError(\n \"Missing required config value at servicenow.oauth.username for 'password' grant type\",\n );\n }\n if (!password) {\n throw new InputError(\n \"Missing required config value at servicenow.oauth.password for 'password' grant type\",\n );\n }\n oauth = {\n grantType,\n clientId,\n clientSecret,\n username,\n password,\n tokenUrl,\n };\n } else {\n throw new InputError(\n `Invalid or missing 'grantType' in servicenow.oauth configuration. Must be 'client_credentials' or 'password'. Received: ${grantType}`,\n );\n }\n }\n\n if (basicAuthConfig) {\n const username = basicAuthConfig.getString('username');\n const password = basicAuthConfig.getString('password');\n if (!username) {\n throw new InputError(\n 'Missing required config value at servicenow.basicAuth.username',\n );\n }\n if (!password) {\n throw new InputError(\n 'Missing required config value at servicenow.basicAuth.password',\n );\n }\n basicAuth = {\n username,\n password,\n };\n }\n\n return {\n servicenow: {\n instanceUrl,\n oauth,\n basicAuth,\n },\n };\n}\n"],"names":["InputError"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* servicenowPlugin backend plugin
|
|
5
|
+
*
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
declare const servicenowPlugin: _backstage_backend_plugin_api.BackendFeature;
|
|
9
|
+
|
|
10
|
+
export { servicenowPlugin as default };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
|
+
var router = require('./service/router.cjs.js');
|
|
5
|
+
var config = require('./config/config.cjs.js');
|
|
6
|
+
|
|
7
|
+
const servicenowPlugin = backendPluginApi.createBackendPlugin({
|
|
8
|
+
pluginId: "servicenow",
|
|
9
|
+
register(env) {
|
|
10
|
+
env.registerInit({
|
|
11
|
+
deps: {
|
|
12
|
+
logger: backendPluginApi.coreServices.logger,
|
|
13
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
14
|
+
httpRouter: backendPluginApi.coreServices.httpRouter,
|
|
15
|
+
httpAuth: backendPluginApi.coreServices.httpAuth
|
|
16
|
+
},
|
|
17
|
+
async init({ logger, config: config$1, httpRouter, httpAuth }) {
|
|
18
|
+
const servicenowConfig = config.readServiceNowConfig(config$1);
|
|
19
|
+
if (!servicenowConfig) {
|
|
20
|
+
logger.error(
|
|
21
|
+
"ServiceNow plugin configuration is missing. The plugin will not be initialized."
|
|
22
|
+
);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
httpRouter.use(
|
|
26
|
+
await router.createRouter({
|
|
27
|
+
logger,
|
|
28
|
+
servicenowConfig,
|
|
29
|
+
httpAuth
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
exports.servicenowPlugin = servicenowPlugin;
|
|
38
|
+
//# sourceMappingURL=plugin.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './service';\nimport { readServiceNowConfig } from './config';\nimport { ServiceNowConfig } from '../config';\n\n/**\n * servicenowPlugin backend plugin\n *\n * @public\n */\nexport const servicenowPlugin = createBackendPlugin({\n pluginId: 'servicenow',\n register(env) {\n env.registerInit({\n deps: {\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n httpRouter: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n },\n async init({ logger, config, httpRouter, httpAuth }) {\n const servicenowConfig: ServiceNowConfig | undefined =\n readServiceNowConfig(config);\n\n if (!servicenowConfig) {\n logger.error(\n 'ServiceNow plugin configuration is missing. The plugin will not be initialized.',\n );\n return;\n }\n\n httpRouter.use(\n await createRouter({\n logger,\n servicenowConfig,\n httpAuth,\n }),\n );\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","config","readServiceNowConfig","createRouter"],"mappings":";;;;;;AA4BO,MAAM,mBAAmBA,oCAAoB,CAAA;AAAA,EAClD,QAAU,EAAA,YAAA;AAAA,EACV,SAAS,GAAK,EAAA;AACZ,IAAA,GAAA,CAAI,YAAa,CAAA;AAAA,MACf,IAAM,EAAA;AAAA,QACJ,QAAQC,6BAAa,CAAA,MAAA;AAAA,QACrB,QAAQA,6BAAa,CAAA,UAAA;AAAA,QACrB,YAAYA,6BAAa,CAAA,UAAA;AAAA,QACzB,UAAUA,6BAAa,CAAA;AAAA,OACzB;AAAA,MACA,MAAM,IAAK,CAAA,EAAE,gBAAQC,QAAQ,EAAA,UAAA,EAAY,UAAY,EAAA;AACnD,QAAM,MAAA,gBAAA,GACJC,4BAAqBD,QAAM,CAAA;AAE7B,QAAA,IAAI,CAAC,gBAAkB,EAAA;AACrB,UAAO,MAAA,CAAA,KAAA;AAAA,YACL;AAAA,WACF;AACA,UAAA;AAAA;AAGF,QAAW,UAAA,CAAA,GAAA;AAAA,UACT,MAAME,mBAAa,CAAA;AAAA,YACjB,MAAA;AAAA,YACA,gBAAA;AAAA,YACA;AAAA,WACD;AAAA,SACH;AAAA;AACF,KACD,CAAA;AAAA;AAEL,CAAC;;;;"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var errors = require('@backstage/errors');
|
|
4
|
+
var validator = require('./validator.cjs.js');
|
|
5
|
+
var express = require('express');
|
|
6
|
+
var Router = require('express-promise-router');
|
|
7
|
+
var client = require('../service-now-rest/client.cjs.js');
|
|
8
|
+
|
|
9
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
12
|
+
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
13
|
+
|
|
14
|
+
async function createRouter(options) {
|
|
15
|
+
const { logger, servicenowConfig, httpAuth } = options;
|
|
16
|
+
logger.info(
|
|
17
|
+
`Creating router for ServiceNow with instance URL: ${servicenowConfig.servicenow?.instanceUrl}`
|
|
18
|
+
);
|
|
19
|
+
const client$1 = new client.DefaultServiceNowClient(
|
|
20
|
+
servicenowConfig,
|
|
21
|
+
logger.child({ service: "servicenow-client" })
|
|
22
|
+
);
|
|
23
|
+
const router = Router__default.default();
|
|
24
|
+
router.use(express__default.default.json());
|
|
25
|
+
router.get("/health", (_, response) => {
|
|
26
|
+
logger.info("PONG!");
|
|
27
|
+
response.json({ status: "ok" });
|
|
28
|
+
});
|
|
29
|
+
router.get("/incidents", async (req, res) => {
|
|
30
|
+
await httpAuth.credentials(req, {
|
|
31
|
+
allow: ["user"]
|
|
32
|
+
});
|
|
33
|
+
const validatedParams = validator.validateIncidentQueryParams(req.query);
|
|
34
|
+
try {
|
|
35
|
+
const incidents = await client$1.fetchIncidents(validatedParams);
|
|
36
|
+
res.json(incidents);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error instanceof errors.InputError) {
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
logger.error("Failed to fetch incidents from ServiceNow", error);
|
|
42
|
+
throw new Error("Failed to fetch incidents from ServiceNow");
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return router;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
exports.createRouter = createRouter;
|
|
49
|
+
//# sourceMappingURL=router.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { InputError } from '@backstage/errors';\nimport { validateIncidentQueryParams } from './validator';\nimport { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { DefaultServiceNowClient } from '../service-now-rest/client';\nimport { ServiceNowConfig } from '../../config';\n\nexport interface RouterOptions {\n logger: LoggerService;\n servicenowConfig: ServiceNowConfig;\n httpAuth: HttpAuthService;\n}\n\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const { logger, servicenowConfig, httpAuth } = options;\n\n logger.info(\n `Creating router for ServiceNow with instance URL: ${servicenowConfig.servicenow?.instanceUrl}`,\n );\n\n const client = new DefaultServiceNowClient(\n servicenowConfig,\n logger.child({ service: 'servicenow-client' }),\n );\n const router = Router();\n router.use(express.json());\n\n router.get('/health', (_, response) => {\n logger.info('PONG!');\n response.json({ status: 'ok' });\n });\n\n router.get('/incidents', async (req, res) => {\n await httpAuth.credentials(req, {\n allow: ['user'],\n });\n\n const validatedParams = validateIncidentQueryParams(req.query);\n try {\n const incidents = await client.fetchIncidents(validatedParams);\n res.json(incidents);\n } catch (error) {\n if (error instanceof InputError) {\n throw error;\n }\n\n // Log the full error and throw a generic one\n logger.error('Failed to fetch incidents from ServiceNow', error);\n throw new Error('Failed to fetch incidents from ServiceNow');\n }\n });\n\n return router;\n}\n"],"names":["client","DefaultServiceNowClient","Router","express","validateIncidentQueryParams","InputError"],"mappings":";;;;;;;;;;;;;AA6BA,eAAsB,aACpB,OACyB,EAAA;AACzB,EAAA,MAAM,EAAE,MAAA,EAAQ,gBAAkB,EAAA,QAAA,EAAa,GAAA,OAAA;AAE/C,EAAO,MAAA,CAAA,IAAA;AAAA,IACL,CAAA,kDAAA,EAAqD,gBAAiB,CAAA,UAAA,EAAY,WAAW,CAAA;AAAA,GAC/F;AAEA,EAAA,MAAMA,WAAS,IAAIC,8BAAA;AAAA,IACjB,gBAAA;AAAA,IACA,MAAO,CAAA,KAAA,CAAM,EAAE,OAAA,EAAS,qBAAqB;AAAA,GAC/C;AACA,EAAA,MAAM,SAASC,uBAAO,EAAA;AACtB,EAAO,MAAA,CAAA,GAAA,CAAIC,wBAAQ,CAAA,IAAA,EAAM,CAAA;AAEzB,EAAA,MAAA,CAAO,GAAI,CAAA,SAAA,EAAW,CAAC,CAAA,EAAG,QAAa,KAAA;AACrC,IAAA,MAAA,CAAO,KAAK,OAAO,CAAA;AACnB,IAAA,QAAA,CAAS,IAAK,CAAA,EAAE,MAAQ,EAAA,IAAA,EAAM,CAAA;AAAA,GAC/B,CAAA;AAED,EAAA,MAAA,CAAO,GAAI,CAAA,YAAA,EAAc,OAAO,GAAA,EAAK,GAAQ,KAAA;AAC3C,IAAM,MAAA,QAAA,CAAS,YAAY,GAAK,EAAA;AAAA,MAC9B,KAAA,EAAO,CAAC,MAAM;AAAA,KACf,CAAA;AAED,IAAM,MAAA,eAAA,GAAkBC,qCAA4B,CAAA,GAAA,CAAI,KAAK,CAAA;AAC7D,IAAI,IAAA;AACF,MAAA,MAAM,SAAY,GAAA,MAAMJ,QAAO,CAAA,cAAA,CAAe,eAAe,CAAA;AAC7D,MAAA,GAAA,CAAI,KAAK,SAAS,CAAA;AAAA,aACX,KAAO,EAAA;AACd,MAAA,IAAI,iBAAiBK,iBAAY,EAAA;AAC/B,QAAM,MAAA,KAAA;AAAA;AAIR,MAAO,MAAA,CAAA,KAAA,CAAM,6CAA6C,KAAK,CAAA;AAC/D,MAAM,MAAA,IAAI,MAAM,2CAA2C,CAAA;AAAA;AAC7D,GACD,CAAA;AAED,EAAO,OAAA,MAAA;AACT;;;;"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var errors = require('@backstage/errors');
|
|
4
|
+
var pluginServicenowCommon = require('@backstage-community/plugin-servicenow-common');
|
|
5
|
+
|
|
6
|
+
const ALLOWED_STATES = ["1", "2", "3", "6", "7", "8"];
|
|
7
|
+
const ALLOWED_PRIORITIES = ["1", "2", "3", "4", "5"];
|
|
8
|
+
const ALLOWED_ORDER_BY_FIELDS = Object.values(pluginServicenowCommon.IncidentFieldEnum);
|
|
9
|
+
function parseAndValidateMultiValueQueryParam(paramQueryValue, allowedValues, paramName) {
|
|
10
|
+
if (paramQueryValue === void 0 || paramQueryValue === null) {
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(paramQueryValue) || typeof paramQueryValue === "object") {
|
|
14
|
+
throw new errors.InputError(
|
|
15
|
+
`Query parameter '${paramName}' must be a single string.`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const valueAsString = String(paramQueryValue);
|
|
19
|
+
if (valueAsString.trim() === "") {
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
if (!valueAsString.startsWith("IN")) {
|
|
23
|
+
throw new errors.InputError(
|
|
24
|
+
`Query parameter '${paramName}' must use the 'IN' prefix format (e.g., 'INvalue1,value2' or 'INvalue').`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const valuesStr = valueAsString.substring(2);
|
|
28
|
+
if (!valuesStr) {
|
|
29
|
+
throw new errors.InputError(
|
|
30
|
+
`Query parameter '${paramName}' with 'IN' prefix cannot have empty values.`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
const items = valuesStr.split(",");
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
const trimmedItem = item.trim();
|
|
36
|
+
if (!trimmedItem) {
|
|
37
|
+
throw new errors.InputError(
|
|
38
|
+
`Invalid ${paramName} format: empty value found in '${valueAsString}'. Allowed values for 'IN' list are: ${allowedValues.join(
|
|
39
|
+
", "
|
|
40
|
+
)}.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const isValid = allowedValues.includes(trimmedItem);
|
|
44
|
+
if (!isValid) {
|
|
45
|
+
throw new errors.InputError(
|
|
46
|
+
`Invalid value '${trimmedItem}' for query parameter '${paramName}' in 'IN' list '${valueAsString}'. Allowed values are: ${allowedValues.join(
|
|
47
|
+
", "
|
|
48
|
+
)}.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return valueAsString;
|
|
53
|
+
}
|
|
54
|
+
function validateIncidentQueryParams(query) {
|
|
55
|
+
const {
|
|
56
|
+
entityId: entityIdQuery,
|
|
57
|
+
state: stateQuery,
|
|
58
|
+
priority: priorityQuery,
|
|
59
|
+
search: searchQuery,
|
|
60
|
+
limit: limitQuery,
|
|
61
|
+
offset: offsetQuery,
|
|
62
|
+
order: orderQuery,
|
|
63
|
+
orderBy: orderByQuery,
|
|
64
|
+
userEmail: userEmailQuery
|
|
65
|
+
} = query;
|
|
66
|
+
const validatedParams = {};
|
|
67
|
+
if (userEmailQuery !== void 0) {
|
|
68
|
+
if (typeof userEmailQuery !== "string" || !userEmailQuery.trim()) {
|
|
69
|
+
throw new errors.InputError("userEmail must be a non-empty string.");
|
|
70
|
+
}
|
|
71
|
+
if (!/.+@.+\..+/.test(userEmailQuery)) {
|
|
72
|
+
throw new errors.InputError("userEmail must be a valid email address.");
|
|
73
|
+
}
|
|
74
|
+
validatedParams.userEmail = userEmailQuery.trim();
|
|
75
|
+
}
|
|
76
|
+
if (entityIdQuery !== void 0) {
|
|
77
|
+
if (typeof entityIdQuery !== "string" || !entityIdQuery.trim()) {
|
|
78
|
+
throw new errors.InputError("entityId must be a non-empty string.");
|
|
79
|
+
}
|
|
80
|
+
validatedParams.entityId = entityIdQuery.trim();
|
|
81
|
+
}
|
|
82
|
+
if (!validatedParams.userEmail && !validatedParams.entityId) {
|
|
83
|
+
throw new errors.InputError("entityId is required if userEmail is not present.");
|
|
84
|
+
}
|
|
85
|
+
if (limitQuery !== void 0) {
|
|
86
|
+
if (typeof limitQuery !== "string") {
|
|
87
|
+
throw new errors.InputError("limit must be a string with number value.");
|
|
88
|
+
}
|
|
89
|
+
const parsedLimit = parseInt(limitQuery, 10);
|
|
90
|
+
if (isNaN(parsedLimit) || parsedLimit < 0) {
|
|
91
|
+
throw new errors.InputError("limit must be a non-negative integer.");
|
|
92
|
+
}
|
|
93
|
+
validatedParams.limit = parsedLimit;
|
|
94
|
+
}
|
|
95
|
+
if (offsetQuery !== void 0) {
|
|
96
|
+
if (typeof offsetQuery !== "string") {
|
|
97
|
+
throw new errors.InputError("offset must be a string with number value.");
|
|
98
|
+
}
|
|
99
|
+
const parsedOffset = parseInt(offsetQuery, 10);
|
|
100
|
+
if (isNaN(parsedOffset) || parsedOffset < 0) {
|
|
101
|
+
throw new errors.InputError("offset must be a non-negative integer.");
|
|
102
|
+
}
|
|
103
|
+
validatedParams.offset = parsedOffset;
|
|
104
|
+
}
|
|
105
|
+
if (orderQuery !== void 0) {
|
|
106
|
+
if (typeof orderQuery !== "string") {
|
|
107
|
+
throw new errors.InputError("order must be a string.");
|
|
108
|
+
}
|
|
109
|
+
if (orderQuery !== "asc" && orderQuery !== "desc") {
|
|
110
|
+
throw new errors.InputError("order must be 'asc' or 'desc'.");
|
|
111
|
+
}
|
|
112
|
+
validatedParams.order = orderQuery;
|
|
113
|
+
}
|
|
114
|
+
validatedParams.state = parseAndValidateMultiValueQueryParam(
|
|
115
|
+
stateQuery,
|
|
116
|
+
ALLOWED_STATES,
|
|
117
|
+
"state"
|
|
118
|
+
);
|
|
119
|
+
validatedParams.priority = parseAndValidateMultiValueQueryParam(
|
|
120
|
+
priorityQuery,
|
|
121
|
+
ALLOWED_PRIORITIES,
|
|
122
|
+
"priority"
|
|
123
|
+
);
|
|
124
|
+
if (searchQuery !== void 0) {
|
|
125
|
+
if (typeof searchQuery !== "string") {
|
|
126
|
+
throw new errors.InputError(`search must be a string.`);
|
|
127
|
+
}
|
|
128
|
+
validatedParams.search = searchQuery;
|
|
129
|
+
}
|
|
130
|
+
if (orderByQuery !== void 0) {
|
|
131
|
+
if (typeof orderByQuery !== "string") {
|
|
132
|
+
throw new errors.InputError("orderBy must be a string.");
|
|
133
|
+
}
|
|
134
|
+
if (!ALLOWED_ORDER_BY_FIELDS.includes(orderByQuery)) {
|
|
135
|
+
throw new errors.InputError(
|
|
136
|
+
`Invalid orderBy field. Allowed values are: ${ALLOWED_ORDER_BY_FIELDS.join(
|
|
137
|
+
", "
|
|
138
|
+
)}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
validatedParams.orderBy = orderByQuery;
|
|
142
|
+
}
|
|
143
|
+
return validatedParams;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
exports.validateIncidentQueryParams = validateIncidentQueryParams;
|
|
147
|
+
//# sourceMappingURL=validator.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.cjs.js","sources":["../../src/service/validator.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { InputError } from '@backstage/errors';\nimport type { ParsedQs } from 'qs';\nimport type { Order } from '@backstage-community/plugin-servicenow-common';\nimport { IncidentFieldEnum } from '@backstage-community/plugin-servicenow-common';\nimport { IncidentQueryParams } from '../service-now-rest';\n\nconst ALLOWED_STATES: readonly string[] = ['1', '2', '3', '6', '7', '8'];\nconst ALLOWED_PRIORITIES: readonly string[] = ['1', '2', '3', '4', '5'];\nconst ALLOWED_ORDER_BY_FIELDS: string[] = Object.values(IncidentFieldEnum);\n\nfunction parseAndValidateMultiValueQueryParam(\n paramQueryValue: string | string[] | ParsedQs | ParsedQs[] | undefined,\n allowedValues: readonly string[],\n paramName: string,\n): string | undefined {\n if (paramQueryValue === undefined || paramQueryValue === null) {\n return undefined;\n }\n\n if (Array.isArray(paramQueryValue) || typeof paramQueryValue === 'object') {\n throw new InputError(\n `Query parameter '${paramName}' must be a single string.`,\n );\n }\n\n const valueAsString = String(paramQueryValue);\n\n if (valueAsString.trim() === '') {\n return undefined;\n }\n\n if (!valueAsString.startsWith('IN')) {\n throw new InputError(\n `Query parameter '${paramName}' must use the 'IN' prefix format (e.g., 'INvalue1,value2' or 'INvalue').`,\n );\n }\n\n // Extract the values after the 'IN' prefix\n const valuesStr = valueAsString.substring(2);\n if (!valuesStr) {\n throw new InputError(\n `Query parameter '${paramName}' with 'IN' prefix cannot have empty values.`,\n );\n }\n\n const items = valuesStr.split(',');\n for (const item of items) {\n const trimmedItem = item.trim();\n if (!trimmedItem) {\n throw new InputError(\n `Invalid ${paramName} format: empty value found in '${valueAsString}'. Allowed values for 'IN' list are: ${allowedValues.join(\n ', ',\n )}.`,\n );\n }\n\n const isValid = allowedValues.includes(trimmedItem);\n\n if (!isValid) {\n throw new InputError(\n `Invalid value '${trimmedItem}' for query parameter '${paramName}' in 'IN' list '${valueAsString}'. Allowed values are: ${allowedValues.join(\n ', ',\n )}.`,\n );\n }\n }\n return valueAsString;\n}\n\nexport function validateIncidentQueryParams(\n query: ParsedQs,\n): IncidentQueryParams {\n const {\n entityId: entityIdQuery,\n state: stateQuery,\n priority: priorityQuery,\n search: searchQuery,\n limit: limitQuery,\n offset: offsetQuery,\n order: orderQuery,\n orderBy: orderByQuery,\n userEmail: userEmailQuery,\n } = query;\n const validatedParams: IncidentQueryParams = {};\n\n // userEmail validation\n if (userEmailQuery !== undefined) {\n if (typeof userEmailQuery !== 'string' || !userEmailQuery.trim()) {\n throw new InputError('userEmail must be a non-empty string.');\n }\n // Basic email format validation\n if (!/.+@.+\\..+/.test(userEmailQuery)) {\n throw new InputError('userEmail must be a valid email address.');\n }\n validatedParams.userEmail = userEmailQuery.trim();\n }\n\n if (entityIdQuery !== undefined) {\n if (typeof entityIdQuery !== 'string' || !entityIdQuery.trim()) {\n throw new InputError('entityId must be a non-empty string.');\n }\n validatedParams.entityId = entityIdQuery.trim();\n }\n\n if (!validatedParams.userEmail && !validatedParams.entityId) {\n throw new InputError('entityId is required if userEmail is not present.');\n }\n\n // limit validation\n if (limitQuery !== undefined) {\n if (typeof limitQuery !== 'string') {\n throw new InputError('limit must be a string with number value.');\n }\n const parsedLimit = parseInt(limitQuery, 10);\n if (isNaN(parsedLimit) || parsedLimit < 0) {\n throw new InputError('limit must be a non-negative integer.');\n }\n validatedParams.limit = parsedLimit;\n }\n\n // offset validation\n if (offsetQuery !== undefined) {\n if (typeof offsetQuery !== 'string') {\n throw new InputError('offset must be a string with number value.');\n }\n const parsedOffset = parseInt(offsetQuery, 10);\n if (isNaN(parsedOffset) || parsedOffset < 0) {\n throw new InputError('offset must be a non-negative integer.');\n }\n validatedParams.offset = parsedOffset;\n }\n\n // order validation\n if (orderQuery !== undefined) {\n if (typeof orderQuery !== 'string') {\n throw new InputError('order must be a string.');\n }\n if (orderQuery !== 'asc' && orderQuery !== 'desc') {\n throw new InputError(\"order must be 'asc' or 'desc'.\");\n }\n validatedParams.order = orderQuery as Order;\n }\n\n // state validation\n validatedParams.state = parseAndValidateMultiValueQueryParam(\n stateQuery as string | undefined,\n ALLOWED_STATES,\n 'state',\n );\n\n // priority validation\n validatedParams.priority = parseAndValidateMultiValueQueryParam(\n priorityQuery as string | undefined,\n ALLOWED_PRIORITIES,\n 'priority',\n );\n\n // search validation\n if (searchQuery !== undefined) {\n if (typeof searchQuery !== 'string') {\n throw new InputError(`search must be a string.`);\n }\n validatedParams.search = searchQuery;\n }\n\n // orderBy validation\n if (orderByQuery !== undefined) {\n if (typeof orderByQuery !== 'string') {\n throw new InputError('orderBy must be a string.');\n }\n if (!ALLOWED_ORDER_BY_FIELDS.includes(orderByQuery)) {\n throw new InputError(\n `Invalid orderBy field. Allowed values are: ${ALLOWED_ORDER_BY_FIELDS.join(\n ', ',\n )}`,\n );\n }\n validatedParams.orderBy = orderByQuery;\n }\n\n return validatedParams;\n}\n"],"names":["IncidentFieldEnum","InputError"],"mappings":"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"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var simpleOauth2 = require('simple-oauth2');
|
|
4
|
+
var axios = require('axios');
|
|
5
|
+
|
|
6
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var axios__default = /*#__PURE__*/_interopDefaultCompat(axios);
|
|
9
|
+
|
|
10
|
+
class DefaultServiceNowClient {
|
|
11
|
+
instanceUrl;
|
|
12
|
+
config;
|
|
13
|
+
logger;
|
|
14
|
+
oauthClient;
|
|
15
|
+
constructor(config, logger) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.logger = logger;
|
|
18
|
+
if (!config.servicenow?.instanceUrl) {
|
|
19
|
+
logger.error("ServiceNow instance url is missing. Please configure it.");
|
|
20
|
+
throw new Error(
|
|
21
|
+
"ServiceNow instance url is missing. Please configure it."
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
this.instanceUrl = config.servicenow?.instanceUrl.replace(/\/$/, "");
|
|
25
|
+
if (!config.servicenow?.oauth && !config.servicenow?.basicAuth) {
|
|
26
|
+
logger.error(
|
|
27
|
+
"ServiceNow authentication configuration is missing. Please configure either OAuth or Basic Auth."
|
|
28
|
+
);
|
|
29
|
+
throw new Error(
|
|
30
|
+
"ServiceNow authentication configuration is missing. Please configure either OAuth or Basic Auth."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (config.servicenow?.oauth) {
|
|
34
|
+
this.setupOAuthClient(config.servicenow.oauth);
|
|
35
|
+
}
|
|
36
|
+
if (config.servicenow?.basicAuth) {
|
|
37
|
+
logger.warn(
|
|
38
|
+
"Basic authentication is configured for ServiceNow. This is not recommended for production environments."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
setupOAuthClient(oauth) {
|
|
43
|
+
const determinedTokenUrl = `${this.instanceUrl}/oauth_token.do`;
|
|
44
|
+
let tokenHost;
|
|
45
|
+
let tokenPath;
|
|
46
|
+
try {
|
|
47
|
+
const parsedTokenUrl = new URL(determinedTokenUrl);
|
|
48
|
+
tokenHost = parsedTokenUrl.origin;
|
|
49
|
+
tokenPath = parsedTokenUrl.pathname;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
this.logger.error(
|
|
52
|
+
`Invalid tokenUrl constructed or provided: ${determinedTokenUrl}. Error: ${e.message}`
|
|
53
|
+
);
|
|
54
|
+
throw new Error(`Invalid tokenUrl: ${determinedTokenUrl}`);
|
|
55
|
+
}
|
|
56
|
+
const oauthModuleOptions = {
|
|
57
|
+
client: {
|
|
58
|
+
id: oauth.clientId,
|
|
59
|
+
secret: oauth.clientSecret
|
|
60
|
+
},
|
|
61
|
+
auth: {
|
|
62
|
+
tokenHost,
|
|
63
|
+
tokenPath
|
|
64
|
+
},
|
|
65
|
+
options: {
|
|
66
|
+
authorizationMethod: "body"
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
if (oauth.grantType === "client_credentials") {
|
|
70
|
+
this.oauthClient = new simpleOauth2.ClientCredentials(oauthModuleOptions);
|
|
71
|
+
} else if (oauth.grantType === "password") {
|
|
72
|
+
if (!oauth.username || !oauth.password) {
|
|
73
|
+
this.logger.error(
|
|
74
|
+
"Username and/or password missing for 'password' grant type in ServiceNow OAuth config."
|
|
75
|
+
);
|
|
76
|
+
throw new Error(
|
|
77
|
+
"Username and/or password missing for 'password' grant type."
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
this.oauthClient = new simpleOauth2.ResourceOwnerPassword(oauthModuleOptions);
|
|
81
|
+
} else {
|
|
82
|
+
const grantType = oauth.grantType;
|
|
83
|
+
this.logger.error(`Unsupported OAuth grantType: ${grantType}`);
|
|
84
|
+
throw new Error(`Unsupported OAuth grantType: ${grantType}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async getAuthHeaders() {
|
|
88
|
+
if (this.config.servicenow?.basicAuth) {
|
|
89
|
+
const { username, password } = this.config.servicenow.basicAuth;
|
|
90
|
+
const encodedCredentials = Buffer.from(
|
|
91
|
+
`${username}:${password}`
|
|
92
|
+
).toString("base64");
|
|
93
|
+
return { Authorization: `Basic ${encodedCredentials}` };
|
|
94
|
+
}
|
|
95
|
+
if (this.config.servicenow?.oauth && this.oauthClient) {
|
|
96
|
+
let accessToken;
|
|
97
|
+
try {
|
|
98
|
+
if (this.config.servicenow.oauth.grantType === "client_credentials") {
|
|
99
|
+
accessToken = await this.oauthClient.getToken(
|
|
100
|
+
{}
|
|
101
|
+
);
|
|
102
|
+
} else if (this.config.servicenow.oauth.grantType === "password") {
|
|
103
|
+
if (!this.config.servicenow.oauth.username || !this.config.servicenow.oauth.password) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"Username or password missing for 'password' grant type during token acquisition."
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
accessToken = await this.oauthClient.getToken({
|
|
109
|
+
username: this.config.servicenow.oauth.username,
|
|
110
|
+
password: this.config.servicenow.oauth.password
|
|
111
|
+
});
|
|
112
|
+
} else {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Unsupported grantType in getAuthHeaders: ${this.config.servicenow.oauth.grantType}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const tokenData = accessToken.token;
|
|
118
|
+
if (!tokenData || typeof tokenData.access_token !== "string" || !tokenData.access_token) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
"Failed to obtain access_token string (token data is invalid or missing)."
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return { Authorization: `Bearer ${tokenData.access_token}` };
|
|
124
|
+
} catch (error) {
|
|
125
|
+
this.logger.error(`Error fetching ServiceNow token: ${error.message}`, {
|
|
126
|
+
error: error.stack || error
|
|
127
|
+
});
|
|
128
|
+
if (error.isAxiosError && error.response) {
|
|
129
|
+
this.logger.error(
|
|
130
|
+
`OAuth2 token error details: Status ${error.response.status}, Data: ${JSON.stringify(error.response.data)}`
|
|
131
|
+
);
|
|
132
|
+
} else if (error.data && error.data.payload) {
|
|
133
|
+
this.logger.error(
|
|
134
|
+
`OAuth2 token error payload: ${JSON.stringify(error.data.payload)}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
throw new Error(`Failed to obtain access token: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw new Error("No authentication method configured.");
|
|
141
|
+
}
|
|
142
|
+
async fetchIncidents(options) {
|
|
143
|
+
const authHeaders = await this.getAuthHeaders();
|
|
144
|
+
const queryParts = [];
|
|
145
|
+
if (options.userEmail) {
|
|
146
|
+
const id = await this.getUserSysIdByEmail(options.userEmail);
|
|
147
|
+
queryParts.push(`caller_id=${id}^ORopened_by=${id}^ORassigned_to=${id}`);
|
|
148
|
+
}
|
|
149
|
+
if (options.state) queryParts.push(`state${options.state}`);
|
|
150
|
+
if (options.priority) queryParts.push(`priority${options.priority}`);
|
|
151
|
+
if (options.search) {
|
|
152
|
+
const searchTerm = options.search;
|
|
153
|
+
queryParts.push(
|
|
154
|
+
`numberLIKE${searchTerm}^ORshort_descriptionLIKE${searchTerm}^ORdescriptionLIKE${searchTerm}`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (options.entityId) {
|
|
158
|
+
queryParts.push(`u_backstage_entity_id=${options.entityId}`);
|
|
159
|
+
}
|
|
160
|
+
if (options.orderBy) {
|
|
161
|
+
queryParts.push(
|
|
162
|
+
`${options.order === "desc" ? "ORDERBYDESC" : "ORDERBY"}${options.orderBy}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const sysparmQuery = queryParts.join("^");
|
|
166
|
+
const params = new URLSearchParams();
|
|
167
|
+
if (sysparmQuery) params.append("sysparm_query", sysparmQuery);
|
|
168
|
+
if (options.limit !== void 0)
|
|
169
|
+
params.append("sysparm_limit", String(options.limit));
|
|
170
|
+
if (options.offset !== void 0)
|
|
171
|
+
params.append("sysparm_offset", String(options.offset));
|
|
172
|
+
params.append(
|
|
173
|
+
"sysparm_fields",
|
|
174
|
+
"sys_id,number,short_description,description,sys_created_on,priority,incident_state"
|
|
175
|
+
);
|
|
176
|
+
params.append("sysparm_count", "true");
|
|
177
|
+
const requestUrl = `${this.instanceUrl}/api/now/table/incident?${params.toString()}`;
|
|
178
|
+
this.logger.info(`Fetching incidents from ServiceNow: ${requestUrl}`);
|
|
179
|
+
try {
|
|
180
|
+
const response = await axios__default.default.get(requestUrl, {
|
|
181
|
+
headers: {
|
|
182
|
+
...authHeaders,
|
|
183
|
+
Accept: "application/json"
|
|
184
|
+
},
|
|
185
|
+
timeout: 3e4
|
|
186
|
+
});
|
|
187
|
+
const countHeader = response.headers["x-total-count"] ?? response.headers["X-Total-Count"];
|
|
188
|
+
const totalCount = Number(countHeader ?? 0);
|
|
189
|
+
const items = response.data?.result?.map((incident) => ({
|
|
190
|
+
...incident,
|
|
191
|
+
url: `${this.instanceUrl}/nav_to.do?uri=incident.do?sys_id=${incident.sys_id}`
|
|
192
|
+
})) ?? [];
|
|
193
|
+
return { items, totalCount };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
this.logger.error(`Failed to fetch incidents: ${error.message}`, {
|
|
196
|
+
error
|
|
197
|
+
});
|
|
198
|
+
throw new Error(`Failed to fetch incidents: ${error.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async getUserSysIdByEmail(email) {
|
|
202
|
+
const authHeaders = await this.getAuthHeaders();
|
|
203
|
+
const url = `${this.instanceUrl}/api/now/table/sys_user?sysparm_query=email=${encodeURIComponent(
|
|
204
|
+
email
|
|
205
|
+
)}&sysparm_fields=sys_id`;
|
|
206
|
+
const response = await axios__default.default.get(url, {
|
|
207
|
+
headers: {
|
|
208
|
+
...authHeaders,
|
|
209
|
+
Accept: "application/json"
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
const users = response.data?.result;
|
|
213
|
+
if (users && users.length > 0) return users[0].sys_id;
|
|
214
|
+
throw new Error(`User with email ${email} not found in ServiceNow.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
exports.DefaultServiceNowClient = DefaultServiceNowClient;
|
|
219
|
+
//# sourceMappingURL=client.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.cjs.js","sources":["../../src/service-now-rest/client.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport {\n ClientCredentials,\n ResourceOwnerPassword,\n ModuleOptions,\n AccessToken,\n} from 'simple-oauth2';\nimport axios from 'axios';\nimport {\n IncidentPick,\n PaginatedIncidents,\n} from '@backstage-community/plugin-servicenow-common';\nimport { OAuthConfig, ServiceNowConfig } from '../../config';\n\nexport type IncidentQueryParams = {\n userEmail?: string;\n entityId?: string;\n state?: string;\n priority?: string;\n search?: string;\n limit?: number;\n offset?: number;\n order?: 'asc' | 'desc';\n orderBy?: string;\n};\n\nexport interface ServiceNowClient {\n fetchIncidents(options: IncidentQueryParams): Promise<PaginatedIncidents>;\n}\n\nexport class DefaultServiceNowClient implements ServiceNowClient {\n private readonly instanceUrl: string;\n private readonly config: ServiceNowConfig;\n private readonly logger: LoggerService;\n private oauthClient?: ClientCredentials | ResourceOwnerPassword;\n\n constructor(config: ServiceNowConfig, logger: LoggerService) {\n this.config = config;\n this.logger = logger;\n\n if (!config.servicenow?.instanceUrl) {\n logger.error('ServiceNow instance url is missing. Please configure it.');\n throw new Error(\n 'ServiceNow instance url is missing. Please configure it.',\n );\n }\n this.instanceUrl = config.servicenow?.instanceUrl.replace(/\\/$/, '');\n\n if (!config.servicenow?.oauth && !config.servicenow?.basicAuth) {\n logger.error(\n 'ServiceNow authentication configuration is missing. Please configure either OAuth or Basic Auth.',\n );\n throw new Error(\n 'ServiceNow authentication configuration is missing. Please configure either OAuth or Basic Auth.',\n );\n }\n\n if (config.servicenow?.oauth) {\n this.setupOAuthClient(config.servicenow.oauth);\n }\n\n if (config.servicenow?.basicAuth) {\n logger.warn(\n 'Basic authentication is configured for ServiceNow. This is not recommended for production environments.',\n );\n }\n }\n\n private setupOAuthClient(oauth: OAuthConfig) {\n const determinedTokenUrl = `${this.instanceUrl}/oauth_token.do`;\n\n let tokenHost: string;\n let tokenPath: string;\n try {\n const parsedTokenUrl = new URL(determinedTokenUrl);\n tokenHost = parsedTokenUrl.origin;\n tokenPath = parsedTokenUrl.pathname;\n } catch (e: any) {\n this.logger.error(\n `Invalid tokenUrl constructed or provided: ${determinedTokenUrl}. Error: ${e.message}`,\n );\n throw new Error(`Invalid tokenUrl: ${determinedTokenUrl}`);\n }\n\n const oauthModuleOptions: ModuleOptions = {\n client: {\n id: oauth.clientId,\n secret: oauth.clientSecret,\n },\n auth: {\n tokenHost: tokenHost,\n tokenPath: tokenPath,\n },\n options: {\n authorizationMethod: 'body',\n },\n };\n\n if (oauth.grantType === 'client_credentials') {\n this.oauthClient = new ClientCredentials(oauthModuleOptions);\n } else if (oauth.grantType === 'password') {\n if (!oauth.username || !oauth.password) {\n this.logger.error(\n \"Username and/or password missing for 'password' grant type in ServiceNow OAuth config.\",\n );\n throw new Error(\n \"Username and/or password missing for 'password' grant type.\",\n );\n }\n this.oauthClient = new ResourceOwnerPassword(oauthModuleOptions);\n } else {\n const grantType = (oauth as any).grantType;\n this.logger.error(`Unsupported OAuth grantType: ${grantType}`);\n throw new Error(`Unsupported OAuth grantType: ${grantType}`);\n }\n }\n\n private async getAuthHeaders(): Promise<{ Authorization: string }> {\n if (this.config.servicenow?.basicAuth) {\n const { username, password } = this.config.servicenow.basicAuth;\n const encodedCredentials = Buffer.from(\n `${username}:${password}`,\n ).toString('base64');\n return { Authorization: `Basic ${encodedCredentials}` };\n }\n\n if (this.config.servicenow?.oauth && this.oauthClient) {\n let accessToken: AccessToken;\n try {\n if (this.config.servicenow.oauth.grantType === 'client_credentials') {\n accessToken = await (this.oauthClient as ClientCredentials).getToken(\n {},\n );\n } else if (this.config.servicenow.oauth.grantType === 'password') {\n if (\n !this.config.servicenow.oauth.username ||\n !this.config.servicenow.oauth.password\n ) {\n throw new Error(\n \"Username or password missing for 'password' grant type during token acquisition.\",\n );\n }\n accessToken = await (\n this.oauthClient as ResourceOwnerPassword\n ).getToken({\n username: this.config.servicenow.oauth.username,\n password: this.config.servicenow.oauth.password,\n });\n } else {\n throw new Error(\n `Unsupported grantType in getAuthHeaders: ${\n (this.config.servicenow.oauth as any).grantType\n }`,\n );\n }\n\n const tokenData = accessToken.token;\n if (\n !tokenData ||\n typeof tokenData.access_token !== 'string' ||\n !tokenData.access_token\n ) {\n throw new Error(\n 'Failed to obtain access_token string (token data is invalid or missing).',\n );\n }\n return { Authorization: `Bearer ${tokenData.access_token}` };\n } catch (error: any) {\n this.logger.error(`Error fetching ServiceNow token: ${error.message}`, {\n error: error.stack || error,\n });\n if (error.isAxiosError && error.response) {\n this.logger.error(\n `OAuth2 token error details: Status ${\n error.response.status\n }, Data: ${JSON.stringify(error.response.data)}`,\n );\n } else if (error.data && error.data.payload) {\n this.logger.error(\n `OAuth2 token error payload: ${JSON.stringify(error.data.payload)}`,\n );\n }\n throw new Error(`Failed to obtain access token: ${error.message}`);\n }\n }\n\n throw new Error('No authentication method configured.');\n }\n\n async fetchIncidents(options: IncidentQueryParams): Promise<{\n items: IncidentPick[];\n totalCount: number;\n }> {\n const authHeaders = await this.getAuthHeaders();\n const queryParts: string[] = [];\n\n if (options.userEmail) {\n const id = await this.getUserSysIdByEmail(options.userEmail);\n queryParts.push(`caller_id=${id}^ORopened_by=${id}^ORassigned_to=${id}`);\n }\n\n if (options.state) queryParts.push(`state${options.state}`);\n if (options.priority) queryParts.push(`priority${options.priority}`);\n\n if (options.search) {\n const searchTerm = options.search;\n queryParts.push(\n `numberLIKE${searchTerm}^ORshort_descriptionLIKE${searchTerm}^ORdescriptionLIKE${searchTerm}`,\n );\n }\n\n if (options.entityId) {\n queryParts.push(`u_backstage_entity_id=${options.entityId}`);\n }\n\n if (options.orderBy) {\n queryParts.push(\n `${options.order === 'desc' ? 'ORDERBYDESC' : 'ORDERBY'}${\n options.orderBy\n }`,\n );\n }\n\n const sysparmQuery = queryParts.join('^');\n\n const params = new URLSearchParams();\n if (sysparmQuery) params.append('sysparm_query', sysparmQuery);\n if (options.limit !== undefined)\n params.append('sysparm_limit', String(options.limit));\n if (options.offset !== undefined)\n params.append('sysparm_offset', String(options.offset));\n params.append(\n 'sysparm_fields',\n 'sys_id,number,short_description,description,sys_created_on,priority,incident_state',\n );\n\n params.append('sysparm_count', 'true');\n\n const requestUrl = `${\n this.instanceUrl\n }/api/now/table/incident?${params.toString()}`;\n this.logger.info(`Fetching incidents from ServiceNow: ${requestUrl}`);\n\n try {\n const response = await axios.get(requestUrl, {\n headers: {\n ...authHeaders,\n Accept: 'application/json',\n },\n timeout: 30000,\n });\n\n const countHeader =\n response.headers['x-total-count'] ?? response.headers['X-Total-Count'];\n const totalCount = Number(countHeader ?? 0);\n\n const items =\n response.data?.result?.map((incident: any) => ({\n ...incident,\n url: `${this.instanceUrl}/nav_to.do?uri=incident.do?sys_id=${incident.sys_id}`,\n })) ?? [];\n\n return { items, totalCount };\n } catch (error: any) {\n this.logger.error(`Failed to fetch incidents: ${error.message}`, {\n error,\n });\n throw new Error(`Failed to fetch incidents: ${error.message}`);\n }\n }\n\n private async getUserSysIdByEmail(email: string): Promise<string | null> {\n const authHeaders = await this.getAuthHeaders();\n const url = `${\n this.instanceUrl\n }/api/now/table/sys_user?sysparm_query=email=${encodeURIComponent(\n email,\n )}&sysparm_fields=sys_id`;\n const response = await axios.get(url, {\n headers: {\n ...authHeaders,\n Accept: 'application/json',\n },\n });\n const users = response.data?.result;\n if (users && users.length > 0) return users[0].sys_id;\n\n throw new Error(`User with email ${email} not found in ServiceNow.`);\n }\n}\n"],"names":["ClientCredentials","ResourceOwnerPassword","axios"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@backstage-community/plugin-servicenow-backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"main": "dist/index.cjs.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"main": "dist/index.cjs.js",
|
|
10
|
+
"types": "dist/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/backstage/community-plugins",
|
|
15
|
+
"directory": "workspaces/servicenow/plugins/servicenow-backend"
|
|
16
|
+
},
|
|
17
|
+
"backstage": {
|
|
18
|
+
"role": "backend-plugin",
|
|
19
|
+
"pluginId": "servicenow",
|
|
20
|
+
"pluginPackages": [
|
|
21
|
+
"@backstage-community/plugin-servicenow",
|
|
22
|
+
"@backstage-community/plugin-servicenow-backend",
|
|
23
|
+
"@backstage-community/plugin-servicenow-common"
|
|
24
|
+
],
|
|
25
|
+
"features": {
|
|
26
|
+
".": "@backstage/BackendFeature"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"start": "backstage-cli package start",
|
|
31
|
+
"build": "backstage-cli package build",
|
|
32
|
+
"lint": "backstage-cli package lint",
|
|
33
|
+
"test": "backstage-cli package test",
|
|
34
|
+
"clean": "backstage-cli package clean",
|
|
35
|
+
"prepack": "backstage-cli package prepack",
|
|
36
|
+
"postpack": "backstage-cli package postpack"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@backstage-community/plugin-servicenow-common": "^1.0.0",
|
|
40
|
+
"@backstage/backend-defaults": "^0.10.0",
|
|
41
|
+
"@backstage/backend-plugin-api": "^1.3.1",
|
|
42
|
+
"@backstage/config": "^1.3.2",
|
|
43
|
+
"@backstage/errors": "^1.2.7",
|
|
44
|
+
"@backstage/plugin-catalog-node": "^1.17.0",
|
|
45
|
+
"axios": "^1.9.0",
|
|
46
|
+
"express": "^4.17.1",
|
|
47
|
+
"express-promise-router": "^4.1.0",
|
|
48
|
+
"simple-oauth2": "^5.1.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@backstage/backend-test-utils": "^1.5.0",
|
|
52
|
+
"@backstage/cli": "^0.32.1",
|
|
53
|
+
"@backstage/plugin-catalog-node": "^1.17.0",
|
|
54
|
+
"@types/express": "^4.17.6",
|
|
55
|
+
"@types/qs": "^6",
|
|
56
|
+
"@types/simple-oauth2": "^5",
|
|
57
|
+
"@types/supertest": "^2.0.12",
|
|
58
|
+
"qs": "^6.14.0",
|
|
59
|
+
"supertest": "^6.2.4"
|
|
60
|
+
},
|
|
61
|
+
"files": [
|
|
62
|
+
"dist"
|
|
63
|
+
],
|
|
64
|
+
"maintainers": [
|
|
65
|
+
"@ciiay",
|
|
66
|
+
"@AndrienkoAleksandr",
|
|
67
|
+
"@PatAKnight",
|
|
68
|
+
"@christoph-jerolimov"
|
|
69
|
+
],
|
|
70
|
+
"bugs": "https://github.com/backstage/community-plugins/issues",
|
|
71
|
+
"author": "Red Hat",
|
|
72
|
+
"keywords": [
|
|
73
|
+
"backstage",
|
|
74
|
+
"plugin"
|
|
75
|
+
],
|
|
76
|
+
"typesVersions": {
|
|
77
|
+
"*": {
|
|
78
|
+
"package.json": [
|
|
79
|
+
"package.json"
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|