Merge branch 'master' of github.com:backstage/backstage into github-installations-limit
This commit is contained in:
@@ -51,8 +51,7 @@ describe('AzureIntegration', () => {
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: '../a.yaml',
|
||||
base:
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml',
|
||||
base: 'https://dev.azure.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml',
|
||||
}),
|
||||
).toBe(
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml',
|
||||
@@ -61,8 +60,7 @@ describe('AzureIntegration', () => {
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: '/a.yaml',
|
||||
base:
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml',
|
||||
base: 'https://dev.azure.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml',
|
||||
lineNumber: 14,
|
||||
}),
|
||||
).toBe(
|
||||
@@ -81,8 +79,7 @@ describe('AzureIntegration', () => {
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: 'https://absolute.com/path',
|
||||
base:
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml',
|
||||
base: 'https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml',
|
||||
}),
|
||||
).toBe('https://absolute.com/path');
|
||||
});
|
||||
|
||||
@@ -43,26 +43,22 @@ describe('azure core', () => {
|
||||
describe('getAzureFileFetchUrl', () => {
|
||||
it.each([
|
||||
{
|
||||
url:
|
||||
'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
|
||||
url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
|
||||
result:
|
||||
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master',
|
||||
},
|
||||
{
|
||||
url:
|
||||
'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
|
||||
url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
|
||||
result:
|
||||
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml',
|
||||
},
|
||||
{
|
||||
url:
|
||||
'https://api.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
|
||||
url: 'https://api.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
|
||||
result:
|
||||
'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml',
|
||||
},
|
||||
{
|
||||
url:
|
||||
'https://api.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
|
||||
url: 'https://api.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
|
||||
result:
|
||||
'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master',
|
||||
},
|
||||
|
||||
@@ -31,13 +31,8 @@ export function getAzureFileFetchUrl(url: string): string {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
const [
|
||||
empty,
|
||||
userOrOrg,
|
||||
project,
|
||||
srcKeyword,
|
||||
repoName,
|
||||
] = parsedUrl.pathname.split('/');
|
||||
const [empty, userOrOrg, project, srcKeyword, repoName] =
|
||||
parsedUrl.pathname.split('/');
|
||||
|
||||
const path = parsedUrl.searchParams.get('path') || '';
|
||||
const ref = parsedUrl.searchParams.get('version')?.substr(2);
|
||||
@@ -117,13 +112,8 @@ export function getAzureCommitsUrl(url: string): string {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
const [
|
||||
empty,
|
||||
userOrOrg,
|
||||
project,
|
||||
srcKeyword,
|
||||
repoName,
|
||||
] = parsedUrl.pathname.split('/');
|
||||
const [empty, userOrOrg, project, srcKeyword, repoName] =
|
||||
parsedUrl.pathname.split('/');
|
||||
|
||||
// Remove the "GB" from "GBmain" for example.
|
||||
const ref = parsedUrl.searchParams.get('version')?.substr(2);
|
||||
|
||||
@@ -56,8 +56,7 @@ describe('GitHubIntegration', () => {
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: '../a.yaml',
|
||||
base:
|
||||
'https://github.com/backstage/backstage/blob/master/test/README.md',
|
||||
base: 'https://github.com/backstage/backstage/blob/master/test/README.md',
|
||||
lineNumber: 17,
|
||||
}),
|
||||
).toBe('https://github.com/backstage/backstage/tree/master/a.yaml#L17');
|
||||
@@ -65,8 +64,7 @@ describe('GitHubIntegration', () => {
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: './',
|
||||
base:
|
||||
'https://github.com/backstage/backstage/blob/master/test/README.md',
|
||||
base: 'https://github.com/backstage/backstage/blob/master/test/README.md',
|
||||
}),
|
||||
).toBe('https://github.com/backstage/backstage/tree/master/test/');
|
||||
});
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('GithubCredentialsProvider tests', () => {
|
||||
expect(token).toEqual('secret_token');
|
||||
});
|
||||
|
||||
it('should fail to issue tokens for an organization when the app is installed for a single repo', async () => {
|
||||
it('should not fail to issue tokens for an organization when the app is installed for a single repo', async () => {
|
||||
octokit.apps.listInstallations.mockResolvedValue({
|
||||
headers: {
|
||||
etag: '123',
|
||||
@@ -156,13 +156,12 @@ describe('GithubCredentialsProvider tests', () => {
|
||||
},
|
||||
} as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']);
|
||||
|
||||
await expect(
|
||||
github.getCredentials({
|
||||
url: 'https://github.com/backstage',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
'The Backstage GitHub application used in the backstage organization must be installed for the entire organization to be able to issue credentials without a specified repository.',
|
||||
);
|
||||
const { token, headers } = await github.getCredentials({
|
||||
url: 'https://github.com/backstage',
|
||||
});
|
||||
const expectedToken = 'secret_token';
|
||||
expect(headers).toEqual({ Authorization: `Bearer ${expectedToken}` });
|
||||
expect(token).toEqual('secret_token');
|
||||
});
|
||||
|
||||
it('should throw if the app is suspended', async () => {
|
||||
@@ -232,9 +231,9 @@ describe('GithubCredentialsProvider tests', () => {
|
||||
],
|
||||
token: 'hardcoded_token',
|
||||
});
|
||||
octokit.apps.listInstallations.mockResolvedValue(({
|
||||
octokit.apps.listInstallations.mockResolvedValue({
|
||||
data: [],
|
||||
} as unknown) as RestEndpointMethodTypes['apps']['listInstallations']['response']);
|
||||
} as unknown as RestEndpointMethodTypes['apps']['listInstallations']['response']);
|
||||
|
||||
await expect(
|
||||
githubProvider.getCredentials({
|
||||
|
||||
@@ -23,7 +23,6 @@ import { DateTime } from 'luxon';
|
||||
type InstallationData = {
|
||||
installationId: number;
|
||||
suspended: boolean;
|
||||
repositorySelection: 'selected' | 'all';
|
||||
};
|
||||
|
||||
class Cache {
|
||||
@@ -87,45 +86,41 @@ class GithubAppManager {
|
||||
owner: string,
|
||||
repo?: string,
|
||||
): Promise<{ accessToken: string }> {
|
||||
const {
|
||||
installationId,
|
||||
suspended,
|
||||
repositorySelection,
|
||||
} = await this.getInstallationData(owner);
|
||||
const { installationId, suspended } = await this.getInstallationData(owner);
|
||||
if (this.allowedInstallationOwners) {
|
||||
if (!this.allowedInstallationOwners?.includes(owner)) {
|
||||
throw new Error(
|
||||
`The GitHub application for ${[owner, repo]
|
||||
.filter(Boolean)
|
||||
.join(
|
||||
'/',
|
||||
)} is not included in the allowed installation list (${installationId}).`,
|
||||
`The GitHub application for ${owner} is not included in the allowed installation list (${installationId}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (suspended) {
|
||||
throw new Error(
|
||||
`The GitHub application for ${[owner, repo]
|
||||
.filter(Boolean)
|
||||
.join('/')} is suspended`,
|
||||
);
|
||||
}
|
||||
if (repositorySelection !== 'all' && !repo) {
|
||||
throw new Error(
|
||||
`The Backstage GitHub application used in the ${owner} organization must be installed for the entire organization to be able to issue credentials without a specified repository.`,
|
||||
);
|
||||
throw new Error(`The GitHub application for ${owner} is suspended`);
|
||||
}
|
||||
|
||||
const cacheKey = !repo ? owner : `${owner}/${repo}`;
|
||||
const repositories = repositorySelection !== 'all' ? [repo!] : undefined;
|
||||
const cacheKey = repo ? `${owner}/${repo}` : owner;
|
||||
|
||||
// Go and grab an access token for the app scoped to a repository if provided, if not use the organisation installation.
|
||||
return this.cache.getOrCreateToken(cacheKey, async () => {
|
||||
const result = await this.appClient.apps.createInstallationAccessToken({
|
||||
installation_id: installationId,
|
||||
headers: HEADERS,
|
||||
repositories,
|
||||
});
|
||||
if (repo && result.data.repository_selection === 'selected') {
|
||||
const installationClient = new Octokit({
|
||||
auth: result.data.token,
|
||||
});
|
||||
const repos =
|
||||
await installationClient.apps.listReposAccessibleToInstallation();
|
||||
const hasRepo = repos.data.repositories.some(repository => {
|
||||
return repository.name === repo;
|
||||
});
|
||||
if (!hasRepo) {
|
||||
throw new Error(
|
||||
`The Backstage GitHub application used in the ${owner} organization does not have access to a repository with the name ${repo}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
token: result.data.token,
|
||||
expiresAt: DateTime.fromISO(result.data.expires_at),
|
||||
@@ -148,7 +143,6 @@ class GithubAppManager {
|
||||
return {
|
||||
installationId: installation.id,
|
||||
suspended: Boolean(installation.suspended_by),
|
||||
repositorySelection: installation.repository_selection,
|
||||
};
|
||||
}
|
||||
const notFoundError = new Error(
|
||||
|
||||
@@ -52,37 +52,32 @@ describe('gitlab core', () => {
|
||||
// Project URLs
|
||||
{
|
||||
config: configWithNoToken,
|
||||
url:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
|
||||
url: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
|
||||
result:
|
||||
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
},
|
||||
{
|
||||
config: configWithNoToken,
|
||||
// Works with non URI encoded link
|
||||
url:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file with spaces.yaml',
|
||||
url: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file with spaces.yaml',
|
||||
result:
|
||||
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile%20with%20spaces.yaml/raw?ref=branch',
|
||||
},
|
||||
{
|
||||
config: configWithNoToken,
|
||||
url:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml',
|
||||
url: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml',
|
||||
result:
|
||||
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
},
|
||||
{
|
||||
config: configWithToken,
|
||||
url:
|
||||
'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml',
|
||||
url: 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml',
|
||||
result:
|
||||
'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
},
|
||||
{
|
||||
config: configWithNoToken,
|
||||
url:
|
||||
'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml', // Repo not in subgroup
|
||||
url: 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml', // Repo not in subgroup
|
||||
result:
|
||||
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
},
|
||||
|
||||
@@ -68,13 +68,8 @@ export function buildRawUrl(target: string): URL {
|
||||
try {
|
||||
const url = new URL(target);
|
||||
|
||||
const [
|
||||
empty,
|
||||
userOrOrg,
|
||||
repoName,
|
||||
blobKeyword,
|
||||
...restOfPath
|
||||
] = url.pathname.split('/');
|
||||
const [empty, userOrOrg, repoName, blobKeyword, ...restOfPath] =
|
||||
url.pathname.split('/');
|
||||
|
||||
if (
|
||||
empty !== '' ||
|
||||
|
||||
@@ -57,8 +57,7 @@ describe('defaultScmResolveUrl', () => {
|
||||
expect(
|
||||
defaultScmResolveUrl({
|
||||
url: './b.yaml',
|
||||
base:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
|
||||
base: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
|
||||
}),
|
||||
).toBe(
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml',
|
||||
@@ -67,8 +66,7 @@ describe('defaultScmResolveUrl', () => {
|
||||
expect(
|
||||
defaultScmResolveUrl({
|
||||
url: './b.yaml',
|
||||
base:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
|
||||
base: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
|
||||
}),
|
||||
).toBe(
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml?at=master',
|
||||
@@ -77,8 +75,7 @@ describe('defaultScmResolveUrl', () => {
|
||||
expect(
|
||||
defaultScmResolveUrl({
|
||||
url: 'b.yaml',
|
||||
base:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
|
||||
base: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
|
||||
}),
|
||||
).toBe(
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml',
|
||||
@@ -89,8 +86,7 @@ describe('defaultScmResolveUrl', () => {
|
||||
expect(
|
||||
defaultScmResolveUrl({
|
||||
url: '/other/b.yaml',
|
||||
base:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
|
||||
base: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
|
||||
}),
|
||||
).toBe(
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/other/b.yaml',
|
||||
@@ -99,8 +95,7 @@ describe('defaultScmResolveUrl', () => {
|
||||
expect(
|
||||
defaultScmResolveUrl({
|
||||
url: '/other/b.yaml',
|
||||
base:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
|
||||
base: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
|
||||
}),
|
||||
).toBe(
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/other/b.yaml?at=master',
|
||||
@@ -111,8 +106,7 @@ describe('defaultScmResolveUrl', () => {
|
||||
expect(
|
||||
defaultScmResolveUrl({
|
||||
url: './b.yaml',
|
||||
base:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
|
||||
base: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
|
||||
lineNumber: 11,
|
||||
}),
|
||||
).toBe(
|
||||
@@ -122,8 +116,7 @@ describe('defaultScmResolveUrl', () => {
|
||||
expect(
|
||||
defaultScmResolveUrl({
|
||||
url: 'b.yaml',
|
||||
base:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
|
||||
base: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
|
||||
lineNumber: 12,
|
||||
}),
|
||||
).toBe(
|
||||
@@ -133,8 +126,7 @@ describe('defaultScmResolveUrl', () => {
|
||||
expect(
|
||||
defaultScmResolveUrl({
|
||||
url: '/other/b.yaml',
|
||||
base:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
|
||||
base: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
|
||||
lineNumber: 13,
|
||||
}),
|
||||
).toBe(
|
||||
@@ -144,8 +136,7 @@ describe('defaultScmResolveUrl', () => {
|
||||
expect(
|
||||
defaultScmResolveUrl({
|
||||
url: '/other/b.yaml',
|
||||
base:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
|
||||
base: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
|
||||
lineNumber: 14,
|
||||
}),
|
||||
).toBe(
|
||||
@@ -157,8 +148,7 @@ describe('defaultScmResolveUrl', () => {
|
||||
expect(
|
||||
defaultScmResolveUrl({
|
||||
url: 'https://b.com/b.yaml',
|
||||
base:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
|
||||
base: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
|
||||
}),
|
||||
).toBe('https://b.com/b.yaml');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user