Skip to content

feat(integrations): allow to rotate client secret of Internal/Public integrations, frontend part #69115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import {RouterFixture} from 'sentry-fixture/routerFixture';
import {SentryAppFixture} from 'sentry-fixture/sentryApp';
import {SentryAppTokenFixture} from 'sentry-fixture/sentryAppToken';

import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
import {
render,
renderGlobalModal,
screen,
userEvent,
waitFor,
} from 'sentry-test/reactTestingLibrary';
import selectEvent from 'sentry-test/selectEvent';

import SentryApplicationDetails from 'sentry/views/settings/organizationDeveloperSettings/sentryApplicationDetails';
Expand Down Expand Up @@ -586,5 +592,49 @@ describe('Sentry Application Details', function () {
)
).toBeInTheDocument();
});

it('handles client secret rotation', async function () {
sentryApp = SentryAppFixture();
sentryApp.clientSecret = null;

MockApiClient.addMockResponse({
url: `/sentry-apps/${sentryApp.slug}/`,
body: sentryApp,
});
const rotateSecretApiCall = MockApiClient.addMockResponse({
method: 'POST',
url: `/sentry-apps/${sentryApp.slug}/rotate-secret/`,
body: {
clientSecret: 'newSecret!',
},
});

render(
<SentryApplicationDetails
router={router}
location={router.location}
routes={router.routes}
route={router.routes[0]}
routeParams={{}}
params={{appSlug: sentryApp.slug}}
/>
);
renderGlobalModal();

expect(screen.getByText('hidden')).toBeInTheDocument();
expect(
screen.getByRole('button', {name: 'Rotate client secret'})
).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', {name: 'Rotate client secret'}));

expect(
screen.getByText('This will be the only time your client secret is visible!')
).toBeInTheDocument();
expect(screen.getByText('Rotated Client Secret')).toBeInTheDocument();
expect(screen.getByText('Your client secret is:')).toBeInTheDocument();
expect(screen.getByText('newSecret!')).toBeInTheDocument();

expect(rotateSecretApiCall).toHaveBeenCalledTimes(1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {Observer} from 'mobx-react';
import scrollToElement from 'scroll-to-element';

import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
import {openModal} from 'sentry/actionCreators/modal';
import {
addSentryAppToken,
removeSentryAppToken,
} from 'sentry/actionCreators/sentryAppTokens';
import {Alert} from 'sentry/components/alert';
import Avatar from 'sentry/components/avatar';
import type {Model} from 'sentry/components/avatarChooser';
import AvatarChooser from 'sentry/components/avatarChooser';
Expand Down Expand Up @@ -315,6 +317,33 @@ class SentryApplicationDetails extends DeprecatedAsyncView<Props, State> {
return tokensToDisplay;
};

rotateClientSecret = async () => {
try {
const rotateResponse = await this.api.requestPromise(
`/sentry-apps/${this.props.params.appSlug}/rotate-secret/`,
{
method: 'POST',
}
);
openModal(({Body, Header}) => (
<Fragment>
<Header>{t('Rotated Client Secret')}</Header>
<Body>
<Alert type="info" showIcon>
{t('This will be the only time your client secret is visible!')}
</Alert>
<p>
{t('Your client secret is:')}
<code>{rotateResponse.clientSecret}</code>
</p>
</Body>
</Fragment>
Comment on lines +329 to +340
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i created a component such that the value is only shown once, would that fit your needs? You wouldn't have to worry about wording and everything because you could just pass in the token

));
} catch {
addErrorMessage(t('Error rotating secret'));
}
};

onFieldChange = (name: string, value: FieldValue): void => {
if (name === 'webhookUrl' && !value && this.isInternal) {
// if no webhook, then set isAlertable to false
Expand Down Expand Up @@ -488,7 +517,12 @@ class SentryApplicationDetails extends DeprecatedAsyncView<Props, State> {
)}
</FormField>
)}
<FormField name="clientSecret" label="Client Secret">
<FormField
name="clientSecret"
label="Client Secret"
help={t(`Your secret is only available briefly after integration creation. Make
sure to save this value!`)}
>
{({value, id}) =>
value ? (
<Tooltip
Expand All @@ -504,7 +538,14 @@ class SentryApplicationDetails extends DeprecatedAsyncView<Props, State> {
</TextCopyInput>
</Tooltip>
) : (
<em>hidden</em>
<ClientSecret>
<HiddenSecret>{t('hidden')}</HiddenSecret>
{this.hasTokenAccess ? (
<Button onClick={this.rotateClientSecret} priority="danger">
Rotate client secret
</Button>
) : undefined}
</ClientSecret>
)
}
</FormField>
Expand Down Expand Up @@ -542,3 +583,15 @@ const AvatarPreviewText = styled('span')`
grid-area: 2 / 2 / 3 / 3;
padding-left: ${space(2)};
`;

const HiddenSecret = styled('span')`
width: 100px;
font-style: italic;
`;

const ClientSecret = styled('div')`
display: flex;
justify-content: right;
align-items: center;
margin-right: 0;
`;
Loading