Skip to content

Commit

Permalink
[Connector] API for adding OAuth support to ServiceNow connectors (#1…
Browse files Browse the repository at this point in the history
…31084)

* Adding new OAuth fields to ServiceNow ExternalIncidentServiceConfigurationBase and ExternalIncidentServiceSecretConfiguration

* Creating new function in ConnectorTokenClient for updating or replacing token

* Update servicenow executors to get Oauth access tokens if configured. Still need to update unit tests for services

* Creating wrapper function for createService to only create one axios instance

* Fixing translation check error

* Adding migration for adding isOAuth to service now connectors

* Fixing unit tests

* Fixing functional test

* Not requiring privateKeyPassword

* Fixing tests

* Adding functional tests for connector creation

* Adding functional tests

* Fixing functional test

* PR feedback

* Fixing test

* PR feedback

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
ymao1 and kibanamachine authored Apr 29, 2022
1 parent cf46ec9 commit 9d15ab1
Show file tree
Hide file tree
Showing 33 changed files with 2,695 additions and 299 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const createConnectorTokenClientMock = () => {
get: jest.fn(),
update: jest.fn(),
deleteConnectorTokens: jest.fn(),
updateOrReplace: jest.fn(),
};
return mocked;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,144 @@ describe('delete()', () => {
`);
});
});

describe('updateOrReplace()', () => {
test('creates new SO if current token is null', async () => {
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'connector_token',
attributes: {
connectorId: '123',
tokenType: 'access_token',
token: 'testtokenvalue',
expiresAt: new Date().toISOString(),
},
references: [],
});
await connectorTokenClient.updateOrReplace({
connectorId: '1',
token: null,
newToken: 'newToken',
expiresInSec: 1000,
deleteExisting: false,
});
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
expect((unsecuredSavedObjectsClient.create.mock.calls[0][1] as ConnectorToken).token).toBe(
'newToken'
);

expect(unsecuredSavedObjectsClient.find).not.toHaveBeenCalled();
expect(unsecuredSavedObjectsClient.delete).not.toHaveBeenCalled();
});

test('creates new SO and deletes all existing tokens for connector if current token is null and deleteExisting is true', async () => {
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'connector_token',
attributes: {
connectorId: '123',
tokenType: 'access_token',
token: 'testtokenvalue',
expiresAt: new Date().toISOString(),
},
references: [],
});
unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
total: 1,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'connector_token',
attributes: {
connectorId: '123',
tokenType: 'access_token',
createdAt: new Date().toISOString(),
expiresAt: new Date().toISOString(),
},
score: 1,
references: [],
},
{
id: '2',
type: 'connector_token',
attributes: {
connectorId: '123',
tokenType: 'access_token',
createdAt: new Date().toISOString(),
expiresAt: new Date().toISOString(),
},
score: 1,
references: [],
},
],
});
await connectorTokenClient.updateOrReplace({
connectorId: '1',
token: null,
newToken: 'newToken',
expiresInSec: 1000,
deleteExisting: true,
});
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
expect((unsecuredSavedObjectsClient.create.mock.calls[0][1] as ConnectorToken).token).toBe(
'newToken'
);

expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(2);
});

test('updates existing SO if current token exists', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'connector_token',
attributes: {
connectorId: '123',
tokenType: 'access_token',
token: 'testtokenvalue',
createdAt: new Date().toISOString(),
},
references: [],
});
unsecuredSavedObjectsClient.checkConflicts.mockResolvedValueOnce({
errors: [],
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'connector_token',
attributes: {
connectorId: '123',
tokenType: 'access_token',
token: 'testtokenvalue',
expiresAt: new Date().toISOString(),
},
references: [],
});
await connectorTokenClient.updateOrReplace({
connectorId: '1',
token: {
id: '3',
connectorId: '123',
tokenType: 'access_token',
token: 'testtokenvalue',
createdAt: new Date().toISOString(),
expiresAt: new Date().toISOString(),
},
newToken: 'newToken',
expiresInSec: 1000,
deleteExisting: true,
});

expect(unsecuredSavedObjectsClient.find).not.toHaveBeenCalled();
expect(unsecuredSavedObjectsClient.delete).not.toHaveBeenCalled();

expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.checkConflicts).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
expect((unsecuredSavedObjectsClient.create.mock.calls[0][1] as ConnectorToken).token).toBe(
'newToken'
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ export interface UpdateOptions {
tokenType?: string;
}

interface UpdateOrReplaceOptions {
connectorId: string;
token: ConnectorToken | null;
newToken: string;
expiresInSec: number;
deleteExisting: boolean;
}
export class ConnectorTokenClient {
private readonly logger: Logger;
private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract;
Expand Down Expand Up @@ -245,4 +252,36 @@ export class ConnectorTokenClient {
throw err;
}
}

public async updateOrReplace({
connectorId,
token,
newToken,
expiresInSec,
deleteExisting,
}: UpdateOrReplaceOptions) {
expiresInSec = expiresInSec ?? 3600;
if (token === null) {
if (deleteExisting) {
await this.deleteConnectorTokens({
connectorId,
tokenType: 'access_token',
});
}

await this.create({
connectorId,
token: newToken,
expiresAtMillis: new Date(Date.now() + expiresInSec * 1000).toISOString(),
tokenType: 'access_token',
});
} else {
await this.update({
id: token.id!.toString(),
token: newToken,
expiresAtMillis: new Date(Date.now() + expiresInSec * 1000).toISOString(),
tokenType: 'access_token',
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { createJWTAssertion } from './create_jwt_assertion';
const jwtSign = jwt.sign as jest.Mock;
const mockLogger = loggingSystemMock.create().get() as jest.Mocked<Logger>;

Date.now = jest.fn(() => 0);

describe('createJWTAssertion', () => {
test('creating a JWT token from provided claims with default values', () => {
jwtSign.mockReturnValueOnce('123456qwertyjwttoken');
Expand All @@ -27,6 +29,28 @@ describe('createJWTAssertion', () => {
subject: '[email protected]',
});

expect(jwtSign).toHaveBeenCalledWith(
{ aud: '1', exp: 3600, iat: 0, iss: 'someappid', sub: '[email protected]' },
{ key: 'test', passphrase: '123456' },
{ algorithm: 'RS256' }
);
expect(assertion).toMatchInlineSnapshot('"123456qwertyjwttoken"');
});

test('creating a JWT token when private key password is null', () => {
jwtSign.mockReturnValueOnce('123456qwertyjwttoken');

const assertion = createJWTAssertion(mockLogger, 'test', null, {
audience: '1',
issuer: 'someappid',
subject: '[email protected]',
});

expect(jwtSign).toHaveBeenCalledWith(
{ aud: '1', exp: 3600, iat: 0, iss: 'someappid', sub: '[email protected]' },
'test',
{ algorithm: 'RS256' }
);
expect(assertion).toMatchInlineSnapshot('"123456qwertyjwttoken"');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ export interface JWTClaims {
audience: string;
subject: string;
issuer: string;
expireInMilisecons?: number;
expireInMilliseconds?: number;
keyId?: string;
}

export function createJWTAssertion(
logger: Logger,
privateKey: string,
privateKeyPassword: string,
privateKeyPassword: string | null,
reservedClaims: JWTClaims,
customClaims?: Record<string, string>
): string {
const { subject, audience, issuer, expireInMilisecons, keyId } = reservedClaims;
const { subject, audience, issuer, expireInMilliseconds, keyId } = reservedClaims;
const iat = Math.floor(Date.now() / 1000);

const headerObj = { algorithm: 'RS256' as Algorithm, ...(keyId ? { keyid: keyId } : {}) };
Expand All @@ -33,17 +33,19 @@ export function createJWTAssertion(
aud: audience, // audience claim identifies the recipients that the JWT is intended for
iss: issuer, // issuer claim identifies the principal that issued the JWT
iat, // issued at claim identifies the time at which the JWT was issued
exp: iat + (expireInMilisecons ?? 3600), // expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing
exp: iat + (expireInMilliseconds ?? 3600), // expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing
...(customClaims ?? {}),
};

try {
const jwtToken = jwt.sign(
JSON.stringify(payloadObj),
{
key: privateKey,
passphrase: privateKeyPassword,
},
payloadObj,
privateKeyPassword
? {
key: privateKey,
passphrase: privateKeyPassword,
}
: privateKey,
headerObj
);
return jwtToken;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ describe('send_email module', () => {

await sendEmail(mockLogger, sendEmailOptions, connectorTokenClientM);
expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(1);
expect(connectorTokenClientM.deleteConnectorTokens.mock.calls.length).toBe(1);
expect(connectorTokenClientM.updateOrReplace.mock.calls.length).toBe(1);

delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities;
sendEmailGraphApiMock.mock.calls[0].pop();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,30 +105,13 @@ async function sendEmailWithExchange(

// try to update connector_token SO
try {
if (connectorToken === null) {
if (hasErrors) {
// delete existing access tokens
await connectorTokenClient.deleteConnectorTokens({
connectorId,
tokenType: 'access_token',
});
}
await connectorTokenClient.create({
connectorId,
token: accessToken,
// convert MS Exchange expiresIn from seconds to milliseconds
expiresAtMillis: new Date(Date.now() + tokenResult.expiresIn * 1000).toISOString(),
tokenType: 'access_token',
});
} else {
await connectorTokenClient.update({
id: connectorToken.id!.toString(),
token: accessToken,
// convert MS Exchange expiresIn from seconds to milliseconds
expiresAtMillis: new Date(Date.now() + tokenResult.expiresIn * 1000).toISOString(),
tokenType: 'access_token',
});
}
await connectorTokenClient.updateOrReplace({
connectorId,
token: connectorToken,
newToken: accessToken,
expiresInSec: tokenResult.expiresIn,
deleteExisting: hasErrors,
});
} catch (err) {
logger.warn(
`Not able to update connector token for connectorId: ${connectorId} due to error: ${err.message}`
Expand Down
Loading

0 comments on commit 9d15ab1

Please sign in to comment.