Skip to content

Commit 24d6dce

Browse files
authored
feat(integrations): allow to rotate client secret of Internal/Public integrations, frontend part (#69115)
To be merged after #69015 <img width="1167" alt="image" src="https://github.com/getsentry/sentry/assets/1127549/eb8ba4bf-2596-4ce5-be74-8386d1bacbc1">
1 parent 1a732c3 commit 24d6dce

File tree

2 files changed

+106
-3
lines changed

2 files changed

+106
-3
lines changed

static/app/views/settings/organizationDeveloperSettings/sentryApplicationDetails.spec.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import {RouterFixture} from 'sentry-fixture/routerFixture';
44
import {SentryAppFixture} from 'sentry-fixture/sentryApp';
55
import {SentryAppTokenFixture} from 'sentry-fixture/sentryAppToken';
66

7-
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
7+
import {
8+
render,
9+
renderGlobalModal,
10+
screen,
11+
userEvent,
12+
waitFor,
13+
} from 'sentry-test/reactTestingLibrary';
814
import selectEvent from 'sentry-test/selectEvent';
915

1016
import SentryApplicationDetails from 'sentry/views/settings/organizationDeveloperSettings/sentryApplicationDetails';
@@ -586,5 +592,49 @@ describe('Sentry Application Details', function () {
586592
)
587593
).toBeInTheDocument();
588594
});
595+
596+
it('handles client secret rotation', async function () {
597+
sentryApp = SentryAppFixture();
598+
sentryApp.clientSecret = null;
599+
600+
MockApiClient.addMockResponse({
601+
url: `/sentry-apps/${sentryApp.slug}/`,
602+
body: sentryApp,
603+
});
604+
const rotateSecretApiCall = MockApiClient.addMockResponse({
605+
method: 'POST',
606+
url: `/sentry-apps/${sentryApp.slug}/rotate-secret/`,
607+
body: {
608+
clientSecret: 'newSecret!',
609+
},
610+
});
611+
612+
render(
613+
<SentryApplicationDetails
614+
router={router}
615+
location={router.location}
616+
routes={router.routes}
617+
route={router.routes[0]}
618+
routeParams={{}}
619+
params={{appSlug: sentryApp.slug}}
620+
/>
621+
);
622+
renderGlobalModal();
623+
624+
expect(screen.getByText('hidden')).toBeInTheDocument();
625+
expect(
626+
screen.getByRole('button', {name: 'Rotate client secret'})
627+
).toBeInTheDocument();
628+
await userEvent.click(screen.getByRole('button', {name: 'Rotate client secret'}));
629+
630+
expect(
631+
screen.getByText('This will be the only time your client secret is visible!')
632+
).toBeInTheDocument();
633+
expect(screen.getByText('Rotated Client Secret')).toBeInTheDocument();
634+
expect(screen.getByText('Your client secret is:')).toBeInTheDocument();
635+
expect(screen.getByText('newSecret!')).toBeInTheDocument();
636+
637+
expect(rotateSecretApiCall).toHaveBeenCalledTimes(1);
638+
});
589639
});
590640
});

static/app/views/settings/organizationDeveloperSettings/sentryApplicationDetails.tsx

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import {Observer} from 'mobx-react';
77
import scrollToElement from 'scroll-to-element';
88

99
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
10+
import {openModal} from 'sentry/actionCreators/modal';
1011
import {
1112
addSentryAppToken,
1213
removeSentryAppToken,
1314
} from 'sentry/actionCreators/sentryAppTokens';
15+
import {Alert} from 'sentry/components/alert';
1416
import Avatar from 'sentry/components/avatar';
1517
import type {Model} from 'sentry/components/avatarChooser';
1618
import AvatarChooser from 'sentry/components/avatarChooser';
@@ -315,6 +317,33 @@ class SentryApplicationDetails extends DeprecatedAsyncView<Props, State> {
315317
return tokensToDisplay;
316318
};
317319

320+
rotateClientSecret = async () => {
321+
try {
322+
const rotateResponse = await this.api.requestPromise(
323+
`/sentry-apps/${this.props.params.appSlug}/rotate-secret/`,
324+
{
325+
method: 'POST',
326+
}
327+
);
328+
openModal(({Body, Header}) => (
329+
<Fragment>
330+
<Header>{t('Rotated Client Secret')}</Header>
331+
<Body>
332+
<Alert type="info" showIcon>
333+
{t('This will be the only time your client secret is visible!')}
334+
</Alert>
335+
<p>
336+
{t('Your client secret is:')}
337+
<code>{rotateResponse.clientSecret}</code>
338+
</p>
339+
</Body>
340+
</Fragment>
341+
));
342+
} catch {
343+
addErrorMessage(t('Error rotating secret'));
344+
}
345+
};
346+
318347
onFieldChange = (name: string, value: FieldValue): void => {
319348
if (name === 'webhookUrl' && !value && this.isInternal) {
320349
// if no webhook, then set isAlertable to false
@@ -488,7 +517,12 @@ class SentryApplicationDetails extends DeprecatedAsyncView<Props, State> {
488517
)}
489518
</FormField>
490519
)}
491-
<FormField name="clientSecret" label="Client Secret">
520+
<FormField
521+
name="clientSecret"
522+
label="Client Secret"
523+
help={t(`Your secret is only available briefly after integration creation. Make
524+
sure to save this value!`)}
525+
>
492526
{({value, id}) =>
493527
value ? (
494528
<Tooltip
@@ -504,7 +538,14 @@ class SentryApplicationDetails extends DeprecatedAsyncView<Props, State> {
504538
</TextCopyInput>
505539
</Tooltip>
506540
) : (
507-
<em>hidden</em>
541+
<ClientSecret>
542+
<HiddenSecret>{t('hidden')}</HiddenSecret>
543+
{this.hasTokenAccess ? (
544+
<Button onClick={this.rotateClientSecret} priority="danger">
545+
Rotate client secret
546+
</Button>
547+
) : undefined}
548+
</ClientSecret>
508549
)
509550
}
510551
</FormField>
@@ -542,3 +583,15 @@ const AvatarPreviewText = styled('span')`
542583
grid-area: 2 / 2 / 3 / 3;
543584
padding-left: ${space(2)};
544585
`;
586+
587+
const HiddenSecret = styled('span')`
588+
width: 100px;
589+
font-style: italic;
590+
`;
591+
592+
const ClientSecret = styled('div')`
593+
display: flex;
594+
justify-content: right;
595+
align-items: center;
596+
margin-right: 0;
597+
`;

0 commit comments

Comments
 (0)