Skip to content

Commit 88a0fed

Browse files
Hartesicsonartech
authored andcommitted
SONAR-25432 Warn instance admins that their automations using Bitbucket Cloud App Passwords will fail
GitOrigin-RevId: d0552d3c9c55f274f63e98160516eba6d2399616
1 parent c9a6134 commit 88a0fed

File tree

10 files changed

+266
-1
lines changed

10 files changed

+266
-1
lines changed

apps/sq-server/src/main/js/apps/settings/components/SettingsAppRenderer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { withRouter } from '~shared/components/hoc/withRouter';
2727
import { Location } from '~shared/types/router';
2828
import { ExtendedSettingDefinition } from '~shared/types/settings';
2929
import ModeBanner from '~sq-server-commons/components/common/ModeBanner';
30+
import { BitbucketCloudAppDeprecationMessage } from '~sq-server-commons/components/devops-platform/BitbucketCloudAppDeprecationMessage';
3031
import { translate } from '~sq-server-commons/helpers/l10n';
3132
import { Component } from '~sq-server-commons/types/types';
3233
import { CATEGORY_OVERRIDES } from '../constants';
@@ -73,6 +74,8 @@ function SettingsAppRenderer(props: Readonly<SettingsAppRendererProps>) {
7374
<LargeCenteredLayout id="settings-page">
7475
<Helmet defer={false} title={translate('settings.page')} />
7576

77+
<BitbucketCloudAppDeprecationMessage className="sw-mt-8" />
78+
7679
<ModeBanner as="wideBanner" />
7780

7881
<div className="sw-my-8">

apps/sq-server/src/main/js/apps/settings/components/__tests__/SettingsApp-it.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,30 @@ import userEvent from '@testing-library/user-event';
2323
import { Route } from 'react-router-dom';
2424
import { registerServiceMocks } from '~shared/api/mocks/server';
2525
import { byRole, byText } from '~shared/helpers/testSelector';
26+
import { MessageTypes } from '~sq-server-commons/api/messages';
27+
import AlmSettingsServiceMock from '~sq-server-commons/api/mocks/AlmSettingsServiceMock';
2628
import {
2729
EntitlementsServiceDefaultDataset,
2830
EntitlementsServiceMock,
2931
mockPurchaseableFeature,
3032
} from '~sq-server-commons/api/mocks/EntitlementsServiceMock';
33+
import MessagesServiceMock from '~sq-server-commons/api/mocks/MessagesServiceMock';
3134
import { ModeServiceMock } from '~sq-server-commons/api/mocks/ModeServiceMock';
3235
import ScaServiceSettingsMock from '~sq-server-commons/api/mocks/ScaServiceSettingsMock';
3336
import SettingsServiceMock from '~sq-server-commons/api/mocks/SettingsServiceMock';
37+
import SystemServiceMock from '~sq-server-commons/api/mocks/SystemServiceMock';
3438
import { KeyboardKeys } from '~sq-server-commons/helpers/keycodes';
39+
import { mockAlmSettingsInstance } from '~sq-server-commons/helpers/mocks/alm-settings';
3540
import { mockComponent } from '~sq-server-commons/helpers/mocks/component';
41+
import { mockLoggedInUser } from '~sq-server-commons/helpers/testMocks';
3642
import {
3743
renderAppRoutes,
3844
renderAppWithComponentContext,
3945
RenderContext,
4046
} from '~sq-server-commons/helpers/testReactTestingUtils';
47+
import { AlmKeys } from '~sq-server-commons/types/alm-settings';
4148
import { Feature } from '~sq-server-commons/types/features';
49+
import { Permissions } from '~sq-server-commons/types/permissions';
4250
import { Component } from '~sq-server-commons/types/types';
4351
import routes from '../../routes';
4452

@@ -52,23 +60,32 @@ jest.mock('~sq-server-addons/index', () => ({
5260
},
5361
}));
5462

63+
let almSettingsMock: AlmSettingsServiceMock;
5564
let settingsMock: SettingsServiceMock;
5665
let scaSettingsMock: ScaServiceSettingsMock;
5766
let modeHandler: ModeServiceMock;
5867
let entitlementsMock: EntitlementsServiceMock;
68+
let messagesMock: MessagesServiceMock;
69+
let systemMock: SystemServiceMock;
5970

6071
beforeAll(() => {
72+
almSettingsMock = new AlmSettingsServiceMock();
6173
settingsMock = new SettingsServiceMock();
6274
scaSettingsMock = new ScaServiceSettingsMock();
6375
modeHandler = new ModeServiceMock();
6476
entitlementsMock = new EntitlementsServiceMock(EntitlementsServiceDefaultDataset);
77+
messagesMock = new MessagesServiceMock();
78+
systemMock = new SystemServiceMock();
6579
});
6680

