зеркало из
1
0
Форкнуть 0
Supports using security groups for portal sudo permissions
instead of the primary GitHub org's sudo configuration, if
present.

Breaking feature change:
This feature is not enabled by default and must use a feature flag
to opt-in.

The feature flag is "FEATURE_FLAG_ALLOW_PORTAL_SUDO".

The standard behavior, when the flag is enabled, is still to use
the first org. Alternatively, configuration and a SG ID can be provided.
This commit is contained in:
Jeff Wilcox 2021-03-17 16:18:44 -07:00
Родитель eebd695acd
Коммит 023113a2fa
9 изменённых файлов: 216 добавлений и 51 удалений

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

@ -33,6 +33,7 @@ import { ILinkProvider } from '../lib/linkProviders';
import { getUserAndManagerById, IGraphEntryWithManager } from '../lib/graphProvider';
import { ICacheHelper } from '../lib/caching';
import getCompanySpecificDeployment from '../middleware/companySpecificDeployment';
import { createPortalSudoInstance, IPortalSudo } from '../features';
const throwIfOrganizationIdsMissing = true;
@ -195,6 +196,7 @@ export class Operations {
private _initialized: Date;
private _dynamicOrganizationSettings: OrganizationSetting[];
private _dynamicOrganizationIds: Set<number>;
private _portalSudo: IPortalSudo;
get initialized(): Date {
return this._initialized;
@ -316,6 +318,7 @@ export class Operations {
if (throwIfOrganizationIdsMissing) {
this.getOrganizationIds();
}
this._portalSudo = createPortalSudoInstance(this._providers);
return this;
}
@ -1400,6 +1403,13 @@ export class Operations {
return this._config.github && this._config.github.systemAccounts ? this._config.github.systemAccounts.logins : [];
}
isPortalSudoer(githubLogin: string, link: ICorporateLink) {
if (!this._initialized) {
throw new Error('The application is not yet initialized');
}
return this._portalSudo.isSudoer(githubLogin, link);
}
isSystemAccountByUsername(username: string): boolean {
const lc = username.toLowerCase();
const usernames = this.systemAccountsByUsername;

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

@ -5,6 +5,7 @@
"allowUnauthorizedTransferLockdownSystem": "env://FEATURE_FLAG_ALLOW_UNAUTHORIZED_TRANSFER_LOCKDOWN_SYSTEM?trueIf=1",
"allowUndoSystem": "env://FEATURE_FLAG_ALLOW_UNDO_SYSTEM?trueIf=1",
"allowOrganizationSudo": "env://FEATURE_FLAG_ALLOW_ORG_SUDO?trueIf=1&default=1",
"allowPortalSudo": "env://FEATURE_FLAG_ALLOW_PORTAL_SUDO?trueIf=1",
"allowAdministratorManualLinking": "env://FEATURE_FLAG_ALLOW_ADMIN_MANUAL_LINKING?trueIf=1",
"allowFossFundElections": "env://FEATURE_FLAG_FOSS_FUND_ELECTIONS?trueIf=1"
}

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

@ -1,4 +0,0 @@
{
"portalSudoOff": "env://DEBUG_GITHUB_PORTAL_SUDO_OFF?trueIf=1",
"portalSudoForce": "env://DEBUG_GITHUB_PORTAL_SUDO_FORCE?trueIf=1"
}

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

@ -3,5 +3,14 @@
"off": "env://DEBUG_GITHUB_ORG_SUDO_OFF?trueIf=1",
"defaultProviderName": "env://SUDO_ORGANIZATION_PROVIDER_DEFAULT_NAME?default=githubteams",
"allowUniqueProvidersByOrganization": "env://SUDO_ORGANIZATION_PROVIDER_ALLOW_UNIQUE?trueIf=1"
},
"portal": {
"off": "env://DEBUG_GITHUB_PORTAL_SUDO_OFF?trueIf=1",
"force": "env://DEBUG_GITHUB_PORTAL_SUDO_FORCE?trueIf=1",
"providerName": "env://SUDO_PORTAL_PROVIDER_NAME?default=primaryorg",
"securityGroup": {
"id": "env://SUDO_PORTAL_SECURITY_GROUP_ID"
}
}
}

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

