Merge pull request #7002 from backstage/rugvip/scm-auth

[PRFC] integration-react: add ScmAuthApi + implementation
This commit is contained in:
Patrik Oldsberg
2021-09-23 15:52:27 +02:00
committed by GitHub
16 changed files with 864 additions and 22 deletions
+40
View File
@@ -0,0 +1,40 @@
---
'@backstage/create-app': patch
---
Added the default `ScmAuth` implementation to the app.
To apply this change to an existing app, head to `packages/app/apis.ts`, import `ScmAuth` from `@backstage/integration-react`, and add a `ScmAuth.createDefaultApiFactory()` to your list of APIs:
```diff
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
+ ScmAuth,
} from '@backstage/integration-react';
export const apis: AnyApiFactory[] = [
...
+ ScmAuth.createDefaultApiFactory(),
...
];
```
If you have integrations towards SCM providers other than the default ones (github.com, gitlab.com, etc.), you will want to create a custom `ScmAuth` factory instead, for example like this:
```ts
createApiFactory({
api: scmAuthApiRef,
deps: {
gheAuthApi: gheAuthApiRef,
githubAuthApi: githubAuthApiRef,
},
factory: ({ githubAuthApi, gheAuthApi }) =>
ScmAuth.merge(
ScmAuth.forGithub(githubAuthApi),
ScmAuth.forGithub(gheAuthApi, {
host: 'ghe.example.com',
}),
),
});
```
+36
View File
@@ -0,0 +1,36 @@
---
'@backstage/plugin-catalog-import': minor
---
Switched to using the `ScmAuthApi` for authentication rather than GitHub auth. If you are instantiating your `CatalogImportClient` manually you now need to pass in an instance of `ScmAuthApi` instead.
Also be sure to register the `scmAuthApiRef` from the `@backstage/integration-react` in your app:
```ts
import { ScmAuth } from '@backstage/integration-react';
// in packages/app/apis.ts
const apis = [
// ... other APIs
ScmAuth.createDefaultApiFactory();
// OR
createApiFactory({
api: scmAuthApiRef,
deps: {
gheAuthApi: gheAuthApiRef,
githubAuthApi: githubAuthApiRef,
},
factory: ({ githubAuthApi, gheAuthApi }) =>
ScmAuth.merge(
ScmAuth.forGithub(githubAuthApi),
ScmAuth.forGithub(gheAuthApi, {
host: 'ghe.example.com',
}),
),
});
]
```
+89
View File
@@ -0,0 +1,89 @@
---
'@backstage/integration-react': patch
---
Added `ScmAuthApi` along with the implementation `ScmAuth`. The `ScmAuthApi` provides methods for client-side authentication towards multiple different source code management services simultaneously.
When requesting credentials you supply a URL along with the same options as the other `OAuthApi`s, and optionally a request for additional high-level scopes.
For example like this:
```ts
const { token } = await scmAuthApi.getCredentials({
url: 'https://ghe.example.com/backstage/backstage',
additionalScope: {
repoWrite: true,
},
});
```
The instantiation of the API can either be done with a default factory that adds support for the public providers (github.com, gitlab.com, etc.):
```ts
// in packages/app/apis.ts
ScmAuth.createDefaultApiFactory();
```
Or with a more custom setup that can add support for additional providers, for example like this:
```ts
createApiFactory({
api: scmAuthApiRef,
deps: {
gheAuthApi: gheAuthApiRef,
githubAuthApi: githubAuthApiRef,
},
factory: ({ githubAuthApi, gheAuthApi }) =>
ScmAuth.merge(
ScmAuth.forGithub(githubAuthApi),
ScmAuth.forGithub(gheAuthApi, {
host: 'ghe.example.com',
}),
),
});
```
The additional `gheAuthApiRef` utility API can be defined either inside the app itself if it's only used for this purpose, for inside an internal common package for APIs, such as `@internal/apis`:
```ts
const gheAuthApiRef: ApiRef<OAuthApi & ProfileInfoApi & SessionApi> =
createApiRef({
id: 'internal.auth.ghe',
});
```
And then implemented using the `GithubAuth` class from `@backstage/core-app-api`:
```ts
createApiFactory({
api: githubAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GithubAuth.create({
provider: {
id: 'ghe',
icon: ...,
title: 'GHE'
},
discoveryApi,
oauthRequestApi,
defaultScopes: ['read:user'],
environment: configApi.getOptionalString('auth.environment'),
}),
})
```
Finally you also need to add and configure another GitHub provider to the `auth-backend` using the provider ID `ghe`:
```ts
// Add the following options to `createRouter` in packages/backend/src/plugins/auth.ts
providerFactories: {
ghe: createGithubProvider(),
},
```
Other providers follow the same steps, but you will want to use the appropriate auth API implementation in the frontend, such as for example `GitlabAuth`.
+3
View File
@@ -17,6 +17,7 @@
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
ScmAuth,
} from '@backstage/integration-react';
import {
costInsightsApiRef,
@@ -41,6 +42,8 @@ export const apis: AnyApiFactory[] = [
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
ScmAuth.createDefaultApiFactory(),
createApiFactory({
api: graphQlBrowseApiRef,
deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef },
@@ -1,6 +1,7 @@
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
ScmAuth,
} from '@backstage/integration-react';
import {
AnyApiFactory,
@@ -14,4 +15,5 @@ export const apis: AnyApiFactory[] = [
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
ScmAuth.createDefaultApiFactory(),
];
+86
View File
@@ -5,9 +5,95 @@
```ts
/// <reference types="react" />
import { ApiFactory } from '@backstage/core-plugin-api';
import { ApiRef } from '@backstage/core-plugin-api';
import { AuthRequestOptions } from '@backstage/core-plugin-api';
import { BackstageIdentityApi } from '@backstage/core-plugin-api';
import { Config } from '@backstage/config';
import { OAuthApi } from '@backstage/core-plugin-api';
import { OpenIdConnectApi } from '@backstage/core-plugin-api';
import { ProfileInfoApi } from '@backstage/core-plugin-api';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { SessionApi } from '@backstage/core-plugin-api';
// @public
export class ScmAuth implements ScmAuthApi {
static createDefaultApiFactory(): ApiFactory<
ScmAuthApi,
ScmAuthApi,
{
github: OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi;
gitlab: OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi;
azure: OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionApi;
}
>;
static forAuthApi(
authApi: OAuthApi,
options: {
host: string;
scopeMapping: {
default: string[];
repoWrite: string[];
};
},
): ScmAuth;
static forAzure(
microsoftAuthApi: OAuthApi,
options?: {
host?: string;
},
): ScmAuth;
static forBitbucket(
bitbucketAuthApi: OAuthApi,
options?: {
host?: string;
},
): ScmAuth;
static forGithub(
githubAuthApi: OAuthApi,
options?: {
host?: string;
},
): ScmAuth;
static forGitlab(
gitlabAuthApi: OAuthApi,
options?: {
host?: string;
},
): ScmAuth;
// (undocumented)
getCredentials(options: ScmAuthTokenOptions): Promise<ScmAuthTokenResponse>;
isUrlSupported(url: URL): boolean;
static merge(...providers: ScmAuth[]): ScmAuthApi;
}
// @public
export interface ScmAuthApi {
getCredentials(options: ScmAuthTokenOptions): Promise<ScmAuthTokenResponse>;
}
// @public
export const scmAuthApiRef: ApiRef<ScmAuthApi>;
// @public (undocumented)
export interface ScmAuthTokenOptions extends AuthRequestOptions {
additionalScope?: {
repoWrite?: boolean;
};
url: string;
}
// @public (undocumented)
export interface ScmAuthTokenResponse {
headers: {
[name: string]: string;
};
token: string;
}
// Warning: (ae-missing-release-tag) "ScmIntegrationIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -0,0 +1,215 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { OAuthApi } from '@backstage/core-plugin-api';
import { ScmAuth } from './ScmAuth';
class MockOAuthApi implements OAuthApi {
constructor(private readonly accessToken: string) {}
getAccessToken = jest.fn(async () => {
return this.accessToken;
});
}
describe('ScmAuth', () => {
it('should provide credentials for GitHub and GHE', async () => {
const mockGithubAuth = new MockOAuthApi('github-access-token');
const mockGheAuth = new MockOAuthApi('ghe-access-token');
const api = ScmAuth.merge(
ScmAuth.forGithub(mockGithubAuth),
ScmAuth.forGithub(mockGheAuth, {
host: 'ghe.example.com',
}),
);
await expect(
api.getCredentials({ url: 'https://github.com/backstage/backstage' }),
).resolves.toEqual({
token: 'github-access-token',
headers: {
Authorization: 'Bearer github-access-token',
},
});
await expect(
api.getCredentials({
url: 'https://ghe.example.com/backstage/backstage',
additionalScope: {
repoWrite: true,
},
}),
).resolves.toEqual({
token: 'ghe-access-token',
headers: {
Authorization: 'Bearer ghe-access-token',
},
});
expect(mockGithubAuth.getAccessToken).toHaveBeenCalledTimes(1);
expect(mockGithubAuth.getAccessToken).toHaveBeenCalledWith(
['repo', 'read:org', 'read:user'],
{},
);
expect(mockGheAuth.getAccessToken).toHaveBeenCalledTimes(1);
expect(mockGheAuth.getAccessToken).toHaveBeenCalledWith(
['repo', 'read:org', 'read:user', 'gist'],
{},
);
});
it('should use correct scopes for each provider', async () => {
const mockAuthApi = {
getAccessToken: async (scopes: string[]) => {
return scopes.join(' ');
},
};
const githubAuth = ScmAuth.forGithub(mockAuthApi);
await expect(
githubAuth.getCredentials({ url: 'http://example.com' }),
).resolves.toMatchObject({
token: 'repo read:org read:user',
});
await expect(
githubAuth.getCredentials({
url: 'http://example.com',
additionalScope: { repoWrite: true },
}),
).resolves.toMatchObject({
token: 'repo read:org read:user gist',
});
const gitlabAuth = ScmAuth.forGitlab(mockAuthApi);
await expect(
gitlabAuth.getCredentials({ url: 'http://example.com' }),
).resolves.toMatchObject({
token: 'read_user read_api read_repository',
});
await expect(
gitlabAuth.getCredentials({
url: 'http://example.com',
additionalScope: { repoWrite: true },
}),
).resolves.toMatchObject({
token: 'read_user read_api read_repository write_repository api',
});
const azureAuth = ScmAuth.forAzure(mockAuthApi);
await expect(
azureAuth.getCredentials({ url: 'http://example.com' }),
).resolves.toMatchObject({
token: 'vso.build vso.code vso.graph vso.project vso.profile',
});
await expect(
azureAuth.getCredentials({
url: 'http://example.com',
additionalScope: { repoWrite: true },
}),
).resolves.toMatchObject({
token:
'vso.build vso.code vso.graph vso.project vso.profile vso.code_manage',
});
const bitbucketAuth = ScmAuth.forBitbucket(mockAuthApi);
await expect(
bitbucketAuth.getCredentials({ url: 'http://example.com' }),
).resolves.toMatchObject({
token: 'account team pullrequest snippet issue',
});
await expect(
bitbucketAuth.getCredentials({
url: 'http://example.com',
additionalScope: { repoWrite: true },
}),
).resolves.toMatchObject({
token:
'account team pullrequest snippet issue pullrequest:write snippet:write issue:write',
});
});
it('should handle host option', () => {
const mockAuthApi = {
getAccessToken: jest.fn(),
};
const expectUrlSupport = (scm: ScmAuth, url: string) => {
expect(scm.isUrlSupported(new URL(url))).toBe(true);
expect(scm.isUrlSupported(new URL('https://not.supported.com'))).toBe(
false,
);
};
expectUrlSupport(ScmAuth.forGithub(mockAuthApi), 'https://github.com');
expectUrlSupport(ScmAuth.forGitlab(mockAuthApi), 'https://gitlab.com');
expectUrlSupport(
ScmAuth.forAzure(mockAuthApi, {}),
'https://dev.azure.com',
);
expectUrlSupport(
ScmAuth.forBitbucket(mockAuthApi, {}),
'https://bitbucket.org',
);
expectUrlSupport(
ScmAuth.forGithub(mockAuthApi, { host: 'example.com' }),
'https://example.com/abc',
);
expectUrlSupport(
ScmAuth.forGitlab(mockAuthApi, { host: 'example.com' }),
'http://example.com',
);
expectUrlSupport(
ScmAuth.forAzure(mockAuthApi, { host: 'example.com' }),
'https://example.com',
);
expectUrlSupport(
ScmAuth.forBitbucket(mockAuthApi, { host: 'example.com:8080' }),
'https://example.com:8080',
);
});
it('should throw an error for unknown URLs', async () => {
const emptyMux = ScmAuth.merge();
await expect(
emptyMux.getCredentials({ url: 'http://example.com' }),
).rejects.toThrow(
"No authentication provider available for access to 'http://example.com'",
);
const scmAuth = ScmAuth.merge(
ScmAuth.forAuthApi(new MockOAuthApi('token'), {
host: 'example.com',
scopeMapping: {
default: ['a'],
repoWrite: ['b'],
},
}),
);
await expect(
scmAuth.getCredentials({ url: 'http://example.com' }),
).resolves.toMatchObject({ token: 'token' });
await expect(
scmAuth.getCredentials({ url: 'http://not.example.com' }),
).rejects.toThrow(
"No authentication provider available for access to 'http://not.example.com'",
);
await expect(
scmAuth.getCredentials({ url: 'http://example.com:8080' }),
).rejects.toThrow(
"No authentication provider available for access to 'http://example.com:8080'",
);
});
});
@@ -0,0 +1,275 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createApiFactory,
githubAuthApiRef,
gitlabAuthApiRef,
microsoftAuthApiRef,
OAuthApi,
} from '@backstage/core-plugin-api';
import {
ScmAuthApi,
scmAuthApiRef,
ScmAuthTokenOptions,
ScmAuthTokenResponse,
} from './ScmAuthApi';
type ScopeMapping = {
/** The base scopes used for all requests */
default: string[];
/** Additional scopes added if `repoWrite` is requested */
repoWrite: string[];
};
class ScmAuthMux implements ScmAuthApi {
#providers: Array<ScmAuth>;
constructor(providers: ScmAuth[]) {
this.#providers = providers;
}
async getCredentials(
options: ScmAuthTokenOptions,
): Promise<ScmAuthTokenResponse> {
const url = new URL(options.url);
const provider = this.#providers.find(p => p.isUrlSupported(url));
if (!provider) {
throw new Error(
`No authentication provider available for access to '${options.url}'`,
);
}
return provider.getCredentials(options);
}
}
/**
* An implementation of the ScmAuthApi that merges together OAuthApi instances
* to form a single instance that can handles authentication for multiple providers.
*
* @public
*
* @example
* ```
* // Supports authentication towards both public GitHub and GHE:
* createApiFactory({
* api: scmAuthApiRef,
* deps: {
* gheAuthApi: gheAuthApiRef,
* githubAuthApi: githubAuthApiRef,
* },
* factory: ({ githubAuthApi, gheAuthApi }) =>
* ScmAuth.merge(
* ScmAuth.forGithub(githubAuthApi),
* ScmAuth.forGithub(gheAuthApi, {
* host: 'ghe.example.com',
* }),
* )
* })
* ```
*/
export class ScmAuth implements ScmAuthApi {
/**
* Creates an API factory that enables auth for each of the default SCM providers.
*/
static createDefaultApiFactory() {
return createApiFactory({
api: scmAuthApiRef,
deps: {
github: githubAuthApiRef,
gitlab: gitlabAuthApiRef,
azure: microsoftAuthApiRef,
},
factory: ({ github, gitlab, azure }) =>
ScmAuth.merge(
ScmAuth.forGithub(github),
ScmAuth.forGitlab(gitlab),
ScmAuth.forAzure(azure),
),
});
}
/**
* Creates a general purpose ScmAuth instance with a custom scope mapping.
*/
static forAuthApi(
authApi: OAuthApi,
options: {
host: string;
scopeMapping: {
default: string[];
repoWrite: string[];
};
},
): ScmAuth {
return new ScmAuth(authApi, options.host, options.scopeMapping);
}
/**
* Creates a new ScmAuth instance that handles authentication towards GitHub.
*
* The host option determines which URLs that are handled by this instance and defaults to `github.com`.
*
* The default scopes are:
*
* `repo read:org read:user`
*
* If the additional `repoWrite` permission is requested, these scopes are added:
*
* `gist`
*/
static forGithub(
githubAuthApi: OAuthApi,
options?: {
host?: string;
},
): ScmAuth {
const host = options?.host ?? 'github.com';
return new ScmAuth(githubAuthApi, host, {
default: ['repo', 'read:org', 'read:user'],
repoWrite: ['gist'],
});
}
/**
* Creates a new ScmAuth instance that handles authentication towards GitLab.
*
* The host option determines which URLs that are handled by this instance and defaults to `gitlab.com`.
*
* The default scopes are:
*
* `read_user read_api read_repository`
*
* If the additional `repoWrite` permission is requested, these scopes are added:
*
* `write_repository api`
*/
static forGitlab(
gitlabAuthApi: OAuthApi,
options?: {
host?: string;
},
): ScmAuth {
const host = options?.host ?? 'gitlab.com';
return new ScmAuth(gitlabAuthApi, host, {
default: ['read_user', 'read_api', 'read_repository'],
repoWrite: ['write_repository', 'api'],
});
}
/**
* Creates a new ScmAuth instance that handles authentication towards Azure.
*
* The host option determines which URLs that are handled by this instance and defaults to `dev.azure.com`.
*
* The default scopes are:
*
* `vso.build vso.code vso.graph vso.project vso.profile`
*
* If the additional `repoWrite` permission is requested, these scopes are added:
*
* `vso.code_manage`
*/
static forAzure(
microsoftAuthApi: OAuthApi,
options?: {
host?: string;
},
): ScmAuth {
const host = options?.host ?? 'dev.azure.com';
return new ScmAuth(microsoftAuthApi, host, {
default: [
'vso.build',
'vso.code',
'vso.graph',
'vso.project',
'vso.profile',
],
repoWrite: ['vso.code_manage'],
});
}
/**
* Creates a new ScmAuth instance that handles authentication towards Bitbucket.
*
* The host option determines which URLs that are handled by this instance and defaults to `bitbucket.org`.
*
* The default scopes are:
*
* `account team pullrequest snippet issue`
*
* If the additional `repoWrite` permission is requested, these scopes are added:
*
* `pullrequest:write snippet:write issue:write`
*/
static forBitbucket(
bitbucketAuthApi: OAuthApi,
options?: {
host?: string;
},
): ScmAuth {
const host = options?.host ?? 'bitbucket.org';
return new ScmAuth(bitbucketAuthApi, host, {
default: ['account', 'team', 'pullrequest', 'snippet', 'issue'],
repoWrite: ['pullrequest:write', 'snippet:write', 'issue:write'],
});
}
/**
* Merges together multiple ScmAuth instances into one that
* routes requests to the correct instance based on the URL.
*/
static merge(...providers: ScmAuth[]): ScmAuthApi {
return new ScmAuthMux(providers);
}
#api: OAuthApi;
#host: string;
#scopeMapping: ScopeMapping;
private constructor(api: OAuthApi, host: string, scopeMapping: ScopeMapping) {
this.#api = api;
this.#host = host;
this.#scopeMapping = scopeMapping;
}
/**
* Checks whether the implementation is able to provide authentication for the given URL.
*/
isUrlSupported(url: URL): boolean {
return url.host === this.#host;
}
async getCredentials(
options: ScmAuthTokenOptions,
): Promise<ScmAuthTokenResponse> {
const { url, additionalScope, ...restOptions } = options;
const scopes = this.#scopeMapping.default.slice();
if (additionalScope?.repoWrite) {
scopes.push(...this.#scopeMapping.repoWrite);
}
const token = await this.#api.getAccessToken(scopes, restOptions);
return {
token,
headers: {
Authorization: `Bearer ${token}`,
},
};
}
}
@@ -0,0 +1,81 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ApiRef,
createApiRef,
AuthRequestOptions,
} from '@backstage/core-plugin-api';
/** @public */
export interface ScmAuthTokenOptions extends AuthRequestOptions {
/**
* The URL of the SCM resource to be accessed.
*
* @example https://github.com/backstage/backstage
*/
url: string;
/**
* Whether to request additional access scope.
*
* Read access to user, organization, and repositories is always included.
*/
additionalScope?: {
/**
* Requests access to be able to write repository content, including
* the ability to create things like issues and pull requests.
*/
repoWrite?: boolean;
};
}
/** @public */
export interface ScmAuthTokenResponse {
/**
* An authorization token that can be used to authenticate requests.
*/
token: string;
/**
* The set of HTTP headers that are needed to authenticate requests.
*/
headers: { [name: string]: string };
}
/**
* ScmAuthApi provides methods for authenticating towards source code management services.
*
* As opposed to using the GitHub, GitLab and other auth APIs
* directly, this API allows for more generic access to SCM services.
*
* @public
*/
export interface ScmAuthApi {
/**
* Requests credentials for accessing an SCM resource.
*/
getCredentials(options: ScmAuthTokenOptions): Promise<ScmAuthTokenResponse>;
}
/**
* The ApiRef for the ScmAuthApi.
*
* @public
*/
export const scmAuthApiRef: ApiRef<ScmAuthApi> = createApiRef({
id: 'core.scmauth',
});
@@ -14,6 +14,13 @@
* limitations under the License.
*/
export { scmAuthApiRef } from './ScmAuthApi';
export { ScmAuth } from './ScmAuth';
export type {
ScmAuthApi,
ScmAuthTokenOptions,
ScmAuthTokenResponse,
} from './ScmAuthApi';
export {
ScmIntegrationsApi,
scmIntegrationsApiRef,
+2 -2
View File
@@ -16,9 +16,9 @@ import { EntityName } from '@backstage/catalog-model';
import { FieldErrors } from 'react-hook-form';
import { IdentityApi } from '@backstage/core-plugin-api';
import { InfoCardVariants } from '@backstage/core-components';
import { OAuthApi } from '@backstage/core-plugin-api';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
import { ScmAuthApi } from '@backstage/integration-react';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { SubmitHandler } from 'react-hook-form';
import { TextFieldProps } from '@material-ui/core/TextField/TextField';
@@ -96,7 +96,7 @@ export const catalogImportApiRef: ApiRef<CatalogImportApi>;
export class CatalogImportClient implements CatalogImportApi {
constructor(options: {
discoveryApi: DiscoveryApi;
githubAuthApi: OAuthApi;
scmAuthApi: ScmAuthApi;
identityApi: IdentityApi;
scmIntegrationsApi: ScmIntegrationRegistry;
catalogApi: CatalogApi;
@@ -47,8 +47,8 @@ jest.doMock('@octokit/rest', () => {
});
import { ConfigReader, UrlPatternDiscovery } from '@backstage/core-app-api';
import { OAuthApi } from '@backstage/core-plugin-api';
import { ScmIntegrations } from '@backstage/integration';
import { ScmAuthApi } from '@backstage/integration-react';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { msw } from '@backstage/test-utils';
import { Octokit } from '@octokit/rest';
@@ -63,8 +63,8 @@ describe('CatalogImportClient', () => {
const mockBaseUrl = 'http://backstage:9191/api/catalog';
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
const githubAuthApi: jest.Mocked<OAuthApi> = {
getAccessToken: jest.fn(),
const scmAuthApi: jest.Mocked<ScmAuthApi> = {
getCredentials: jest.fn().mockResolvedValue({ token: 'token' }),
};
const identityApi = {
getUserId: () => {
@@ -112,7 +112,7 @@ describe('CatalogImportClient', () => {
beforeEach(() => {
catalogImportClient = new CatalogImportClient({
discoveryApi,
githubAuthApi,
scmAuthApi,
scmIntegrationsApi,
identityApi,
catalogApi,
@@ -20,12 +20,12 @@ import {
ConfigApi,
DiscoveryApi,
IdentityApi,
OAuthApi,
} from '@backstage/core-plugin-api';
import {
GitHubIntegrationConfig,
ScmIntegrationRegistry,
} from '@backstage/integration';
import { ScmAuthApi } from '@backstage/integration-react';
import { Octokit } from '@octokit/rest';
import { Base64 } from 'js-base64';
import { PartialEntity } from '../types';
@@ -36,21 +36,21 @@ import { trimEnd } from 'lodash';
export class CatalogImportClient implements CatalogImportApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
private readonly githubAuthApi: OAuthApi;
private readonly scmAuthApi: ScmAuthApi;
private readonly scmIntegrationsApi: ScmIntegrationRegistry;
private readonly catalogApi: CatalogApi;
private readonly configApi: ConfigApi;
constructor(options: {
discoveryApi: DiscoveryApi;
githubAuthApi: OAuthApi;
scmAuthApi: ScmAuthApi;
identityApi: IdentityApi;
scmIntegrationsApi: ScmIntegrationRegistry;
catalogApi: CatalogApi;
configApi: ConfigApi;
}) {
this.discoveryApi = options.discoveryApi;
this.githubAuthApi = options.githubAuthApi;
this.scmAuthApi = options.scmAuthApi;
this.identityApi = options.identityApi;
this.scmIntegrationsApi = options.scmIntegrationsApi;
this.catalogApi = options.catalogApi;
@@ -157,6 +157,7 @@ the component will become available.\n\nFor more information, read an \
if (ghConfig) {
return await this.submitGitHubPrToRepo({
...ghConfig,
repositoryUrl,
fileContent,
title,
body,
@@ -215,7 +216,7 @@ the component will become available.\n\nFor more information, read an \
entities: EntityName[];
}>
> {
const token = await this.githubAuthApi.getAccessToken(['repo']);
const { token } = await this.scmAuthApi.getCredentials({ url });
const octo = new Octokit({
auth: token,
baseUrl: githubIntegrationConfig.apiBaseUrl,
@@ -270,6 +271,7 @@ the component will become available.\n\nFor more information, read an \
title,
body,
fileContent,
repositoryUrl,
githubIntegrationConfig,
}: {
owner: string;
@@ -277,9 +279,15 @@ the component will become available.\n\nFor more information, read an \
title: string;
body: string;
fileContent: string;
repositoryUrl: string;
githubIntegrationConfig: GitHubIntegrationConfig;
}): Promise<{ link: string; location: string }> {
const token = await this.githubAuthApi.getAccessToken(['repo']);
const { token } = await this.scmAuthApi.getCredentials({
url: repositoryUrl,
additionalScope: {
repoWrite: true,
},
});
const octo = new Octokit({
auth: token,
@@ -55,8 +55,8 @@ describe('<DefaultImportPage />', () => {
catalogImportApiRef,
new CatalogImportClient({
discoveryApi: {} as any,
githubAuthApi: {
getAccessToken: async () => 'token',
scmAuthApi: {
getCredentials: async () => ({ token: 'token', headers: {} }),
},
identityApi,
scmIntegrationsApi: {} as any,
@@ -61,10 +61,8 @@ describe('<ImportPage />', () => {
catalogImportApiRef,
new CatalogImportClient({
discoveryApi: {} as any,
githubAuthApi: {
getAccessToken: async () => 'token',
},
identityApi,
scmAuthApi: {} as any,
scmIntegrationsApi: {} as any,
catalogApi: {} as any,
configApi: new ConfigReader({}),
+7 -5
View File
@@ -21,10 +21,12 @@ import {
createRoutableExtension,
createRouteRef,
discoveryApiRef,
githubAuthApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import {
scmAuthApiRef,
scmIntegrationsApiRef,
} from '@backstage/integration-react';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { catalogImportApiRef, CatalogImportClient } from './api';
@@ -40,7 +42,7 @@ export const catalogImportPlugin = createPlugin({
api: catalogImportApiRef,
deps: {
discoveryApi: discoveryApiRef,
githubAuthApi: githubAuthApiRef,
scmAuthApi: scmAuthApiRef,
identityApi: identityApiRef,
scmIntegrationsApi: scmIntegrationsApiRef,
catalogApi: catalogApiRef,
@@ -48,7 +50,7 @@ export const catalogImportPlugin = createPlugin({
},
factory: ({
discoveryApi,
githubAuthApi,
scmAuthApi,
identityApi,
scmIntegrationsApi,
catalogApi,
@@ -56,7 +58,7 @@ export const catalogImportPlugin = createPlugin({
}) =>
new CatalogImportClient({
discoveryApi,
githubAuthApi,
scmAuthApi,
scmIntegrationsApi,
identityApi,
catalogApi,