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

Custom properties, token refactor, open access teams

- Removes CENTRAL_OPERATIONS_TOKEN
- System teams - open access
  - adds open access concept, which is a broad access team anyone in the org can join without approval
  - open access teams are not recommended the same way as broad access teams during new repo setup
- TypeScript: prefer types to interfaces
- GitHub Apps and REST APIs:
  - Simplifying bound function calls
  - Relocated app and token management files
  - Improves types for header/tokens
  - Allow custom app purposes to retrieve app token instances
  - Custom app purpose debug display fix
  - PAT/app token type identification helper method
  - Collections now expose "collectAllPages" and "collectAllPagesViaHttpGet" to move specific code out of the file
  - Fix for custom apps initialized after startup
- Custom Properties Beta support
This commit is contained in:
Jeff Wilcox 2023-11-11 16:24:15 -05:00
Родитель 9bf880975f
Коммит 00eb7c2ddc
44 изменённых файлов: 1168 добавлений и 498 удалений

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

@ -1,4 +1,3 @@
GITHUB_CENTRAL_OPERATIONS_TOKEN=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_CALLBACK_URL=http://localhost:3000/auth/github/callback

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

@ -115,7 +115,6 @@ router.get(
}
const userAggregateContext = activeContext.aggregations;
const maintainedTeams = new Set<string>();
const broadTeams = new Set<number>(req.organization.broadAccessTeams);
const userTeams = userAggregateContext.reduceOrganizationTeams(
organization,
await userAggregateContext.teams()

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

@ -51,6 +51,7 @@ router.get(
const userAggregateContext = req.apiContext.aggregations;
const maintainedTeams = new Set<string>();
const broadTeams = new Set<number>(req.organization.broadAccessTeams);
const openAccessTeams = new Set<number>(req.organization.openAccessTeams);
const userTeams = userAggregateContext.reduceOrganizationTeams(
organization,
await userAggregateContext.teams()
@ -62,6 +63,7 @@ router.get(
const personalizedTeams = Array.from(combinedTeams.values()).map((combinedTeam) => {
return {
broad: broadTeams.has(Number(combinedTeam.id)),
isOpenAccessTeam: openAccessTeams.has(Number(combinedTeam.id)),
description: combinedTeam.description,
id: Number(combinedTeam.id),
name: combinedTeam.name,
@ -86,6 +88,7 @@ router.get(
const queryCache = providers.queryCache;
const organization = req.organization as Organization;
const broadTeams = new Set(organization.broadAccessTeams);
const openAccessTeams = new Set<number>(req.organization.openAccessTeams);
if (req.query.refresh === undefined && queryCache && queryCache.supportsTeams) {
// Use the newer method in this case...
const organizationTeams = await queryCache.organizationTeams(organization.id.toString());
@ -96,6 +99,9 @@ router.get(
if (broadTeams.has(Number(t.id))) {
t['broad'] = true;
}
if (openAccessTeams.has(Number(t.id))) {
t['openAccess'] = true;
}
return t;
}),
}) as unknown as void;

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

@ -10,7 +10,7 @@ import * as common from './common';
import { wrapError } from '../utils';
import { corporateLinkToJson } from './corporateLink';
import { Organization } from './organization';
import { AppPurpose } from './githubApps';
import { AppPurpose } from '../lib/github/appPurposes';
import { ILinkProvider } from '../lib/linkProviders';
import { CacheDefault, getMaxAgeSeconds } from '.';
import {
@ -18,7 +18,7 @@ import {
CoreCapability,
ICacheOptions,
ICorporateLink,
IGetAuthorizationHeader,
GetAuthorizationHeader,
IGitHubAccountDetails,
IOperationsInstance,
IOperationsLinks,
@ -46,7 +46,7 @@ const secondaryAccountProperties = [];
export class Account {
private _operations: IOperationsInstance;
private _getAuthorizationHeader: IGetAuthorizationHeader;
private _getAuthorizationHeader: GetAuthorizationHeader;
private _link: ICorporateLink;
private _id: number;
@ -137,7 +137,7 @@ export class Account {
return this._originalEntity ? this._originalEntity.name : undefined;
}
constructor(entity, operations: IOperationsInstance, getAuthorizationHeader: IGetAuthorizationHeader) {
constructor(entity, operations: IOperationsInstance, getAuthorizationHeader: GetAuthorizationHeader) {
common.assignKnownFieldsPrefixed(
this,
entity,
@ -150,7 +150,7 @@ export class Account {
this._getAuthorizationHeader = getAuthorizationHeader;
}
overrideAuthorization(getAuthorizationHeader: IGetAuthorizationHeader) {
overrideAuthorization(getAuthorizationHeader: GetAuthorizationHeader) {
this._getAuthorizationHeader = getAuthorizationHeader;
}
@ -571,11 +571,8 @@ export class Account {
return { history, error };
}
private authorize(purpose: AppPurpose): IGetAuthorizationHeader | string {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
private authorize(purpose: AppPurpose): GetAuthorizationHeader | string {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(this, purpose) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
}

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

@ -7,7 +7,7 @@ import { OrganizationSetting } from '../entities/organizationSettings/organizati
import {
IOperationsGitHubRestLibrary,
IOperationsDefaultCacheTimes,
IGetAuthorizationHeader,
GetAuthorizationHeader,
IGitHubAppInstallation,
ICacheOptions,
} from '../interfaces';
@ -50,7 +50,7 @@ export default class GitHubApplication {
public id: number,
public slug: string,
public friendlyName: string,
private getAuthorizationHeader: IGetAuthorizationHeader
private getAuthorizationHeader: GetAuthorizationHeader
) {}
static PrimaryInstallationProperties = primaryInstallationProperties;
@ -115,7 +115,7 @@ export default class GitHubApplication {
async getInstallations(options?: ICacheOptions): Promise<IGitHubAppInstallation[]> {
options = options || {};
const operations = this.operations;
const getAuthorizationHeader = this.getAuthorizationHeader.bind(this) as IGetAuthorizationHeader;
const getAuthorizationHeader = this.getAuthorizationHeader.bind(this) as GetAuthorizationHeader;
const github = operations.github;
const caching = {
maxAgeSeconds: options.maxAgeSeconds || operations.defaults.orgRepoDetailsStaleSeconds, // borrowing from another value
@ -132,8 +132,8 @@ export default class GitHubApplication {
return installations;
}
private authorize(): IGetAuthorizationHeader | string {
const getAuthorizationHeader = this.getAuthorizationHeader.bind(this) as IGetAuthorizationHeader;
private authorize(): GetAuthorizationHeader | string {
const getAuthorizationHeader = this.getAuthorizationHeader.bind(this) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
}

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

@ -3,13 +3,13 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { AppPurpose, AppPurposeTypes } from './githubApps';
import { AppPurpose, AppPurposeTypes } from '../lib/github/appPurposes';
import { Organization } from '.';
import {
IOperationsInstance,
IPurposefulGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
throwIfNotGitHubCapable,
IGetAuthorizationHeader,
GetAuthorizationHeader,
} from '../interfaces';
import {
decorateIterable,
@ -44,15 +44,15 @@ export class OrganizationDomains {
private _organization: Organization;
private _operations: IOperationsInstance;
private _getAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _purpose: AppPurpose;
constructor(
organization: Organization,
operations: IOperationsInstance,
getAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader
getAuthorizationHeader: PurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader
) {
this._getAuthorizationHeader = getAuthorizationHeader;
this._getSpecificAuthorizationHeader = getSpecificAuthorizationHeader;
@ -110,19 +110,16 @@ export class OrganizationDomains {
}
}
private authorize(purpose: AppPurpose = this._purpose): IGetAuthorizationHeader {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
private authorize(purpose: AppPurpose = this._purpose): GetAuthorizationHeader {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(this, purpose) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
private authorizeSpecificPurpose(purpose: AppPurposeTypes): IGetAuthorizationHeader | string {
private authorizeSpecificPurpose(purpose: AppPurposeTypes): GetAuthorizationHeader | string {
const getAuthorizationHeader = this._getSpecificAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
}

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

@ -4,8 +4,13 @@
//
import { OrganizationSetting } from '../../entities/organizationSettings/organizationSetting';
import { GitHubAppAuthenticationType, AppPurpose, ICustomAppPurpose, AppPurposeTypes } from '../githubApps';
import { GitHubTokenManager } from '../githubApps/tokenManager';
import {
GitHubAppAuthenticationType,
AppPurpose,
ICustomAppPurpose,
AppPurposeTypes,
} from '../../lib/github/appPurposes';
import { GitHubTokenManager } from '../../lib/github/tokenManager';
import {
IProviders,
ICacheDefaultTimes,
@ -19,9 +24,11 @@ import {
throwIfNotGitHubCapable,
throwIfNotCapable,
IOperationsCentralOperationsToken,
IAuthorizationHeaderValue,
AuthorizationHeaderValue,
SiteConfiguration,
ExecutionEnvironment,
IPagedCacheOptions,
ICacheOptionsWithPurpose,
} from '../../interfaces';
import { RestLibrary } from '../../lib/github';
import { CreateError } from '../../transitional';
@ -32,6 +39,28 @@ import GitHubApplication from '../application';
import Debug from 'debug';
const debugGitHubTokens = Debug('github:tokens');
const symbolCost = Symbol('cost');
const symbolHeaders = Symbol('headers');
export function symbolizeApiResponse<T>(response: any): T {
if (response && response.headers) {
response[symbolHeaders] = response.headers;
delete response.headers;
}
if (response && response.cost) {
response[symbolCost] = response.cost;
delete response.cost;
}
return response;
}
export function getApiSymbolMetadata(response: any) {
if (response) {
return { headers: response[symbolHeaders], cost: response[symbolCost] };
}
throw CreateError.ParameterRequired('response');
}
export interface IOperationsCoreOptions {
github: RestLibrary;
providers: IProviders;
@ -63,6 +92,7 @@ export enum CacheDefault {
teamDetailStaleSeconds = 'teamDetailStaleSeconds',
orgRepoWebhooksStaleSeconds = 'orgRepoWebhooksStaleSeconds',
teamRepositoryPermissionStaleSeconds = 'teamRepositoryPermissionStaleSeconds',
defaultStaleSeconds = 'defaultStaleSeconds',
}
// defaults could move to configuration alternatively
@ -90,6 +120,7 @@ const defaults: ICacheDefaultTimes = {
[CacheDefault.teamDetailStaleSeconds]: 60 * 60 * 2 /* 2h */,
[CacheDefault.orgRepoWebhooksStaleSeconds]: 60 * 60 * 8 /* 8h */,
[CacheDefault.teamRepositoryPermissionStaleSeconds]: 0 /* 0m */,
[CacheDefault.defaultStaleSeconds]: 60 /* 1m */,
};
export const DefaultPageSize = 100;
@ -108,6 +139,46 @@ export function getPageSize(operations: IOperationsInstance, options?: IOptionWi
return DefaultPageSize;
}
export function createCacheOptions(
operations: IOperationsInstance,
options?: ICacheOptions,
cacheDefault: CacheDefault = CacheDefault.defaultStaleSeconds
) {
const cacheOptions: ICacheOptions = {
maxAgeSeconds: getMaxAgeSeconds(operations, cacheDefault, options, 60),
};
if (options.backgroundRefresh !== undefined) {
cacheOptions.backgroundRefresh = options.backgroundRefresh;
}
return cacheOptions;
}
export function createPagedCacheOptions(
operations: IOperationsInstance,
options?: IPagedCacheOptions,
cacheDefault: CacheDefault = CacheDefault.defaultStaleSeconds
) {
const cacheOptions: IPagedCacheOptions = {
maxAgeSeconds: getMaxAgeSeconds(operations, cacheDefault, options, 60),
};
if (options.pageRequestDelay !== undefined) {
cacheOptions.pageRequestDelay = options.pageRequestDelay;
}
if (options.backgroundRefresh !== undefined) {
cacheOptions.backgroundRefresh = options.backgroundRefresh;
}
return cacheOptions;
}
export function popPurpose(options: ICacheOptionsWithPurpose, defaultPurpose: AppPurposeTypes) {
if (options.purpose) {
const purpose = options.purpose;
delete options.purpose;
return purpose;
}
return defaultPurpose;
}
export function getMaxAgeSeconds(
operations: IOperationsInstance,
cacheDefault: CacheDefault,
@ -197,7 +268,7 @@ export abstract class OperationsCore
async getAccountByUsername(username: string, options?: ICacheOptions): Promise<Account> {
options = options || {};
const operations = throwIfNotGitHubCapable(this);
const centralOperations = throwIfNotCapable<IOperationsCentralOperationsToken>(
const ops = throwIfNotCapable<IOperationsCentralOperationsToken>(
this,
CoreCapability.GitHubCentralOperations
);
@ -214,10 +285,9 @@ export abstract class OperationsCore
cacheOptions.backgroundRefresh = options.backgroundRefresh;
}
try {
const getHeaderFunction = centralOperations.getCentralOperationsToken();
const authorizationHeader = await getHeaderFunction(AppPurpose.Data);
const getHeaderFunction = ops.getPublicAuthorizationToken();
const entity = await operations.github.call(
authorizationHeader,
getHeaderFunction,
'users.getByUsername',
parameters,
cacheOptions
@ -337,17 +407,16 @@ export abstract class OperationsCore
organizationName: string,
organizationSettings: OrganizationSetting,
legacyOwnerToken: string,
centralOperationsFallbackToken: string,
appAuthenticationType: GitHubAppAuthenticationType,
purpose: AppPurposeTypes
): Promise<IAuthorizationHeaderValue> {
): Promise<AuthorizationHeaderValue> {
const customPurpose = purpose as ICustomAppPurpose;
const isCustomPurpose = customPurpose?.isCustomAppPurpose === true;
if (
!isCustomPurpose &&
!this.tokenManager.organizationSupportsAnyPurpose(organizationName, organizationSettings)
) {
const legacyTokenValue = legacyOwnerToken || centralOperationsFallbackToken;
const legacyTokenValue = legacyOwnerToken;
if (!legacyTokenValue) {
throw new Error(
`Organization ${organizationName} is not configured with a GitHub app, Personal Access Token ownerToken configuration value, or a fallback central operations token for the ${
@ -358,7 +427,7 @@ export abstract class OperationsCore
return {
value: `token ${legacyTokenValue}`,
purpose: null,
source: legacyOwnerToken ? 'legacyOwnerToken' : 'centralOperationsFallbackToken',
source: 'legacyOwnerToken',
};
}
if (!purpose) {

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

@ -5,11 +5,12 @@
import axios from 'axios';
import throat from 'throat';
import { shuffle } from 'lodash';
import { Account } from '../account';
import { GraphManager } from '../graphManager';
import { GitHubOrganizationResponse, Organization } from '../organization';
import { GitHubTokenManager } from '../githubApps/tokenManager';
import { GitHubTokenManager } from '../../lib/github/tokenManager';
import RenderHtmlMail from '../../lib/emailRender';
import { wrapError, sortByCaseInsensitive } from '../../utils';
import { Repository } from '../repository';
@ -20,7 +21,7 @@ import {
GitHubAppAuthenticationType,
GitHubAppPurposes,
IGitHubAppConfiguration,
} from '../githubApps';
} from '../../lib/github/appPurposes';
import {
OrganizationFeature,
OrganizationSetting,
@ -42,7 +43,7 @@ import {
ICreateLinkOptions,
ICrossOrganizationMembershipByOrganization,
ICrossOrganizationTeamMembership,
IGetAuthorizationHeader,
GetAuthorizationHeader,
IGitHubAppInstallation,
IMapPlusMetaCost,
IOperationsCentralOperationsToken,
@ -56,7 +57,7 @@ import {
IOperationsTemplates,
IPagedCrossOrganizationCacheOptions,
IPromisedLinks,
IPurposefulGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
ISupportedLinkTypeOutcome,
IUnlinkMailStatus,
NoCacheNoBackground,
@ -68,6 +69,7 @@ import { Team } from '../team';
import { IRepositoryMetadataProvider } from '../../entities/repositoryMetadata/repositoryMetadataProvider';
import { isAuthorizedSystemAdministrator } from './administration';
import type { ConfigGitHubOrganizationsSpecializedList } from '../../config/github.organizations.types';
import { type GitHubTokenType, getGitHubTokenTypeFromValue } from '../../lib/github/appTokens';
export * from './core';
@ -116,7 +118,7 @@ export class Operations
private _invisibleOrganizations: Map<string, Organization>;
private _uncontrolledOrganizations: Map<string, Organization>;
private _organizationOriginalNames: any;
private _organizationNamesWithAuthorizationHeaders: Map<string, IPurposefulGetAuthorizationHeader>;
private _organizationNamesWithAuthorizationHeaders: Map<string, PurposefulGetAuthorizationHeader>;
private _defaultPageSize: number;
private _organizationIds: Map<number, Organization>;
private _organizationSettings: OrganizationSetting[];
@ -172,10 +174,10 @@ export class Operations
}
}
this._tokenManager = new GitHubTokenManager({
operations: this,
configurations: purposesToConfigurations,
executionEnvironment: options.executionEnvironment,
});
GitHubTokenManager.RegisterManagerForOperations(this, this._tokenManager);
this._dynamicOrganizationIds = new Set();
this._organizationSettings = [];
}
@ -339,7 +341,6 @@ export class Operations
private createOrganization(
name: string,
settings: OrganizationSetting,
centralOperationsFallbackToken: string,
appAuthenticationType: GitHubAppAuthenticationType
): Organization {
name = name.toLowerCase();
@ -355,20 +356,12 @@ export class Operations
this,
name,
settings,
this.getAuthorizationHeader.bind(this, name, settings, ownerToken, appAuthenticationType),
this.getAuthorizationHeader.bind(
this,
name,
settings,
ownerToken,
centralOperationsFallbackToken,
appAuthenticationType
),
this.getAuthorizationHeader.bind(
this,
name,
settings,
ownerToken,
centralOperationsFallbackToken,
GitHubAppAuthenticationType.ForceSpecificInstallation
),
hasDynamicSettings
@ -379,7 +372,6 @@ export class Operations
if (!this._organizations) {
const organizations = new Map<string, Organization>();
const names = this.organizationNames;
const centralOperationsToken = this.config.github.operations.centralOperationsToken;
for (let i = 0; i < names.length; i++) {
const name = names[i];
let settings: OrganizationSetting = null;
@ -395,7 +387,6 @@ export class Operations
const organization = this.createOrganization(
name,
settings,
centralOperationsToken,
GitHubAppAuthenticationType.BestAvailable
);
organizations.set(name, organization);
@ -416,11 +407,9 @@ export class Operations
for (let i = 0; i < list.length; i++) {
const settings = list[i];
if (settings && settings.name && settings.name.toLowerCase() === lowercase) {
const centralOperationsToken = this.config.github.operations.centralOperationsToken;
return this.createOrganization(
lowercase,
OrganizationSetting.CreateFromStaticSettings(settings),
centralOperationsToken,
GitHubAppAuthenticationType.BestAvailable
);
}
@ -441,7 +430,6 @@ export class Operations
return this.createOrganization(
settings.organizationName.toLowerCase(),
settings,
null,
GitHubAppAuthenticationType.BestAvailable
);
}
@ -474,7 +462,7 @@ export class Operations
dynamicSettings = options.settings;
}
const authenticationType = options?.authenticationType || GitHubAppAuthenticationType.BestAvailable;
const organization = this.createOrganization(name, dynamicSettings, null, authenticationType);
const organization = this.createOrganization(name, dynamicSettings, authenticationType);
if (!options || options?.storeInstanceByName) {
this._invisibleOrganizations.set(name, organization);
}
@ -492,11 +480,9 @@ export class Operations
}
const emptySettings = new OrganizationSetting();
emptySettings.operationsNotes = `Uncontrolled Organization - ${organizationName}`;
const centralOperationsToken = this.config.github.operations.centralOperationsToken;
const org = this.createOrganization(
organizationName,
emptySettings,
centralOperationsToken,
GitHubAppAuthenticationType.ForceSpecificInstallation
);
this._uncontrolledOrganizations.set(organizationName, org);
@ -506,16 +492,18 @@ export class Operations
getPublicOnlyAccessOrganization(organizationName: string, organizationId?: number): Organization {
organizationName = organizationName.toLowerCase();
const emptySettings = new OrganizationSetting();
emptySettings.operationsNotes = `Uncontrolled public organization - ${organizationName}`;
const publicAccessToken = this.config.github.operations.publicAccessToken;
if (!publicAccessToken) {
throw new Error('not configured for public read-only tokens');
throw CreateError.InvalidParameters('not configured for public read-only tokens');
}
const emptySettings = OrganizationSetting.CreateEmptyWithOldToken(
publicAccessToken,
`Uncontrolled public organization - ${organizationName}`,
organizationId
);
const org = this.createOrganization(
organizationName,
emptySettings,
publicAccessToken,
GitHubAppAuthenticationType.ForceSpecificInstallation
);
this._uncontrolledOrganizations.set(organizationName, org);
@ -593,7 +581,7 @@ export class Operations
get organizationNamesWithAuthorizationHeaders() {
if (!this._organizationNamesWithAuthorizationHeaders) {
const tokens = new Map<string, IPurposefulGetAuthorizationHeader>();
const tokens = new Map<string, PurposefulGetAuthorizationHeader>();
const visited = new Set<string>();
for (const entry of this._organizationSettings) {
const lowercase = entry.organizationName.toLowerCase();
@ -825,7 +813,8 @@ export class Operations
const lc = name.toLowerCase();
const organization = this.organizations.get(lc);
if (!organization) {
throw CreateError.NotFound(`Could not find configuration for the "${name}" organization.`);
// will this impact things?
throw CreateError.InvalidParameters(`Could not find configuration for the "${name}" organization.`);
}
return organization;
}
@ -913,6 +902,45 @@ export class Operations
}
async getOrganizationProfileById(id: number, options?: ICacheOptions): Promise<GitHubOrganizationResponse> {
options = options || {};
if (!id) {
throw new Error('Must provide a repository ID to retrieve the repository.');
}
const organization = this._organizationIds.get(id);
return this._getOrganizationProfileById(id, organization ? id : null, options);
}
async getOrganizationPublicProfileById(
id: number,
options?: ICacheOptions
): Promise<GitHubOrganizationResponse> {
options = options || {};
if (!id) {
throw new Error('Must provide a repository ID to retrieve the repository.');
}
let lookupId: number | null = this._organizationIds.get(id) ? id : null;
if (lookupId) {
const allIdsExcludingOrg = this.getOrganizationIds().filter((orgId) => orgId !== id);
const shuffledIds = shuffle(allIdsExcludingOrg);
if (shuffledIds.length > 0) {
lookupId = shuffledIds[0];
}
}
if (lookupId === null) {
throw CreateError.InvalidParameters(
'This approach requires configuring at least two organizations (getOrganizationPublicProfileById).'
);
}
return this._getOrganizationProfileById(id, lookupId, options);
}
private async _getOrganizationProfileById(
id: number,
lookupUsingIdOrCentralToken: number | null,
options?: ICacheOptions
): Promise<GitHubOrganizationResponse> {
// EMU note: you need to use an EMU-installed app vs public...
// Cache note: this will be a cache miss if you switch between public/non-public entrypoints
options = options || {};
if (!id) {
throw new Error('Must provide a repository ID to retrieve the repository.');
@ -926,13 +954,15 @@ export class Operations
if (options.backgroundRefresh !== undefined) {
cacheOptions.backgroundRefresh = options.backgroundRefresh;
}
const organization = this._organizationIds.get(lookupUsingIdOrCentralToken);
let header: GetAuthorizationHeader = null;
if (organization) {
header = organization.getAuthorizationHeader() as GetAuthorizationHeader; // fallback will happen
} else {
header = this.getPublicAuthorizationToken();
}
try {
const entity = await this.github.request(
this.getCentralOperationsToken(),
'GET /organizations/:id',
parameters,
cacheOptions
);
const entity = await this.github.request(header, 'GET /organizations/:id', parameters, cacheOptions);
return entity;
} catch (error) {
if (error.status && error.status === 404) {
@ -1278,19 +1308,32 @@ export class Operations
return false;
}
getCentralOperationsToken(): IGetAuthorizationHeader {
const func = getCentralOperationsAuthorizationHeader.bind(null, this) as IGetAuthorizationHeader;
return func;
getPublicReadOnlyStaticToken(): GetAuthorizationHeader {
const { config } = this.providers;
if (config?.github?.operations?.publicAccessToken) {
const capturedToken = config.github.operations.publicAccessToken;
return async () => {
return {
value: `token ${capturedToken}`,
purpose: null,
source: 'public read-only token',
};
};
}
throw CreateError.InvalidParameters('No configured read-only static token');
}
getPublicReadOnlyToken(): IGetAuthorizationHeader {
const func = getReadOnlyAuthorizationHeader.bind(null, this) as IGetAuthorizationHeader;
return func;
getPublicAuthorizationToken(): GetAuthorizationHeader {
try {
return this._tokenManager.getAuthorizationHeaderForAnyApp.bind(this._tokenManager);
} catch (error) {
return this.getPublicReadOnlyStaticToken();
}
}
getAccount(id: string) {
const entity = { id };
return new Account(entity, this, getCentralOperationsAuthorizationHeader.bind(null, this));
return new Account(entity, this, this.getPublicAuthorizationToken.bind(this));
}
async getAccountWithDetailsAndLink(id: string): Promise<Account> {
@ -1299,14 +1342,27 @@ export class Operations
}
async getAuthenticatedAccount(token: string): Promise<Account> {
// Returns an account instance based on the account identified in the token.
const github = this.github;
const parameters = {};
const fullToken = `token ${token}`;
let tokenType: GitHubTokenType = null;
try {
const entity = await github.post(`token ${token}`, 'users.getAuthenticated', parameters);
const account = new Account(entity, this, getCentralOperationsAuthorizationHeader.bind(null, this));
tokenType = getGitHubTokenTypeFromValue(fullToken);
} catch (error) {
// ignoring any issue here on identifying the type of token
}
try {
const entity = await github.post(fullToken, 'users.getAuthenticated', parameters);
const account = new Account(entity, this, this.getPublicAuthorizationToken.bind(this));
return account;
} catch (error) {
throw wrapError(error, 'Could not get details about the authenticated account');
throw wrapError(
error,
`Could not get details about the authenticated account${
tokenType ? ' using token type "' + tokenType + '"' : '.'
}`
);
}
}
@ -1420,42 +1476,6 @@ async function fireEvent(config, configurationName, value): Promise<IFireEventRe
return results;
}
export function getReadOnlyAuthorizationHeader(self: Operations): IPurposefulGetAuthorizationHeader {
const s = (self || this) as Operations;
if (s.config?.github?.operations?.publicAccessToken) {
const capturedToken = s.config.github.operations.publicAccessToken;
return async () => {
return {
value: `token ${capturedToken}`,
purpose: null,
source: 'public read-only token',
};
};
} else {
throw new Error('No public read-only token configured.');
}
}
export function getCentralOperationsAuthorizationHeader(self: Operations): IPurposefulGetAuthorizationHeader {
const s = (self || this) as Operations;
if (s.config.github && s.config.github.operations && s.config.github.operations.centralOperationsToken) {
const capturedToken = s.config.github.operations.centralOperationsToken;
return async () => {
return {
value: `token ${capturedToken}`,
purpose: null, // legacy
source: 'central operations token',
};
};
} else if (s.getOrganizations().length === 0) {
throw new Error('No central operations token nor any organizations configured.');
}
// Fallback to the first configured organization as a convenience
// CONSIDER: would randomizing the organization be better, or a priority based on known-rate limit remaining?
const firstOrganization = s.getOrganizations()[0];
return firstOrganization.getAuthorizationHeader();
}
function crossOrganizationResults(operations: Operations, results, keyProperty) {
keyProperty = keyProperty || 'id';
const map: IMapPlusMetaCost = new Map();

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

@ -13,11 +13,11 @@ import { Repository } from './repository';
import { wrapError } from '../utils';
import { StripGitHubEntity } from '../lib/github/restApi';
import { GitHubResponseType } from '../lib/github/endpointEntities';
import { AppPurpose, AppPurposeTypes } from './githubApps';
import { AppPurpose, AppPurposeTypes } from '../lib/github/appPurposes';
import {
OrganizationFeature,
OrganizationSetting,
SpecialTeam,
SystemTeam,
} from '../entities/organizationSettings/organizationSetting';
import { createOrganizationSudoInstance, IOrganizationSudo } from '../features';
import { CacheDefault, getMaxAgeSeconds, getPageSize, OperationsCore } from './operations/core';
@ -28,12 +28,12 @@ import {
GitHubRepositoryVisibility,
IAccountBasics,
IAddOrganizationMembershipOptions,
IAuthorizationHeaderValue,
AuthorizationHeaderValue,
ICacheOptions,
ICacheOptionsWithPurpose,
ICorporateLink,
ICreateRepositoryResult,
IGetAuthorizationHeader,
type GetAuthorizationHeader,
IGetOrganizationAuditLogOptions,
IGetOrganizationMembersOptions,
IGitHubAccountDetails,
@ -49,7 +49,7 @@ import {
IOrganizationMemberPair,
IOrganizationMembership,
IPagedCacheOptions,
IPurposefulGetAuthorizationHeader,
type PurposefulGetAuthorizationHeader,
IReposError,
IReposRestRedisCacheCost,
NoCacheNoBackground,
@ -64,10 +64,11 @@ import { CreateError, ErrorHelper } from '../transitional';
import { jsonError } from '../middleware';
import getCompanySpecificDeployment from '../middleware/companySpecificDeployment';
import { ConfigGitHubTemplates } from '../config/github.templates.types';
import { GitHubTokenManager } from './githubApps/tokenManager';
import { GitHubTokenManager } from '../lib/github/tokenManager';
import { OrganizationProjects } from './projects';
import { OrganizationDomains } from './domains';
import { OrganizationCopilot } from './organizationCopilot';
import { OrganizationProperties } from './organizationProperties';
interface IGetMembersParameters {
org: string;
@ -208,8 +209,8 @@ export class Organization {
private _nativeManagementUrl: string;
private _operations: IOperationsInstance;
private _getAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _usesGitHubApp: boolean;
private _settings: OrganizationSetting;
@ -221,6 +222,7 @@ export class Organization {
private _domains: OrganizationDomains;
private _copilot: OrganizationCopilot;
private _customProperties: OrganizationProperties;
id: number;
uncontrolled: boolean;
@ -229,8 +231,8 @@ export class Organization {
operations: IOperationsInstance,
name: string,
settings: OrganizationSetting,
getAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getAuthorizationHeader: PurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader,
public hasDynamicSettings: boolean
) {
this._name = settings.organizationName || name;
@ -365,6 +367,17 @@ export class Organization {
return this._copilot;
}
get customProperties() {
if (!this._customProperties) {
this._customProperties = new OrganizationProperties(
this,
this._getSpecificAuthorizationHeader.bind(this),
this._operations
);
}
return this._customProperties;
}
get domains() {
if (!this._domains) {
this._domains = new OrganizationDomains(
@ -568,7 +581,11 @@ export class Organization {
}
get broadAccessTeams(): number[] {
return this.getSpecialTeam(SpecialTeam.Everyone, 'everyone membership');
return this.getSystemTeam(SystemTeam.Everyone, 'everyone membership');
}
get openAccessTeams(): number[] {
return this.getSystemTeam(SystemTeam.OpenAccess, 'open access');
}
get invitationTeam(): Team {
@ -580,7 +597,7 @@ export class Organization {
}
get systemSudoersTeam(): Team {
const teams = this.getSpecialTeam(SpecialTeam.GlobalSudo, 'system sudoers');
const teams = this.getSystemTeam(SystemTeam.GlobalSudo, 'system sudoers');
if (teams.length > 1) {
throw new Error('Multiple system sudoer teams are not supported.');
}
@ -592,7 +609,7 @@ export class Organization {
}
get sudoersTeam(): Team {
const teams = this.getSpecialTeam(SpecialTeam.Sudo, 'organization sudoers');
const teams = this.getSystemTeam(SystemTeam.Sudo, 'organization sudoers');
if (teams.length > 1) {
throw new Error('Multiple sudoer teams are not supported.');
}
@ -606,15 +623,15 @@ export class Organization {
return this._settings;
}
get specialRepositoryPermissionTeams() {
get specialSystemTeams() {
return {
read: this.getSpecialTeam(SpecialTeam.SystemRead, 'read everything'),
write: this.getSpecialTeam(SpecialTeam.SystemWrite, 'write everything'),
admin: this.getSpecialTeam(SpecialTeam.SystemAdmin, 'administer everything'),
read: this.getSystemTeam(SystemTeam.SystemRead, 'read everything'),
write: this.getSystemTeam(SystemTeam.SystemWrite, 'write everything'),
admin: this.getSystemTeam(SystemTeam.SystemAdmin, 'administer everything'),
};
}
getAuthorizationHeader(): IPurposefulGetAuthorizationHeader {
getAuthorizationHeader(): PurposefulGetAuthorizationHeader {
return this._getAuthorizationHeader;
}
@ -688,7 +705,13 @@ export class Organization {
teamIds.push(broadAccessTeams[i]); // is the actual ID, not the team object
}
}
const specialTeams = this.specialRepositoryPermissionTeams;
const openAccessTeams = this.openAccessTeams;
if (openAccessTeams) {
for (let i = 0; i < openAccessTeams.length; i++) {
teamIds.push(openAccessTeams[i]); // is the actual ID, not the team object
}
}
const specialTeams = this.specialSystemTeams;
const keys = Object.getOwnPropertyNames(specialTeams);
keys.forEach((type) => {
const values = specialTeams[type];
@ -721,12 +744,12 @@ export class Organization {
);
}
async getRepositoryCreateGitHubToken(): Promise<IAuthorizationHeaderValue> {
async getRepositoryCreateGitHubToken(): Promise<AuthorizationHeaderValue> {
// This method leaks/releases the owner token. In the future a more crisp
// way of accomplishing this without exposing the token should be created.
// The function name is specific to the intended use instead of a general-
// purpose token name.
const token = await (this.authorize(AppPurpose.Operations) as IGetAuthorizationHeader)();
const token = await (this.authorize(AppPurpose.Operations) as GetAuthorizationHeader)();
token.source = 'repository create token';
return token;
}
@ -1190,7 +1213,7 @@ export class Organization {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
AppPurpose.Data
) as IGetAuthorizationHeader;
) as GetAuthorizationHeader;
const github = operations.github;
const parameters: IGetMembersParameters = {
org: this.name,
@ -1299,7 +1322,7 @@ export class Organization {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
AppPurpose.Data
) as IGetAuthorizationHeader;
) as GetAuthorizationHeader;
const teamEntities = await github.collections.getOrgTeams(getAuthorizationHeader, parameters, caching);
const teams = common.createInstances<Team>(this, this.teamFromEntity, teamEntities);
return teams;
@ -1325,12 +1348,12 @@ export class Organization {
if (queryCache?.supportsOrganizationMembership) {
try {
if (!optionalId) {
const centralOps = operationsWithCapability<IOperationsCentralOperationsToken>(
const ops = operationsWithCapability<IOperationsCentralOperationsToken>(
operations,
CoreCapability.GitHubCentralOperations
);
if (centralOps) {
const account = await centralOps.getAccountByUsername(login);
if (ops) {
const account = await ops.getAccountByUsername(login);
optionalId = account.id.toString();
}
}
@ -1362,19 +1385,16 @@ export class Organization {
}
}
private authorize(purpose: AppPurpose): IGetAuthorizationHeader {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
private authorize(purpose: AppPurpose): GetAuthorizationHeader {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(this, purpose) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
private authorizeSpecificPurpose(purpose: AppPurposeTypes): IGetAuthorizationHeader {
private authorizeSpecificPurpose(purpose: AppPurposeTypes): GetAuthorizationHeader {
const getAuthorizationHeader = this._getSpecificAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
@ -1516,23 +1536,15 @@ export class Organization {
return { settings, operations };
}
private getSpecialTeam(specialTeam: SpecialTeam, friendlyName: string, throwIfMissing?: boolean): number[] {
let teamId: number = null;
for (const entry of this._settings.specialTeams) {
if (entry.specialTeam === specialTeam) {
teamId = entry.teamId;
break;
}
}
if (throwIfMissing) {
private getSystemTeam(teamType: SystemTeam, friendlyName: string, throwIfMissing?: boolean): number[] {
const allOrgSystemTeams = this._settings.specialTeams;
const matchingSystemTeamTypes = allOrgSystemTeams.filter((t) => t.specialTeam === teamType);
const teams: number[] = matchingSystemTeamTypes.map((t) => t.teamId);
if (throwIfMissing && teams.length === 0) {
throw new Error(
`Missing configured organization "${this.name}" special team ${specialTeam} - ${friendlyName}`
`Missing configured organization "${this.name}" special team ${teamType} - ${friendlyName}`
);
}
const teams: number[] = [];
if (teamId) {
teams.push(teamId);
}
return teams;
}

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

@ -4,14 +4,14 @@
//
import {
IGetAuthorizationHeader,
GetAuthorizationHeader,
IOperationsInstance,
IPagedCacheOptions,
IPurposefulGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
throwIfNotGitHubCapable,
} from '../interfaces';
import type { CollectionCopilotSeatsOptions } from '../lib/github/collections';
import { AppPurpose, AppPurposeTypes } from './githubApps';
import { AppPurpose, AppPurposeTypes } from '../lib/github/appPurposes';
import { CacheDefault, getMaxAgeSeconds, getPageSize } from './operations/core';
import { Organization } from './organization';
@ -30,7 +30,7 @@ export type CopilotSeatData = {
export class OrganizationCopilot {
constructor(
private organization: Organization,
private getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader,
private getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader,
private operations: IOperationsInstance
) {}
@ -43,7 +43,7 @@ export class OrganizationCopilot {
const getAuthorizationHeader = this.getSpecificAuthorizationHeader.bind(
this,
appPurpose
) as IGetAuthorizationHeader;
) as GetAuthorizationHeader;
const github = operations.github;
const parameters: CollectionCopilotSeatsOptions = {
org: this.organization.name,

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

@ -0,0 +1,242 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import {
ICacheOptions,
ICacheOptionsWithPurpose,
GetAuthorizationHeader,
IOperationsInstance,
PurposefulGetAuthorizationHeader,
PagedCacheOptionsWithPurpose,
throwIfNotGitHubCapable,
} from '../interfaces';
import { HttpMethod } from '../lib/github';
import { CreateError } from '../transitional';
import { AppPurpose, AppPurposeTypes } from '../lib/github/appPurposes';
import {
CacheDefault,
createCacheOptions,
createPagedCacheOptions,
getMaxAgeSeconds,
getPageSize,
popPurpose,
symbolizeApiResponse,
} from './operations/core';
import { Organization } from './organization';
export enum CustomPropertyValueType {
String = 'string',
SingleSelect = 'single_select',
}
export type OrganizationCustomPropertyEntity = {
property_name: string;
value_type: CustomPropertyValueType;
required: boolean;
description?: string;
default_value?: string;
allowed_values?: string[];
};
type SetPropertyValue = {
property_name: string;
value: string;
};
type CreateOrUpdateResponse = {
properties: OrganizationCustomPropertyEntity[];
};
export class OrganizationProperties {
private _defaultPurpose = AppPurpose.Data;
constructor(
private organization: Organization,
private getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader,
private operations: IOperationsInstance
) {}
private authorize(purpose: AppPurposeTypes): GetAuthorizationHeader | string {
const getAuthorizationHeader = this.getSpecificAuthorizationHeader.bind(
this,
purpose
) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
async getRepositoryCustomProperties(
repositoryName: string,
options?: ICacheOptionsWithPurpose
): Promise<Record<string, string>> {
options = options || {};
const operations = throwIfNotGitHubCapable(this.operations);
const { github } = operations;
const purpose = popPurpose(options, this._defaultPurpose);
const parameters = {
owner: this.organization.name,
repo: repositoryName,
};
const cacheOptions = createCacheOptions(operations, options);
try {
const responseArray = await github.request(
this.authorize(purpose),
'GET /repos/:owner/:repo/properties/values',
parameters,
cacheOptions
);
return symbolizeApiResponse(arrayToSetProperties(responseArray));
} catch (error) {
throw error;
}
}
async getCustomProperties(
options?: PagedCacheOptionsWithPurpose
): Promise<OrganizationCustomPropertyEntity[]> {
options = options || {};
const operations = throwIfNotGitHubCapable(this.operations);
const { github } = operations;
const purpose = popPurpose(options, this._defaultPurpose);
const parameters = {
org: this.organization.name,
per_page: getPageSize(operations),
};
const cacheOptions = createPagedCacheOptions(operations, options);
try {
const entities = await github.collections.collectAllPagesViaHttpGet<
any,
OrganizationCustomPropertyEntity
>(
this.authorize(purpose),
'orgCustomProps',
'GET /orgs/:org/properties/schema',
parameters,
cacheOptions
);
return symbolizeApiResponse(entities);
} catch (error) {
throw error;
}
}
async getCustomProperty(
propertyName: string,
options?: ICacheOptionsWithPurpose
): Promise<OrganizationCustomPropertyEntity> {
options = options || {};
const operations = throwIfNotGitHubCapable(this.operations);
const { github } = operations;
if (!propertyName) {
throw CreateError.InvalidParameters('propertyName');
}
const purpose = popPurpose(options, this._defaultPurpose);
const parameters = {
org: this.organization.name,
custom_property_name: propertyName,
};
const cacheOptions: ICacheOptions = {
maxAgeSeconds: getMaxAgeSeconds(operations, CacheDefault.accountDetailStaleSeconds, options, 60),
};
if (options.backgroundRefresh !== undefined) {
cacheOptions.backgroundRefresh = options.backgroundRefresh;
}
try {
const entity = (await github.request(
this.authorize(purpose),
'GET /orgs/:org/properties/schema/:custom_property_name',
parameters,
cacheOptions
)) as OrganizationCustomPropertyEntity;
return symbolizeApiResponse(entity);
} catch (error) {
throw error;
}
}
async deleteProperty(propertyName: string, purpose?: AppPurposeTypes): Promise<void> {
const operations = throwIfNotGitHubCapable(this.operations);
const parameters = {
org: this.organization.name,
custom_property_name: propertyName,
};
await operations.github.restApi(
this.authorize(purpose || this._defaultPurpose),
HttpMethod.Delete,
'/orgs/:org/properties/schema/:custom_property_name',
parameters
);
}
async createOrUpdate(
properties: OrganizationCustomPropertyEntity[],
purpose?: AppPurposeTypes
): Promise<OrganizationCustomPropertyEntity[]> {
const operations = throwIfNotGitHubCapable(this.operations);
const parameters = {
org: this.organization.name,
properties,
};
const res = (await operations.github.restApi(
this.authorize(purpose || this._defaultPurpose),
HttpMethod.Patch,
'/orgs/:org/properties/schema',
parameters
)) as CreateOrUpdateResponse;
return res.properties;
}
createOrUpdateRepositoryProperties(
repositoryName: string,
propertiesAndValues: Record<string, string>,
purpose?: AppPurposeTypes
): Promise<void> {
const names = [repositoryName];
return this.createOrUpdateRepositoriesProperties(names, propertiesAndValues, purpose);
}
async createOrUpdateRepositoriesProperties(
organizationRepositoryNames: string[],
propertiesAndValues: Record<string, string>,
purpose?: AppPurposeTypes
): Promise<void> {
const operations = throwIfNotGitHubCapable(this.operations);
if (organizationRepositoryNames.length > 30) {
throw CreateError.InvalidParameters(
'GitHub has a hard limit of 30 repositories to update in a single patch'
);
}
const parameters = {
repository_names: organizationRepositoryNames,
org: this.organization.name,
properties: setPropertiesRecordToArray(propertiesAndValues),
};
(await operations.github.restApi(
this.authorize(purpose || this._defaultPurpose),
HttpMethod.Patch,
'/orgs/:org/properties/values',
parameters
)) as CreateOrUpdateResponse;
}
}
function setPropertiesRecordToArray(propertiesAndValues: Record<string, string>) {
const keys = Object.getOwnPropertyNames(propertiesAndValues);
const properties: SetPropertyValue[] = [];
for (const key of keys) {
properties.push({
property_name: key,
value: propertiesAndValues[key],
});
}
return properties;
}
function arrayToSetProperties(properties: SetPropertyValue[]) {
const propertiesAndValues: Record<string, string> = {};
for (const property of properties) {
propertiesAndValues[property.property_name] = property.value;
}
return propertiesAndValues;
}

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

@ -3,13 +3,13 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { AppPurpose, AppPurposeTypes } from './githubApps';
import { AppPurpose, AppPurposeTypes } from '../lib/github/appPurposes';
import { Organization, Repository } from '.';
import {
IOperationsInstance,
IPurposefulGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
throwIfNotGitHubCapable,
IGetAuthorizationHeader,
GetAuthorizationHeader,
} from '../interfaces';
import {
decorateIterable,
@ -117,8 +117,8 @@ export class OrganizationProject {
private _projects: OrganizationProjects;
private _operations: IOperationsInstance;
private _getAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _purpose: AppPurpose;
private _id: string;
@ -126,8 +126,8 @@ export class OrganizationProject {
constructor(
organizationProjects: OrganizationProjects,
operations: IOperationsInstance,
getAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getAuthorizationHeader: PurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader,
projectId: string
) {
this._getAuthorizationHeader = getAuthorizationHeader;
@ -413,19 +413,16 @@ export class OrganizationProject {
}
}
private authorize(purpose: AppPurpose = this._purpose): IGetAuthorizationHeader {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
private authorize(purpose: AppPurpose = this._purpose): GetAuthorizationHeader {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(this, purpose) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
private authorizeSpecificPurpose(purpose: AppPurposeTypes): IGetAuthorizationHeader | string {
private authorizeSpecificPurpose(purpose: AppPurposeTypes): GetAuthorizationHeader | string {
const getAuthorizationHeader = this._getSpecificAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
}

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

@ -3,13 +3,13 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { AppPurpose, AppPurposeTypes } from './githubApps';
import { AppPurpose, AppPurposeTypes } from '../lib/github/appPurposes';
import { Organization } from '.';
import {
IOperationsInstance,
IPurposefulGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
throwIfNotGitHubCapable,
IGetAuthorizationHeader,
GetAuthorizationHeader,
} from '../interfaces';
import {
decorateIterable,
@ -30,8 +30,8 @@ export class OrganizationProjectView {
private _project: OrganizationProject;
private _operations: IOperationsInstance;
private _getAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _purpose: AppPurpose;
private _id: string;
@ -41,8 +41,8 @@ export class OrganizationProjectView {
constructor(
organizationProject: OrganizationProject,
operations: IOperationsInstance,
getAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getAuthorizationHeader: PurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader,
projectId: string,
essentials?: ProjectViewEssentials
) {
@ -217,19 +217,16 @@ export class OrganizationProjectView {
// }
// }
private authorize(purpose: AppPurpose = this._purpose): IGetAuthorizationHeader {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
private authorize(purpose: AppPurpose = this._purpose): GetAuthorizationHeader {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(this, purpose) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
private authorizeSpecificPurpose(purpose: AppPurposeTypes): IGetAuthorizationHeader | string {
private authorizeSpecificPurpose(purpose: AppPurposeTypes): GetAuthorizationHeader | string {
const getAuthorizationHeader = this._getSpecificAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
}

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

@ -3,13 +3,13 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { AppPurpose, AppPurposeTypes } from './githubApps';
import { AppPurpose, AppPurposeTypes } from '../lib/github/appPurposes';
import { Organization } from '.';
import {
IOperationsInstance,
IPurposefulGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
throwIfNotGitHubCapable,
IGetAuthorizationHeader,
GetAuthorizationHeader,
} from '../interfaces';
import {
decorateIterable,
@ -35,15 +35,15 @@ export class OrganizationProjects {
private _organization: Organization;
private _operations: IOperationsInstance;
private _getAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _purpose: AppPurpose;
constructor(
organization: Organization,
operations: IOperationsInstance,
getAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader
getAuthorizationHeader: PurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader
) {
this._getAuthorizationHeader = getAuthorizationHeader;
this._getSpecificAuthorizationHeader = getSpecificAuthorizationHeader;
@ -134,19 +134,16 @@ export class OrganizationProjects {
}
}
private authorize(purpose: AppPurpose = this._purpose): IGetAuthorizationHeader {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
private authorize(purpose: AppPurpose = this._purpose): GetAuthorizationHeader {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(this, purpose) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
private authorizeSpecificPurpose(purpose: AppPurposeTypes): IGetAuthorizationHeader | string {
private authorizeSpecificPurpose(purpose: AppPurposeTypes): GetAuthorizationHeader | string {
const getAuthorizationHeader = this._getSpecificAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
}

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

@ -17,9 +17,9 @@ import {
RepositoryIssue,
} from '.';
import { RepositoryMetadataEntity } from '../entities/repositoryMetadata/repositoryMetadata';
import { AppPurpose, AppPurposeTypes } from './githubApps';
import { AppPurpose, AppPurposeTypes } from '../lib/github/appPurposes';
import {
IPurposefulGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
IOperationsInstance,
ICacheOptions,
throwIfNotGitHubCapable,
@ -42,7 +42,7 @@ import {
IGitHubSecretScanningAlert,
operationsWithCapability,
IOperationsServiceAccounts,
IGetAuthorizationHeader,
GetAuthorizationHeader,
IRepositoryGetIssuesOptions,
IOperationsRepositoryMetadataProvider,
IOperationsUrls,
@ -214,8 +214,8 @@ export class Repository {
private _awesomeness: number;
private _getAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _operations: IOperationsInstance;
private _organization: Organization;
@ -366,8 +366,8 @@ export class Repository {
constructor(
organization: Organization,
entity: any,
getAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getAuthorizationHeader: PurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader,
operations: IOperationsInstance
) {
this._organization = organization;
@ -884,9 +884,8 @@ export class Repository {
cacheOptions.backgroundRefresh = options.backgroundRefresh;
}
try {
// CONSIDER: need a fallback authentication approach: try and app for a specific capability (the installation knows) and fallback to central ops
// const centralOps = operationsWithCapability<IOperationsCentralOperationsToken>(operations, CoreCapability.GitHubCentralOperations);
const tokenSource = this._getSpecificAuthorizationHeader(AppPurpose.Data); // centralOps ? centralOps.getCentralOperationsToken()(AppPurpose.Data) :
// CONSIDER: need a fallback authentication approach: try and app for a specific capability
const tokenSource = this._getSpecificAuthorizationHeader(AppPurpose.Data);
const token = await tokenSource;
return await operations.github.call(token, 'repos.getPages', parameters, cacheOptions);
} catch (error) {
@ -1365,9 +1364,6 @@ export class Repository {
private: options.private,
}
);
// BUG: GitHub Apps do not work with locking down no repository permissions as documented here: https://github.community/t5/GitHub-API-Development-and/GitHub-App-cannot-patch-repo-visibility-in-org-with-repo/m-p/33448#M3150
// const token = this._operations.getCentralOperationsToken();
// return this._operations.github.post(token, 'repos.update', parameters);
return operations.github.post(this.authorize(AppPurpose.Operations), 'repos.update', parameters);
}
@ -1589,7 +1585,10 @@ export class Repository {
const teams = (await this.getTeamPermissions()).filter((tp) => tp.permission === 'admin');
for (let i = 0; i < teams.length; i++) {
const team = teams[i];
if (excludeBroadAndSystemTeams && (team.team.isSystemTeam || team.team.isBroadAccessTeam)) {
if (
excludeBroadAndSystemTeams &&
(team.team.isSystemTeam || team.team.isBroadAccessTeam || team.team.isOpenAccessTeam)
) {
// Do not include broad access teams
continue;
}
@ -1699,19 +1698,16 @@ export class Repository {
return Array.from(users.values());
}
private authorize(purpose: AppPurposeTypes): IGetAuthorizationHeader | string {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
private authorize(purpose: AppPurposeTypes): GetAuthorizationHeader | string {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(this, purpose) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
private specificAuthorization(purpose: AppPurposeTypes): IGetAuthorizationHeader | string {
private specificAuthorization(purpose: AppPurposeTypes): GetAuthorizationHeader | string {
const getSpecificHeader = this._getSpecificAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
) as GetAuthorizationHeader;
return getSpecificHeader;
}

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

@ -5,13 +5,13 @@
import { Repository } from './repository';
import { getPageSize, getMaxAgeSeconds, CacheDefault } from '.';
import { AppPurpose } from './githubApps';
import { AppPurpose } from '../lib/github/appPurposes';
import {
IPurposefulGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
IOperationsInstance,
throwIfNotGitHubCapable,
ICacheOptions,
IGetAuthorizationHeader,
GetAuthorizationHeader,
} from '../interfaces';
export interface IGitHubActionWorkflowsResponse {
@ -33,16 +33,16 @@ export interface IGitHubActionWorkflow {
}
export class RepositoryActions {
private _getAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _operations: IOperationsInstance;
private _repository: Repository;
constructor(
repository: Repository,
getAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getAuthorizationHeader: PurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader,
operations: IOperationsInstance
) {
this._repository = repository;
@ -175,11 +175,11 @@ export class RepositoryActions {
return entity;
}
private authorize(purpose: AppPurpose): IGetAuthorizationHeader | string {
private authorize(purpose: AppPurpose): GetAuthorizationHeader | string {
const getAuthorizationHeader = this._getSpecificAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
}

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

@ -5,16 +5,16 @@
import { Repository } from './repository';
import { wrapError } from '../utils';
import { AppPurpose } from './githubApps';
import { AppPurpose } from '../lib/github/appPurposes';
import { CacheDefault, getMaxAgeSeconds, Operations } from '.';
import {
IOperationsInstance,
IPurposefulGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
GitHubIssueState,
IIssueLabel,
throwIfNotGitHubCapable,
ICacheOptions,
IGetAuthorizationHeader,
GetAuthorizationHeader,
GitHubIssuePatchParameters,
GitHubStateReason,
} from '../interfaces';
@ -22,7 +22,7 @@ import { CreateError, ErrorHelper } from '../transitional';
export class RepositoryIssue {
private _operations: IOperationsInstance;
private _getAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _number: number;
private _repository: Repository;
@ -33,7 +33,7 @@ export class RepositoryIssue {
repository: Repository,
issueNumber: number,
operations: IOperationsInstance,
getAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getAuthorizationHeader: PurposefulGetAuthorizationHeader,
entity?: any
) {
this._getAuthorizationHeader = getAuthorizationHeader;
@ -211,11 +211,8 @@ export class RepositoryIssue {
return false;
}
private authorize(purpose: AppPurpose): IGetAuthorizationHeader | string {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
private authorize(purpose: AppPurpose): GetAuthorizationHeader | string {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(this, purpose) as GetAuthorizationHeader;
return getAuthorizationHeader;
}

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

@ -5,15 +5,15 @@
import { Repository } from './repository';
import { wrapError } from '../utils';
import { AppPurpose, AppPurposeTypes } from './githubApps';
import { AppPurpose, AppPurposeTypes } from '../lib/github/appPurposes';
import { CacheDefault, getMaxAgeSeconds } from '.';
import {
IOperationsInstance,
IPurposefulGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
GitHubIssueState,
throwIfNotGitHubCapable,
ICacheOptions,
IGetAuthorizationHeader,
GetAuthorizationHeader,
ICacheOptionsWithPurpose,
} from '../interfaces';
import { ErrorHelper } from '../transitional';
@ -22,8 +22,8 @@ import * as common from './common';
export class RepositoryProject {
private _operations: IOperationsInstance;
private _getAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _id: number;
private _repository: Repository;
@ -36,8 +36,8 @@ export class RepositoryProject {
repository: Repository,
projectId: number,
operations: IOperationsInstance,
getAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getAuthorizationHeader: PurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader,
entity?: any
) {
this._getAuthorizationHeader = getAuthorizationHeader;
@ -213,11 +213,11 @@ export class RepositoryProject {
return false;
}
private authorizeSpecificPurpose(purpose: AppPurposeTypes): IGetAuthorizationHeader | string {
private authorizeSpecificPurpose(purpose: AppPurposeTypes): GetAuthorizationHeader | string {
const getAuthorizationHeader = this._getSpecificAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
}

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

@ -3,17 +3,13 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { AppPurpose } from './githubApps';
import {
IOperationsInstance,
IPurposefulGetAuthorizationHeader,
IGetAuthorizationHeader,
} from '../interfaces';
import { AppPurpose } from '../lib/github/appPurposes';
import { IOperationsInstance, PurposefulGetAuthorizationHeader, GetAuthorizationHeader } from '../interfaces';
import { RepositoryProjectColumn } from './repositoryProjectColumn';
export class RepositoryProjectCard {
private _operations: IOperationsInstance;
private _getAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _id: number;
private _column: RepositoryProjectColumn;
@ -24,7 +20,7 @@ export class RepositoryProjectCard {
column: RepositoryProjectColumn,
cardId: number,
operations: IOperationsInstance,
getAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getAuthorizationHeader: PurposefulGetAuthorizationHeader,
entity?: any
) {
this._getAuthorizationHeader = getAuthorizationHeader;
@ -64,11 +60,8 @@ export class RepositoryProjectCard {
return this._column;
}
private authorize(purpose: AppPurpose): IGetAuthorizationHeader | string {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
private authorize(purpose: AppPurpose): GetAuthorizationHeader | string {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(this, purpose) as GetAuthorizationHeader;
return getAuthorizationHeader;
}

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

@ -4,11 +4,11 @@
//
import { CacheDefault, getMaxAgeSeconds, RepositoryIssue } from '.';
import { AppPurpose, AppPurposeTypes } from './githubApps';
import { AppPurpose, AppPurposeTypes } from '../lib/github/appPurposes';
import {
IOperationsInstance,
IPurposefulGetAuthorizationHeader,
IGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
GetAuthorizationHeader,
ICacheOptionsWithPurpose,
throwIfNotGitHubCapable,
ICacheOptions,
@ -20,7 +20,7 @@ import { CreateError } from '../transitional';
export class RepositoryProjectColumn {
private _operations: IOperationsInstance;
private _getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _id: number;
private _project: RepositoryProject;
@ -31,7 +31,7 @@ export class RepositoryProjectColumn {
project: RepositoryProject,
columnId: number,
operations: IOperationsInstance,
getSpecificAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getSpecificAuthorizationHeader: PurposefulGetAuthorizationHeader,
entity?: any
) {
this._getSpecificAuthorizationHeader = getSpecificAuthorizationHeader;
@ -136,11 +136,11 @@ export class RepositoryProjectColumn {
return card;
}
private authorizeSpecificPurpose(purpose: AppPurposeTypes): IGetAuthorizationHeader | string {
private authorizeSpecificPurpose(purpose: AppPurposeTypes): GetAuthorizationHeader | string {
const getAuthorizationHeader = this._getSpecificAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
}

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

@ -5,16 +5,16 @@
import { Repository } from './repository';
import { wrapError } from '../utils';
import { AppPurpose } from './githubApps';
import { AppPurpose } from '../lib/github/appPurposes';
import { CacheDefault, getMaxAgeSeconds } from '.';
import {
IOperationsInstance,
IPurposefulGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
GitHubIssueState,
IIssueLabel,
throwIfNotGitHubCapable,
ICacheOptions,
IGetAuthorizationHeader,
GetAuthorizationHeader,
} from '../interfaces';
import { ErrorHelper } from '../transitional';
@ -23,7 +23,7 @@ import { ErrorHelper } from '../transitional';
export class RepositoryPullRequest {
private _operations: IOperationsInstance;
private _getAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _number: number;
private _repository: Repository;
@ -34,7 +34,7 @@ export class RepositoryPullRequest {
repository: Repository,
pullRequestNumber: number,
operations: IOperationsInstance,
getAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getAuthorizationHeader: PurposefulGetAuthorizationHeader,
entity?: any
) {
this._getAuthorizationHeader = getAuthorizationHeader;
@ -219,11 +219,8 @@ export class RepositoryPullRequest {
return false;
}
private authorize(purpose: AppPurpose): IGetAuthorizationHeader | string {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
private authorize(purpose: AppPurpose): GetAuthorizationHeader | string {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(this, purpose) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
}

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

@ -13,11 +13,11 @@ import { TeamMember } from './teamMember';
import { TeamRepositoryPermission } from './teamRepositoryPermission';
import { IApprovalProvider } from '../entities/teamJoinApproval/approvalProvider';
import { TeamJoinApprovalEntity } from '../entities/teamJoinApproval/teamJoinApproval';
import { AppPurpose } from './githubApps';
import { AppPurpose } from '../lib/github/appPurposes';
import { CacheDefault, getMaxAgeSeconds, getPageSize, Organization } from '.';
import {
IOperationsInstance,
IPurposefulGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
TeamJsonFormat,
throwIfNotCapable,
IOperationsUrls,
@ -25,7 +25,7 @@ import {
ICacheOptions,
throwIfNotGitHubCapable,
IPagedCacheOptions,
IGetAuthorizationHeader,
GetAuthorizationHeader,
IUpdateTeamMembershipOptions,
GitHubTeamRole,
ITeamMembershipRoleState,
@ -82,7 +82,7 @@ export class Team {
private _organization: Organization;
private _operations: IOperationsInstance;
private _getAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _getAuthorizationHeader: PurposefulGetAuthorizationHeader;
private _id: number;
@ -139,7 +139,7 @@ export class Team {
constructor(
organization: Organization,
entity,
getAuthorizationHeader: IPurposefulGetAuthorizationHeader,
getAuthorizationHeader: PurposefulGetAuthorizationHeader,
operations: IOperationsInstance
) {
if (!entity || !entity.id) {
@ -181,6 +181,7 @@ export class Team {
clone.corporateMetadata = {
isSystemTeam: this.isSystemTeam,
isBroadAccessTeam: this.isBroadAccessTeam,
isOpenAccessTeam: this.isOpenAccessTeam,
};
return clone;
}
@ -303,7 +304,7 @@ export class Team {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
AppPurpose.Data
) as IGetAuthorizationHeader;
) as GetAuthorizationHeader;
const teamEntities = await github.collections.getTeamChildTeams(
getAuthorizationHeader,
parameters,
@ -323,6 +324,15 @@ export class Team {
return res >= 0;
}
get isOpenAccessTeam(): boolean {
const teams = this._organization.openAccessTeams;
if (typeof this._id !== 'number') {
throw new Error('Team.id must be a number');
}
const res = teams.indexOf(this._id);
return res >= 0;
}
get isSystemTeam(): boolean {
const systemTeams = this._organization.systemTeamIds;
const res = systemTeams.indexOf(this._id);
@ -687,11 +697,8 @@ export class Team {
};
}
private authorize(purpose: AppPurpose): IGetAuthorizationHeader | string {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(
this,
purpose
) as IGetAuthorizationHeader;
private authorize(purpose: AppPurpose): GetAuthorizationHeader | string {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(this, purpose) as GetAuthorizationHeader;
return getAuthorizationHeader;
}
}

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

@ -1,5 +1,4 @@
{
"centralOperationsToken": "env://GITHUB_CENTRAL_OPERATIONS_TOKEN",
"publicAccessToken": "env://GITHUB_PUBLIC_OPERATIONS_TOKEN",
"primaryOrganizationId": "env://GITHUB_PRIMARY_ORGANIZATION_ID?type=integer"
}

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

@ -19,6 +19,7 @@ export type ConfigGitHubOrganization = {
teamAllReposRead: string; // | number
teamAllReposWrite: string; // | number
teamAllReposAdmin: string;
teamOpenAccess: string;
teamSudoers: string;
templates: string[];
onboarding: boolean;

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

@ -85,7 +85,7 @@ Here is a short overview about the meanings of the different parameters:
- **setupbycorporateusername** (string) - Username (from the corporate identity system) of the user who set up the organization
- **setupbycorporateid** (string) - Unique identifier (from the corporate identity system) for the user who set up the organization
- **setupbycorporatedisplayname** (string) - Display name (from the corporate identity system) for the user who set up the organization
- **specialteams** (object{specialTeam: string, teamId: integer}) - Special team configuration for the organization supported values for `specialTeam` types are: `everyone, sudo, globalSudo, systemWrite, systemRead, systemAdmin`. The `teamId` is the GitHub team ID for the special team.
- **specialteams** (object{specialTeam: string, teamId: integer}) - Special team configuration for the organization supported values for `specialTeam` types are: `everyone, sudo, globalSudo, systemWrite, systemRead, systemAdmin, openAccess`. The `teamId` is the GitHub team ID for the special team.
### Static settings

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

@ -29,8 +29,9 @@ export interface IBasicGitHubAppInstallation {
appPurposeId?: string;
}
export enum SpecialTeam {
export enum SystemTeam {
Everyone = 'everyone', // teamAllMembers
OpenAccess = 'openAccess',
Sudo = 'sudo', // teamSudoers
GlobalSudo = 'globalSudo', // teamPortalSudoers
SystemWrite = 'systemWrite', // teamAllReposWrite
@ -56,7 +57,7 @@ export enum OrganizationProperty {
}
export interface ISpecialTeam {
specialTeam: SpecialTeam;
specialTeam: SystemTeam;
teamId: number;
}
@ -158,6 +159,16 @@ export class OrganizationSetting implements IOrganizationSettingProperties {
return this.#ownerToken;
}
static CreateEmptyWithOldToken(token: string, notes: string, organizationId?: number) {
const settings = new OrganizationSetting();
settings.#ownerToken = token;
if (organizationId) {
settings.organizationId = organizationId;
}
settings.operationsNotes = notes;
return settings;
}
static CreateFromStaticSettings(staticSettings: ConfigGitHubOrganization): OrganizationSetting {
const clone = { ...staticSettings };
const settings = new OrganizationSetting();
@ -260,20 +271,33 @@ export class OrganizationSetting implements IOrganizationSettingProperties {
: [clone.teamAllMembers as any];
for (const value of arr) {
settings.specialTeams.push({
specialTeam: SpecialTeam.Everyone,
specialTeam: SystemTeam.Everyone,
teamId: Number(value),
});
}
}
delete clone.teamAllMembers;
if (clone.teamOpenAccess) {
const arr = Array.isArray(clone.teamOpenAccess)
? (clone.teamOpenAccess as any[])
: [clone.teamOpenAccess as any];
for (const value of arr) {
settings.specialTeams.push({
specialTeam: SystemTeam.OpenAccess,
teamId: Number(value),
});
}
}
delete clone.teamOpenAccess;
if (clone.teamAllReposRead) {
const arr = Array.isArray(clone.teamAllReposRead)
? (clone.teamAllReposRead as any[])
: [clone.teamAllReposRead as any];
for (const value of arr) {
settings.specialTeams.push({
specialTeam: SpecialTeam.SystemRead,
specialTeam: SystemTeam.SystemRead,
teamId: Number(value),
});
}
@ -286,7 +310,7 @@ export class OrganizationSetting implements IOrganizationSettingProperties {
: [clone.teamAllReposWrite as any];
for (const value of arr) {
settings.specialTeams.push({
specialTeam: SpecialTeam.SystemWrite,
specialTeam: SystemTeam.SystemWrite,
teamId: Number(value),
});
}
@ -299,7 +323,7 @@ export class OrganizationSetting implements IOrganizationSettingProperties {
: [clone.teamAllReposAdmin as any];
for (const value of arr) {
settings.specialTeams.push({
specialTeam: SpecialTeam.SystemAdmin,
specialTeam: SystemTeam.SystemAdmin,
teamId: Number(value),
});
}
@ -312,7 +336,7 @@ export class OrganizationSetting implements IOrganizationSettingProperties {
: [clone.teamSudoers as any];
for (const value of arr) {
settings.specialTeams.push({
specialTeam: SpecialTeam.Sudo,
specialTeam: SystemTeam.Sudo,
teamId: Number(value),
});
}
@ -325,7 +349,7 @@ export class OrganizationSetting implements IOrganizationSettingProperties {
: [clone.teamPortalSudoers as any];
for (const value of arr) {
settings.specialTeams.push({
specialTeam: SpecialTeam.GlobalSudo,
specialTeam: SystemTeam.GlobalSudo,
teamId: Number(value),
});
}

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

@ -18,9 +18,9 @@ export async function lockdownRepository(
const organization = repository.organization;
try {
const specialPermittedTeams = new Set([
...organization.specialRepositoryPermissionTeams.admin,
...organization.specialRepositoryPermissionTeams.write,
...organization.specialRepositoryPermissionTeams.read,
...organization.specialSystemTeams.admin,
...organization.specialSystemTeams.write,
...organization.specialSystemTeams.read,
]);
const teamPermissions = await repository.getTeamPermissions();
for (const tp of teamPermissions) {

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

@ -3,7 +3,13 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { CoreCapability, ICacheDefaultTimes, IPurposefulGetAuthorizationHeader, ICacheOptions } from '.';
import {
CoreCapability,
ICacheDefaultTimes,
PurposefulGetAuthorizationHeader,
ICacheOptions,
GetAuthorizationHeader,
} from '.';
import { IProviders, ICorporateLink, ICachedEmployeeInformation } from '..';
import { IRepositoryMetadataProvider } from '../../entities/repositoryMetadata/repositoryMetadataProvider';
import { RestLibrary } from '../../lib/github';
@ -78,8 +84,9 @@ export interface IOperationsLockdownFeatureFlags {
}
export interface IOperationsCentralOperationsToken {
getCentralOperationsToken(): IPurposefulGetAuthorizationHeader; // IGetAuthorizationHeader ?;
getAccountByUsername(username: string, options?: ICacheOptions): Promise<Account>;
getPublicReadOnlyStaticToken(): GetAuthorizationHeader;
getPublicAuthorizationToken(): GetAuthorizationHeader;
}
export function operationsIsCapable<T>(

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

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { AppPurposeTypes } from '../../business/githubApps';
import { AppPurposeTypes } from '../../lib/github/appPurposes';
export interface ICacheOptions {
backgroundRefresh?: any | null | undefined;
@ -14,25 +14,29 @@ export interface ICacheOptionsWithPurpose extends ICacheOptions {
purpose?: AppPurposeTypes;
}
export type WithOptionalPurpose = {
purpose?: AppPurposeTypes;
};
export interface IPagedCacheOptions extends ICacheOptions {
pageRequestDelay?: number | null | undefined; // FUTURE: could be a function, too
}
export interface IPurposefulGetAuthorizationHeader {
(purpose: AppPurposeTypes): Promise<IAuthorizationHeaderValue>;
}
export type PagedCacheOptionsWithPurpose = IPagedCacheOptions & WithOptionalPurpose;
export interface IGetAuthorizationHeader {
(): Promise<IAuthorizationHeaderValue>;
}
export type PurposefulGetAuthorizationHeader = (
purpose: AppPurposeTypes
) => Promise<AuthorizationHeaderValue>;
export interface IAuthorizationHeaderValue {
export type GetAuthorizationHeader = () => Promise<AuthorizationHeaderValue>;
export type AuthorizationHeaderValue = {
value: string;
purpose: AppPurposeTypes;
source?: string;
installationId?: number;
organizationName?: string;
}
};
export interface ICacheDefaultTimes {
orgReposStaleSeconds: number;
@ -58,6 +62,7 @@ export interface ICacheDefaultTimes {
teamDetailStaleSeconds: number;
orgRepoWebhooksStaleSeconds: number;
teamRepositoryPermissionStaleSeconds: number;
defaultStaleSeconds: number;
}
// These "core capabilities" were created when the GitHub operations

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

@ -7,9 +7,10 @@ import { ExecutionEnvironment } from '../../interfaces';
import { CreateError } from '../../transitional';
import Debug from 'debug';
import GitHubApplication from '../application';
import { Operations } from '../operations';
import { GitHubTokenManager } from './tokenManager';
import GitHubApplication from '../../business/application';
import { Operations, OperationsCore } from '../../business';
const debug = Debug('github:tokens');
export enum AppPurpose {
@ -167,6 +168,9 @@ const appPurposeToConfigurationName = {
};
export function getAppPurposeId(purpose: AppPurposeTypes) {
if (!purpose) {
return 'n/a';
}
if ((purpose as ICustomAppPurpose).isCustomAppPurpose === true) {
return (purpose as ICustomAppPurpose).id;
}
@ -266,6 +270,7 @@ export interface IGitHubAppConfiguration {
}
export interface IGitHubAppsOptions {
operations: OperationsCore;
// app: IReposApplication;
configurations: Map<AppPurposeTypes, IGitHubAppConfiguration>;
executionEnvironment: ExecutionEnvironment;

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

@ -7,10 +7,11 @@ import { request } from '@octokit/request';
import { createAppAuth, InstallationAccessTokenAuthentication } from '@octokit/auth-app';
import { AppAuthentication, AuthInterface } from '@octokit/auth-app/dist-types/types';
import { AppPurposeTypes, ICustomAppPurpose } from '.';
import { IAuthorizationHeaderValue } from '../../interfaces';
import { AppPurposeTypes, ICustomAppPurpose } from './appPurposes';
import { AuthorizationHeaderValue } from '../../interfaces';
import Debug from 'debug';
import { CreateError } from '../../transitional';
const debug = Debug('github:tokens');
interface IInstallationToken {
@ -27,6 +28,45 @@ interface IInstallationToken {
const InstallationTokenLifetimeMilliseconds = 1000 * 60 * 60;
const ValidityOffsetAfterNowMilliseconds = 1000 * 120; // how long to require validity in the future
export enum GitHubTokenType {
PersonalAccessToken = 'ghp',
OAuthAccessToken = 'gho',
UserToServerToken = 'ghu',
ServerToServerToken = 'ghs',
RefreshToken = 'ghr',
FineGrainedPersonalAccessToken = 'github_pat',
}
export const GitHubTokenTypes = [
GitHubTokenType.PersonalAccessToken,
GitHubTokenType.OAuthAccessToken,
GitHubTokenType.UserToServerToken,
GitHubTokenType.ServerToServerToken,
GitHubTokenType.RefreshToken,
GitHubTokenType.FineGrainedPersonalAccessToken,
];
export function getGitHubTokenTypeFromValue(value: string | AuthorizationHeaderValue): GitHubTokenType {
if (!value) {
throw CreateError.ParameterRequired('value');
}
if (typeof value === 'object') {
value = value.value;
} else if (typeof value !== 'string') {
throw CreateError.InvalidParameters('value must be a string or AuthorizationHeaderValue');
}
if (!value.startsWith('token ')) {
throw CreateError.InvalidParameters('value must start with "token "');
}
const tokenValue = value.substr(6);
for (const tokenType of GitHubTokenTypes) {
if (tokenValue.startsWith(tokenType)) {
return tokenType;
}
}
throw CreateError.InvalidParameters('value does not appear to be a GitHub token');
}
export class GitHubAppTokens {
#privateKey: string;
private _appId: number;
@ -39,23 +79,25 @@ export class GitHubAppTokens {
static CreateFromBase64EncodedFileString(
purpose: AppPurposeTypes,
slug: string,
friendlyName: string,
applicationId: number,
fileContents: string,
baseUrl?: string
): GitHubAppTokens {
const keyContents = Buffer.from(fileContents, 'base64').toString('utf8').replace(/\r\n/g, '\n');
return new GitHubAppTokens(purpose, friendlyName, applicationId, keyContents, baseUrl);
return new GitHubAppTokens(purpose, slug, friendlyName, applicationId, keyContents, baseUrl);
}
static CreateFromString(
purpose: AppPurposeTypes,
slug: string,
friendlyName: string,
applicationId: number,
value: string,
baseUrl?: string
): GitHubAppTokens {
return new GitHubAppTokens(purpose, friendlyName, applicationId, value, baseUrl);
return new GitHubAppTokens(purpose, slug, friendlyName, applicationId, value, baseUrl);
}
get appId() {
@ -68,6 +110,7 @@ export class GitHubAppTokens {
constructor(
purpose: AppPurposeTypes,
public slug: string,
public friendlyName: string,
appId: number,
privateKey: string,
@ -114,7 +157,7 @@ export class GitHubAppTokens {
async getInstallationToken(
installationId: number,
organizationName: string
): Promise<IAuthorizationHeaderValue> {
): Promise<AuthorizationHeaderValue> {
const now = new Date();
const requiredValidityPeriod = new Date(now.getTime() + ValidityOffsetAfterNowMilliseconds);
const latestToken = this.getLatestValidToken(installationId, requiredValidityPeriod);

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

@ -13,7 +13,7 @@ import { IRestResponse, flattenData } from './core';
import { CompositeApiContext, CompositeIntelligentEngine } from './composite';
import { Collaborator } from '../../business/collaborator';
import { Team } from '../../business/team';
import { IPagedCacheOptions, IGetAuthorizationHeader, IDictionary } from '../../interfaces';
import { IPagedCacheOptions, GetAuthorizationHeader, IDictionary } from '../../interfaces';
import { RestLibrary } from '.';
import { sleep } from '../../utils';
import GitHubApplication from '../../business/application';
@ -26,6 +26,8 @@ export interface IGetAppInstallationsParameters {
type WithPage<T> = T & { page?: number };
type WithOctokitRequest<T> = T & { octokitRequest?: string };
export type CollectionCopilotSeatsOptions = {
org: string;
per_page?: number;
@ -151,8 +153,56 @@ export class RestCollections {
this.githubCall = githubCall;
}
collectAllPages<ParametersType = any, EntityType = any>(
token: string | GetAuthorizationHeader,
collectionCacheKey: string,
octokitApiName: string,
parameters: ParametersType,
cacheOptions: IPagedCacheOptions,
fieldNamesToKeep?: string[] | WithSubPropertyReducer,
arrayReducePropertyName?: string
): Promise<EntityType[]> {
return this.generalizedCollectionWithFilter(
collectionCacheKey,
octokitApiName,
fieldNamesToKeep,
token,
parameters,
cacheOptions,
arrayReducePropertyName
);
}
collectAllPagesViaHttpGet<ParametersType = any, EntityType = any>(
token: string | GetAuthorizationHeader,
collectionCacheKey: string,
getRestUrl: string,
parameters: ParametersType,
cacheOptions: IPagedCacheOptions,
fieldNamesToKeep?: string[] | WithSubPropertyReducer,
arrayReducePropertyName?: string
): Promise<EntityType[]> {
const expandedOptions: WithOctokitRequest<ParametersType> = Object.assign(
{
octokitRequest: getRestUrl.startsWith('GET ') ? getRestUrl.substr(4) : getRestUrl,
},
parameters
);
return this.collectAllPages<ParametersType, EntityType>(
token,
collectionCacheKey,
'request',
expandedOptions,
cacheOptions,
fieldNamesToKeep,
arrayReducePropertyName
);
}
// ---
getOrgRepos(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -167,7 +217,7 @@ export class RestCollections {
}
getOrgTeams(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -182,7 +232,7 @@ export class RestCollections {
}
getTeamChildTeams(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -197,7 +247,7 @@ export class RestCollections {
}
getUserActivity(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -212,7 +262,7 @@ export class RestCollections {
}
getOrgMembers(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -227,7 +277,7 @@ export class RestCollections {
}
getOrganizationCopilotSeats(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options: CollectionCopilotSeatsOptions,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -252,7 +302,7 @@ export class RestCollections {
}
getAppInstallations(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
parameters: IGetAppInstallationsParameters,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -273,7 +323,7 @@ export class RestCollections {
}
getRepoIssues(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any[]> {
@ -288,7 +338,7 @@ export class RestCollections {
}
getRepoProjects(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any[]> {
@ -303,7 +353,7 @@ export class RestCollections {
}
getRepoTeams(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -318,7 +368,7 @@ export class RestCollections {
}
getRepoContributors(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -333,7 +383,7 @@ export class RestCollections {
}
getRepoCollaborators(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -348,7 +398,7 @@ export class RestCollections {
}
getRepoInvitations(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -363,7 +413,7 @@ export class RestCollections {
}
getRepoBranches(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -378,7 +428,7 @@ export class RestCollections {
}
getRepoPullRequests(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options: IListPullsParameters,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -393,7 +443,7 @@ export class RestCollections {
}
getTeamMembers(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -408,7 +458,7 @@ export class RestCollections {
}
getTeamRepos(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options,
cacheOptions: IPagedCacheOptions
): Promise<any> {
@ -423,7 +473,7 @@ export class RestCollections {
}
private async getGithubCollection<OptionsType>(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
methodName: string,
options: OptionsType,
cacheOptions: IPagedCacheOptions,
@ -515,7 +565,7 @@ export class RestCollections {
}
private async getFilteredGithubCollection<DataType, OptionsType>(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
methodName: string,
options: OptionsType,
cacheOptions: IPagedCacheOptions,
@ -580,7 +630,7 @@ export class RestCollections {
}
private async getFilteredGithubCollectionWithMetadataAnalysis<DataType, OptionsType>(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
methodName: string,
options: OptionsType,
cacheOptions: IPagedCacheOptions,
@ -632,7 +682,7 @@ export class RestCollections {
}
private generalizedCollectionMethod(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
apiName: string,
method,
options,
@ -650,7 +700,7 @@ export class RestCollections {
}
private getCollectionAndFilter<DataType, OptionsType>(
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options: OptionsType,
cacheOptions: IPagedCacheOptions,
githubClientMethod: string,
@ -658,7 +708,7 @@ export class RestCollections {
arrayReducePropertyName?: string
) {
const capturedThis = this;
return function (token: string | IGetAuthorizationHeader, options: OptionsType) {
return function (token: string | GetAuthorizationHeader, options: OptionsType) {
return capturedThis.getFilteredGithubCollectionWithMetadataAnalysis<DataType, OptionsType>(
token,
githubClientMethod,
@ -674,7 +724,7 @@ export class RestCollections {
name: string,
githubClientMethod: string,
propertiesToKeep: string[],
token: string | IGetAuthorizationHeader,
token: string | GetAuthorizationHeader,
options: OptionsType,
cacheOptions: IPagedCacheOptions,
arrayReducePropertyName?: string

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

@ -22,7 +22,7 @@ import {
IRestMetadata,
IRestResponse,
} from './core';
import { IGetAuthorizationHeader } from '../../interfaces';
import { GetAuthorizationHeader } from '../../interfaces';
import appPackage from '../../package.json';
@ -34,7 +34,7 @@ const acceleratedExpirationMinutes = 60; // 1 hour
export class CompositeApiContext extends ApiContext {
private _apiMethod: any;
private _apiTypePrefix: string;
private _token: string | IGetAuthorizationHeader;
private _token: string | GetAuthorizationHeader;
private _cacheValues: IApiContextCacheValues;
private _redisKeys: IApiContextRedisKeys;
@ -74,11 +74,11 @@ export class CompositeApiContext extends ApiContext {
return this._cacheValues;
}
get token(): string | IGetAuthorizationHeader {
get token(): string | GetAuthorizationHeader {
return this._token;
}
overrideToken(token: string | IGetAuthorizationHeader) {
overrideToken(token: string | GetAuthorizationHeader) {
this._token = token;
}

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

@ -8,7 +8,7 @@ import { randomUUID } from 'crypto';
import moment from 'moment';
import { RestLibrary } from '.';
import { IAuthorizationHeaderValue } from '../../interfaces';
import { AuthorizationHeaderValue } from '../../interfaces';
import { sleep } from '../../utils';
import cost from './cost';
@ -99,7 +99,7 @@ export abstract class ApiContext {
libraryContext: RestLibrary;
etag?: string;
tokenSource: IAuthorizationHeaderValue;
tokenSource: AuthorizationHeaderValue;
abstract get apiTypePrefix(): string;
abstract get cacheValues(): IApiContextCacheValues;

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

@ -11,10 +11,10 @@ import {
ICacheOptions,
IGetOrganizationMembersOptions,
IPagedCrossOrganizationCacheOptions,
IPurposefulGetAuthorizationHeader,
PurposefulGetAuthorizationHeader,
ITeamMembershipOptions,
} from '../../interfaces';
import { AppPurpose } from '../../business/githubApps';
import { AppPurpose } from './appPurposes';
interface IOrganizationsResponse extends IRestResponse {
orgs?: any;
@ -42,7 +42,7 @@ export class CrossOrganizationCollator {
}
async orgMembers(
orgsAndTokens: Map<string, IPurposefulGetAuthorizationHeader>,
orgsAndTokens: Map<string, PurposefulGetAuthorizationHeader>,
options: IGetOrganizationMembersOptions,
cacheOptions: ICacheOptions
): Promise<any> {
@ -57,13 +57,13 @@ export class CrossOrganizationCollator {
return flattenData(data);
}
async teams(orgsAndTokens: Map<string, IPurposefulGetAuthorizationHeader>, options, cacheOptions) {
async teams(orgsAndTokens: Map<string, PurposefulGetAuthorizationHeader>, options, cacheOptions) {
const allTeams = await this.getAllTeams(orgsAndTokens, options, cacheOptions);
return flattenData(allTeams);
}
async teamMembers(
orgsAndTokens: Map<string, IPurposefulGetAuthorizationHeader>,
orgsAndTokens: Map<string, PurposefulGetAuthorizationHeader>,
options: ITeamMembershipOptions,
cacheOptions: ICacheOptions
): Promise<any> {
@ -90,7 +90,7 @@ export class CrossOrganizationCollator {
}
async repos(
orgsAndTokens: Map<string, IPurposefulGetAuthorizationHeader>,
orgsAndTokens: Map<string, PurposefulGetAuthorizationHeader>,
options,
cacheOptions: ICacheOptions
): Promise<any> {
@ -99,7 +99,7 @@ export class CrossOrganizationCollator {
}
async repoCollaborators(
orgsAndTokens: Map<string, IPurposefulGetAuthorizationHeader>,
orgsAndTokens: Map<string, PurposefulGetAuthorizationHeader>,
options,
cacheOptions: ICacheOptions
): Promise<any> {
@ -126,7 +126,7 @@ export class CrossOrganizationCollator {
}
async repoTeams(
orgsAndTokens: Map<string, IPurposefulGetAuthorizationHeader>,
orgsAndTokens: Map<string, PurposefulGetAuthorizationHeader>,
options,
cacheOptions: ICacheOptions
): Promise<any> {
@ -173,7 +173,7 @@ export class CrossOrganizationCollator {
}
private async getCrossOrganizationMethod(
orgsAndTokens: Map<string, IPurposefulGetAuthorizationHeader>,
orgsAndTokens: Map<string, PurposefulGetAuthorizationHeader>,
apiName: string,
methodName: string,
options,
@ -242,7 +242,7 @@ export class CrossOrganizationCollator {
private crossOrganizationCollection(
capturedThis: CrossOrganizationCollator,
orgsAndTokens: Map<string, IPurposefulGetAuthorizationHeader>,
orgsAndTokens: Map<string, PurposefulGetAuthorizationHeader>,
options,
cacheOptions: IPagedCrossOrganizationCacheOptions,
innerKeyType,
@ -303,7 +303,7 @@ export class CrossOrganizationCollator {
const localOptions = Object.assign(localOptionsTarget, options);
delete localOptions.maxAgeSeconds;
delete localOptions.backgroundRefresh;
const token = orgsAndTokens.get(orgName.toLowerCase()) as IPurposefulGetAuthorizationHeader;
const token = orgsAndTokens.get(orgName.toLowerCase()) as PurposefulGetAuthorizationHeader;
if (!token) {
throw new Error(`No token available for the organization ${orgName}`);
}
@ -348,7 +348,7 @@ export class CrossOrganizationCollator {
}
private async getAllTeams(
orgsAndTokens: Map<string, IPurposefulGetAuthorizationHeader>,
orgsAndTokens: Map<string, PurposefulGetAuthorizationHeader>,
options,
cacheOptions: IPagedCrossOrganizationCacheOptions
): Promise<any> {
@ -364,7 +364,7 @@ export class CrossOrganizationCollator {
}
private async getAllRepos(
orgsAndTokens: Map<string, IPurposefulGetAuthorizationHeader>,
orgsAndTokens: Map<string, PurposefulGetAuthorizationHeader>,
options,
cacheOptions: IPagedCrossOrganizationCacheOptions
): Promise<any> {

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

@ -13,17 +13,26 @@ import { CompositeIntelligentEngine } from './composite';
import { RestCollections } from './collections';
import { CrossOrganizationCollator } from './crossOrganization';
import { LinkMethods } from './links';
import { IGetAuthorizationHeader, IAuthorizationHeaderValue } from '../../interfaces';
import { GetAuthorizationHeader, AuthorizationHeaderValue } from '../../interfaces';
import { ICacheHelper } from '../caching';
import { ICustomAppPurpose } from '../../business/githubApps';
import { ICustomAppPurpose } from './appPurposes';
import { CreateError } from '../../transitional';
export enum CacheMode {
ValidateCache = 'ValidateCache',
BackgroundRefresh = 'BackgroundRefresh',
}
export enum HttpMethod {
Get = 'GET',
Post = 'POST',
Put = 'PUT',
Patch = 'PATCH',
Delete = 'DELETE',
}
export interface IGitHubPostFunction {
(awaitToken: IGetAuthorizationHeader, api: string, parameters: any): Promise<any>;
(awaitToken: GetAuthorizationHeader, api: string, parameters: any): Promise<any>;
}
export type OctokitGraphqlOptions = {
@ -128,25 +137,27 @@ export class RestLibrary {
hasNextPage?: (any) => boolean;
private async resolveAuthorizationHeader(
authorizationHeader: IGetAuthorizationHeader | IAuthorizationHeaderValue | string
): Promise<string | IAuthorizationHeaderValue> {
authorizationHeader: GetAuthorizationHeader | AuthorizationHeaderValue | string
): Promise<string | AuthorizationHeaderValue> {
let authorizationValue = null;
try {
if (typeof authorizationHeader === 'string') {
if (!authorizationHeader) {
throw CreateError.InvalidParameters('No authorization header');
} else if (typeof authorizationHeader === 'string') {
authorizationValue = authorizationHeader as string;
} else if (typeof authorizationHeader === 'function') {
let asFunc = authorizationHeader as IGetAuthorizationHeader;
let resolved = asFunc.call(null) as Promise<IAuthorizationHeaderValue | string>;
let asFunc = authorizationHeader as GetAuthorizationHeader;
let resolved = asFunc.call(null) as Promise<AuthorizationHeaderValue | string>;
authorizationValue = await resolved;
if (typeof resolved === 'function') {
asFunc = resolved as IGetAuthorizationHeader;
resolved = asFunc.call(null) as Promise<IAuthorizationHeaderValue | string>;
asFunc = resolved as GetAuthorizationHeader;
resolved = asFunc.call(null) as Promise<AuthorizationHeaderValue | string>;
authorizationValue = await resolved;
}
} else if (authorizationHeader && authorizationHeader['value']) {
authorizationValue = authorizationHeader as IAuthorizationHeaderValue;
authorizationValue = authorizationHeader as AuthorizationHeaderValue;
} else {
throw new Error('Invalid resolveAuthorizationHeader');
throw CreateError.InvalidParameters('Unknown resolveAuthorizationHeader type');
}
} catch (getTokenError) {
console.dir(getTokenError);
@ -156,7 +167,7 @@ export class RestLibrary {
}
async call(
awaitToken: IGetAuthorizationHeader | IAuthorizationHeaderValue | string,
awaitToken: GetAuthorizationHeader | AuthorizationHeaderValue | string,
api: string,
options,
cacheOptions = null
@ -181,20 +192,32 @@ export class RestLibrary {
return result;
}
request(token, restEndpoint, parameters: any, cacheOptions): Promise<any> {
request(token: GetAuthorizationHeader | string, restEndpoint, parameters: any, cacheOptions): Promise<any> {
parameters = parameters || {};
parameters['octokitRequest'] = restEndpoint;
return this.call(token, 'request', parameters, cacheOptions);
}
requestAsPost(token, restEndpoint, parameters: any): Promise<any> {
requestAsPost(token: GetAuthorizationHeader | string, restEndpoint, parameters: any): Promise<any> {
parameters = parameters || {};
parameters['octokitRequest'] = restEndpoint;
return this.post(token, 'request', parameters);
}
restApi(
token: GetAuthorizationHeader | string,
httpMethod: HttpMethod,
restEndpoint: string,
parameters: any
): Promise<any> {
const requestUrlValue = `${httpMethod} ${restEndpoint}`;
return httpMethod === HttpMethod.Get
? this.request(token, requestUrlValue, parameters, {})
: this.requestAsPost(token, requestUrlValue, parameters);
}
graphql<T = any>(
token,
token: GetAuthorizationHeader | string,
query: string,
parameters: any,
graphqlOptions: OctokitGraphqlOptions = {}
@ -203,7 +226,7 @@ export class RestLibrary {
}
graphqlIteration<T = any>(
token,
token: GetAuthorizationHeader | string,
query: string,
parameters: any,
graphqlOptions: OctokitGraphqlOptions = {}
@ -228,7 +251,7 @@ export class RestLibrary {
return this.post(token, api, parameters);
}
async post(awaitToken: IGetAuthorizationHeader | string, api: string, options: any): Promise<any> {
async post(awaitToken: GetAuthorizationHeader | string, api: string, options: any): Promise<any> {
const method = restApi.IntelligentGitHubEngine.findLibraryMethod(this.github, api);
if (!options.headers) {
options.headers = {};
@ -239,14 +262,14 @@ export class RestLibrary {
delete options.allowEmptyResponse;
massageData = noDataMassage;
}
let diagnosticHeaderInformation: IAuthorizationHeaderValue = null;
let diagnosticHeaderInformation: AuthorizationHeaderValue = null;
if (!options.headers.authorization) {
const value = await this.resolveAuthorizationHeader(awaitToken);
if ((value as IAuthorizationHeaderValue)?.purpose) {
diagnosticHeaderInformation = value as IAuthorizationHeaderValue;
if ((value as AuthorizationHeaderValue)?.purpose) {
diagnosticHeaderInformation = value as AuthorizationHeaderValue;
}
options.headers.authorization =
typeof value === 'string' ? (value as string) : (value as IAuthorizationHeaderValue).value;
typeof value === 'string' ? (value as string) : (value as AuthorizationHeaderValue).value;
}
const diagnostic: Record<string, any> = {};
try {
@ -283,6 +306,10 @@ export class RestLibrary {
return finalized;
} catch (error) {
console.log(`API ${api} POST error: ${error.message}`);
if (error?.message?.includes('Unexpected end of JSON input')) {
console.log('Usually a unicorn and bad GitHub 500');
console.dir(error);
}
if (
error?.message?.includes('Resource not accessible by integration') ||
error?.message?.includes('Not Found')

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

@ -28,12 +28,12 @@ import { getEntityDefinitions, GitHubResponseType, ResponseBodyType } from './en
import appPackage from '../../package.json';
import { ErrorHelper } from '../../transitional';
import type { IGetAuthorizationHeader, IAuthorizationHeaderValue } from '../../interfaces';
import type { GetAuthorizationHeader, AuthorizationHeaderValue } from '../../interfaces';
import {
type IGitHubAppConfiguration,
getAppPurposeId,
tryGetAppPurposeAppConfiguration,
} from '../../business/githubApps';
} from './appPurposes';
const appVersion = appPackage.version;
@ -446,7 +446,7 @@ export class GitHubApiContext extends ApiContext {
private _apiMethod: any;
private _redisKeys: IApiContextRedisKeys;
private _cacheValues: IApiContextCacheValues;
private _token: string | IGetAuthorizationHeader | IAuthorizationHeaderValue;
private _token: string | GetAuthorizationHeader | AuthorizationHeaderValue;
public fakeLink?: IGitHubLink;
@ -472,7 +472,7 @@ export class GitHubApiContext extends ApiContext {
};
}
get token(): string | IGetAuthorizationHeader | IAuthorizationHeaderValue {
get token(): string | GetAuthorizationHeader | AuthorizationHeaderValue {
return this._token;
}
@ -511,9 +511,9 @@ export class GitHubApiContext extends ApiContext {
this.libraryContext = libraryContext;
}
overrideToken(token: string | IGetAuthorizationHeader | IAuthorizationHeaderValue) {
overrideToken(token: string | GetAuthorizationHeader | AuthorizationHeaderValue) {
if (token && token['value']) {
const asPair = token as IAuthorizationHeaderValue;
const asPair = token as AuthorizationHeaderValue;
this._token = asPair.value;
this.tokenSource = asPair;
} else if (typeof token === 'string') {

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

@ -15,20 +15,20 @@ import {
GitHubAppPurposes,
AppPurposeTypes,
getAppPurposeId,
} from '.';
} from './appPurposes';
import { GitHubAppTokens } from './appTokens';
import { IAuthorizationHeaderValue, NoCacheNoBackground } from '../../interfaces';
import { AuthorizationHeaderValue, GetAuthorizationHeader, NoCacheNoBackground } from '../../interfaces';
import { OrganizationSetting } from '../../entities/organizationSettings/organizationSetting';
import { readFileToText } from '../../utils';
import { Operations, OperationsCore, Organization } from '..';
import { Operations, OperationsCore, Organization } from '../../business';
import { CreateError } from '../../transitional';
export interface IGitHubRateLimit {
export type GitHubRateLimit = {
limit: number;
remaining: number;
reset: number;
used: number;
}
};
// Installation redirect format:
// /setup/app/APP_ID?installation_id=INSTALLATION_ID&setup_action=install
@ -53,7 +53,7 @@ export class GitHubTokenManager {
private _forceInstanceTokensToPurpose: AppPurposeTypes;
private _allowReadOnlyFallbackToOtherInstallations: boolean;
static RegisterManagerForOperations(operations: OperationsCore, manager: GitHubTokenManager) {
private static RegisterManagerForOperations(operations: OperationsCore, manager: GitHubTokenManager) {
GitHubTokenManager._managersForOperations.set(operations, manager);
}
@ -69,6 +69,11 @@ export class GitHubTokenManager {
this.#options = options;
GitHubTokenManager._forceBackgroundTokens =
executionEnvironment.isJob && !executionEnvironment.enableAllGitHubApps;
GitHubTokenManager.RegisterManagerForOperations(options.operations, this);
}
private operations() {
return this.#options.operations as Operations;
}
private getFallbackList(input: AppPurposeTypes[]) {
@ -147,6 +152,8 @@ export class GitHubTokenManager {
}
private getPurposeDisplayId(purpose: AppPurposeTypes) {
// is this identical to the method getAppPurposeId?
const asCustom = purpose as ICustomAppPurpose;
if (asCustom?.isCustomAppPurpose === true) {
return asCustom.id;
@ -189,9 +196,11 @@ export class GitHubTokenManager {
preferredPurpose: AppPurposeTypes,
organizationSettings: OrganizationSetting,
appAuthenticationType: GitHubAppAuthenticationType
): Promise<IAuthorizationHeaderValue> {
): Promise<AuthorizationHeaderValue> {
debug(
`getOrganizationAuthorizationHeader(${organizationName}, ${preferredPurpose}, ${appAuthenticationType})`
`getOrganizationAuthorizationHeader(${organizationName}, ${this.getPurposeDisplayId(
preferredPurpose
)}, ${appAuthenticationType})`
);
const installationIdPair = this.getPrioritizedOrganizationInstallationId(
preferredPurpose,
@ -200,8 +209,10 @@ export class GitHubTokenManager {
appAuthenticationType
);
if (!installationIdPair) {
throw new Error(
`GitHubTokenManager: organization ${organizationName} does not have a configured GitHub App installation, or, the installation information is not in this environment. The API preferred purpose was ${preferredPurpose} with the selection type ${appAuthenticationType}.`
throw CreateError.InvalidParameters(
`GitHubTokenManager: organization ${organizationName} does not have a configured GitHub App installation, or, the installation information is not in this environment. The API preferred purpose was ${getAppPurposeId(
preferredPurpose
)} with the selection type ${appAuthenticationType}.`
);
}
if (
@ -255,7 +266,9 @@ export class GitHubTokenManager {
);
const value = await app.getInstallationToken(installationIdPair.installationId, organizationName);
debug(
`returned installation ID pair: installationId=${value?.installationId}, source=${value?.source}, purpose=${value?.purpose}`
`returned installation ID pair: installationId=${value?.installationId}, source=${value?.source}, purpose=${this.getPurposeDisplayId(
value?.purpose
)}`
);
return value;
}
@ -264,7 +277,7 @@ export class GitHubTokenManager {
appId: number,
installationId: number,
organizationName: string
): Promise<IAuthorizationHeaderValue> {
): Promise<AuthorizationHeaderValue> {
const app = this._appsById.get(appId);
if (!app) {
throw new Error(`App ID=${appId} is not configured in this application instance`);
@ -272,10 +285,50 @@ export class GitHubTokenManager {
return app.getInstallationToken(installationId, organizationName);
}
getAppForPurpose(purpose: AppPurposeTypes) {
getAppForPurpose(purpose: AppPurposeTypes, organizationName?: string) {
const asCustomPurpose = this.getCustomPurpose(purpose);
if (asCustomPurpose?.getForOrganizationName) {
const configForOrganization = asCustomPurpose.getForOrganizationName(organizationName);
const appId = configForOrganization.appId;
if (appId) {
return this._appsById.get(appId);
}
} else if (asCustomPurpose?.getApplicationConfigurationForInitialization) {
const config = asCustomPurpose.getApplicationConfigurationForInitialization();
const appId = config.appId;
if (appId) {
return this._appsById.get(appId);
}
}
return this._apps.get(purpose);
}
getAnyConfiguredInstallationIdForAppId(operations: Operations, appId: number) {
const orgs = operations.getOrganizations();
for (const org of orgs) {
const settings = org.getDynamicSettings();
if (settings?.installations) {
for (const { appId: appConfiguredId, installationId } of settings.installations) {
if (appConfiguredId === appId) {
return { installationId, organizationName: org.name };
}
}
}
}
}
getAnyConfiguredInstallationIdForAnyApp(operations: Operations) {
const orgs = operations.getOrganizations();
for (const org of orgs) {
const settings = org.getDynamicSettings();
if (settings?.installations) {
for (const { installationId, appId } of settings.installations) {
return { installationId, organizationName: org.name, appId };
}
}
}
}
getInstallationIdForOrganization(purpose: AppPurposeTypes, organization: Organization) {
const settings = organization.getDynamicSettings();
if (settings?.installations) {
@ -286,7 +339,18 @@ export class GitHubTokenManager {
}
}
}
// CONSIDER: custom purposes could expose the installation ID here
const asCustomPurpose = this.getCustomPurpose(purpose);
if (asCustomPurpose?.getForOrganizationName) {
const configForOrganization = asCustomPurpose.getForOrganizationName(organization.name);
if (configForOrganization.slug) {
throw CreateError.NotImplemented(
`While a custom purpose is configured for the "${organization.name}" with the app ${configForOrganization.slug}, the installation ID is not yet available with this call. This is a known limitation.`
);
}
throw CreateError.NotImplemented(
`This custom purpose is not configured for the "${organization.name}" org with the app ${configForOrganization.slug}, the installation ID is not yet available with this call. This is a known limitation.`
);
}
if (!organization.hasDynamicSettings) {
throw CreateError.InvalidParameters(
`Organization ${organization.name} does not have dynamic settings or purpose-directed configuration`
@ -294,6 +358,74 @@ export class GitHubTokenManager {
}
}
getAuthorizationHeaderForAnyApp(): AuthorizationHeaderValue {
const anyConfigured = this.getAnyConfiguredInstallationIdForAnyApp(this.operations());
if (anyConfigured) {
return this.getInstallationAuthorizationHeader.bind(
this,
anyConfigured.appId,
anyConfigured.installationId,
anyConfigured.organizationName
);
}
throw CreateError.InvalidParameters('No configured applications available.');
}
async getAppInformation(purpose: AppPurposeTypes, organizationName?: string) {
const appTokens = this.getAppForPurpose(purpose, organizationName);
if (!appTokens) {
throw CreateError.InvalidParameters(`No app configured yet for purpose ${purpose}`);
}
const slug = appTokens.slug;
if (!slug) {
throw CreateError.InvalidParameters(`No slug configured for purpose ${purpose}`);
}
return this.getAppInformationBySlug(this.operations(), slug);
}
async getAppInformationBySlug(operations: Operations, slug: string) {
let appId: number = null;
for (const entry of this._appSlugs.entries()) {
if (entry[1] === slug) {
appId = entry[0];
break;
}
}
let authorizationHeader: GetAuthorizationHeader = null;
// Have the app call itself via the slug-based API (works if it's a private single-org app)
if (appId) {
const anyConfiguredForApp = this.getAnyConfiguredInstallationIdForAppId(operations, appId);
if (anyConfiguredForApp) {
authorizationHeader = this.getInstallationAuthorizationHeader.bind(
this,
appId,
anyConfiguredForApp.installationId,
anyConfiguredForApp.organizationName
);
}
}
// Call using any configured app
if (!authorizationHeader) {
const anyConfigured = this.getAnyConfiguredInstallationIdForAnyApp(operations);
if (anyConfigured) {
authorizationHeader = this.getInstallationAuthorizationHeader.bind(
this,
anyConfigured.appId,
anyConfigured.installationId,
anyConfigured.organizationName
);
}
}
// Fallback to a static token
if (!authorizationHeader) {
authorizationHeader = operations.getPublicReadOnlyStaticToken.bind(operations);
}
const value = await operations.github.post(authorizationHeader, 'apps.getBySlug', {
app_slug: slug,
});
return value;
}
async getRateLimitInformation(purpose: AppPurposeTypes, organization: Organization) {
const settings = organization.getDynamicSettings();
if (settings?.installations) {
@ -312,7 +444,7 @@ export class GitHubTokenManager {
NoCacheNoBackground
);
if (value?.rate) {
return value.rate as IGitHubRateLimit;
return value.rate as GitHubRateLimit;
}
console.warn(value);
throw CreateError.InvalidParameters('No rate limit information returned');
@ -348,12 +480,18 @@ export class GitHubTokenManager {
const allPurposes = GitHubAppPurposes.AllAvailableAppPurposes;
const fallbackPurposePriorities = this.getFallbackList(allPurposes);
const customPurposes = allPurposes.filter((p) => (p as ICustomAppPurpose).isCustomAppPurpose === true);
const matchingCustomPurposes =
(preferredPurpose as ICustomAppPurpose)?.isCustomAppPurpose === true
? customPurposes.filter(
(cp) => (cp as ICustomAppPurpose).id === (preferredPurpose as ICustomAppPurpose).id
)
: customPurposes;
let order =
this.isForcingBackgroundJobType() === true
? fallbackPurposePriorities
: [preferredPurpose, ...fallbackPurposePriorities];
if (appAuthenticationType === GitHubAppAuthenticationType.ForceSpecificInstallation) {
order = [preferredPurpose, ...customPurposes];
order = [preferredPurpose, ...matchingCustomPurposes];
}
for (const purpose of order) {
let customAppPurpose = purpose as ICustomAppPurpose;
@ -406,11 +544,12 @@ export class GitHubTokenManager {
// Not base64-encoded, use the CreateFromString method.
skipDecodingBase64 = true;
}
const slug = appConfig.slug;
const friendlyName = customPurpose?.name || appConfig.description || 'Unknown';
const baseUrl = appConfig.baseUrl;
const app = skipDecodingBase64
? GitHubAppTokens.CreateFromString(purpose, friendlyName, appId, key, baseUrl)
: GitHubAppTokens.CreateFromBase64EncodedFileString(purpose, friendlyName, appId, key, baseUrl);
? GitHubAppTokens.CreateFromString(purpose, slug, friendlyName, appId, key, baseUrl)
: GitHubAppTokens.CreateFromBase64EncodedFileString(purpose, slug, friendlyName, appId, key, baseUrl);
const hasCustomConfigurationByOrganization = customPurpose?.getForOrganizationName;
const standardPurpose = purpose as AppPurpose;
if (!hasCustomConfigurationByOrganization) {

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

@ -83,7 +83,7 @@ import type {
} from '../interfaces';
import initializeRepositoryProvider from '../entities/repository';
import { tryGetImmutableStorageProvider } from '../lib/immutable';
import { GitHubAppPurposes } from '../business/githubApps';
import { GitHubAppPurposes } from '../lib/github/appPurposes';
const DefaultApplicationProfile: IApplicationProfile = {
applicationName: 'Open Source Management Portal',
@ -319,16 +319,6 @@ async function initializeAsync(
}
function configureGitHubLibrary(cacheProvider: ICacheHelper, config): RestLibrary {
if (
config &&
config.github &&
config.github.operations &&
!config.github.operations.centralOperationsToken
) {
debug(
'WARNING: no central GitHub operations token is defined, some library operations may not succeed. ref: config.github.operations.centralOperationsToken var: GITHUB_CENTRAL_OPERATIONS_TOKEN'
);
}
const libraryContext = new RestLibrary({
config,
cacheProvider,

128
package-lock.json сгенерированный
Просмотреть файл

@ -14,15 +14,15 @@
"@azure/identity": "4.0.0",
"@azure/keyvault-secrets": "4.7.0",
"@azure/service-bus": "7.9.3",
"@azure/storage-blob": "12.16.0",
"@azure/storage-queue": "12.15.0",
"@azure/storage-blob": "12.17.0",
"@azure/storage-queue": "12.16.0",
"@octokit/auth-app": "6.0.1",
"@octokit/plugin-paginate-graphql": "4.0.0",
"@octokit/request": "8.1.4",
"@octokit/request": "8.1.5",
"@octokit/rest": "20.0.2",
"@primer/octicons": "19.8.0",
"app-root-path": "3.1.0",
"applicationinsights": "2.9.0",
"applicationinsights": "2.9.1",
"async-prompt": "1.0.1",
"axios": "1.6.1",
"basic-auth": "2.0.1",
@ -73,7 +73,7 @@
},
"devDependencies": {
"@types/cors": "2.8.16",
"@types/debug": "4.1.11",
"@types/debug": "4.1.12",
"@types/express": "4.17.21",
"@types/express-session": "1.17.10",
"@types/jest": "29.5.8",
@ -98,7 +98,7 @@
"cspell": "8.0.0",
"eslint": "8.53.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-n": "16.3.0",
"eslint-plugin-n": "16.3.1",
"eslint-plugin-prettier": "5.0.1",
"husky": "8.0.3",
"jest": "29.7.0",
@ -501,9 +501,9 @@
}
},
"node_modules/@azure/storage-blob": {
"version": "12.16.0",
"resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.16.0.tgz",
"integrity": "sha512-jz33rUSUGUB65FgYrTRgRDjG6hdPHwfvHe+g/UrwVG8MsyLqSxg9TaW7Yuhjxu1v1OZ5xam2NU6+IpCN0xJO8Q==",
"version": "12.17.0",
"resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.17.0.tgz",
"integrity": "sha512-sM4vpsCpcCApagRW5UIjQNlNylo02my2opgp0Emi8x888hZUvJ3dN69Oq20cEGXkMUWnoCrBaB0zyS3yeB87sQ==",
"dependencies": {
"@azure/abort-controller": "^1.0.0",
"@azure/core-http": "^3.0.0",
@ -531,9 +531,9 @@
}
},
"node_modules/@azure/storage-queue": {
"version": "12.15.0",
"resolved": "https://registry.npmjs.org/@azure/storage-queue/-/storage-queue-12.15.0.tgz",
"integrity": "sha512-qotyfMcmocaT1r9bfX7RyQQnkRJcTXGSG/m6LIchtEsNmqLu3ulVqkOlNM+O0XP9wwJ6Da1XQbWz5jQtrDe3DQ==",
"version": "12.16.0",
"resolved": "https://registry.npmjs.org/@azure/storage-queue/-/storage-queue-12.16.0.tgz",
"integrity": "sha512-HzwzMsNAh2PwLtx9WfXndj9elUr6duDFak5CjM6BxdWhLqf37VywPCWmEzjxuFfrq30c1T34+MjMXnN6YgqRUw==",
"dependencies": {
"@azure/abort-controller": "^1.0.0",
"@azure/core-http": "^3.0.0",
@ -2528,9 +2528,9 @@
}
},
"node_modules/@octokit/request": {
"version": "8.1.4",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.4.tgz",
"integrity": "sha512-M0aaFfpGPEKrg7XoA/gwgRvc9MSXHRO2Ioki1qrPDbl1e9YhjIwVoHE7HIKmv/m3idzldj//xBujcFNqGX6ENA==",
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.5.tgz",
"integrity": "sha512-zVKbNbX1xUluD9ZR4/tPs1yuYrK9xeh5fGZUXA6u04XGsTvomg0YO8/ZUC0FqAd49hAOEMFPAVUTh+2lBhOhLA==",
"dependencies": {
"@octokit/endpoint": "^9.0.0",
"@octokit/request-error": "^5.0.0",
@ -2944,9 +2944,9 @@
}
},
"node_modules/@types/debug": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.11.tgz",
"integrity": "sha512-R2qflTjHDs4CL6D/6TkqBeIHr54WzZfIxN729xvCNlYIVp2LknlnCro5Yo3frNaX2E5gO9pZ3/QAPVdGmu+q9w==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"dev": true,
"dependencies": {
"@types/ms": "*"
@ -3684,9 +3684,9 @@
}
},
"node_modules/applicationinsights": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.9.0.tgz",
"integrity": "sha512-W90WNjtvZ10GUInpkyNM0xBGe2qRYChHhdb44SE5KU7hXtCZLxs3IZjWw1gJINQem0qGAgtZlxrVvKPj5SlTbQ==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.9.1.tgz",
"integrity": "sha512-hrpe/OvHFZlq+SQERD1fxaYICyunxzEBh9SolJebzYnIXkyA9zxIR87dZAh+F3+weltbqdIP8W038cvtpMNhQg==",
"dependencies": {
"@azure/core-auth": "^1.5.0",
"@azure/core-rest-pipeline": "1.10.1",
@ -4180,6 +4180,18 @@
"node": ">=4"
}
},
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"dev": true,
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/builtins": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
@ -5701,9 +5713,9 @@
}
},
"node_modules/eslint-plugin-n": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.3.0.tgz",
"integrity": "sha512-/XZLH5CUXGK3laz3xYFNza8ZxLCq8ZNW6MsVw5z3d5hc2AwZzi0fPiySFZHQTdVDOHGs2cGv91aqzWmgBdq2gQ==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.3.1.tgz",
"integrity": "sha512-w46eDIkxQ2FaTHcey7G40eD+FhTXOdKudDXPUO2n9WNcslze/i/HT2qJ3GXjHngYSGDISIgPNhwGtgoix4zeOw==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
@ -5711,6 +5723,7 @@
"eslint-plugin-es-x": "^7.1.0",
"get-tsconfig": "^4.7.0",
"ignore": "^5.2.4",
"is-builtin-module": "^3.2.1",
"is-core-module": "^2.12.1",
"minimatch": "^3.1.2",
"resolve": "^1.22.2",
@ -6942,6 +6955,21 @@
"node": ">=4"
}
},
"node_modules/is-builtin-module": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
"dev": true,
"dependencies": {
"builtin-modules": "^3.3.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
@ -11818,9 +11846,9 @@
}
},
"@azure/storage-blob": {
"version": "12.16.0",
"resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.16.0.tgz",
"integrity": "sha512-jz33rUSUGUB65FgYrTRgRDjG6hdPHwfvHe+g/UrwVG8MsyLqSxg9TaW7Yuhjxu1v1OZ5xam2NU6+IpCN0xJO8Q==",
"version": "12.17.0",
"resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.17.0.tgz",
"integrity": "sha512-sM4vpsCpcCApagRW5UIjQNlNylo02my2opgp0Emi8x888hZUvJ3dN69Oq20cEGXkMUWnoCrBaB0zyS3yeB87sQ==",
"requires": {
"@azure/abort-controller": "^1.0.0",
"@azure/core-http": "^3.0.0",
@ -11844,9 +11872,9 @@
}
},
"@azure/storage-queue": {
"version": "12.15.0",
"resolved": "https://registry.npmjs.org/@azure/storage-queue/-/storage-queue-12.15.0.tgz",
"integrity": "sha512-qotyfMcmocaT1r9bfX7RyQQnkRJcTXGSG/m6LIchtEsNmqLu3ulVqkOlNM+O0XP9wwJ6Da1XQbWz5jQtrDe3DQ==",
"version": "12.16.0",
"resolved": "https://registry.npmjs.org/@azure/storage-queue/-/storage-queue-12.16.0.tgz",
"integrity": "sha512-HzwzMsNAh2PwLtx9WfXndj9elUr6duDFak5CjM6BxdWhLqf37VywPCWmEzjxuFfrq30c1T34+MjMXnN6YgqRUw==",
"requires": {
"@azure/abort-controller": "^1.0.0",
"@azure/core-http": "^3.0.0",
@ -13483,9 +13511,9 @@
}
},
"@octokit/request": {
"version": "8.1.4",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.4.tgz",
"integrity": "sha512-M0aaFfpGPEKrg7XoA/gwgRvc9MSXHRO2Ioki1qrPDbl1e9YhjIwVoHE7HIKmv/m3idzldj//xBujcFNqGX6ENA==",
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.5.tgz",
"integrity": "sha512-zVKbNbX1xUluD9ZR4/tPs1yuYrK9xeh5fGZUXA6u04XGsTvomg0YO8/ZUC0FqAd49hAOEMFPAVUTh+2lBhOhLA==",
"requires": {
"@octokit/endpoint": "^9.0.0",
"@octokit/request-error": "^5.0.0",
@ -13832,9 +13860,9 @@
}
},
"@types/debug": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.11.tgz",
"integrity": "sha512-R2qflTjHDs4CL6D/6TkqBeIHr54WzZfIxN729xvCNlYIVp2LknlnCro5Yo3frNaX2E5gO9pZ3/QAPVdGmu+q9w==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"dev": true,
"requires": {
"@types/ms": "*"
@ -14421,9 +14449,9 @@
"integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA=="
},
"applicationinsights": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.9.0.tgz",
"integrity": "sha512-W90WNjtvZ10GUInpkyNM0xBGe2qRYChHhdb44SE5KU7hXtCZLxs3IZjWw1gJINQem0qGAgtZlxrVvKPj5SlTbQ==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.9.1.tgz",
"integrity": "sha512-hrpe/OvHFZlq+SQERD1fxaYICyunxzEBh9SolJebzYnIXkyA9zxIR87dZAh+F3+weltbqdIP8W038cvtpMNhQg==",
"requires": {
"@azure/core-auth": "^1.5.0",
"@azure/core-rest-pipeline": "1.10.1",
@ -14792,6 +14820,12 @@
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="
},
"builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"dev": true
},
"builtins": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
@ -15892,9 +15926,9 @@
}
},
"eslint-plugin-n": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.3.0.tgz",
"integrity": "sha512-/XZLH5CUXGK3laz3xYFNza8ZxLCq8ZNW6MsVw5z3d5hc2AwZzi0fPiySFZHQTdVDOHGs2cGv91aqzWmgBdq2gQ==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.3.1.tgz",
"integrity": "sha512-w46eDIkxQ2FaTHcey7G40eD+FhTXOdKudDXPUO2n9WNcslze/i/HT2qJ3GXjHngYSGDISIgPNhwGtgoix4zeOw==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.4.0",
@ -15902,6 +15936,7 @@
"eslint-plugin-es-x": "^7.1.0",
"get-tsconfig": "^4.7.0",
"ignore": "^5.2.4",
"is-builtin-module": "^3.2.1",
"is-core-module": "^2.12.1",
"minimatch": "^3.1.2",
"resolve": "^1.22.2",
@ -16749,6 +16784,15 @@
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
},
"is-builtin-module": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
"dev": true,
"requires": {
"builtin-modules": "^3.3.0"
}
},
"is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",

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

@ -11,7 +11,7 @@ import { getProviders } from '../../transitional';
import {
OrganizationSetting,
IBasicGitHubAppInstallation,
SpecialTeam,
SystemTeam,
} from '../../entities/organizationSettings/organizationSetting';
import { IndividualContext } from '../../business/user';
import { Operations, Organization } from '../../business';
@ -296,22 +296,25 @@ router.post(
// explicitly now allowing globalSudo to be set here
throw new Error(`Unsupported team type: ${teamType}`);
}
let specialTeamType: SpecialTeam = null;
let specialTeamType: SystemTeam = null;
switch (teamType) {
case 'everyone':
specialTeamType = SpecialTeam.Everyone;
specialTeamType = SystemTeam.Everyone;
break;
case 'openAccess':
specialTeamType = SystemTeam.OpenAccess;
break;
case 'systemAdmin':
specialTeamType = SpecialTeam.SystemAdmin;
specialTeamType = SystemTeam.SystemAdmin;
break;
case 'systemWrite':
specialTeamType = SpecialTeam.SystemWrite;
specialTeamType = SystemTeam.SystemWrite;
break;
case 'systemRead':
specialTeamType = SpecialTeam.SystemRead;
specialTeamType = SystemTeam.SystemRead;
break;
case 'sudo':
specialTeamType = SpecialTeam.Sudo;
specialTeamType = SystemTeam.Sudo;
break;
// case 'globalSudo':
// specialTeamType = SpecialTeam.GlobalSudo;

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

@ -386,7 +386,7 @@ export async function calculateGroupedPermissionsViewForRepository(repository: R
collaborators, // Collaborator[]
outsideCollaborators, // Collaborator[]
} = await calculateRepoPermissions(organization, repository);
const systemTeams = combineAllTeams(organization.specialRepositoryPermissionTeams); // number[]
const systemTeams = combineAllTeams(organization.specialSystemTeams); // number[]
const teamBasedPermissions = consolidateTeamPermissions(permissions, systemTeams); // busted?
const groupedPermissions = slicePermissionsForView(
filterSystemTeams(teamsFilterType.systemTeamsExcluded, systemTeams, permissions)
@ -443,7 +443,7 @@ router.get(
organization,
repository
);
const systemTeams = combineAllTeams(organization.specialRepositoryPermissionTeams);
const systemTeams = combineAllTeams(organization.specialSystemTeams);
const teamBasedPermissions = consolidateTeamPermissions(permissions, systemTeams);
const title = `${repository.name} - Repository`;
const details = await repository.organization.getDetails();

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

@ -155,10 +155,11 @@ router.get(
asyncHandler(async function (req: ILocalRequest, res: Response, next: NextFunction) {
const team2 = req.team2 as Team;
const organization = req.organization as Organization;
// The broad access "all members" team is always open for automatic joining without
// approval. This short circuit is to show that option.
const broadAccessTeams = new Set(organization.broadAccessTeams);
if (broadAccessTeams.has(team2.id)) {
// The broad access "all members" team and any "easy access" teams are
// always open for automatic joining without approval. This short circuit
// is to show that option.
const allowSelfJoinTeams = new Set([...organization.broadAccessTeams, ...organization.openAccessTeams]);
if (allowSelfJoinTeams.has(team2.id)) {
req.individualContext.webContext.render({
view: 'org/team/join',
title: `Join ${team2.name}`,
@ -255,7 +256,8 @@ export async function submitTeamJoinRequest(
): Promise<ITeamJoinRequestSubmitOutcome> {
const { approvalProvider, config, graphProvider, mailProvider, insights, operations } = providers;
const organization = team.organization;
const broadAccessTeams = new Set(organization.broadAccessTeams);
const { broadAccessTeams, openAccessTeams } = organization;
const allowSelfJoinTeams = new Set([...broadAccessTeams, ...openAccessTeams]);
if (!approvalProvider) {
return { error: new Error('No approval provider available') };
}
@ -263,8 +265,17 @@ export async function submitTeamJoinRequest(
if (!username) {
return { error: new Error('Active context required') };
}
if (broadAccessTeams.has(team.id)) {
if (allowSelfJoinTeams.has(team.id)) {
try {
const eventName = broadAccessTeams.includes(team.id) ? 'JoinBroadAccessTeam' : 'JoinOpenAccessTeam';
insights?.trackEvent({
name: eventName,
properties: {
organization: organization.name,
id: team.id,
slug: team.slug,
},
});
await team.addMembership(username);
} catch (error) {
insights?.trackEvent({

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

@ -31,7 +31,7 @@ interface ICustomDataEventName {
export default class AutomaticTeamsWebhookProcessor implements WebhookProcessor {
processOrgSpecialTeams(organization: Organization) {
const specialTeams = organization.specialRepositoryPermissionTeams;
const specialTeams = organization.specialSystemTeams;
const specials = [];
const specialTeamIds = new Set<number>();
const specialTeamLevels = new Map<number, string>();