Skip to content

Commit

Permalink
Add CHEFS admin approval for Sending user token
Browse files Browse the repository at this point in the history
Signed-off-by: Jason Sherman <[email protected]>
  • Loading branch information
usingtechnology committed Jun 17, 2024
1 parent 3b29bd9 commit 97406ea
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 75 deletions.
12 changes: 12 additions & 0 deletions app/frontend/src/components/admin/AdminAPIsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default {
name: null,
endpointUrl: null,
code: null,
allowSendUserToken: false,
},
show: false,
},
Expand Down Expand Up @@ -98,6 +99,7 @@ export default {
name: null,
endpointUrl: null,
code: null,
allowSendUserToken: false,
},
show: false,
};
Expand Down Expand Up @@ -271,6 +273,16 @@ export default {
variant="outlined"
:lang="lang"
></v-select>
<v-checkbox
v-model="editDialog.item.allowSendUserToken"
class="my-0 pt-0"
>
<template #label>
<span :class="{ 'mr-2': isRTL }" :lang="lang">
{{ $t('trans.adminAPIsTable.allowSendUserToken') }}
</span>
</template>
</v-checkbox>
</v-form>
</template>
<template #button-text-continue>
Expand Down
85 changes: 43 additions & 42 deletions app/frontend/src/components/forms/manage/ExternalAPIs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export default {
sendApiKey: false,
apiKeyHeader: null,
apiKey: null,
allowSendUserToken: false,
sendUserToken: false,
userTokenHeader: null,
userTokenBearer: false,
Expand Down Expand Up @@ -456,48 +457,6 @@ export default {
:rules="apiKeyHeaderRules"
/></v-col>
</v-row>
<!-- User Token -->
<hr />
<v-row>
<v-col cols="4" class="pb-0">
<v-checkbox
v-model="editDialog.item.sendUserToken"
class="my-0 pt-0"
>
<template #label>
<span :class="{ 'mr-2': isRTL }" :lang="lang">
{{ $t('trans.externalAPI.formSendUserToken') }}
</span>
</template>
</v-checkbox></v-col
>
<v-col cols="8" class="pb-0">
<v-checkbox
v-model="editDialog.item.userTokenBearer"
class="my-0 pt-0"
>
<template #label>
<span :class="{ 'mr-2': isRTL }" :lang="lang">
{{ $t('trans.externalAPI.formUserTokenBearer') }}
</span>
</template>
</v-checkbox></v-col
>
</v-row>
<v-row class="mt-0">
<v-col cols="4"></v-col>
<v-col cols="8">
<v-text-field
v-model="editDialog.item.userTokenHeader"
density="compact"
solid
variant="outlined"
:label="$t('trans.externalAPI.formUserTokenHeader')"
data-test="text-userTokenHeader"
:lang="lang"
:rules="userTokenHeaderRules"
/></v-col>
</v-row>
<!-- User Information -->
<hr />
<v-row>
Expand Down Expand Up @@ -567,6 +526,48 @@ export default {
:rules="userInfoHeaderRules"
/></v-col>
</v-row>
<!-- User Token -->
<hr v-if="editDialog.item.allowSendUserToken" />
<v-row v-if="editDialog.item.allowSendUserToken">
<v-col cols="4" class="pb-0">
<v-checkbox
v-model="editDialog.item.sendUserToken"
class="my-0 pt-0"
>
<template #label>
<span :class="{ 'mr-2': isRTL }" :lang="lang">
{{ $t('trans.externalAPI.formSendUserToken') }}
</span>
</template>
</v-checkbox></v-col
>
<v-col cols="8" class="pb-0">
<v-checkbox
v-model="editDialog.item.userTokenBearer"
class="my-0 pt-0"
>
<template #label>
<span :class="{ 'mr-2': isRTL }" :lang="lang">
{{ $t('trans.externalAPI.formUserTokenBearer') }}
</span>
</template>
</v-checkbox></v-col
>
</v-row>
<v-row v-if="editDialog.item.allowSendUserToken" class="mt-0">
<v-col cols="4"></v-col>
<v-col cols="8">
<v-text-field
v-model="editDialog.item.userTokenHeader"
density="compact"
solid
variant="outlined"
:label="$t('trans.externalAPI.formUserTokenHeader')"
data-test="text-userTokenHeader"
:lang="lang"
:rules="userTokenHeaderRules"
/></v-col>
</v-row>
</v-form>
</template>
<template #button-text-continue>
Expand Down
3 changes: 2 additions & 1 deletion app/frontend/src/internationalization/trans/chefs/en/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion app/src/db/migrations/20240521210143_046_external_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;`
Expand Down
9 changes: 8 additions & 1 deletion app/src/forms/admin/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions app/src/forms/common/models/tables/externalAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
22 changes: 21 additions & 1 deletion app/src/forms/form/externalApi/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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,
Expand All @@ -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({
Expand Down
7 changes: 4 additions & 3 deletions app/tests/unit/forms/form/externalApi/controller.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
97 changes: 91 additions & 6 deletions app/tests/unit/forms/form/externalApi/service.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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'));

Expand Down
Loading

0 comments on commit 97406ea

Please sign in to comment.