Substituted UserAgent with Integration Name in emails

refs https://github.com/TryGhost/Toolbox/issues/292

- Providing user-defined Integration name instead of API client's UserAgent gives a lot more control to instance administrators identifying which integration is being used incorrectly.
- It's best practice to create an Integration with a set of API keys per API client - which should be enough to identify an outdated one.
This commit is contained in:
Naz 2022-05-10 17:33:15 +08:00
parent 09594cb5e1
commit 8cc9fc4353
2 changed files with 49 additions and 49 deletions

View File

@ -7,16 +7,18 @@ class APIVersionCompatibilityService {
* *
* @param {Object} options * @param {Object} options
* @param {Object} options.UserModel - ghost user model * @param {Object} options.UserModel - ghost user model
* @param {Object} options.ApiKeyModel - ghost api key model
* @param {Object} options.settingsService - ghost settings service * @param {Object} options.settingsService - ghost settings service
* @param {(Object: {subject: String, to: String, text: String, html: String}) => Promise<any>} options.sendEmail - email sending function * @param {(Object: {subject: String, to: String, text: String, html: String}) => Promise<any>} options.sendEmail - email sending function
* @param {Function} options.getSiteUrl * @param {Function} options.getSiteUrl
* @param {Function} options.getSiteTitle * @param {Function} options.getSiteTitle
*/ */
constructor({UserModel, settingsService, sendEmail, getSiteUrl, getSiteTitle}) { constructor({UserModel, ApiKeyModel, settingsService, sendEmail, getSiteUrl, getSiteTitle}) {
this.sendEmail = sendEmail; this.sendEmail = sendEmail;
this.versionNotificationsDataService = new VersionNotificationsDataService({ this.versionNotificationsDataService = new VersionNotificationsDataService({
UserModel, UserModel,
ApiKeyModel,
settingsService settingsService
}); });
@ -32,11 +34,14 @@ class APIVersionCompatibilityService {
* @param {Object} options * @param {Object} options
* @param {string} options.acceptVersion - client's accept-version header value * @param {string} options.acceptVersion - client's accept-version header value
* @param {string} options.contentVersion - server's content-version header value * @param {string} options.contentVersion - server's content-version header value
* @param {string} options.apiKeyValue - key value (secret for Content API and kid for Admin API) used to access the API
* @param {string} options.apiKeyType - key type used to access the API
* @param {string} options.requestURL - url that was requested and failed compatibility test * @param {string} options.requestURL - url that was requested and failed compatibility test
* @param {string} [options.userAgent] - client's user-agent header value * @param {string} [options.userAgent] - client's user-agent header value
*/ */
async handleMismatch({acceptVersion, contentVersion, requestURL, userAgent = ''}) { async handleMismatch({acceptVersion, contentVersion, apiKeyValue, apiKeyType, requestURL, userAgent = ''}) {
if (!await this.versionNotificationsDataService.fetchNotification(acceptVersion)) { if (!await this.versionNotificationsDataService.fetchNotification(acceptVersion)) {
const integrationName = await this.versionNotificationsDataService.getIntegrationName(apiKeyValue, apiKeyType);
const trimmedUseAgent = userAgent.split('/')[0]; const trimmedUseAgent = userAgent.split('/')[0];
const emails = await this.versionNotificationsDataService.getNotificationEmails(); const emails = await this.versionNotificationsDataService.getNotificationEmails();
@ -54,7 +59,7 @@ class APIVersionCompatibilityService {
data: { data: {
acceptVersion, acceptVersion,
contentVersion, contentVersion,
clientName: trimmedUseAgent, clientName: integrationName,
recipientEmail: email, recipientEmail: email,
requestURL: requestURL requestURL: requestURL
} }

View File

@ -6,6 +6,7 @@ describe('APIVersionCompatibilityService', function () {
const getSiteUrl = () => 'https://amazeballsghostsite.com'; const getSiteUrl = () => 'https://amazeballsghostsite.com';
const getSiteTitle = () => 'Tahini and chickpeas'; const getSiteTitle = () => 'Tahini and chickpeas';
let UserModel; let UserModel;
let ApiKeyModel;
let settingsService; let settingsService;
beforeEach(function () { beforeEach(function () {
@ -27,6 +28,24 @@ describe('APIVersionCompatibilityService', function () {
}] }]
}) })
}; };
ApiKeyModel = {
findOne: sinon
.stub()
.withArgs({
secret: 'super_secret'
}, {
withRelated: ['integration']
})
.resolves({
relations: {
integration: {
get: () => 'Elaborate Fox'
}
}
})
};
settingsService = { settingsService = {
read: sinon.stub().resolves({ read: sinon.stub().resolves({
version_notifications: { version_notifications: {
@ -48,6 +67,7 @@ describe('APIVersionCompatibilityService', function () {
const sendEmail = sinon.spy(); const sendEmail = sinon.spy();
const compatibilityService = new APIVersionCompatibilityService({ const compatibilityService = new APIVersionCompatibilityService({
UserModel, UserModel,
ApiKeyModel,
settingsService, settingsService,
sendEmail, sendEmail,
getSiteUrl, getSiteUrl,
@ -58,7 +78,9 @@ describe('APIVersionCompatibilityService', function () {
acceptVersion: 'v4.5', acceptVersion: 'v4.5',
contentVersion: 'v5.1', contentVersion: 'v5.1',
userAgent: 'Elaborate Fox', userAgent: 'Elaborate Fox',
requestURL: 'https://amazeballsghostsite.com/ghost/api/admin/posts/dew023d9203se4' requestURL: 'https://amazeballsghostsite.com/ghost/api/admin/posts/dew023d9203se4',
apiKeyValue: 'secret',
apiKeyType: 'content'
}); });
assert.equal(sendEmail.called, true); assert.equal(sendEmail.called, true);
@ -109,6 +131,7 @@ describe('APIVersionCompatibilityService', function () {
const compatibilityService = new APIVersionCompatibilityService({ const compatibilityService = new APIVersionCompatibilityService({
sendEmail, sendEmail,
ApiKeyModel,
UserModel, UserModel,
settingsService, settingsService,
getSiteUrl, getSiteUrl,
@ -119,7 +142,9 @@ describe('APIVersionCompatibilityService', function () {
acceptVersion: 'v4.5', acceptVersion: 'v4.5',
contentVersion: 'v5.1', contentVersion: 'v5.1',
userAgent: 'Elaborate Fox', userAgent: 'Elaborate Fox',
requestURL: 'https://amazeballsghostsite.com/ghost/api/admin/posts/dew023d9203se4' requestURL: 'https://amazeballsghostsite.com/ghost/api/admin/posts/dew023d9203se4',
apiKeyValue: 'secret',
apiKeyType: 'content'
}); });
assert.equal(sendEmail.called, true); assert.equal(sendEmail.called, true);
@ -147,7 +172,9 @@ describe('APIVersionCompatibilityService', function () {
acceptVersion: 'v4.5', acceptVersion: 'v4.5',
contentVersion: 'v5.1', contentVersion: 'v5.1',
userAgent: 'Elaborate Fox', userAgent: 'Elaborate Fox',
requestURL: 'does not matter' requestURL: 'does not matter',
apiKeyValue: 'secret',
apiKeyType: 'content'
}); });
assert.equal(sendEmail.calledOnce, true); assert.equal(sendEmail.calledOnce, true);
@ -211,6 +238,7 @@ describe('APIVersionCompatibilityService', function () {
const compatibilityService = new APIVersionCompatibilityService({ const compatibilityService = new APIVersionCompatibilityService({
sendEmail, sendEmail,
UserModel, UserModel,
ApiKeyModel,
settingsService, settingsService,
getSiteUrl, getSiteUrl,
getSiteTitle getSiteTitle
@ -220,7 +248,9 @@ describe('APIVersionCompatibilityService', function () {
acceptVersion: 'v4.5', acceptVersion: 'v4.5',
contentVersion: 'v5.1', contentVersion: 'v5.1',
userAgent: 'Elaborate Fox', userAgent: 'Elaborate Fox',
requestURL: 'https://amazeballsghostsite.com/ghost/api/admin/posts/dew023d9203se4' requestURL: 'https://amazeballsghostsite.com/ghost/api/admin/posts/dew023d9203se4',
apiKeyValue: 'secret',
apiKeyType: 'content'
}); });
assert.equal(sendEmail.calledTwice, true); assert.equal(sendEmail.calledTwice, true);
@ -269,7 +299,9 @@ describe('APIVersionCompatibilityService', function () {
acceptVersion: 'v4.8', acceptVersion: 'v4.8',
contentVersion: 'v5.1', contentVersion: 'v5.1',
userAgent: 'Elaborate Fox', userAgent: 'Elaborate Fox',
requestURL: 'https://amazeballsghostsite.com/ghost/api/admin/posts/dew023d9203se4' requestURL: 'https://amazeballsghostsite.com/ghost/api/admin/posts/dew023d9203se4',
apiKeyValue: 'secret',
apiKeyType: 'content'
}); });
assert.equal(sendEmail.callCount, 4); assert.equal(sendEmail.callCount, 4);
@ -287,52 +319,13 @@ describe('APIVersionCompatibilityService', function () {
assert.match(sendEmail.args[2][0].text, /https:\/\/amazeballsghostsite.com\/ghost\/api\/admin\/posts\/dew023d9203se4/); assert.match(sendEmail.args[2][0].text, /https:\/\/amazeballsghostsite.com\/ghost\/api\/admin\/posts\/dew023d9203se4/);
}); });
it('Trims down the name of the integration when a lot of meta information is present in user-agent header', async function (){
const sendEmail = sinon.spy();
const compatibilityService = new APIVersionCompatibilityService({
sendEmail,
UserModel,
settingsService,
getSiteUrl,
getSiteTitle
});
await compatibilityService.handleMismatch({
acceptVersion: 'v4.5',
contentVersion: 'v5.1',
userAgent: 'Fancy Pants/2.3 GhostAdminSDK/2.4.0',
requestURL: 'https://amazeballsghostsite.com/ghost/api/admin/posts/dew023d9203se4'
});
assert.equal(sendEmail.called, true);
assert.equal(sendEmail.args[0][0].to, 'simon@example.com');
assert.equal(sendEmail.args[0][0].subject, `Attention required: Your Fancy Pants integration has failed`);
assert.match(sendEmail.args[0][0].html, /Ghost has noticed that your <strong style="font-weight: 600;">Fancy Pants<\/strong> is no longer working as expected\./);
assert.match(sendEmail.args[0][0].html, /Fancy Pants integration expected Ghost version:<\/strong>&nbsp; v4.5/);
assert.match(sendEmail.args[0][0].html, /Current Ghost version:<\/strong>&nbsp; v5.1/);
assert.match(sendEmail.args[0][0].html, /Failed request URL:<\/strong>&nbsp; https:\/\/amazeballsghostsite.com\/ghost\/api\/admin\/posts\/dew023d9203se4/);
assert.match(sendEmail.args[0][0].html, /This email was sent from <a href="https:\/\/amazeballsghostsite.com"/);
assert.match(sendEmail.args[0][0].html, /to <a href="mailto:simon@example.com"/);
assert.match(sendEmail.args[0][0].text, /Ghost has noticed that your Fancy Pants is no longer working as expected\./);
assert.match(sendEmail.args[0][0].text, /Fancy Pants integration expected Ghost version:v4.5/);
assert.match(sendEmail.args[0][0].text, /Current Ghost version:v5.1/);
assert.match(sendEmail.args[0][0].text, /Failed request URL:/);
assert.match(sendEmail.args[0][0].text, /https:\/\/amazeballsghostsite.com\/ghost\/api\/admin\/posts\/dew023d9203se4/);
assert.match(sendEmail.args[0][0].text, /This email was sent from https:\/\/amazeballsghostsite.com/);
assert.match(sendEmail.args[0][0].text, /to simon@example.com/);
});
it('Sends Zapier-specific email when userAgent is a Zapier client', async function (){ it('Sends Zapier-specific email when userAgent is a Zapier client', async function (){
const sendEmail = sinon.spy(); const sendEmail = sinon.spy();
const compatibilityService = new APIVersionCompatibilityService({ const compatibilityService = new APIVersionCompatibilityService({
sendEmail, sendEmail,
UserModel, UserModel,
ApiKeyModel,
settingsService, settingsService,
getSiteUrl, getSiteUrl,
getSiteTitle getSiteTitle
@ -342,7 +335,9 @@ describe('APIVersionCompatibilityService', function () {
acceptVersion: 'v4.5', acceptVersion: 'v4.5',
contentVersion: 'v5.1', contentVersion: 'v5.1',
userAgent: 'Zapier/4.20 GhostAdminSDK/2.4.0', userAgent: 'Zapier/4.20 GhostAdminSDK/2.4.0',
requestURL: 'https://amazeballsghostsite.com/ghost/api/admin/posts/dew023d9203se4' requestURL: 'https://amazeballsghostsite.com/ghost/api/admin/posts/dew023d9203se4',
apiKeyValue: 'secret',
apiKeyType: 'content'
}); });
assert.equal(sendEmail.called, true); assert.equal(sendEmail.called, true);