зеркало из
1
0
Форкнуть 0

Corporate: removing unused company-specific bits

This commit is contained in:
Jeff Wilcox 2023-01-17 15:52:04 -08:00
Родитель ecae7fc77d
Коммит 182a8e2c9e
11 изменённых файлов: 391 добавлений и 836 удалений

14
.vscode/launch.json поставляемый
Просмотреть файл

@ -194,20 +194,6 @@
"DEBUG": "redis,restapi,startup,appinsights,cache"
}
},
{
"type": "node",
"request": "launch",
"name": "Job: Manager hygiene (5)",
"program": "${workspaceRoot}/dist/jobs/managers/index.js",
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
"sourceMaps": true,
"console": "integratedTerminal",
"env": {
"NODE_ENV": "development",
"DEBUG": "redis,restapi,startup,appinsights"
}
},
{
"type": "node",
"request": "launch",

Просмотреть файл

@ -57,7 +57,6 @@ export interface IProviders {
basedir?: string;
campaignStateProvider?: ICampaignHelper;
campaign?: any; // campaign redirection route, poor variable name
corporateContactProvider?: ICorporateContactProvider;
config?: SiteConfiguration;
customizedNewRepositoryLogic?: ICustomizedNewRepositoryLogic;
customizedTeamPermissionsWebhookLogic?: ICustomizedTeamPermissionsWebhookLogic;

Просмотреть файл

@ -1,11 +0,0 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import Job from './task';
import app from '../../app';
app.runJob(Job, {
insightsPrefix: 'JobRefreshManagers',
});

Просмотреть файл

@ -1,263 +0,0 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
// A simple job to cache the last-known manager e-mail address for linked users
// in Redis, using this app's abstracted APIs to be slightly more generic.
import throat from 'throat';
import { ICachedEmployeeInformation, ICorporateLink, IReposJob, IReposJobResult } from '../../interfaces';
import { createAndInitializeLinkProviderInstance } from '../../lib/linkProviders';
import { sleep } from '../../utils';
import { IMicrosoftIdentityServiceBasics } from '../../lib/corporateContactProvider';
import { RedisPrefixManagerInfoCache } from '../../business';
export default async function refresh({ providers }: IReposJob): Promise<IReposJobResult> {
const { config } = providers;
if (config?.jobs?.refreshWrites !== true) {
console.log('job is currently disabled to avoid metadata refresh/rewrites');
return;
}
const graphProvider = providers.graphProvider;
const cacheHelper = providers.cacheProvider;
const insights = providers.insights;
const linkProvider = await createAndInitializeLinkProviderInstance(providers, config);
console.log('reading all links to gather manager info ahead of any terminations');
const allLinks = await linkProvider.getAll();
console.log(`READ: ${allLinks.length} links`);
insights.trackEvent({
name: 'JobRefreshManagersReadLinks',
properties: { links: String(allLinks.length) },
});
let errors = 0;
let notFoundErrors = 0;
const errorList = [];
let managerUpdates = 0;
let managerSets = 0;
let managerMetadataUpdates = 0;
const userDetailsThroatCount = 1;
const secondsDelayAfterError = 1;
const secondsDelayAfterSuccess = 0.09; //0.1;
const managerInfoCachePeriodMinutes = 60 * 24 * 7 * 12; // 12 weeks
let processed = 0;
const bulkContacts = new Map<string, IMicrosoftIdentityServiceBasics | boolean>();
const throttle = throat(userDetailsThroatCount);
const unknownServiceAccounts: ICorporateLink[] = [];
const formerAccounts: ICorporateLink[] = [];
await Promise.all(
allLinks.map((link: ICorporateLink) =>
throttle(async () => {
const employeeDirectoryId = link.corporateId;
++processed;
bulkContacts.set(link.corporateUsername, false);
if (processed % 25 === 0) {
console.log(`${processed}/${allLinks.length}.`);
}
if (link.isServiceAccount) {
console.log(`Service account: ${link.corporateUsername}`);
}
let info = null,
infoError = null;
try {
info = await graphProvider.getUserAndManagerById(employeeDirectoryId);
if (link.isServiceAccount) {
console.log();
// console.dir(info);
console.log(`info OK for SA ${link.corporateUsername}`);
}
} catch (retrievalError) {
if (link.isServiceAccount) {
// console.dir(retrievalError);
console.log(`no info for SA: ${link.corporateUsername}`);
unknownServiceAccounts.push(link);
} else {
console.log();
console.log(`Not present: ${link.corporateUsername} ${retrievalError}`);
infoError = retrievalError;
}
infoError = retrievalError;
}
if (
providers.corporateContactProvider &&
((info && info.userPrincipalName) || link.corporateUsername)
) {
try {
const userPrincipalName =
info && info.userPrincipalName ? info.userPrincipalName : link.corporateUsername;
const contactsCache = await providers.corporateContactProvider.lookupContacts(userPrincipalName);
if (contactsCache || (!contactsCache && link.isServiceAccount)) {
bulkContacts.set(userPrincipalName, contactsCache);
}
} catch (identityServiceError) {
// Bulk cache is a secondary function of this job
console.warn(identityServiceError);
}
}
if (link.isServiceAccount) {
console.log(`skipping service account link ${link.corporateUsername}`);
console.log();
return;
}
try {
if (infoError) {
throw infoError;
}
if (!info || !info.manager) {
console.log(
`No manager info is set for ${employeeDirectoryId} - ${info.displayName} ${info.userPrincipalName}`
);
return; // no sleep
}
// Has the user's corporate display information changed?
let linkChanges = false;
if (info.displayName !== link.corporateDisplayName) {
linkChanges = true;
console.log(
`Update to corporate link: display name changed from ${link.corporateDisplayName} to ${info.displayName}`
);
link.corporateDisplayName = info.displayName;
}
if (info.userPrincipalName !== link.corporateUsername) {
linkChanges = true;
console.log(
`Update to corporate link: username changed from ${link.corporateUsername} to ${info.userPrincipalName}`
);
link.corporateUsername = info.userPrincipalName;
}
if (linkChanges) {
await linkProvider.updateLink(link);
console.log(`Updated link for ${link.corporateId}`);
}
if (!info.manager.mail) {
console.log('No manager mail address');
throw new Error('No manager mail address in graph');
}
const reducedWithManagerInfo: ICachedEmployeeInformation = {
id: info.id,
displayName: info.displayName,
userPrincipalName: info.userPrincipalName,
managerId: info.manager.id,
managerDisplayName: info.manager.displayName,
managerMail: info.manager.mail,
};
const key = `${RedisPrefixManagerInfoCache}${employeeDirectoryId}`;
const currentManagerIfAny = (await cacheHelper.getObjectCompressed(key)) as any;
if (!currentManagerIfAny) {
await cacheHelper.setObjectCompressedWithExpire(
key,
reducedWithManagerInfo,
managerInfoCachePeriodMinutes
);
++managerSets;
console.log(
`Manager for ${reducedWithManagerInfo.displayName} set to ${reducedWithManagerInfo.managerDisplayName}`
);
} else {
let updateEntry = false;
if (currentManagerIfAny.managerId !== reducedWithManagerInfo.managerId) {
updateEntry = true;
++managerUpdates;
console.log(
`Manager for ${reducedWithManagerInfo.displayName} updated to ${reducedWithManagerInfo.managerDisplayName}`
);
} else if (
currentManagerIfAny.id !== reducedWithManagerInfo.id ||
currentManagerIfAny.displayName !== reducedWithManagerInfo.displayName ||
currentManagerIfAny.userPrincipalName !== reducedWithManagerInfo.userPrincipalName ||
currentManagerIfAny.managerDisplayName !== reducedWithManagerInfo.managerDisplayName ||
currentManagerIfAny.managerMail !== reducedWithManagerInfo.managerMail
) {
updateEntry = true;
++managerMetadataUpdates;
console.log(`Metadata for ${reducedWithManagerInfo.displayName} updated`);
}
if (updateEntry) {
await cacheHelper.setObjectCompressedWithExpire(
key,
reducedWithManagerInfo,
managerInfoCachePeriodMinutes
);
}
}
} catch (retrievalError) {
if (retrievalError && retrievalError.status && retrievalError.status === 404) {
++notFoundErrors;
formerAccounts.push(link);
// Not deleting links so proactively: await linkProvider.deleteLink(link);
insights.trackEvent({
name: 'JobRefreshManagersNotFound',
properties: { error: retrievalError.message },
});
} else {
console.dir(retrievalError);
++errors;
insights.trackEvent({
name: 'JobRefreshManagersError',
properties: { error: retrievalError.message },
});
}
await sleep(secondsDelayAfterError * 1000);
return;
}
await sleep(secondsDelayAfterSuccess * 1000);
})
)
);
console.log('All done with', errors, 'errors. Not found errors:', notFoundErrors);
console.dir(errorList);
console.log();
console.log(`Service Accounts not in the directory: ${unknownServiceAccounts.length}`);
console.log(
unknownServiceAccounts
.map((x) => x.corporateUsername)
.sort()
.join('\n')
);
console.log();
console.log(`Former accounts not in the directory: ${formerAccounts.length}`);
console.log(
formerAccounts
.map((x) => x.corporateUsername)
.sort()
.join('\n')
);
console.log();
if (bulkContacts.size) {
console.log(`Writing ${bulkContacts.size} contacts to bulk cache...`);
try {
await providers.corporateContactProvider.setBulkCachedContacts(bulkContacts);
console.log('Cached.');
} catch (cacheError) {
console.log('Cache problem:');
console.warn(cacheError);
}
}
console.log(`Manager updates: ${managerUpdates}`);
console.log(`Manager sets: ${managerSets}`);
console.log(`Other updates: ${managerMetadataUpdates}`);
console.log();
return {
successProperties: {
managerUpdates,
managerSets,
managerMetadataUpdates,
errors,
},
};
}

Просмотреть файл

@ -3,16 +3,6 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import axios, { AxiosError } from 'axios';
import { CreateError } from '../transitional';
import { ICacheHelper } from './caching';
const DefaultCacheMinutesPerContact = 120;
const BulkCacheMinutes = 60 * 24 * 14;
const BulkCacheKey = 'cc:bulk';
export interface ICorporateContactInformation {
openSourceContact?: string;
primaryLegalContact?: string;
@ -31,154 +21,3 @@ export interface ICorporateContactProvider {
getBulkCachedContacts(): Promise<Map<string, ICorporateContactInformation | boolean>>;
setBulkCachedContacts(map: Map<string, ICorporateContactInformation | boolean>): Promise<void>;
}
export default function createCorporateContactProviderInstance(
config,
cacheHelper: ICacheHelper
): ICorporateContactProvider {
return new MicrosoftIdentityService(config, cacheHelper);
}
export interface IMicrosoftIdentityServiceBasics {
aadId?: string;
alias?: string;
costCenterCode?: string;
emailAddress?: string;
functionHierarchyExecCode?: string;
manager?: string;
preferredName?: string;
userPrincipalName?: string;
}
interface IMicrosoftIdentityServiceResponse extends IMicrosoftIdentityServiceBasics {
attorney?: string;
group?: string;
highRiskBusiness?: string;
immediate?: boolean;
legal?: string;
legalOssContact?: string;
legalPrimaryContact?: string;
legalSecondaryContact?: string;
lowRiskBusiness?: string;
maintainer?: string;
structure?: IMicrosoftIdentityServiceBasics[];
system?: string;
}
class MicrosoftIdentityService implements ICorporateContactProvider {
#identityConfig: any;
#cacheHelper: ICacheHelper;
constructor(config: any, cacheHelper: ICacheHelper) {
this.#identityConfig = config.identity;
this.#cacheHelper = cacheHelper;
}
async lookupContacts(corporateUsername: string): Promise<ICorporateContactInformation> {
let response: IMicrosoftIdentityServiceResponse;
const cacheKey = `cc:${corporateUsername}`;
if (this.#cacheHelper) {
try {
response = await this.#cacheHelper.getObject(cacheKey);
} catch (ignoreError) {
/* ignored */
}
}
if (!response || !Object.keys(response).length) {
response = await this.callIdentityService(corporateUsername);
if (this.#cacheHelper && response) {
// kicks off an async operation
this.#cacheHelper.setObjectWithExpire(cacheKey, response, DefaultCacheMinutesPerContact);
}
}
if (!response) {
return null;
}
let managerUsername = null,
managerDisplayName = null;
const manager = response.structure && response.structure.length ? response.structure[0] : null;
if (manager) {
managerDisplayName = manager.preferredName;
managerUsername = manager.userPrincipalName;
}
return {
openSourceContact: response.legalOssContact,
primaryLegalContact: response.legalPrimaryContact,
secondaryLegalContact: response.legalSecondaryContact,
highRiskBusinessReviewer: response.highRiskBusiness,
lowRiskBusinessReviewer: response.lowRiskBusiness,
alias: response.alias,
emailAddress: response.emailAddress,
managerUsername,
managerDisplayName,
legal: response.legal,
};
}
async getBulkCachedContacts(): Promise<Map<string, ICorporateContactInformation | boolean>> {
let map = new Map<string, IMicrosoftIdentityServiceResponse | boolean>();
if (!this.#cacheHelper) {
return map;
}
const bulk = await this.#cacheHelper.getObject(BulkCacheKey);
if (bulk && bulk.entities) {
if (Array.isArray(bulk.entities)) {
map = new Map<string, IMicrosoftIdentityServiceResponse>(bulk.entities);
if (bulk.empties) {
for (let i = 0; i < bulk.empties.length; i++) {
map.set(bulk.empties[i], false);
}
}
} else {
console.warn(`Cached bulk entry ${BulkCacheKey} does not contain an array of entities`);
}
}
return map;
}
async setBulkCachedContacts(map: Map<string, ICorporateContactInformation | boolean>): Promise<void> {
if (!this.#cacheHelper) {
return;
}
const all = Array.from(map.entries());
const entities = all.filter((e) => typeof e[1] !== 'boolean');
const empties = all
.filter((e) => typeof e[1] === 'boolean')
.map((e) => e[0])
.filter((e) => e);
const obj = { entities, empties };
await this.#cacheHelper.setObjectCompressedWithExpire(BulkCacheKey, obj, BulkCacheMinutes);
}
private getIdentityServiceRequestOptions(endpoint: string) {
const url = this.#identityConfig.url + endpoint;
const authToken = 'Basic ' + Buffer.from(this.#identityConfig.pat + ':', 'utf8').toString('base64');
const headers = {
Authorization: authToken,
};
return { url, headers };
}
async callIdentityService(corporateUsername: string): Promise<IMicrosoftIdentityServiceResponse> {
try {
const response = await axios(this.getIdentityServiceRequestOptions(`/${corporateUsername}`));
if ((response.data as any).error?.message) {
// axios returns unknown now
throw CreateError.InvalidParameters((response.data as any).error.message);
}
const entity = response.data as IMicrosoftIdentityServiceResponse;
return entity;
} catch (error) {
const axiosError = error as AxiosError;
if (axiosError?.response?.status === 404) {
return null;
} else if (axiosError?.response?.status >= 300) {
throw CreateError.CreateStatusCodeError(
axiosError.response.status,
`Response code: ${axiosError.response.status}`
);
}
throw error;
}
}
}

Просмотреть файл

@ -7,10 +7,6 @@ import { IProviders } from '../../interfaces';
import { IMailAddressProvider } from '.';
export default function createMailAddressProvider(options): IMailAddressProvider {
const config = options.config;
if (!config.identity || !config.identity.url || !config.identity.pat) {
throw new Error('Not configured for the Identity service');
}
const providers = options.providers as IProviders;
if (!providers) {
throw new Error(

Просмотреть файл

@ -61,7 +61,6 @@ import CosmosCache from '../lib/caching/cosmosdb';
import BlobCache from '../lib/caching/blob';
import { StatefulCampaignProvider } from '../lib/campaigns';
import CosmosHelper from '../lib/cosmosHelper';
import createCorporateContactProviderInstance from '../lib/corporateContactProvider';
import { IQueueProcessor } from '../lib/queues';
import ServiceBusQueueProcessor from '../lib/queues/servicebus';
import AzureQueuesProcessor from '../lib/queues/azurequeue';
@ -266,11 +265,6 @@ async function initializeAsync(
});
await providers.diagnosticsDrop.initialize();
}
providers.corporateContactProvider = createCorporateContactProviderInstance(
config,
providers.cacheProvider
);
providers.corporateAdministrationProfile = getCompanySpecificDeployment()?.administrationSection;
providers.corporateViews = await initializeCorporateViews(providers, rootdir);

713
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -77,7 +77,7 @@
"app-root-path": "3.1.0",
"applicationinsights": "2.4.0",
"async-prompt": "1.0.1",
"axios": "1.2.2",
"axios": "1.2.3",
"basic-auth": "2.0.1",
"body-parser": "1.20.1",
"color-contrast-checker": "2.1.0",
@ -150,9 +150,9 @@
"@types/simple-oauth2": "4.1.1",
"@types/validator": "13.7.10",
"@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.48.1",
"cspell": "6.18.1",
"eslint": "8.31.0",
"@typescript-eslint/parser": "5.48.2",
"cspell": "6.19.1",
"eslint": "8.32.0",
"eslint-config-prettier": "8.6.0",
"eslint-plugin-n": "15.6.1",
"eslint-plugin-prettier": "4.2.1",
@ -161,7 +161,7 @@
"jest-junit": "15.0.0",
"lint-staged": "13.1.0",
"markdownlint-cli2": "0.6.0",
"prettier": "2.8.2",
"prettier": "2.8.3",
"ts-jest": "29.0.5",
"ts-node": "10.9.1",
"typescript": "4.9.4"

Просмотреть файл

@ -7,8 +7,6 @@ import { Router } from 'express';
import asyncHandler from 'express-async-handler';
const router: Router = Router();
import { getProviders } from '../../transitional';
import approvalsRoute from './approvals';
import authorizationsRoute from './authorizations';
import personalAccessTokensRoute from './personalAccessTokens';
@ -23,23 +21,11 @@ router.use(asyncHandler(AddLinkToRequest));
router.get(
'/',
asyncHandler(async (req: ReposAppRequest, res) => {
const providers = getProviders(req);
const link = req.individualContext.link;
let legalContactInformation = null;
try {
if (providers.corporateContactProvider) {
legalContactInformation = await providers.corporateContactProvider.lookupContacts(
link.corporateUsername
);
}
} catch (ignoredError) {
/* ignored */
}
req.individualContext.webContext.render({
view: 'settings',
title: 'Settings',
state: {
legalContactInformation,
link,
},
});

Просмотреть файл

@ -40,33 +40,3 @@ block content
p
a.btn.btn-muted.btn-sm(href='/link/remove') Remove link
h1 Business profile
if !legalContactInformation
p No corporate profile information is available.
if legalContactInformation && legalContactInformation.legal
h4 Legal contact
p For legal advice you will want to refer to your team's primary legal contact:
p: strong: a(href='mailto:' + legalContactInformation.legal)= legalContactInformation.legal
if legalContactInformation.attorney && legalContactInformation.attorney !== legalContactInformation.legal
h4 Attorney
p: strong: a(href='mailto:' + legalContactInformation.attorney)= legalContactInformation.attorney
if legalContactInformation.highRiskBusiness || legalContactInformation.lowRiskBusiness
h4 Open source approval business reviewers
p.
Requests to release open source or to use certain components may
be subject to a business review. The reviewer(s) associated with
your corporate identity are:
if legalContactInformation.group
h5 Group
p= legalContactInformation.group
if legalContactInformation.lowRiskBusiness
h5 Reviewer
p= legalContactInformation.lowRiskBusiness
if legalContactInformation.highRiskBusiness
h5 High-risk reviewer
p= legalContactInformation.highRiskBusiness