6781
afterEach(() => {
82+
almSettingsMock.reset();
6883
settingsMock.reset();
6984
scaSettingsMock.reset();
7085
modeHandler.reset();
7186
entitlementsMock.reset();
87+
messagesMock.reset();
88+
systemMock.reset();
7289
});
7390

7491
beforeEach(() => {
@@ -78,6 +95,9 @@ beforeEach(() => {
7895

7996
const ui = {
8097
announcementHeading: byRole('heading', { name: 'property.category.general.Announcement' }),
98+
appPasswordDeprecationMessageTitle: byText(
99+
'admin_notification.bitbucket_cloud_app_deprecation.link',
100+
),
81101
categoryLink: (category: string) => byRole('link', { name: category }),
82102
externalAnalyzersAndroidHeading: byRole('heading', {
83103
name: 'property.category.External Analyzers.Android',
@@ -232,6 +252,60 @@ describe('Project Settings', () => {
232252
});
233253
});
234254

255+
describe('BitbucketCloudAppDeprecationMessage', () => {
256+
it('should render the message if the conditions are met', async () => {
257+
almSettingsMock.setAlmSettings([mockAlmSettingsInstance({ alm: AlmKeys.BitbucketCloud })]);
258+
259+
renderSettingsApp(undefined, {
260+
currentUser: mockLoggedInUser({ permissions: { global: [Permissions.Admin] } }),
261+
});
262+
expect(await ui.settingsSearchInput.find()).toBeInTheDocument();
263+
264+
expect(await ui.appPasswordDeprecationMessageTitle.find()).toBeInTheDocument();
265+
});
266+
267+
it('should not render the message if there is no Bitbucket Cloud ALM settings', async () => {
268+
almSettingsMock.setAlmSettings([]);
269+
270+
renderSettingsApp(undefined, {
271+
currentUser: mockLoggedInUser({ permissions: { global: [Permissions.Admin] } }),
272+
});
273+
expect(await ui.settingsSearchInput.find()).toBeInTheDocument();
274+
275+
expect(ui.appPasswordDeprecationMessageTitle.query()).not.toBeInTheDocument();
276+
});
277+
278+
it('should not render the message if the message has been dismissed', async () => {
279+
almSettingsMock.setAlmSettings([mockAlmSettingsInstance({ alm: AlmKeys.BitbucketCloud })]);
280+
messagesMock.setMessageDismissed({
281+
messageType: MessageTypes.BitbucketCloudAppDeprecation,
282+
});
283+
284+
renderSettingsApp(undefined, {
285+
currentUser: mockLoggedInUser({ permissions: { global: [Permissions.Admin] } }),
286+
});
287+
expect(await ui.settingsSearchInput.find()).toBeInTheDocument();
288+
289+
expect(ui.appPasswordDeprecationMessageTitle.query()).not.toBeInTheDocument();
290+
});
291+
292+
it('should not render the message if the installation date is after the maximum installation date', async () => {
293+
almSettingsMock.setAlmSettings([mockAlmSettingsInstance({ alm: AlmKeys.BitbucketCloud })]);
294+
systemMock.supportInformation = {
295+
statistics: {
296+
installationDate: '2025-10-01',
297+
},
298+
};
299+
300+
renderSettingsApp(undefined, {
301+
currentUser: mockLoggedInUser({ permissions: { global: [Permissions.Admin] } }),
302+
});
303+
expect(await ui.settingsSearchInput.find()).toBeInTheDocument();
304+
305+
expect(ui.appPasswordDeprecationMessageTitle.query()).not.toBeInTheDocument();
306+
});
307+
});
308+
235309
function renderSettingsApp(component?: Component, context: RenderContext = {}) {
236310
const path = component ? 'project' : 'admin';
237311
const wrapperRoutes = () => <Route path={path}>{routes()}</Route>;

libs/sq-server-commons/src/api/messages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { getJSON } from '~adapters/helpers/request';
2323
import { post } from '../helpers/request';
2424

2525
export enum MessageTypes {
26+
BitbucketCloudAppDeprecation = 'BITBUCKET_CLOUD_APP_DEPRECATION',
2627
GlobalNcd90 = 'GLOBAL_NCD_90',
2728
GlobalNcdPage90 = 'GLOBAL_NCD_PAGE_90',
2829
ProjectNcd90 = 'PROJECT_NCD_90',

libs/sq-server-commons/src/api/mocks/AlmSettingsServiceMock.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,10 @@ export default class AlmSettingsServiceMock {
363363
return this.reply(undefined);
364364
};
365365

366+
setAlmSettings = (almSettings: AlmSettingsInstance[]) => {
367+
this.#almSettings = almSettings;
368+
};
369+
366370
reset = () => {
367371
this.#almSettings = cloneDeep(defaultAlmSettings);
368372
this.#almDefinitions = cloneDeep(defaultAlmDefinitions);

libs/sq-server-commons/src/api/mocks/SystemServiceMock.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
mockPaging,
2727
mockStandaloneSysInfo,
2828
} from '../../helpers/testMocks';
29+
import { SupportInformation } from '../../types/settings';
2930
import { EmailConfiguration, LogsLevels } from '../../types/system';
3031
import {
3132
Provider,
@@ -36,6 +37,7 @@ import {
3637
} from '../../types/types';
3738
import {
3839
getEmailConfigurations,
40+
getSupportInformation,
3941
getSystemInfo,
4042
getSystemStatus,
4143
getSystemUpgrades,
@@ -66,6 +68,12 @@ export default class SystemServiceMock {
6668

6769
emailConfigurations: EmailConfiguration[] = [];
6870

71+
supportInformation: SupportInformation = {
72+
statistics: {
73+
installationDate: new Date('2025-01-01').toISOString(),
74+
},
75+
};
76+
6977
constructor() {
7078
this.updateSystemInfo();
7179
jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo);
@@ -75,6 +83,7 @@ export default class SystemServiceMock {
7583
jest.mocked(getEmailConfigurations).mockImplementation(this.handleGetEmailConfigurations);
7684
jest.mocked(postEmailConfiguration).mockImplementation(this.handlePostEmailConfiguration);
7785
jest.mocked(patchEmailConfiguration).mockImplementation(this.handlePatchEmailConfiguration);
86+
jest.mocked(getSupportInformation).mockImplementation(this.handleGetSupportInformation);
7887
}
7988

8089
handleGetSystemInfo = () => {
@@ -157,6 +166,10 @@ export default class SystemServiceMock {
157166
return this.reply(this.emailConfigurations[index]);
158167
};
159168

169+
handleGetSupportInformation = () => {
170+
return this.reply(this.supportInformation);
171+
};
172+
160173
addEmailConfiguration = (configuration: EmailConfiguration) => {
161174
this.emailConfigurations.push(configuration);
162175
};

libs/sq-server-commons/src/api/system.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { axiosClient } from '~shared/helpers/axios-clients';
2424
import { requestTryAndRepeatUntil } from '~shared/helpers/request';
2525
import { Paging } from '~shared/types/paging';
2626
import { post, postJSON } from '../helpers/request';
27+
import { SupportInformation } from '../types/settings';
2728
import {
2829
EmailConfiguration,
2930
MigrationStatus,
@@ -108,3 +109,7 @@ export function patchEmailConfiguration(
108109
emailConfiguration,
109110
);
110111
}
112+
113+
export function getSupportInformation(): Promise<SupportInformation> {
114+
return axiosClient.get('/api/support/info');
115+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* SonarQube
3+
* Copyright (C) 2009-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
import { Link, MessageCallout, MessageVariety } from '@sonarsource/echoes-react';
22+
import { isBefore } from 'date-fns';
23+
import { noop } from 'lodash';
24+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
25+
import { FormattedMessage, useIntl } from 'react-intl';
26+
import { useCurrentUser } from '~adapters/helpers/users';
27+
import { getAlmSettings } from '../../api/alm-settings';
28+
import { MessageTypes } from '../../api/messages';
29+
import { hasGlobalPermission } from '../../helpers/users';
30+
import {
31+
useMessageDismissedMutation,
32+
useMessageDismissedQuery,
33+
} from '../../queries/dismissed-messages';
34+
import { useSupportInformationQuery } from '../../queries/system';
35+
import { AlmKeys, AlmSettingsInstance } from '../../types/alm-settings';
36+
import { Permissions } from '../../types/permissions';
37+
38+
interface BitbucketCloudAppDeprecationMessageProps {
39+
className?: string;
40+
}
41+
42+
const MAXIMUM_INSTANCE_INSTALLATION_DATE = new Date('2025-09-09');
43+
const APP_PASSWORD_DEACTIVATION_DATE = new Date('2026-06-09');
44+
const BITBUCKET_API_TOKEN_DOCUMENTATION_URL =
45+
'https://support.atlassian.com/bitbucket-cloud/docs/create-an-api-token/';
46+
47+
export function BitbucketCloudAppDeprecationMessage({
48+
className,
49+
}: Readonly<BitbucketCloudAppDeprecationMessageProps>) {
50+
const isFetchingAlmSettings = useRef(false);
51+
const [bitbucketCloudAlmSettings, setBitbucketCloudAlmSettings] =
52+
useState<AlmSettingsInstance[]>();
53+
54+
const { formatMessage } = useIntl();
55+
56+
const { currentUser } = useCurrentUser();
57+
58+
const isGlobalAdmin = hasGlobalPermission(currentUser, Permissions.Admin);
59+
60+
const { data: installationDate, error: supportInformationError } = useSupportInformationQuery({
61+
select: (data) => data.statistics.installationDate,
62+
});
63+
const { data: isMessageDismissed } = useMessageDismissedQuery(
64+
{
65+
messageType: MessageTypes.BitbucketCloudAppDeprecation,
66+
},
67+
{
68+
select: (data) => data.dismissed,
69+
},
70+
);
71+
const { mutate: dismissMessage } = useMessageDismissedMutation();
72+
73+
const shouldDisplayBanner = useMemo(() => {
74+
return (
75+
isGlobalAdmin &&
76+
isMessageDismissed === false &&
77+
Boolean(bitbucketCloudAlmSettings?.length) &&
78+
isBefore(new Date(), APP_PASSWORD_DEACTIVATION_DATE) &&
79+
((installationDate !== undefined &&
80+
isBefore(new Date(installationDate), MAXIMUM_INSTANCE_INSTALLATION_DATE)) ||
81+
supportInformationError !== null)
82+
);
83+
}, [
84+
bitbucketCloudAlmSettings,
85+
installationDate,
86+
isMessageDismissed,
87+
isGlobalAdmin,
88+
supportInformationError,
89+
]);
90+
91+
const onDismissMessage = useCallback(() => {
92+
dismissMessage({
93+
messageType: MessageTypes.BitbucketCloudAppDeprecation,
94+
});
95+
}, [dismissMessage]);
96+
97+
useEffect(() => {
98+
if (
99+
!isGlobalAdmin ||
100+
bitbucketCloudAlmSettings !== undefined ||
101+
isFetchingAlmSettings.current ||
102+
isMessageDismissed === undefined ||
103+
isMessageDismissed === true
104+
) {
105+
return;
106+
}
107+
108+
isFetchingAlmSettings.current = true;
109+
110+
getAlmSettings()
111+
.then((almSettings) => {
112+
const bbcSettings = almSettings.filter(
113+
(almSetting) => almSetting.alm === AlmKeys.BitbucketCloud,
114+
);
115+
116+
if (bbcSettings.length === 0) {
117+
onDismissMessage();
118+
return;
119+
}
120+
121+
setBitbucketCloudAlmSettings(bbcSettings);
122+
})
123+
.catch(noop);
124+
}, [bitbucketCloudAlmSettings, isMessageDismissed, isGlobalAdmin, onDismissMessage]);
125+
126+
if (!shouldDisplayBanner) {
127+
return null;
128+
}
129+
130+
return (
131+
<MessageCallout
132+
action={
133+
<Link to={BITBUCKET_API_TOKEN_DOCUMENTATION_URL}>
134+
<FormattedMessage id="admin_notification.bitbucket_cloud_app_deprecation.link" />
135+
</Link>
136+
}
137+
className={className}
138+
onDismiss={onDismissMessage}
139+
title={formatMessage({ id: 'admin_notification.bitbucket_cloud_app_deprecation.title' })}
140+
variety={MessageVariety.Info}
141+
>
142+
<FormattedMessage id="admin_notification.bitbucket_cloud_app_deprecation.banner" />
143+
</MessageCallout>
144+
);
145+
}

libs/sq-server-commons/src/l10n/default.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,11 @@ export const defaultMessages = {
599599
'New SonarQube Server version available',
600600
'admin_notification.update.new_sqs_version_when_running_sqcb.upgrade':
601601
'Upgrade to SonarQube Server and get access to enterprise features',
602+
'admin_notification.bitbucket_cloud_app_deprecation.title':
603+
'Bitbucket API tokens have replaced app passwords',
604+
'admin_notification.bitbucket_cloud_app_deprecation.body':
605+
'Bitbucket Cloud app is deprecated and will be removed in the future. Please migrate to the new Bitbucket app.',
606+
'admin_notification.bitbucket_cloud_app_deprecation.link': 'Create API token on Bitbucket',
602607

603608
//------------------------------------------------------------------------------
604609
//

0 commit comments

Comments
 (0)