feat(azure): support service principals and managed identities
Signed-off-by: Sander Aernouts <sander.aernouts@gmail.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/backend-common': minor
|
||||
'@backstage/integration': minor
|
||||
'@backstage/plugin-catalog-backend-module-azure': patch
|
||||
---
|
||||
|
||||
Support authentication with a service principal or managed identity for Azure DevOps
|
||||
|
||||
Azure DevOps recently released support, in public preview, for authenticating with a service principal or managed identity instead of a personal access token (PAT): https://devblogs.microsoft.com/devops/introducing-service-principal-and-managed-identity-support-on-azure-devops/. With this change the Azure integration now supports service principals and managed identities for Azure AD backed Azure DevOps organizations. Service principal and managed identity authentication is not supported on Azure DevOps Server (on-premises) organizations.
|
||||
@@ -13,6 +13,30 @@ or registered with the
|
||||
[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import)
|
||||
plugin.
|
||||
|
||||
Using a service principal:
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
azure:
|
||||
- host: dev.azure.com
|
||||
credential:
|
||||
clientId: ${CLIENT_ID}
|
||||
clientSecret: ${CLIENT_SECRET}
|
||||
tenantId: ${TENANT_ID}
|
||||
```
|
||||
|
||||
Using a managed identity:
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
azure:
|
||||
- host: dev.azure.com
|
||||
credential:
|
||||
clientId: ${CLIENT_ID}
|
||||
```
|
||||
|
||||
Using a personal access token (PAT):
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
azure:
|
||||
@@ -22,11 +46,19 @@ integrations:
|
||||
|
||||
> Note: An Azure DevOps provider is added automatically at startup for
|
||||
> convenience, so you only need to list it if you want to supply a
|
||||
> [token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate).
|
||||
> [token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate),
|
||||
> a [service principal](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity),
|
||||
> or a [managed identity](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity)
|
||||
|
||||
The configuration is a structure with two elements:
|
||||
The configuration is a structure with these elements:
|
||||
|
||||
- `host`: The DevOps host; only `dev.azure.com` is supported.
|
||||
- `token` (optional): A personal access token as expected by Azure DevOps.
|
||||
- `credential`: (optional): A service principal or managed identity
|
||||
|
||||
> Note: The token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API
|
||||
> Note:
|
||||
>
|
||||
> - `token` and `credential` are mutually exclusive.
|
||||
> - You cannot use a service principal or managed identity for Azure DevOps Server (on-premises) organizations
|
||||
> - You can only use a service principal or managed identity for Azure AD backed Azure DevOps organizations
|
||||
> - The token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API
|
||||
|
||||
@@ -76,7 +76,7 @@ export class AzureUrlReader implements UrlReader {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(builtUrl, {
|
||||
...getAzureRequestOptions(this.integration.config),
|
||||
...(await getAzureRequestOptions(this.integration.config)),
|
||||
// TODO(freben): The signal cast is there because pre-3.x versions of
|
||||
// node-fetch have a very slightly deviating AbortSignal type signature.
|
||||
// The difference does not affect us in practice however. The cast can
|
||||
@@ -113,7 +113,7 @@ export class AzureUrlReader implements UrlReader {
|
||||
|
||||
const commitsAzureResponse = await fetch(
|
||||
getAzureCommitsUrl(url),
|
||||
getAzureRequestOptions(this.integration.config),
|
||||
await getAzureRequestOptions(this.integration.config),
|
||||
);
|
||||
if (!commitsAzureResponse.ok) {
|
||||
const message = `Failed to read tree from ${url}, ${commitsAzureResponse.status} ${commitsAzureResponse.statusText}`;
|
||||
@@ -129,9 +129,9 @@ export class AzureUrlReader implements UrlReader {
|
||||
}
|
||||
|
||||
const archiveAzureResponse = await fetch(getAzureDownloadUrl(url), {
|
||||
...getAzureRequestOptions(this.integration.config, {
|
||||
...(await getAzureRequestOptions(this.integration.config, {
|
||||
Accept: 'application/zip',
|
||||
}),
|
||||
})),
|
||||
// TODO(freben): The signal cast is there because pre-3.x versions of
|
||||
// node-fetch have a very slightly deviating AbortSignal type signature.
|
||||
// The difference does not affect us in practice however. The cast can be
|
||||
|
||||
@@ -38,6 +38,9 @@ export type AwsS3IntegrationConfig = {
|
||||
externalId?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type AzureCredential = ClientSecret | ManagedIdentity;
|
||||
|
||||
// @public
|
||||
export class AzureIntegration implements ScmIntegration {
|
||||
constructor(integrationConfig: AzureIntegrationConfig);
|
||||
@@ -63,6 +66,7 @@ export class AzureIntegration implements ScmIntegration {
|
||||
export type AzureIntegrationConfig = {
|
||||
host: string;
|
||||
token?: string;
|
||||
credential?: AzureCredential;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -154,6 +158,13 @@ export type BitbucketServerIntegrationConfig = {
|
||||
password?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ClientSecret = {
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class DefaultGithubCredentialsProvider
|
||||
implements GithubCredentialsProvider
|
||||
@@ -216,9 +227,9 @@ export function getAzureFileFetchUrl(url: string): string;
|
||||
export function getAzureRequestOptions(
|
||||
config: AzureIntegrationConfig,
|
||||
additionalHeaders?: Record<string, string>,
|
||||
): {
|
||||
): Promise<{
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export function getBitbucketCloudDefaultBranch(
|
||||
@@ -535,6 +546,11 @@ export interface IntegrationsByType {
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ManagedIdentity = {
|
||||
clientId: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function parseGerritGitilesUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"clean": "backstage-cli package clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/identity": "^3.2.1",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@octokit/auth-app": "^4.0.0",
|
||||
|
||||
@@ -51,19 +51,42 @@ describe('readAzureIntegrationConfig', () => {
|
||||
return new ConfigReader((processed[0].data as any).integrations.azure[0]);
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
it('reads all values when using a token', () => {
|
||||
const output = readAzureIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
token: 't',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
host: 'a.com',
|
||||
token: 't',
|
||||
});
|
||||
});
|
||||
|
||||
it('reads all values when using a credential', () => {
|
||||
const output = readAzureIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'dev.azure.com',
|
||||
credential: {
|
||||
clientId: 'id',
|
||||
clientSecret: 'secret',
|
||||
tenantId: 'tenant',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
host: 'dev.azure.com',
|
||||
credential: {
|
||||
clientId: 'id',
|
||||
clientSecret: 'secret',
|
||||
tenantId: 'tenant',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('inserts the defaults if missing', () => {
|
||||
const output = readAzureIntegrationConfig(buildConfig({}));
|
||||
expect(output).toEqual({ host: 'dev.azure.com' });
|
||||
@@ -71,15 +94,86 @@ describe('readAzureIntegrationConfig', () => {
|
||||
|
||||
it('rejects funky configs', () => {
|
||||
const valid: any = {
|
||||
host: 'a.com',
|
||||
token: 't',
|
||||
host: 'dev.azure.com',
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(buildConfig({ ...valid, host: 7 })),
|
||||
).toThrow(/host/);
|
||||
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(buildConfig({ ...valid, token: 7 })),
|
||||
).toThrow(/token/);
|
||||
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(buildConfig({ ...valid, credential: 7 })),
|
||||
).toThrow(/credential/);
|
||||
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(
|
||||
buildConfig({ ...valid, credential: { clientId: 7 } }),
|
||||
),
|
||||
).toThrow(/credential/);
|
||||
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(
|
||||
buildConfig({ ...valid, credential: { clientSecret: 7 } }),
|
||||
),
|
||||
).toThrow(/credential/);
|
||||
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(
|
||||
buildConfig({ ...valid, credential: { tenantId: 7 } }),
|
||||
),
|
||||
).toThrow(/credential/);
|
||||
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(buildConfig({ ...valid, credential: {} })),
|
||||
).toThrow(/credential/);
|
||||
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(
|
||||
buildConfig({ ...valid, credential: { clientSecret: 'secret' } }),
|
||||
),
|
||||
).toThrow(/credential/);
|
||||
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(
|
||||
buildConfig({ ...valid, credential: { tenantId: 'tenant' } }),
|
||||
),
|
||||
).toThrow(/credential/);
|
||||
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(
|
||||
buildConfig({
|
||||
...valid,
|
||||
credential: { clientId: 'id', clientSecret: 'secret' },
|
||||
}),
|
||||
),
|
||||
).toThrow(/credential/);
|
||||
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(
|
||||
buildConfig({
|
||||
...valid,
|
||||
credential: { clientId: 'id', tenantId: 'tenant' },
|
||||
}),
|
||||
),
|
||||
).toThrow(/credential/);
|
||||
|
||||
expect(() =>
|
||||
readAzureIntegrationConfig(
|
||||
buildConfig({
|
||||
...valid,
|
||||
host: 'a.com',
|
||||
credential: {
|
||||
clientId: 'id',
|
||||
tenantId: 'tenant',
|
||||
clientSecret: 'secret',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toThrow(/credential/);
|
||||
});
|
||||
|
||||
it('works on the frontend', async () => {
|
||||
@@ -101,7 +195,7 @@ describe('readAzureIntegrationConfigs', () => {
|
||||
return data.map(item => new ConfigReader(item));
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
it('reads all values when using a token', () => {
|
||||
const output = readAzureIntegrationConfigs(
|
||||
buildConfig([
|
||||
{
|
||||
@@ -116,6 +210,29 @@ describe('readAzureIntegrationConfigs', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reads all values when using a credential', () => {
|
||||
const output = readAzureIntegrationConfigs(
|
||||
buildConfig([
|
||||
{
|
||||
host: 'dev.azure.com',
|
||||
credential: {
|
||||
clientId: 'id',
|
||||
clientSecret: 'secret',
|
||||
tenantId: 'tenant',
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(output).toContainEqual({
|
||||
host: 'dev.azure.com',
|
||||
credential: {
|
||||
clientId: 'id',
|
||||
clientSecret: 'secret',
|
||||
tenantId: 'tenant',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('adds a default entry when missing', () => {
|
||||
const output = readAzureIntegrationConfigs(buildConfig([]));
|
||||
expect(output).toEqual([
|
||||
|
||||
@@ -38,6 +38,71 @@ export type AzureIntegrationConfig = {
|
||||
* If no token is specified, anonymous access is used.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* The credential to use for requests.
|
||||
*
|
||||
* If no credential is specified anonymous access is used.
|
||||
*/
|
||||
credential?: AzureCredential;
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate using a client secret that was generated for an App Registration.
|
||||
* @public
|
||||
*/
|
||||
export type ClientSecret = {
|
||||
/**
|
||||
* The Azure Active Directory tenant
|
||||
*/
|
||||
tenantId: string;
|
||||
/**
|
||||
* The client id
|
||||
*/
|
||||
clientId: string;
|
||||
|
||||
/**
|
||||
* The client secret
|
||||
*/
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate using a managed identity available at the deployment environment.
|
||||
* @public
|
||||
*/
|
||||
export type ManagedIdentity = {
|
||||
/**
|
||||
* The clientId
|
||||
*/
|
||||
clientId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Credential used to authenticate to Azure Active Directory.
|
||||
* @public
|
||||
*/
|
||||
export type AzureCredential = ClientSecret | ManagedIdentity;
|
||||
export const isServicePrincipal = (
|
||||
credential: Partial<AzureCredential>,
|
||||
): credential is ClientSecret => {
|
||||
const clientSecretCredential = credential as ClientSecret;
|
||||
|
||||
return (
|
||||
Object.keys(credential).length === 3 &&
|
||||
clientSecretCredential.clientId !== undefined &&
|
||||
clientSecretCredential.clientSecret !== undefined &&
|
||||
clientSecretCredential.tenantId !== undefined
|
||||
);
|
||||
};
|
||||
|
||||
export const isManagedIdentity = (
|
||||
credential: Partial<AzureCredential>,
|
||||
): credential is ManagedIdentity => {
|
||||
return (
|
||||
Object.keys(credential).length === 1 &&
|
||||
(credential as ManagedIdentity).clientId !== undefined
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -52,13 +117,43 @@ export function readAzureIntegrationConfig(
|
||||
const host = config.getOptionalString('host') ?? AZURE_HOST;
|
||||
const token = config.getOptionalString('token');
|
||||
|
||||
const credential = config.getOptional<AzureCredential>('credential')
|
||||
? {
|
||||
tenantId: config.getOptionalString('credential.tenantId'),
|
||||
clientId: config.getOptionalString('credential.clientId'),
|
||||
clientSecret: config.getOptionalString('credential.clientSecret'),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (!isValidHost(host)) {
|
||||
throw new Error(
|
||||
`Invalid Azure integration config, '${host}' is not a valid host`,
|
||||
);
|
||||
}
|
||||
|
||||
return { host, token };
|
||||
if (
|
||||
credential &&
|
||||
!isServicePrincipal(credential) &&
|
||||
!isManagedIdentity(credential)
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid Azure integration config, credential is not valid`,
|
||||
);
|
||||
}
|
||||
|
||||
if (credential && host !== AZURE_HOST) {
|
||||
throw new Error(
|
||||
`Invalid Azure integration config, credential can only be used with ${AZURE_HOST}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (credential && token) {
|
||||
throw new Error(
|
||||
`Invalid Azure integration config, specify either a token or a credential but not both`,
|
||||
);
|
||||
}
|
||||
|
||||
return { host, token, credential };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,21 +19,85 @@ import {
|
||||
getAzureDownloadUrl,
|
||||
getAzureRequestOptions,
|
||||
} from './core';
|
||||
import {
|
||||
AccessToken,
|
||||
ClientSecretCredential,
|
||||
ManagedIdentityCredential,
|
||||
} from '@azure/identity';
|
||||
|
||||
const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass<
|
||||
typeof ClientSecretCredential
|
||||
>;
|
||||
|
||||
const MockedManagedIdentityCredential =
|
||||
ManagedIdentityCredential as jest.MockedClass<
|
||||
typeof ManagedIdentityCredential
|
||||
>;
|
||||
|
||||
jest.mock('@azure/identity');
|
||||
|
||||
MockedClientSecretCredential.prototype.getToken.mockImplementation(() =>
|
||||
Promise.resolve({ token: 'fake-client-secret-token' } as AccessToken),
|
||||
);
|
||||
MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() =>
|
||||
Promise.resolve({ token: 'fake-managed-identity-token' } as AccessToken),
|
||||
);
|
||||
|
||||
describe('azure core', () => {
|
||||
describe('getAzureRequestOptions', () => {
|
||||
it('fills in the token if necessary', () => {
|
||||
expect(getAzureRequestOptions({ host: '', token: '0123456789' })).toEqual(
|
||||
it('should not add authorization header when not using token or credential', async () => {
|
||||
expect(await getAzureRequestOptions({ host: '' })).toEqual(
|
||||
expect.objectContaining({
|
||||
headers: expect.not.objectContaining({
|
||||
Authorization: expect.anything(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add authorization header when using a personal access token', async () => {
|
||||
expect(
|
||||
await getAzureRequestOptions({ host: '', token: '0123456789' }),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Basic OjAxMjM0NTY3ODk=',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(getAzureRequestOptions({ host: '' })).toEqual(
|
||||
});
|
||||
|
||||
it('should add authorization header when using a client secret', async () => {
|
||||
expect(
|
||||
await getAzureRequestOptions({
|
||||
host: '',
|
||||
credential: {
|
||||
clientId: 'fake-id',
|
||||
clientSecret: 'fake-secret',
|
||||
tenantId: 'fake-tenant',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
headers: expect.not.objectContaining({
|
||||
Authorization: expect.anything(),
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer fake-client-secret-token',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add authorization header when using a managed identity', async () => {
|
||||
expect(
|
||||
await getAzureRequestOptions({
|
||||
host: '',
|
||||
credential: {
|
||||
clientId: 'fake-id',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer fake-managed-identity-token',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -15,7 +15,15 @@
|
||||
*/
|
||||
|
||||
import { AzureUrl } from './AzureUrl';
|
||||
import { AzureIntegrationConfig } from './config';
|
||||
import {
|
||||
AzureIntegrationConfig,
|
||||
isManagedIdentity,
|
||||
isServicePrincipal,
|
||||
} from './config';
|
||||
import {
|
||||
ClientSecretCredential,
|
||||
ManagedIdentityCredential,
|
||||
} from '@azure/identity';
|
||||
|
||||
/**
|
||||
* Given a URL pointing to a file on a provider, returns a URL that is suitable
|
||||
@@ -59,17 +67,37 @@ export function getAzureCommitsUrl(url: string): string {
|
||||
* Gets the request options necessary to make requests to a given provider.
|
||||
*
|
||||
* @param config - The relevant provider config
|
||||
* @param additionalHeaders - Additional headers for the request
|
||||
* @public
|
||||
*/
|
||||
export function getAzureRequestOptions(
|
||||
export async function getAzureRequestOptions(
|
||||
config: AzureIntegrationConfig,
|
||||
additionalHeaders?: Record<string, string>,
|
||||
): { headers: Record<string, string> } {
|
||||
): Promise<{ headers: Record<string, string> }> {
|
||||
const azureDevOpsScope = '499b84ac-1321-427f-aa17-267ca6975798/.default';
|
||||
const headers: Record<string, string> = additionalHeaders
|
||||
? { ...additionalHeaders }
|
||||
: {};
|
||||
|
||||
if (config.token) {
|
||||
const { token, credential } = config;
|
||||
if (credential) {
|
||||
if (isServicePrincipal(credential)) {
|
||||
const servicePrincipal = new ClientSecretCredential(
|
||||
credential.tenantId,
|
||||
credential.clientId,
|
||||
credential.clientSecret,
|
||||
);
|
||||
|
||||
const accessToken = await servicePrincipal.getToken(azureDevOpsScope);
|
||||
headers.Authorization = `Bearer ${accessToken.token}`;
|
||||
} else if (isManagedIdentity(credential)) {
|
||||
const managedIdentity = new ManagedIdentityCredential(
|
||||
credential.clientId,
|
||||
);
|
||||
const accessToken = await managedIdentity.getToken(azureDevOpsScope);
|
||||
headers.Authorization = `Bearer ${accessToken.token}`;
|
||||
}
|
||||
} else if (token) {
|
||||
const buffer = Buffer.from(`:${config.token}`, 'utf8');
|
||||
headers.Authorization = `Basic ${buffer.toString('base64')}`;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,12 @@ export {
|
||||
readAzureIntegrationConfig,
|
||||
readAzureIntegrationConfigs,
|
||||
} from './config';
|
||||
export type { AzureIntegrationConfig } from './config';
|
||||
export type {
|
||||
AzureIntegrationConfig,
|
||||
AzureCredential,
|
||||
ManagedIdentity,
|
||||
ClientSecret,
|
||||
} from './config';
|
||||
export {
|
||||
getAzureCommitsUrl,
|
||||
getAzureDownloadUrl,
|
||||
|
||||
@@ -58,9 +58,9 @@ export async function codeSearch(
|
||||
|
||||
do {
|
||||
const response = await fetch(searchUrl, {
|
||||
...getAzureRequestOptions(azureConfig, {
|
||||
...(await getAzureRequestOptions(azureConfig, {
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
})),
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
searchText: `path:${path} repo:${repo || '*'} proj:${project || '*'}`,
|
||||
|
||||
@@ -1978,6 +1978,30 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@azure/identity@npm:^3.2.1":
|
||||
version: 3.2.1
|
||||
resolution: "@azure/identity@npm:3.2.1"
|
||||
dependencies:
|
||||
"@azure/abort-controller": ^1.0.0
|
||||
"@azure/core-auth": ^1.3.0
|
||||
"@azure/core-client": ^1.4.0
|
||||
"@azure/core-rest-pipeline": ^1.1.0
|
||||
"@azure/core-tracing": ^1.0.0
|
||||
"@azure/core-util": ^1.0.0
|
||||
"@azure/logger": ^1.0.0
|
||||
"@azure/msal-browser": ^2.32.2
|
||||
"@azure/msal-common": ^9.0.2
|
||||
"@azure/msal-node": ^1.14.6
|
||||
events: ^3.0.0
|
||||
jws: ^4.0.0
|
||||
open: ^8.0.0
|
||||
stoppable: ^1.1.0
|
||||
tslib: ^2.2.0
|
||||
uuid: ^8.3.0
|
||||
checksum: c66405cd84c22e16031e1b6aeb09eacbabeeb9d73090f67ed770337e3cc7abe87950cf5c2f6dc7a1d354fa1676fed72fc2142916c4929699534e7ad37d7b1dd1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@azure/logger@npm:^1.0.0":
|
||||
version: 1.0.1
|
||||
resolution: "@azure/logger@npm:1.0.1"
|
||||
@@ -2015,16 +2039,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@azure/msal-browser@npm:^2.26.0":
|
||||
version: 2.27.0
|
||||
resolution: "@azure/msal-browser@npm:2.27.0"
|
||||
"@azure/msal-browser@npm:^2.26.0, @azure/msal-browser@npm:^2.32.2":
|
||||
version: 2.37.0
|
||||
resolution: "@azure/msal-browser@npm:2.37.0"
|
||||
dependencies:
|
||||
"@azure/msal-common": ^7.1.0
|
||||
checksum: 6cafb4d41a92f1bbb82e7128f64ccd6538ec116a2521f412108d94c4173601d31ea62febb7b22206dcae691e6a01cf7f9ce92793420599437b50a24c7e6ab851
|
||||
"@azure/msal-common": 13.0.0
|
||||
checksum: e57d04517d51db4b608da6ca127c89e5c134b42743b00e95d8f1aae011d1cc6d5c02fb4f72cd0f96636e500a3eeee7c4884cd537ac8796bbd50f0ff9ac983b4e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@azure/msal-common@npm:^7.0.0, @azure/msal-common@npm:^7.1.0":
|
||||
"@azure/msal-common@npm:13.0.0":
|
||||
version: 13.0.0
|
||||
resolution: "@azure/msal-common@npm:13.0.0"
|
||||
checksum: 89f56f9fbf0edffa6023d46c6970dd37a9ad571b7d2952fdef41950ec2f3b4b3fd22d9841e2caaf52619ad97ab415ed22c30797cdb896f459eb665d3d97f778a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@azure/msal-common@npm:^7.0.0":
|
||||
version: 7.1.0
|
||||
resolution: "@azure/msal-common@npm:7.1.0"
|
||||
checksum: 761a8b9363c7b620d831aba7d7e7a66e341685e306034b027b57dc31dae11abdf86c3187c08fc8008448c845ffdcbbc42c767c75355625cf87aba25ba7e2832e
|
||||
@@ -2038,14 +2069,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@azure/msal-node@npm:^1.10.0":
|
||||
version: 1.14.6
|
||||
resolution: "@azure/msal-node@npm:1.14.6"
|
||||
"@azure/msal-node@npm:^1.10.0, @azure/msal-node@npm:^1.14.6":
|
||||
version: 1.17.2
|
||||
resolution: "@azure/msal-node@npm:1.17.2"
|
||||
dependencies:
|
||||
"@azure/msal-common": ^9.0.2
|
||||
"@azure/msal-common": 13.0.0
|
||||
jsonwebtoken: ^9.0.0
|
||||
uuid: ^8.3.0
|
||||
checksum: 7fb2085fe772474bb4c6c1e0a2467da6b2380dfa66f7c67f63d4a8774f591744dc64a6f3337d43c810ff433bc327be470ece59dcd0fa7aff4459e32beb4c738b
|
||||
checksum: 5ac809dae6b02ab7de2aafb85733501ba6c3268f1d9371bc857215dad9d47e4f6e10b3e58ebda49504b375c96934761e9734acc241d3bc34d3b4cdeaabdf102e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4467,6 +4498,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/integration@workspace:packages/integration"
|
||||
dependencies:
|
||||
"@azure/identity": ^3.2.1
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/config-loader": "workspace:^"
|
||||
|
||||
Reference in New Issue
Block a user