diff --git a/app/frontend/src/components/admin/AdminAPIsTable.vue b/app/frontend/src/components/admin/AdminAPIsTable.vue
index 6d0af58b3..cafb69d10 100644
--- a/app/frontend/src/components/admin/AdminAPIsTable.vue
+++ b/app/frontend/src/components/admin/AdminAPIsTable.vue
@@ -26,6 +26,7 @@ export default {
name: null,
endpointUrl: null,
code: null,
+ allowSendUserToken: false,
},
show: false,
},
@@ -98,6 +99,7 @@ export default {
name: null,
endpointUrl: null,
code: null,
+ allowSendUserToken: false,
},
show: false,
};
@@ -271,6 +273,16 @@ export default {
variant="outlined"
:lang="lang"
>
+
+
+
+ {{ $t('trans.adminAPIsTable.allowSendUserToken') }}
+
+
+
diff --git a/app/frontend/src/components/forms/manage/ExternalAPIs.vue b/app/frontend/src/components/forms/manage/ExternalAPIs.vue
index 5cba3850b..93c73403e 100644
--- a/app/frontend/src/components/forms/manage/ExternalAPIs.vue
+++ b/app/frontend/src/components/forms/manage/ExternalAPIs.vue
@@ -163,6 +163,7 @@ export default {
sendApiKey: false,
apiKeyHeader: null,
apiKey: null,
+ allowSendUserToken: false,
sendUserToken: false,
userTokenHeader: null,
userTokenBearer: false,
@@ -456,48 +457,6 @@ export default {
:rules="apiKeyHeaderRules"
/>
-
-
-
-
-
-
-
- {{ $t('trans.externalAPI.formSendUserToken') }}
-
-
-
-
-
-
-
- {{ $t('trans.externalAPI.formUserTokenBearer') }}
-
-
-
-
-
-
-
-
-
@@ -567,6 +526,48 @@ export default {
:rules="userInfoHeaderRules"
/>
+
+
+
+
+
+
+
+ {{ $t('trans.externalAPI.formSendUserToken') }}
+
+
+
+
+
+
+
+ {{ $t('trans.externalAPI.formUserTokenBearer') }}
+
+
+
+
+
+
+
+
+
diff --git a/app/frontend/src/internationalization/trans/chefs/en/en.json b/app/frontend/src/internationalization/trans/chefs/en/en.json
index 841ae59be..8c5156621 100644
--- a/app/frontend/src/internationalization/trans/chefs/en/en.json
+++ b/app/frontend/src/internationalization/trans/chefs/en/en.json
@@ -387,7 +387,8 @@
"display": "Status",
"actions": "Actions",
"edit": "Edit",
- "editTitle": "Update External API Status"
+ "editTitle": "Update External API Status",
+ "allowSendUserToken": "Allow 'Send User Token'"
},
"adminUsersTable": {
"search": "Search",
diff --git a/app/src/db/migrations/20240521210143_046_external_api.js b/app/src/db/migrations/20240521210143_046_external_api.js
index f67b58773..254f41e34 100644
--- a/app/src/db/migrations/20240521210143_046_external_api.js
+++ b/app/src/db/migrations/20240521210143_046_external_api.js
@@ -38,6 +38,7 @@ exports.up = function (knex) {
table.string('apiKeyHeader');
table.string('apiKey');
+ table.boolean('allowSendUserToken').defaultTo(false);
table.boolean('sendUserToken').defaultTo(false);
table.string('userTokenHeader');
table.boolean('userTokenBearer').defaultTo(true);
@@ -54,7 +55,7 @@ exports.up = function (knex) {
knex.schema.raw(
`create or replace view external_api_vw as
select e.id, e."formId", f.ministry, f.name as "formName", e.name, e."endpointUrl",
- e.code, easc.display from external_api e
+ e.code, easc.display, e."allowSendUserToken" from external_api e
inner join external_api_status_code easc on e.code = easc.code
inner join form f on e."formId" = f.id
order by f.ministry, "formName", e.name;`
diff --git a/app/src/forms/admin/service.js b/app/src/forms/admin/service.js
index fe3f47717..35b681075 100644
--- a/app/src/forms/admin/service.js
+++ b/app/src/forms/admin/service.js
@@ -139,11 +139,18 @@ const service = {
try {
await ExternalAPI.query().findById(id).throwIfNotFound();
trx = await ExternalAPI.startTransaction();
- // admins only change the status code.
+ // admins only change the status code and allow send user token
const upd = {
code: data.code,
+ allowSendUserToken: data.allowSendUserToken,
updatedBy: 'ADMIN',
};
+ // if we are not allowing sending user token, ensure any user token fields are cleared out
+ if (!data.allowSendUserToken) {
+ upd['sendUserToken'] = false;
+ upd['userTokenHeader'] = null;
+ upd['userTokenBearer'] = false;
+ }
await ExternalAPI.query(trx).patchAndFetchById(id, upd);
diff --git a/app/src/forms/common/models/tables/externalAPI.js b/app/src/forms/common/models/tables/externalAPI.js
index 866403452..c0e1f2da0 100644
--- a/app/src/forms/common/models/tables/externalAPI.js
+++ b/app/src/forms/common/models/tables/externalAPI.js
@@ -88,6 +88,7 @@ class ExternalAPI extends Timestamps(Model) {
sendApiKey: { type: 'boolean', default: false },
apiKeyHeader: { type: ['string', 'null'] },
apiKey: { type: ['string', 'null'] },
+ allowSendUserToken: { type: 'boolean', default: false },
sendUserToken: { type: 'boolean', default: false },
userTokenHeader: { type: ['string', 'null'] },
userTokenBearer: { type: 'boolean', default: true },
diff --git a/app/src/forms/form/externalApi/service.js b/app/src/forms/form/externalApi/service.js
index 49da60ec4..e99c87f9d 100644
--- a/app/src/forms/form/externalApi/service.js
+++ b/app/src/forms/form/externalApi/service.js
@@ -56,6 +56,23 @@ const service = {
}
},
+ checkAllowSendUserToken: (data, allowSendUserToken) => {
+ if (!data) {
+ throw new Problem(422, `'externalAPI record' cannot be empty.`);
+ }
+
+ if (data.sendUserToken && !allowSendUserToken) {
+ throw new Problem(422, 'Sending User Token has not been authorized for this External API.');
+ }
+ if (!allowSendUserToken) {
+ // make sure all user token fields are cleared out...
+ data.allowSendUserToken = false;
+ data.sendUserToken = false;
+ data.userTokenHeader = null;
+ data.userTokenBearer = false;
+ }
+ },
+
createExternalAPI: async (formId, data, currentUser) => {
service.validateExternalAPI(data);
@@ -65,6 +82,8 @@ const service = {
data.id = uuidv4();
// set status to SUBMITTED
data.code = ExternalAPIStatuses.SUBMITTED;
+ // ensure that new records don't send user tokens.
+ service.checkAllowSendUserToken(data, false);
await ExternalAPI.query(trx).insert({
...data,
createdBy: currentUser.usernameIdp,
@@ -85,9 +104,10 @@ const service = {
try {
const existing = await ExternalAPI.query().modify('findByIdAndFormId', externalAPIId, formId).first().throwIfNotFound();
trx = await ExternalAPI.startTransaction();
- // let's use a different method for the administrators to update status code.
+ // let's use a different method for the administrators to update status code and allow send user token
// this method should not change the status code.
data.code = existing.code;
+ service.checkAllowSendUserToken(data, existing.allowSendUserToken);
await ExternalAPI.query(trx)
.modify('findByIdAndFormId', externalAPIId, formId)
.update({
diff --git a/app/tests/unit/forms/form/externalApi/controller.spec.js b/app/tests/unit/forms/form/externalApi/controller.spec.js
index b83245b8f..69aa55532 100644
--- a/app/tests/unit/forms/form/externalApi/controller.spec.js
+++ b/app/tests/unit/forms/form/externalApi/controller.spec.js
@@ -105,9 +105,10 @@ describe('createExternalAPI', () => {
sendApiKey: true,
apiKeyHeader: 'X-API-KEY',
apiKey: 'my-api-key',
- sendUserToken: true,
- userTokenHeader: 'Authorization',
- userTokenBearer: true,
+ allowSendUserToken: false,
+ sendUserToken: false,
+ userTokenHeader: null,
+ userTokenBearer: false,
sendUserInfo: true,
userInfoHeader: 'X-API-USER',
userInfoEncrypted: true,
diff --git a/app/tests/unit/forms/form/externalApi/service.spec.js b/app/tests/unit/forms/form/externalApi/service.spec.js
index 2a8b22fad..3dfc48462 100644
--- a/app/tests/unit/forms/form/externalApi/service.spec.js
+++ b/app/tests/unit/forms/form/externalApi/service.spec.js
@@ -17,6 +17,50 @@ afterEach(() => {
jest.restoreAllMocks();
});
+describe('checkAllowSendUserToken', () => {
+ let validData = null;
+ beforeEach(() => {
+ validData = {
+ id: uuidv4(),
+ formId: uuidv4(),
+ name: 'test_api',
+ endpointUrl: 'http://external.api/',
+ sendApiKey: true,
+ apiKeyHeader: 'X-API-KEY',
+ apiKey: 'my-api-key',
+ sendUserToken: true,
+ userTokenHeader: 'Authorization',
+ userTokenBearer: true,
+ sendUserInfo: true,
+ userInfoHeader: 'X-API-USER',
+ userInfoEncrypted: true,
+ userInfoEncryptionKey: '0489aa2a7882dc53be7c76db43be1800e56627c31a88a0011d85ccc255b79d00',
+ userInfoEncryptionAlgo: ENCRYPTION_ALGORITHMS.AES_256_GCM,
+ code: ExternalAPIStatuses.SUBMITTED,
+ };
+ });
+
+ it('should not throw errors with valid data', () => {
+ service.checkAllowSendUserToken(validData, true);
+ });
+
+ it('should throw 422 with no data', () => {
+ expect(() => service.checkAllowSendUserToken(undefined, true)).toThrow();
+ });
+
+ it('should throw 422 when sendUserToken = true but not allowed', () => {
+ expect(() => service.checkAllowSendUserToken(validData, false)).toThrow();
+ });
+
+ it('should blank out user token fields when not allowed', () => {
+ validData.sendUserToken = false;
+ service.checkAllowSendUserToken(validData, false);
+ expect(validData.sendUserToken).toBe(false);
+ expect(validData.userTokenHeader).toBe(null);
+ expect(validData.userTokenBearer).toBe(false);
+ });
+});
+
describe('validateExternalAPI', () => {
let validData = null;
beforeEach(() => {
@@ -93,9 +137,10 @@ describe('createExternalAPI', () => {
sendApiKey: true,
apiKeyHeader: 'X-API-KEY',
apiKey: 'my-api-key',
- sendUserToken: true,
- userTokenHeader: 'Authorization',
- userTokenBearer: true,
+ allowSendUserToken: false,
+ sendUserToken: false,
+ userTokenHeader: null,
+ userTokenBearer: false,
sendUserInfo: true,
userInfoHeader: 'X-API-USER',
userInfoEncrypted: true,
@@ -146,9 +191,10 @@ describe('updateExternalAPI', () => {
sendApiKey: true,
apiKeyHeader: 'X-API-KEY',
apiKey: 'my-api-key',
- sendUserToken: true,
- userTokenHeader: 'Authorization',
- userTokenBearer: true,
+ allowSendUserToken: false,
+ sendUserToken: false,
+ userTokenHeader: null,
+ userTokenBearer: false,
sendUserInfo: true,
userInfoHeader: 'X-API-USER',
userInfoEncrypted: true,
@@ -178,6 +224,45 @@ describe('updateExternalAPI', () => {
expect(MockTransaction.commit).toBeCalledTimes(1);
});
+ it('should update user token fields when allowed', async () => {
+ // mark as allowed by admin, and set some user token config values...
+ validData.allowSendUserToken = true;
+ validData.sendUserToken = true;
+ validData.userTokenHeader = 'Authorization';
+ validData.userTokenBearer = true;
+ MockModel.throwIfNotFound = jest.fn().mockResolvedValueOnce(Object.assign({}, validData));
+
+ await service.updateExternalAPI(validData.formId, validData.id, validData, user);
+ expect(MockModel.update).toBeCalledTimes(1);
+ expect(MockModel.update).toBeCalledWith({
+ updatedBy: user.usernameIdp,
+ code: ExternalAPIStatuses.SUBMITTED,
+ ...validData,
+ });
+ expect(MockTransaction.commit).toBeCalledTimes(1);
+ });
+
+ it('should blank out user token fields when not allowed', async () => {
+ // mark as allowed by admin, and set some user token config values...
+ validData.allowSendUserToken = true;
+ validData.sendUserToken = false; // don't want to throw a 422...
+ validData.userTokenHeader = 'Authorization';
+ validData.userTokenBearer = true;
+ MockModel.throwIfNotFound = jest.fn().mockResolvedValueOnce(Object.assign({}, validData));
+
+ await service.updateExternalAPI(validData.formId, validData.id, validData, user);
+ expect(MockModel.update).toBeCalledTimes(1);
+ expect(MockModel.update).toBeCalledWith({
+ updatedBy: user.usernameIdp,
+ code: ExternalAPIStatuses.SUBMITTED,
+ sendUserToken: false,
+ userTokenHeader: null,
+ userTokenBearer: false,
+ ...validData,
+ });
+ expect(MockTransaction.commit).toBeCalledTimes(1);
+ });
+
it('should not commit when not found', async () => {
MockModel.throwIfNotFound = jest.fn().mockRejectedValueOnce(new Error('SQL Error'));
diff --git a/docs/chefs-external-api-configuration.md b/docs/chefs-external-api-configuration.md
index d0a8f6c35..a625a79a3 100644
--- a/docs/chefs-external-api-configuration.md
+++ b/docs/chefs-external-api-configuration.md
@@ -37,33 +37,29 @@ See the following table for description of the External API form fields.
"sendApiKey": true,
"apiKeyHeader": "X-API-KEY",
"apiKey": "",
- "sendUserToken": false,
- "userTokenHeader": "Authorization",
- "userTokenBearer": true,
"sendUserInfo": true,
"userInfoEncrypted": false,
"userInfoHeader": "X-USER-INFO",
"userInfoEncryptionKey": "999999999",
- "userInfoEncryptionAlgo": "aes-256-gcm"
+ "userInfoEncryptionAlgo": "aes-256-gcm",
+ "code": "Submitted"
}
```
-| Attribute | Form Field | Purpose |
-| ---------------------- | ------------------------------------- | ------------------------------------------------------------------------ |
-| formId | N/A | CHEFS form id |
-| name | Name | Name should be unique per form and should easily identify this API |
-| endpointUrl | Endpoint URL | Endpoint URL for the API (could be a full path or just a base path) |
-| sendApiKey | Send API Key | boolean - send an API Key in a header |
-| apiKeyHeader | API Key Header Name | the name for the API Key header |
-| apiKey | API Key Value | The value for the API Key, stored encrypted in the db. |
-| sendUserToken | Send User Token | boolean - send the user token in a header |
-| userTokenHeader | User Token Header Name | the name for the user token header |
-| userTokenBearer | User Token as Bearer | boolean - whether to prefix the user token with "Bearer " |
-| sendUserInfo | Send User Information | boolean - send current user information in headers |
-| userInfoEncrypted | Encrypt User Information | boolean - whether we encrypt the current user information |
-| userInfoHeader | Encrypt User Information Header Name | when userInfoEncrypted = true, this is the name for the user info header |
-| userInfoEncryptionKey | User Information Encryption Key | encryption key supplied by Form Designer, stored encrypted in the db. |
-| userInfoEncryptionAlgo | User Information Encryption Algorithm | A CHEFS supported encryption algorithm. |
+| Attribute | Form Field | Purpose |
+| ---------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------- |
+| formId | N/A | CHEFS form id |
+| name | Name | Name should be unique per form and should easily identify this API |
+| endpointUrl | Endpoint URL | Endpoint URL for the API (could be a full path or just a base path) |
+| sendApiKey | Send API Key | boolean - send an API Key in a header |
+| apiKeyHeader | API Key Header Name | the name for the API Key header |
+| apiKey | API Key Value | The value for the API Key, stored encrypted in the db. |
+| sendUserInfo | Send User Information | boolean - send current user information in headers |
+| userInfoEncrypted | Encrypt User Information | boolean - whether we encrypt the current user information |
+| userInfoHeader | Encrypt User Information Header Name | when userInfoEncrypted = true, this is the name for the user info header |
+| userInfoEncryptionKey | User Information Encryption Key | encryption key supplied by Form Designer, stored encrypted in the db. |
+| userInfoEncryptionAlgo | User Information Encryption Algorithm | A CHEFS supported encryption algorithm. |
+| code | N/A | Status Code. Only CHEFS Admin users can change this value. Used for the approval process. |
**Recommendation** - secure the External API with an API Key and send user information in plain text.