diff --git a/package-lock.json b/package-lock.json index 4d20c36..0176b2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "proxy", - "version": "1.3.3", + "version": "1.3.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index dfed64e..884f370 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxy", - "version": "1.3.3", + "version": "1.3.5", "description": "", "scripts": { "build": "tsc -p tsconfig.build.json", diff --git a/src/anonymizer/anonymizer.config.ts b/src/anonymizer/anonymizer.config.ts index f73ce37..5d14190 100644 --- a/src/anonymizer/anonymizer.config.ts +++ b/src/anonymizer/anonymizer.config.ts @@ -3,13 +3,13 @@ import { configFactory, toArray, toBoolean, toPem, toString } from '../config' const anonymizerConfig = { baseUrl: toString(), internalDomainList: toArray(), - anonymizeExternalEmailDomain: toBoolean(true), - anonymizeExternalEmailUsername: toBoolean(true), + anonymizeExternalEmailDomain: toBoolean(false), + anonymizeExternalEmailUsername: toBoolean(false), anonymizeInternalEmailDomain: toBoolean(false), - anonymizeInternalEmailUsername: toBoolean(true), + anonymizeInternalEmailUsername: toBoolean(false), enableInternalEmailPlusAddressing: toBoolean(true), enableExternalEmailPlusAddressing: toBoolean(false), - extractDomains: toBoolean(false), + extractDomains: toBoolean(true), extractDomainsWhitelist: toArray(), anonymizationSalt: toString(), rsaPrivateKey: toPem(), diff --git a/src/bootstrap.ts b/src/bootstrap.ts index b5d314e..99d9387 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -6,6 +6,7 @@ import config from './app.config' import extractToken from './helpers/extract-token' import googleapis from './modules/googleapis/googleapis.module' import microsoftgraph from './modules/microsoftgraph/microsoftgraph.module' +import office365 from './modules/office365/office365.module' import { Route } from './router/interfaces/router.interface' // Register modules @@ -38,9 +39,11 @@ const authMiddleware: (requireAuth: boolean) => express.RequestHandler = (requir export const bootstrap = async () => { const microsoftgraphModule = await microsoftgraph() const googleapisModule = await googleapis() + const office365Module = await office365() const modules: Module[] = [ microsoftgraphModule, - googleapisModule + googleapisModule, + office365Module ] const app = express() diff --git a/src/modules/office365/common/interfaces.ts b/src/modules/office365/common/interfaces.ts new file mode 100644 index 0000000..f30e410 --- /dev/null +++ b/src/modules/office365/common/interfaces.ts @@ -0,0 +1,13 @@ +export interface Webhook { + authId: string + address: string + expiration: string + status: string +} + +export interface Subscription { + contentType: string + status: string + webhook: Webhook +} + diff --git a/src/modules/office365/common/schema.ts b/src/modules/office365/common/schema.ts new file mode 100644 index 0000000..c896113 --- /dev/null +++ b/src/modules/office365/common/schema.ts @@ -0,0 +1,15 @@ +import { Schema, TYPES } from '../../../mapper' +import { Subscription, Webhook } from './interfaces' + +export const webhookSchema: Schema = { + authId: TYPES.String, + address: TYPES.String, + expiration: TYPES.String, + status: TYPES.String, +} + +export const subscriptionSchema: Schema = { + contentType: TYPES.String, + status: TYPES.String, + webhook: webhookSchema, +} \ No newline at end of file diff --git a/src/modules/office365/mappers/index.ts b/src/modules/office365/mappers/index.ts new file mode 100644 index 0000000..a3c24a1 --- /dev/null +++ b/src/modules/office365/mappers/index.ts @@ -0,0 +1,3 @@ +export * from './start-subscription.mapper' +export * from './stop-subscription.mapper' +export * from './list-subscriptions.mapper' \ No newline at end of file diff --git a/src/modules/office365/mappers/list-subscriptions.mapper.ts b/src/modules/office365/mappers/list-subscriptions.mapper.ts new file mode 100644 index 0000000..d707722 --- /dev/null +++ b/src/modules/office365/mappers/list-subscriptions.mapper.ts @@ -0,0 +1,5 @@ +import { jsonMapper } from '../../../mapper' +import { subscriptionSchema } from '../common/schema' +import { Subscription } from '../common/interfaces' + +export const listSubscriptionMapper = jsonMapper([subscriptionSchema]) diff --git a/src/modules/office365/mappers/start-subscription.mapper.ts b/src/modules/office365/mappers/start-subscription.mapper.ts new file mode 100644 index 0000000..246148c --- /dev/null +++ b/src/modules/office365/mappers/start-subscription.mapper.ts @@ -0,0 +1,5 @@ +import { jsonMapper } from '../../../mapper' +import { Subscription } from '../common/interfaces' +import { subscriptionSchema } from '../common/schema' + +export const startSubscriptionMapper = jsonMapper(subscriptionSchema) diff --git a/src/modules/office365/mappers/stop-subscription.mapper.ts b/src/modules/office365/mappers/stop-subscription.mapper.ts new file mode 100644 index 0000000..64145cc --- /dev/null +++ b/src/modules/office365/mappers/stop-subscription.mapper.ts @@ -0,0 +1,7 @@ +import { jsonMapper, Schema } from '../../../mapper' + +export interface StopSubscription {} + +const schema: Schema = {} + +export const stopSubscriptionMapper = jsonMapper(schema) diff --git a/src/modules/office365/office365.config.ts b/src/modules/office365/office365.config.ts new file mode 100644 index 0000000..b298b56 --- /dev/null +++ b/src/modules/office365/office365.config.ts @@ -0,0 +1,45 @@ +import {configFactory, toString} from '../../config' + +const o365ApisConfig = { + o365TenantId: toString(), + o365ClientId: toString(), + o365ClientSecret: toString(), + o365RefreshToken: toString() +} + +const config = configFactory(o365ApisConfig, [ + 'o365TenantId', + 'o365ClientId', + 'o365ClientSecret', + 'o365RefreshToken' +]) + +// Hosts +export const hosts = [ + 'manage.office.com' +] + +// Router paths +const subscriptionsStartPath = '/api/v1.0/:tenantId/activity/feed/subscriptions/start' +const subscriptionsStopPath = '/api/v1.0/:tenantId/activity/feed/subscriptions/stop' +const subscriptionsListPath = '/api/v1.0/:tenantId/activity/feed/subscriptions/list' + +export const paths = { + subscriptionsStartPath: subscriptionsStartPath, + subscriptionsStopPath: subscriptionsStopPath, + subscriptionsListPath: subscriptionsListPath +} + +export const tenantId = config.o365TenantId +export const clientId = config.o365ClientId +export const clientSecret = config.o365ClientSecret +export const refreshToken = config.o365RefreshToken + +export default { + tenantId, + clientId, + clientSecret, + refreshToken, + hosts, + paths +} diff --git a/src/modules/office365/office365.module.ts b/src/modules/office365/office365.module.ts new file mode 100644 index 0000000..662c7ec --- /dev/null +++ b/src/modules/office365/office365.module.ts @@ -0,0 +1,148 @@ +import { proxyFactory } from '../../proxy' +import { oauth2Request, TokenHandlerOptions } from '../../oauth2/handlers/oauth2-token.handler' +import { Route } from '../../router/interfaces/router.interface' +import { + startSubscriptionMapper, + stopSubscriptionMapper, + listSubscriptionMapper +} from './mappers' + +import tokenService from '../../token/token.service' + +import config, { + hosts, + paths +} from './office365.config' + +type TokenType = 'application' | 'delegated' +const MICROSOFTGRAPH_DELEGATED_TOKEN_ID = 'microsoftgraph.delegated' +const MICROSOFTGRAPH_APPLICATION_TOKEN_ID = 'microsoftgraph.application' +const TOKEN_HOST = 'login.windows.net' + +interface RefreshTokenConfig { + tokenId: string + tenantId: string + clientId: string + clientSecret?: string + refreshToken?: string +} + +const requestToken = async (config: RefreshTokenConfig) => { + const { tokenId, tenantId, clientId, clientSecret, refreshToken } = config + const TOKEN_URL = `https://${TOKEN_HOST}/${tenantId}/oauth2/v2.0/token` + + const oauth2Options: TokenHandlerOptions = { + url: TOKEN_URL, + clientId, + extra: { + scope: 'https://manage.office.com/.default' + } + } + + if (refreshToken) { + oauth2Options.grantType = 'refresh_token' + oauth2Options.refreshToken = refreshToken + } else { + oauth2Options.clientSecret = clientSecret + } + + const response = await oauth2Request(oauth2Options) + const token = tokenService.update({ + id: tokenId, + ...response.data + }) + + return token +} + +const getAuthorizationFactory = (forceTokenType?: TokenType) => async () => { + const [tenantId, clientId, clientSecret, refreshToken] = await Promise.all([config.tenantId, config.clientId, config.clientSecret, config.refreshToken]) + const hasApplicationTokenCredentials = tenantId && clientId && clientSecret + const hasDelegatedTokenCredentials = tenantId && clientId && refreshToken + + const getTokenType = (): TokenType => { + if (forceTokenType) return forceTokenType + if (hasDelegatedTokenCredentials) return 'delegated' + if (hasApplicationTokenCredentials) return 'application' + } + + const tokenType = getTokenType() + const tokenId = tokenType === 'delegated' + ? MICROSOFTGRAPH_DELEGATED_TOKEN_ID + : MICROSOFTGRAPH_APPLICATION_TOKEN_ID + + let token = tokenService.getById(tokenId) + + const isValid = !!token && tokenService.isValid(token) + + if (!isValid) { + token = await requestToken({ + tokenId, + tenantId, + clientId, + clientSecret, + refreshToken + }) + } + + return `Bearer ${token.token}` +} + +const startSubscriptionRoute: Route = { + hosts, + method: 'post', + path: paths.subscriptionsStartPath, + handler: proxyFactory({ + authorizationFactory: getAuthorizationFactory('application'), + dataMapper: startSubscriptionMapper + }) +} + +const stopSubscriptionRoute: Route = { + hosts, + method: 'post', + path: paths.subscriptionsStopPath, + handler: proxyFactory({ + authorizationFactory: getAuthorizationFactory('application'), + dataMapper: stopSubscriptionMapper + }) +} + +const listSubscriptionsRoute: Route = { + hosts, + method: 'get', + path: paths.subscriptionsListPath, + handler: proxyFactory({ + authorizationFactory: getAuthorizationFactory('application'), + dataMapper: listSubscriptionMapper + }) +} + +export default async () => { + const applicationCredentials = await Promise.all([ + config.tenantId, + config.clientId, + config.clientSecret + ]) + + const delegatedCredentials = await Promise.all([ + config.tenantId, + config.clientId, + config.refreshToken + ]) + + const hasAll = (secrets: string[]) => secrets + .reduce((result, item) => { + return result && Boolean(item) + }, true) + + const enabled = hasAll(applicationCredentials) || hasAll(delegatedCredentials) + return { + enabled, + routes: [ + startSubscriptionRoute, + stopSubscriptionRoute, + listSubscriptionsRoute + ] + } +} diff --git a/src/request/index.ts b/src/request/index.ts index f911bae..5d0c7ae 100644 --- a/src/request/index.ts +++ b/src/request/index.ts @@ -32,7 +32,9 @@ export const request = async (url: string, options: RequestOptions = {}) => { return await new Promise((resolve, reject) => { const { data, headers = {}, method = 'GET' } = options const { protocol, hostname, port, pathname, search } = new URL(url) - + if (data) { + headers['content-length'] = String(data).length + } const path = `${pathname}${search}` const requestOptions: Record = { protocol,