Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "proxy",
"version": "1.3.3",
"version": "1.3.5",
"description": "",
"scripts": {
"build": "tsc -p tsconfig.build.json",
Expand Down
8 changes: 4 additions & 4 deletions src/anonymizer/anonymizer.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
5 changes: 4 additions & 1 deletion src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
13 changes: 13 additions & 0 deletions src/modules/office365/common/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface Webhook {
authId: string
address: string
expiration: string
status: string
}

export interface Subscription {
contentType: string
status: string
webhook: Webhook
}

15 changes: 15 additions & 0 deletions src/modules/office365/common/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Schema, TYPES } from '../../../mapper'
import { Subscription, Webhook } from './interfaces'

export const webhookSchema: Schema<Webhook> = {
authId: TYPES.String,
address: TYPES.String,
expiration: TYPES.String,
status: TYPES.String,
}

export const subscriptionSchema: Schema<Subscription> = {
contentType: TYPES.String,
status: TYPES.String,
webhook: webhookSchema,
}
3 changes: 3 additions & 0 deletions src/modules/office365/mappers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './start-subscription.mapper'
export * from './stop-subscription.mapper'
export * from './list-subscriptions.mapper'
5 changes: 5 additions & 0 deletions src/modules/office365/mappers/list-subscriptions.mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { jsonMapper } from '../../../mapper'
import { subscriptionSchema } from '../common/schema'
import { Subscription } from '../common/interfaces'

export const listSubscriptionMapper = jsonMapper<typeof subscriptionSchema[], Subscription>([subscriptionSchema])
5 changes: 5 additions & 0 deletions src/modules/office365/mappers/start-subscription.mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { jsonMapper } from '../../../mapper'
import { Subscription } from '../common/interfaces'
import { subscriptionSchema } from '../common/schema'

export const startSubscriptionMapper = jsonMapper<typeof subscriptionSchema, Subscription>(subscriptionSchema)
7 changes: 7 additions & 0 deletions src/modules/office365/mappers/stop-subscription.mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { jsonMapper, Schema } from '../../../mapper'

export interface StopSubscription {}

const schema: Schema<StopSubscription> = {}

export const stopSubscriptionMapper = jsonMapper<typeof schema, StopSubscription>(schema)
45 changes: 45 additions & 0 deletions src/modules/office365/office365.config.ts
Original file line number Diff line number Diff line change
@@ -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
}
148 changes: 148 additions & 0 deletions src/modules/office365/office365.module.ts
Original file line number Diff line number Diff line change
@@ -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
]
}
}
4 changes: 3 additions & 1 deletion src/request/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export const request = async (url: string, options: RequestOptions = {}) => {
return await new Promise<Response>((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<string, unknown> = {
protocol,
Expand Down