зеркало из
1
0
Форкнуть 0
opensource-management-portal/routes/link.ts

324 строки
11 KiB
TypeScript

//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
const router: Router = Router();
import {
ReposAppRequest,
IAppSession,
SupportedLinkType,
ICorporateLink,
LinkOperationSource,
} from '../interfaces';
import { getProviders, splitSemiColonCommas } from '../lib/transitional';
import { IndividualContext } from '../business/user';
import { isCodespacesAuthenticating, storeOriginalUrlAsReferrer, wrapError } from '../lib/utils';
import validator from 'validator';
import unlinkRoute from './unlink';
import { jsonError } from '../middleware';
interface IRequestWithSession extends ReposAppRequest {
session: IAppSession;
}
interface IRequestHacked extends ReposAppRequest {
overrideLinkUserPrincipalName?: any;
}
router.use((req: IRequestHacked, res: Response, next: NextFunction) => {
const config = getProviders(req).config;
if (
config &&
config.github &&
config.github.links &&
config.github.links.provider &&
config.github.links.provider.linkingOfflineMessage
) {
return next(
new Error(`Linking is temporarily offline: ${config.github.links.provider.linkingOfflineMessage}`)
);
} else {
return next();
}
});
router.use(
'/',
asyncHandler(async function (req: ReposAppRequest, res: Response, next: NextFunction) {
// Make sure both account types are authenticated before showing the link pg [wi 12690]
const individualContext = req.individualContext;
if (!individualContext.corporateIdentity || !individualContext.getGitHubIdentity()) {
req.insights.trackEvent({ name: 'PortalSessionNeedsBothGitHubAndAadUsernames' });
return res.redirect('/?signin');
}
return next();
})
);
// TODO: graph provider non-guest check should be middleware and in the link business process
router.use(
asyncHandler(async (req: IRequestHacked, res: Response, next: NextFunction) => {
const individualContext = req.individualContext as IndividualContext;
const providers = getProviders(req);
const insights = providers.insights;
const config = providers.config;
let validateAndBlockGuests = false;
if (config && config.activeDirectory && config.activeDirectory.blockGuestUserTypes) {
validateAndBlockGuests = true;
}
// If the app has not been configured to check whether a user is a guest before linking, continue:
if (!validateAndBlockGuests) {
return next();
}
const aadId = individualContext.corporateIdentity.id;
// If the app is configured to check guest status, do this now, before linking:
const graphProvider = providers.graphProvider;
// REFACTOR: delegate the decision to the auth provider
if (!graphProvider || !graphProvider.getUserById) {
return next(
new Error(
'User type validation cannot be performed because there is no graphProvider configured for this type of account'
)
);
}
insights.trackEvent({
name: 'LinkValidateNotGuestStart',
properties: {
aadId: aadId,
},
});
try {
const details = await graphProvider.getUserById(aadId);
const userType = details.userType;
const displayName = details.displayName;
const userPrincipalName = details.userPrincipalName;
let block = (userType as string) === 'Guest';
let blockedRecord = block ? 'BLOCKED' : 'not blocked';
// If the app is configured to check for guests, but this is a specifically permitted guest user, continue:
if (block && config?.activeDirectoryGuests) {
const authorizedGuests = Array.isArray(config.activeDirectoryGuests)
? (config.activeDirectoryGuests as string[])
: splitSemiColonCommas(config.activeDirectoryGuests?.authorizedIds);
if (!authorizedGuests.includes(aadId)) {
block = false;
blockedRecord = 'specifically authorized user ' + aadId + ' ' + userPrincipalName;
req.overrideLinkUserPrincipalName = userPrincipalName;
return next(
new Error(
'This feature is not currently available. Please reach out to support to re-enable this feature.'
)
);
}
}
insights.trackEvent({
name: 'LinkValidateNotGuestGraphSuccess',
properties: {
aadId: aadId,
userType: userType,
displayName: displayName,
userPrincipalName: userPrincipalName,
blocked: blockedRecord,
},
});
if (block) {
insights.trackMetric({ name: 'LinksBlockedForGuests', value: 1 });
return next(
new Error(
`This system is not available to guests. You are currently signed in as ${displayName} ${userPrincipalName}. Please sign out or try a private browser window.`
)
);
}
const manager = await providers.graphProvider.getManagerById(aadId);
if (!manager || !manager.userPrincipalName) {
throw new Error(`You do not have an active manager entry in the directory and so cannot yet link.`);
}
return next();
} catch (graphError) {
insights.trackException({
exception: graphError,
properties: {
aadId: aadId,
name: 'LinkValidateNotGuestGraphFailure',
},
});
return next(graphError);
}
})
);
router.get(
'/',
asyncHandler(async function (req: ReposAppRequest, res: Response, next: NextFunction) {
const { config } = getProviders(req);
const individualContext = req.individualContext;
const link = individualContext.link;
if (!individualContext.corporateIdentity && !individualContext.getGitHubIdentity()) {
req.insights.trackEvent({ name: 'PortalSessionNeedsBothGitHubAndAadUsernames' });
return res.redirect('/?signin');
}
if (!individualContext.getGitHubIdentity()) {
req.insights.trackEvent({ name: 'PortalSessionNeedsGitHubUsername' });
const signinPath = isCodespacesAuthenticating(config, 'github') ? 'sign-in' : 'signin';
return res.redirect(`/${signinPath}/github/`);
}
if (!link) {
return await showLinkPage(req, res);
} else {
req.insights.trackEvent({ name: 'LinkRouteLinkLocated' });
let organizations = null;
try {
organizations = await individualContext.aggregations.organizations();
} catch (ignoredError) {
/* ignore */
}
return individualContext.webContext.render({
view: 'linkConfirmed',
title: "You're already linked",
state: {
organizations,
},
});
}
})
);
async function showLinkPage(req: ReposAppRequest, res) {
const individualContext = req.individualContext as IndividualContext;
function render(options) {
individualContext.webContext.render({
view: 'link',
title: 'Link GitHub with corporate identity',
optionalObject: options || {},
});
}
const { config, graphProvider } = getProviders(req);
if (config.authentication.scheme !== 'aad' || !graphProvider) {
return render(null);
}
const aadId = individualContext.corporateIdentity.id;
const { operations } = getProviders(req);
// By design, we want to log the errors but do not want any individual
// lookup problem to break the underlying experience of letting a user
// link. This is important if someone is new in the company, they may
// not be in the graph fully yet.
const userLinkData = await operations.validateCorporateAccountCanLink(aadId);
render({
graphUser: userLinkData.graphEntry,
isServiceAccountCandidate: userLinkData.type === SupportedLinkType.ServiceAccount,
});
}
router.get('/enableMultipleAccounts', function (req: IRequestWithSession, res) {
// LEGACY
// TODO: is this code still ever really used?
if (req.user.github) {
req.session.enableMultipleAccounts = true;
return res.redirect('/link/cleanup');
}
req.insights.trackEvent({ name: 'PortalUserEnabledMultipleAccounts' });
storeOriginalUrlAsReferrer(
req,
res,
'/auth/github',
'multiple accounts enabled need to auth with GitHub again now'
);
});
router.post(
'/',
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const individualContext = req.individualContext as IndividualContext;
try {
await interactiveLinkUser(false, individualContext, req, res, next);
} catch (error) {
return next(error);
}
})
);
export async function interactiveLinkUser(
isJson: boolean,
individualContext: IndividualContext,
req,
res,
next
) {
const isServiceAccount = req.body.sa === '1';
const serviceAccountMail = req.body.serviceAccountMail;
const { operations } = getProviders(req);
if (isServiceAccount && !validator.isEmail(serviceAccountMail)) {
const errorMessage = 'Please enter a valid e-mail address for the Service Account maintainer.';
return next(isJson ? jsonError(errorMessage, 400) : wrapError(null, errorMessage, true));
}
let newLinkObject: ICorporateLink = null;
try {
newLinkObject = individualContext.createGitHubLinkObject();
} catch (missingInformationError) {
return next(missingInformationError);
}
if (isServiceAccount) {
newLinkObject.isServiceAccount = true;
newLinkObject.serviceAccountMail = serviceAccountMail;
const address = operations.getOperationsMailAddress();
const errorMessage = `Service Account linking is not available. Please reach out to ${address} for more information.`;
return next(isJson ? jsonError(errorMessage, 400) : new Error(errorMessage));
}
try {
await operations.linkAccounts({
link: newLinkObject,
operationSource: LinkOperationSource.Portal,
correlationId: individualContext.webContext?.correlationId || 'N/A',
skipGitHubValidation: true, // already has been verified in the recent session
});
if (isJson) {
res.status(201);
return res.end();
} else {
return res.redirect('/?onboarding=yes');
}
} catch (createError) {
const errorMessage = `We had trouble linking your corporate and GitHub accounts: ${createError.message}`;
return next(isJson ? jsonError(errorMessage, 500) : wrapError(createError, errorMessage));
}
}
router.use('/remove', unlinkRoute);
router.get('/reconnect', function (req: ReposAppRequest, res: Response, next: NextFunction) {
const config = getProviders(req).config;
if (config.authentication.scheme !== 'aad') {
return next(
wrapError(
null,
'Account reconnection is only needed for Active Directory authentication applications.',
true
)
);
}
// If the request comes back to the reconnect page, the authenticated app will
// actually update the link the next time around.
const ghi = req.individualContext.getGitHubIdentity();
const hasToken = !!req.individualContext.webContext.tokens.gitHubReadToken;
if (ghi && ghi.id && ghi.username && hasToken) {
req.insights.trackEvent({ name: 'PortalUserReconnected' });
return res.redirect('/');
}
req.insights.trackEvent({ name: 'PortalUserReconnectNeeded' });
req.individualContext.webContext.render({
view: 'reconnectGitHub',
title: 'Please sign in with GitHub',
state: {
expectedUsername: ghi.username,
},
});
});
export default router;