@ -11,6 +11,10 @@ export interface IOrganizationSudo {
isSudoer(githubLogin: string, link?: ICorporateLink): Promise<boolean>;
}
export interface IPortalSudo {
isSudoer(githubLogin: string, link?: ICorporateLink): Promise<boolean>;
}
export abstract class OrganizationSudo implements IOrganizationSudo {
constructor(protected providers: IProviders, protected organization: Organization) {}
abstract isSudoer(githubLogin: string, link?: ICorporateLink): Promise<boolean>;
@ -28,6 +32,8 @@ export abstract class OrganizationSudo implements IOrganizationSudo {
export { OrganizationFeatureSecurityGroupProperty } from './securityGroup';
export * from './portal';
import { OrganizationSudoNoop } from './noop';
import { OrganizationSudoSecurityGroup } from './securityGroup';
import { OrganizationSudoGitHubTeams } from './teams';

148
features/sudo/portal.ts Normal file
Просмотреть файл

@ -0,0 +1,148 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { IPortalSudo } from '.';
import { ICorporateLink, Organization } from '../../business';
import getCompanySpecificDeployment from '../../middleware/companySpecificDeployment';
import { ErrorHelper, IProviders } from '../../transitional';
abstract class PortalSudoBase {
constructor(private providers: IProviders) { }
protected isOff() {
const config = this.providers.config;
if (config?.sudo?.portal?.off) {
console.warn('DEBUG WARNING: Portal sudo support is turned off in the current environment');
return true;
}
return false;
}
protected forceAlways() {
const config = this.providers.config;
if (config?.sudo?.portal?.force) {
console.warn('DEBUG WARNING: Portal sudo is turned on for all users in the current environment');
return true;
}
return false;
}
}
class PortalSudoPrimaryOrganization extends PortalSudoBase implements IPortalSudo {
private _org: Organization;
private _providers: IProviders;
constructor(providers: IProviders) {
super(providers);
this._providers = providers;
}
isSudoer(githubLogin: string, link?: ICorporateLink): Promise<boolean> {
if (this.isOff()) {
return Promise.resolve(false);
}
if (this.forceAlways()) {
return Promise.resolve(true);
}
if (this._org === undefined) {
const operations = this._providers.operations;
const primaryOrganizationName = operations.getPrimaryOrganizationName();
this._org = primaryOrganizationName ? operations.getOrganization(primaryOrganizationName) : false as any as Organization;
}
return this._org ? this._org.isSudoer(githubLogin, link) : Promise.resolve(false);
}
}
class PortalSudoSecurityGroup extends PortalSudoBase implements IPortalSudo {
private _providers: IProviders;
private _groupId: string;
constructor(providers: IProviders) {
super(providers);
if (!providers.graphProvider) {
throw new Error('No graph provider instance available');
}
this._providers = providers;
const securityGroupId = providers.config.sudo?.portal?.securityGroup?.id;
if (!securityGroupId) {
throw new Error('No configured security group ID');
}
this._groupId = securityGroupId;
}
async isSudoer(githubLogin: string, link?: ICorporateLink): Promise<boolean> {
if (this.isOff()) {
return false;
}
if (this.forceAlways()) {
return true;
}
if (!link || !link.corporateId) {
return false;
}
const insights = this._providers.insights;
try {
if (await this._providers.graphProvider.isUserInGroup(link.corporateId, this._groupId)) {
insights?.trackEvent({
name: 'PortalSudoAuthorized',
properties: {
corporateId: link.corporateId,
securityGroupId: this._groupId,
},
});
return true;
}
} catch (error) {
if (ErrorHelper.IsNotFound(error)) { // security groups do get deleted and should not bring down any system in that case
return false;
}
console.warn(error);
insights?.trackException({
exception: error,
properties: {
eventName: 'PortalSudoSecurityGroupError',
className: 'PortalSudoSecurityGroup',
callName: 'isUserInGroup',
corporateId: link.corporateId,
securityGroupId: this._groupId,
},
});
return false;
}
}
}
export function createPortalSudoInstance(providers: IProviders): IPortalSudo {
const override = getCompanySpecificDeployment();
let instance = override?.features?.portalSudo?.tryCreateInstance(providers);
if (instance) {
return instance;
}
const config = providers.config;
const providerName = config?.sudo?.portal?.providerName;
instance = createProviderInstance(providerName, providers);
return instance;
}
function createProviderInstance(providerName: string, providers: IProviders): IPortalSudo {
switch (providerName) {
case null:
case '':
case 'none': {
return {
isSudoer: () => { return Promise.resolve(false); }
}
}
case 'primaryorg': {
return new PortalSudoPrimaryOrganization(providers);
}
case 'securityGroup':
case 'securitygroup': {
return new PortalSudoSecurityGroup(providers);
}
default:
throw new Error(`PortalSudo: unsupported or unconfigured provider name=${providerName}`);
}
}

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

@ -1,5 +1,5 @@
# sudo / organization sudo
# organization sudo
Organization-level sudo allows users that are not technically organization owners on
GitHub to perform administrative actions that the portal provides, such as managing repos,
@ -38,3 +38,35 @@ approach, or use a different company-internal system for these decisions.
There is an environmental off-switch enabled that can turn off sudo, allowing for testing
as a regular user in local environments. That env variable name is `DEBUG_GITHUB_ORG_SUDO_OFF`.
# portal sudo
Portal sudo applies sudo for all organizations configured within the application.
Used by system administrators typically.
The original design was to use the sudo configuration from the first/primary GitHub org
that was configured in the environment.
## Feature flag: FEATURE_FLAG_ALLOW_PORTAL_SUDO
> This feature is not on by default.
To opt in to the feature, set the value to `1`.
## Configuration: providerName
Can be:
- `primaryorg` (default): use the sudo configuration from the primary/first-configured org
- `none` or '': no portal-wide sudo
- `securitygroup`: use a security group to determine if a linked user is a portal administrator
For the security group provider, configuration should set `SUDO_PORTAL_SECURITY_GROUP_ID` to the
security group ID to use.
## Debug flags
Two environment variables designed for development work exist:
- `DEBUG_GITHUB_PORTAL_SUDO_OFF`: set to `1` to turn off portal sudo
- `DEBUG_GITHUB_PORTAL_SUDO_FORCE`: set to `1` to turn portal sudo on for ALL users

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

@ -6,7 +6,7 @@
import { Router } from 'express';
import { Organization } from '../business';
import { Repository } from '../business/repository';
import { OrganizationSudo } from '../features/sudo';
import { IOrganizationSudo, IPortalSudo } from '../features/sudo';
import { IContextualRepositoryPermissions } from '../middleware/github/repoPermissions';
import { IDictionary, IProviders } from '../transitional';
import { IndividualContext } from '../user';
@ -19,11 +19,16 @@ export interface IAttachCompanySpecificRoutes {
}
export interface ICompanySpecificFeatureOrganizationSudo {
tryCreateInstance: (providers: IProviders, organization: Organization) => OrganizationSudo;
tryCreateInstance: (providers: IProviders, organization: Organization) => IOrganizationSudo;
}
export interface ICompanySpecificFeaturePortalSudo {
tryCreateInstance: (providers: IProviders) => IPortalSudo;
}
export interface ICompanySpecificFeatures {
organizationSudo?: ICompanySpecificFeatureOrganizationSudo;
portalSudo?: ICompanySpecificFeaturePortalSudo;
}
export interface ICompanySpecificStartupProperties {

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

@ -504,8 +504,8 @@ export class IndividualContext {
async isPortalAdministrator(): Promise<boolean> {
const operations = this._operations;
const ghi = this.getGitHubIdentity().username;
const isAdmin = await legacyCallbackIsPortalAdministrator(operations, ghi);
this._isPortalAdministrator = isAdmin;
const link = this._link;
this._isPortalAdministrator = await operations.isPortalSudoer(ghi, link);
return this._isPortalAdministrator;
}
@ -517,45 +517,3 @@ export class IndividualContext {
return Object.assign({}, this._initialView);
}
}
async function legacyCallbackIsPortalAdministrator(operations: Operations, gitHubUsername: string): Promise<boolean> {
const config = operations.config;
// ----------------------------------------------------------------------------
// SECURITY METHOD:
// Determine whether the authenticated user is an Administrator of the org. At
// this time there is a special "portal sudoers" team that is used. The GitHub
// admin flag is not used [any longer] for performance reasons to reduce REST
// calls to GitHub.
// ----------------------------------------------------------------------------
if (config.github.debug && config.github.debug.portalSudoOff) {
console.warn('DEBUG WARNING: Portal sudo support is turned off in the current environment');
return false;
}
if (config.github.debug && config.github.debug.portalSudoForce) {
console.warn('DEBUG WARNING: Portal sudo is turned on for all users in the current environment');
return true;
}
/*
var self = this;
if (self.entities && self.entities.primaryMembership) {
var pm = self.entities.primaryMembership;
if (pm.role && pm.role === 'admin') {
return callback(null, true);
}
}
*/
const primaryName = operations.getPrimaryOrganizationName();
const primaryOrganization = operations.getOrganization(primaryName);
const sudoTeam = primaryOrganization.systemSudoersTeam;
if (!sudoTeam) {
return false;
}
try {
const isMember = await sudoTeam.isMember(gitHubUsername);
return (isMember === true || isMember === GitHubTeamRole.Member || isMember === GitHubTeamRole.Maintainer);
} catch (error) {
throw wrapError(error, 'We had trouble querying GitHub for important team management information. Please try again later or report this issue.');
}
}