Portal sudo SG support
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:
Родитель
eebd695acd
Коммит
023113a2fa
|
@ -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';
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче