Merge branch 'master' into catalog/github-app-discovery
Signed-off-by: Benjamin Janssens <benji.janssens@gmail.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider';
|
||||
import {
|
||||
AccessToken,
|
||||
ClientAssertionCredential,
|
||||
ClientSecretCredential,
|
||||
ManagedIdentityCredential,
|
||||
} from '@azure/identity';
|
||||
@@ -24,6 +25,11 @@ const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass<
|
||||
typeof ClientSecretCredential
|
||||
>;
|
||||
|
||||
const MockedClientAssertionCredential =
|
||||
ClientAssertionCredential as jest.MockedClass<
|
||||
typeof ClientAssertionCredential
|
||||
>;
|
||||
|
||||
const MockedManagedIdentityCredential =
|
||||
ManagedIdentityCredential as jest.MockedClass<
|
||||
typeof ManagedIdentityCredential
|
||||
@@ -46,6 +52,13 @@ describe('CachedAzureDevOpsCredentialsProvider', () => {
|
||||
} as AccessToken),
|
||||
);
|
||||
|
||||
MockedClientAssertionCredential.prototype.getToken.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
expiresOnTimestamp: Date.now() + hours(8),
|
||||
token: 'fake-client-assertion-token',
|
||||
} as AccessToken),
|
||||
);
|
||||
|
||||
MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
expiresOnTimestamp: Date.now() + hours(8),
|
||||
@@ -89,6 +102,25 @@ describe('CachedAzureDevOpsCredentialsProvider', () => {
|
||||
expect(token).toBe('fake-client-secret-token');
|
||||
});
|
||||
|
||||
it('Should return a bearer credential when a managed identity client assertion is configured', async () => {
|
||||
const manager =
|
||||
CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({
|
||||
kind: 'ManagedIdentityClientAssertion',
|
||||
clientId: 'id',
|
||||
managedIdentityClientId: 'managedIdentityClientId',
|
||||
tenantId: 'tenantId',
|
||||
});
|
||||
|
||||
const { headers, type, token } = await manager.getCredentials();
|
||||
|
||||
expect(headers).toStrictEqual({
|
||||
Authorization: `Bearer fake-client-assertion-token`,
|
||||
});
|
||||
|
||||
expect(type).toBe('bearer');
|
||||
expect(token).toBe('fake-client-assertion-token');
|
||||
});
|
||||
|
||||
it('Should return a bearer credential when a managed identity is configured', async () => {
|
||||
const manager =
|
||||
CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({
|
||||
@@ -124,6 +156,24 @@ describe('CachedAzureDevOpsCredentialsProvider', () => {
|
||||
).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should not refresh the managed identity client assertion token when it does not expire within 10 minutes', async () => {
|
||||
const manager =
|
||||
CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({
|
||||
kind: 'ManagedIdentityClientAssertion',
|
||||
clientId: 'id',
|
||||
managedIdentityClientId: 'managedIdentityClientId',
|
||||
tenantId: 'tenantId',
|
||||
});
|
||||
|
||||
await manager.getCredentials();
|
||||
await manager.getCredentials();
|
||||
await manager.getCredentials();
|
||||
|
||||
expect(
|
||||
MockedClientAssertionCredential.prototype.getToken,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should return the managed identity token when it does not expire within 10 minutes', async () => {
|
||||
const manager =
|
||||
CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({
|
||||
@@ -164,6 +214,30 @@ describe('CachedAzureDevOpsCredentialsProvider', () => {
|
||||
).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('Should refresh the managed identity client assertion token when it expires within 10 minutes', async () => {
|
||||
const manager =
|
||||
CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({
|
||||
kind: 'ManagedIdentityClientAssertion',
|
||||
clientId: 'id',
|
||||
managedIdentityClientId: 'managedIdentityClientId',
|
||||
tenantId: 'tenantId',
|
||||
});
|
||||
|
||||
MockedClientAssertionCredential.prototype.getToken.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
expiresOnTimestamp: Date.now() + minutes(9) + seconds(59),
|
||||
token: 'fake-client-assertion-token',
|
||||
} as AccessToken),
|
||||
);
|
||||
|
||||
await manager.getCredentials();
|
||||
await manager.getCredentials();
|
||||
|
||||
expect(
|
||||
MockedClientAssertionCredential.prototype.getToken,
|
||||
).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('Should refresh the managed identity token when it expires within 10 minutes', async () => {
|
||||
const manager =
|
||||
CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
import { AzureDevOpsCredential, PersonalAccessTokenCredential } from './config';
|
||||
import {
|
||||
ClientAssertionCredential,
|
||||
ClientSecretCredential,
|
||||
ManagedIdentityCredential,
|
||||
TokenCredential,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
AzureDevOpsCredentials,
|
||||
AzureDevOpsCredentialsProvider,
|
||||
} from './types';
|
||||
import { ManagedIdentityClientAssertion } from './ManagedIdentityClientAssertion';
|
||||
|
||||
type CachedAzureDevOpsCredentials = AzureDevOpsCredentials & {
|
||||
expiresAt?: number;
|
||||
@@ -59,9 +61,26 @@ export class CachedAzureDevOpsCredentialsProvider
|
||||
credential.clientSecret,
|
||||
),
|
||||
);
|
||||
|
||||
case 'ManagedIdentityClientAssertion': {
|
||||
const clientAssertion = new ManagedIdentityClientAssertion({
|
||||
clientId: credential.managedIdentityClientId,
|
||||
});
|
||||
|
||||
return CachedAzureDevOpsCredentialsProvider.fromTokenCredential(
|
||||
new ClientAssertionCredential(
|
||||
credential.tenantId,
|
||||
credential.clientId,
|
||||
() => clientAssertion.getSignedAssertion(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
case 'ManagedIdentity':
|
||||
return CachedAzureDevOpsCredentialsProvider.fromTokenCredential(
|
||||
new ManagedIdentityCredential(credential.clientId),
|
||||
credential.clientId === 'system-assigned'
|
||||
? new ManagedIdentityCredential()
|
||||
: new ManagedIdentityCredential(credential.clientId),
|
||||
);
|
||||
default:
|
||||
exhaustiveCheck(credential);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*/
|
||||
|
||||
export type ClientAssertion = {
|
||||
/**
|
||||
* The signed assertion token
|
||||
*/
|
||||
signedAssertion: string;
|
||||
/**
|
||||
* The assertion's expiration timestamp in milliseconds, UNIX epoch time.
|
||||
*/
|
||||
expiresOnTimestamp: number;
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2025 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 { ManagedIdentityClientAssertion } from './ManagedIdentityClientAssertion';
|
||||
import { ManagedIdentityCredential, AccessToken } from '@azure/identity';
|
||||
|
||||
const seconds = (s: number) => s * 1000;
|
||||
const minutes = (m: number) => seconds(60) * m;
|
||||
const hours = (h: number) => minutes(60) * h;
|
||||
const MockedManagedIdentityCredential =
|
||||
ManagedIdentityCredential as jest.MockedClass<
|
||||
typeof ManagedIdentityCredential
|
||||
>;
|
||||
|
||||
jest.mock('@azure/identity');
|
||||
|
||||
describe('ManagedIdentityClientAssertion', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
expiresOnTimestamp: Date.now() + hours(8),
|
||||
token: 'fake-managed-identity-token',
|
||||
} as AccessToken),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should return a cached token if it does not expire within 5 minutes', async () => {
|
||||
const clientAssertion = new ManagedIdentityClientAssertion({
|
||||
clientId: 'clientId',
|
||||
});
|
||||
|
||||
// First call to getToken to cache the token
|
||||
await clientAssertion.getSignedAssertion();
|
||||
// Second call should return the cached token
|
||||
const token = await clientAssertion.getSignedAssertion();
|
||||
|
||||
expect(token).toBe('fake-managed-identity-token');
|
||||
expect(
|
||||
MockedManagedIdentityCredential.prototype.getToken,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should obtain a new token if the cached token expires within 5 minutes', async () => {
|
||||
const clientAssertion = new ManagedIdentityClientAssertion({
|
||||
clientId: 'clientId',
|
||||
});
|
||||
|
||||
MockedManagedIdentityCredential.prototype.getToken.mockImplementationOnce(
|
||||
() =>
|
||||
Promise.resolve({
|
||||
expiresOnTimestamp: Date.now() + minutes(4),
|
||||
token: 'expiring-soon-token',
|
||||
} as AccessToken),
|
||||
);
|
||||
|
||||
// First call to getToken to cache the expiring token
|
||||
await clientAssertion.getSignedAssertion();
|
||||
|
||||
MockedManagedIdentityCredential.prototype.getToken.mockImplementationOnce(
|
||||
() =>
|
||||
Promise.resolve({
|
||||
expiresOnTimestamp: Date.now() + hours(8),
|
||||
token: 'new-managed-identity-token',
|
||||
} as AccessToken),
|
||||
);
|
||||
|
||||
// Second call should obtain a new token
|
||||
const token = await clientAssertion.getSignedAssertion();
|
||||
|
||||
expect(token).toBe('new-managed-identity-token');
|
||||
expect(
|
||||
MockedManagedIdentityCredential.prototype.getToken,
|
||||
).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('Should obtain a new token if no token is cached', async () => {
|
||||
const clientAssertion = new ManagedIdentityClientAssertion({
|
||||
clientId: 'clientId',
|
||||
});
|
||||
|
||||
const token = await clientAssertion.getSignedAssertion();
|
||||
|
||||
expect(token).toBe('fake-managed-identity-token');
|
||||
expect(
|
||||
MockedManagedIdentityCredential.prototype.getToken,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should request a token for the correct scope', async () => {
|
||||
const clientAssertion = new ManagedIdentityClientAssertion({
|
||||
clientId: 'clientId',
|
||||
});
|
||||
|
||||
await clientAssertion.getSignedAssertion();
|
||||
|
||||
expect(
|
||||
MockedManagedIdentityCredential.prototype.getToken,
|
||||
).toHaveBeenCalledWith('api://AzureADTokenExchange');
|
||||
});
|
||||
|
||||
it('Should handle system-assigned managed identity', async () => {
|
||||
const clientAssertion = new ManagedIdentityClientAssertion();
|
||||
|
||||
await clientAssertion.getSignedAssertion();
|
||||
|
||||
expect(
|
||||
MockedManagedIdentityCredential.prototype.getToken,
|
||||
).toHaveBeenCalledWith('api://AzureADTokenExchange');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 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 { ManagedIdentityCredential } from '@azure/identity';
|
||||
import { ClientAssertion } from './ClientAssertion';
|
||||
|
||||
export type ManagedIdentityClientAssertionOptions = {
|
||||
clientId?: string;
|
||||
};
|
||||
|
||||
const fiveMinutes = 5 * 60 * 1000; // 5 minutes in milliseconds
|
||||
const expiresWithinFiveMinutes = (clientAssertion: ClientAssertion) =>
|
||||
clientAssertion.expiresOnTimestamp - Date.now() <= fiveMinutes;
|
||||
|
||||
/**
|
||||
* Class representing a Managed Identity Client Assertion.
|
||||
* This class is responsible for obtaining a signed client assertion using Azure Managed Identity.
|
||||
*/
|
||||
export class ManagedIdentityClientAssertion {
|
||||
private credential: ManagedIdentityCredential;
|
||||
private clientAssertion?: ClientAssertion;
|
||||
|
||||
/**
|
||||
* Creates an instance of ManagedIdentityClientAssertion.
|
||||
* @param options - Optional parameters for the ManagedIdentityClientAssertion.
|
||||
* - clientId: The client ID of the managed identity. If not provided, 'system-assigned' is used.
|
||||
*/
|
||||
constructor(options?: ManagedIdentityClientAssertionOptions) {
|
||||
let { clientId } = options || {};
|
||||
clientId ??= 'system-assigned';
|
||||
|
||||
this.credential =
|
||||
clientId === 'system-assigned'
|
||||
? new ManagedIdentityCredential()
|
||||
: new ManagedIdentityCredential(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a signed client assertion.
|
||||
* If a valid client assertion is already cached which doesn't expire soon, it returns the cached assertion.
|
||||
* Otherwise, it obtains a new access token and creates a new client assertion.
|
||||
* @returns A promise that resolves to the signed client assertion.
|
||||
*/
|
||||
public async getSignedAssertion(): Promise<string> {
|
||||
if (
|
||||
this.clientAssertion !== undefined &&
|
||||
!expiresWithinFiveMinutes(this.clientAssertion)
|
||||
) {
|
||||
return this.clientAssertion.signedAssertion;
|
||||
}
|
||||
|
||||
const accessToken = await this.credential.getToken(
|
||||
'api://AzureADTokenExchange',
|
||||
);
|
||||
|
||||
this.clientAssertion = {
|
||||
signedAssertion: accessToken.token,
|
||||
expiresOnTimestamp: accessToken.expiresOnTimestamp,
|
||||
};
|
||||
|
||||
return accessToken.token;
|
||||
}
|
||||
}
|
||||
@@ -167,7 +167,7 @@ describe('readAzureIntegrationConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reads all values when using a managed identity credential', () => {
|
||||
it('reads all values when using a managed identity client assertion credential', () => {
|
||||
const output = readAzureIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'dev.azure.com',
|
||||
@@ -175,6 +175,62 @@ describe('readAzureIntegrationConfig', () => {
|
||||
{
|
||||
organizations: ['org1', 'org2'],
|
||||
clientId: 'id',
|
||||
managedIdentityClientId: 'system-assigned',
|
||||
tenantId: 'tenant',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
host: 'dev.azure.com',
|
||||
credentials: [
|
||||
{
|
||||
kind: 'ManagedIdentityClientAssertion',
|
||||
organizations: ['org1', 'org2'],
|
||||
clientId: 'id',
|
||||
managedIdentityClientId: 'system-assigned',
|
||||
tenantId: 'tenant',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('reads all values when using a managed identity client assertion credential (without organizations)', () => {
|
||||
const output = readAzureIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'dev.azure.com',
|
||||
credentials: [
|
||||
{
|
||||
clientId: 'id',
|
||||
managedIdentityClientId: 'system-assigned',
|
||||
tenantId: 'tenant',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
host: 'dev.azure.com',
|
||||
credentials: [
|
||||
{
|
||||
kind: 'ManagedIdentityClientAssertion',
|
||||
clientId: 'id',
|
||||
managedIdentityClientId: 'system-assigned',
|
||||
tenantId: 'tenant',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('reads all values when using a managed identity credential', () => {
|
||||
const output = readAzureIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'dev.azure.com',
|
||||
credentials: [
|
||||
{
|
||||
organizations: ['org1', 'org2'],
|
||||
clientId: 'system-assigned',
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -186,7 +242,7 @@ describe('readAzureIntegrationConfig', () => {
|
||||
{
|
||||
kind: 'ManagedIdentity',
|
||||
organizations: ['org1', 'org2'],
|
||||
clientId: 'id',
|
||||
clientId: 'system-assigned',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -71,7 +71,8 @@ export type AzureIntegrationConfig = {
|
||||
export type AzureDevOpsCredentialKind =
|
||||
| 'PersonalAccessToken'
|
||||
| 'ClientSecret'
|
||||
| 'ManagedIdentity';
|
||||
| 'ManagedIdentity'
|
||||
| 'ManagedIdentityClientAssertion';
|
||||
|
||||
/**
|
||||
* Common fields for the Azure DevOps credentials.
|
||||
@@ -109,6 +110,31 @@ export type AzureClientSecretCredential = AzureCredentialBase & {
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A client assertion credential that uses a managed identity to generate a client assertion (JWT).
|
||||
* @public
|
||||
*/
|
||||
export type AzureManagedIdentityClientAssertionCredential =
|
||||
AzureCredentialBase & {
|
||||
kind: 'ManagedIdentityClientAssertion';
|
||||
/**
|
||||
* The Entra ID tenant
|
||||
*/
|
||||
tenantId: string;
|
||||
|
||||
/**
|
||||
* The client ID of the app registration you want to authenticate as.
|
||||
*/
|
||||
clientId: string;
|
||||
|
||||
/**
|
||||
* The client ID of the managed identity used to generate a client assertion (JWT).
|
||||
* Set to "system-assigned" to automatically use the system-assigned managed identity.
|
||||
* For user-assigned managed identities, specify the client ID of the managed identity you want to use.
|
||||
*/
|
||||
managedIdentityClientId: 'system-assigned' | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A managed identity credential.
|
||||
* @public
|
||||
@@ -118,7 +144,7 @@ export type AzureManagedIdentityCredential = AzureCredentialBase & {
|
||||
/**
|
||||
* The clientId
|
||||
*/
|
||||
clientId: string;
|
||||
clientId: 'system-assigned' | string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -136,6 +162,7 @@ export type PersonalAccessTokenCredential = AzureCredentialBase & {
|
||||
*/
|
||||
export type AzureDevOpsCredentialLike = Omit<
|
||||
Partial<AzureClientSecretCredential> &
|
||||
Partial<AzureManagedIdentityClientAssertionCredential> &
|
||||
Partial<AzureManagedIdentityCredential> &
|
||||
Partial<PersonalAccessTokenCredential>,
|
||||
'kind'
|
||||
@@ -147,12 +174,14 @@ export type AzureDevOpsCredentialLike = Omit<
|
||||
*/
|
||||
export type AzureDevOpsCredential =
|
||||
| AzureClientSecretCredential
|
||||
| AzureManagedIdentityClientAssertionCredential
|
||||
| AzureManagedIdentityCredential
|
||||
| PersonalAccessTokenCredential;
|
||||
|
||||
const AzureDevOpsCredentialFields = [
|
||||
'clientId',
|
||||
'clientSecret',
|
||||
'managedIdentityClientId',
|
||||
'tenantId',
|
||||
'personalAccessToken',
|
||||
] as const;
|
||||
@@ -164,6 +193,10 @@ const AzureDevopsCredentialFieldMap = new Map<
|
||||
>([
|
||||
['ClientSecret', ['clientId', 'clientSecret', 'tenantId']],
|
||||
['ManagedIdentity', ['clientId']],
|
||||
[
|
||||
'ManagedIdentityClientAssertion',
|
||||
['clientId', 'managedIdentityClientId', 'tenantId'],
|
||||
],
|
||||
['PersonalAccessToken', ['personalAccessToken']],
|
||||
]);
|
||||
|
||||
@@ -213,9 +246,12 @@ export function readAzureIntegrationConfig(
|
||||
personalAccessToken: credential
|
||||
.getOptionalString('personalAccessToken')
|
||||
?.trim(),
|
||||
tenantId: credential.getOptionalString('tenantId'),
|
||||
clientId: credential.getOptionalString('clientId'),
|
||||
tenantId: credential.getOptionalString('tenantId')?.trim(),
|
||||
clientId: credential.getOptionalString('clientId')?.trim(),
|
||||
clientSecret: credential.getOptionalString('clientSecret')?.trim(),
|
||||
managedIdentityClientId: credential
|
||||
.getOptionalString('managedIdentityClientId')
|
||||
?.trim(),
|
||||
};
|
||||
|
||||
return result;
|
||||
|
||||
@@ -25,6 +25,7 @@ export type {
|
||||
AzureCredentialBase,
|
||||
AzureClientSecretCredential,
|
||||
AzureManagedIdentityCredential,
|
||||
AzureManagedIdentityClientAssertionCredential,
|
||||
PersonalAccessTokenCredential,
|
||||
AzureDevOpsCredentialLike,
|
||||
AzureDevOpsCredential,
|
||||
|
||||
@@ -188,7 +188,7 @@ describe('gitlab core', () => {
|
||||
});
|
||||
|
||||
describe('getGitLabRequestOptions', () => {
|
||||
it('should return Authorization header when oauthToken is provided', () => {
|
||||
it('should return Authorization bearer header when a token is provided', () => {
|
||||
const token = '1234567890';
|
||||
const result = getGitLabRequestOptions(
|
||||
configSelfHosteWithRelativePath,
|
||||
@@ -202,29 +202,12 @@ describe('gitlab core', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return private-token header when gl-token is provided', () => {
|
||||
const token = 'glpat-1234566';
|
||||
const result = getGitLabRequestOptions(
|
||||
configSelfHosteWithRelativePath,
|
||||
token,
|
||||
);
|
||||
it('should return Authorization bearer header using the config token when no token is provided', () => {
|
||||
const result = getGitLabRequestOptions(configSelfHosteWithRelativePath);
|
||||
|
||||
expect(result).toEqual({
|
||||
headers: {
|
||||
'PRIVATE-TOKEN': token,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return private-token header when oauthToken is undefined', () => {
|
||||
const oauthToken = undefined;
|
||||
const result = getGitLabRequestOptions(
|
||||
configSelfHosteWithRelativePath,
|
||||
oauthToken,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
headers: {
|
||||
'PRIVATE-TOKEN': configSelfHosteWithRelativePath.token,
|
||||
Authorization: `Bearer ${configSelfHosteWithRelativePath.token}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,8 +40,9 @@ import {
|
||||
export async function getGitLabFileFetchUrl(
|
||||
url: string,
|
||||
config: GitLabIntegrationConfig,
|
||||
token?: string,
|
||||
): Promise<string> {
|
||||
const projectID = await getProjectId(url, config);
|
||||
const projectID = await getProjectId(url, config, token);
|
||||
return buildProjectUrl(url, projectID, config).toString();
|
||||
}
|
||||
|
||||
@@ -56,20 +57,17 @@ export function getGitLabRequestOptions(
|
||||
config: GitLabIntegrationConfig,
|
||||
token?: string,
|
||||
): { headers: Record<string, string> } {
|
||||
if (token) {
|
||||
// If token comes from the user and starts with "gl", it's a private token (see https://docs.gitlab.com/ee/security/token_overview.html#token-prefixes)
|
||||
return {
|
||||
headers: token.startsWith('gl')
|
||||
? { 'PRIVATE-TOKEN': token }
|
||||
: { Authorization: `Bearer ${token}` }, // Otherwise, it's a bearer token
|
||||
};
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
const accessToken = token || config.token;
|
||||
if (accessToken) {
|
||||
// OAuth, Personal, Project, and Group access tokens can all be passed via
|
||||
// a bearer authorization header
|
||||
// https://docs.gitlab.com/api/rest/authentication/#personalprojectgroup-access-tokens
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
// If token not provided, fetch the integration token
|
||||
const { token: configToken = '' } = config;
|
||||
return {
|
||||
headers: { 'PRIVATE-TOKEN': configToken },
|
||||
};
|
||||
return { headers };
|
||||
}
|
||||
|
||||
// Converts
|
||||
@@ -113,6 +111,7 @@ export function buildProjectUrl(
|
||||
export async function getProjectId(
|
||||
target: string,
|
||||
config: GitLabIntegrationConfig,
|
||||
token?: string,
|
||||
): Promise<number> {
|
||||
const url = new URL(target);
|
||||
|
||||
@@ -143,12 +142,18 @@ export async function getProjectId(
|
||||
|
||||
const response = await fetch(
|
||||
repoIDLookup.toString(),
|
||||
getGitLabRequestOptions(config),
|
||||
getGitLabRequestOptions(config, token),
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
throw new Error(
|
||||
'GitLab Error: 401 - Unauthorized. The access token used is either expired, or does not have permission to read the project',
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`GitLab Error '${data.error}', ${data.error_description}`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user