Skip to content

Commit dcbdb1e

Browse files
subirjollySubir Jolly
andauthored
feat: add certificate support for subscriptions (#5369)
* feat: add certificate support for subscriptions * chore: update form to validate certificate fields * chore: update modal to be wrapped in ClickOutside component * chore: remove extra fragment * chore: cert provided timestamp param name * chore: add placeholders for certificate textareas * chore: update per product feedback * chore: update import path * chore: update per review Co-authored-by: Subir Jolly <sjolly@influxdata.com>
1 parent 2075279 commit dcbdb1e

File tree

12 files changed

+402
-39
lines changed

12 files changed

+402
-39
lines changed

src/overlays/components/OverlayController.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import ConfirmationOverlay from 'src/support/components/ConfirmationOverlay'
4646

4747
// Actions
4848
import {dismissOverlay} from 'src/overlays/actions/overlays'
49+
import {ReplaceCertificateOverlay} from 'src/writeData/subscriptions/components/CertificateInput'
4950

5051
export interface OverlayContextType {
5152
onClose: () => void
@@ -163,6 +164,9 @@ export const OverlayController: FunctionComponent = () => {
163164
case 'help-bar-confirmation':
164165
activeOverlay.current = <ConfirmationOverlay onClose={onClose} />
165166
break
167+
case 'subscription-replace-certificate':
168+
activeOverlay.current = <ReplaceCertificateOverlay onClose={onClose} />
169+
break
166170
default:
167171
activeOverlay.current = null
168172
break

src/overlays/reducers/overlays.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export type OverlayID =
3434
| 'free-account-support'
3535
| 'feedback-questions'
3636
| 'help-bar-confirmation'
37+
| 'subscription-replace-certificate'
3738

3839
export interface OverlayState {
3940
id: OverlayID | null

src/types/subscriptions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {SubwayNavModel} from '@influxdata/clockface'
22

3+
export type BrokerAuthType = 'none' | 'user' | 'certificate'
4+
35
export interface Subscription {
46
id?: string
57
name?: string
@@ -33,6 +35,11 @@ export interface Subscription {
3335
flowVersion?: number
3436
timestampPrecision?: string
3537
notebookID?: string
38+
brokerClientKey?: string
39+
brokerClientCert?: string
40+
brokerCACert?: string
41+
authType: BrokerAuthType
42+
certProvidedAt?: string
3643
}
3744

3845
export interface SubscriptionStatus {

src/writeData/subscriptions/components/BrokerFormContent.tsx

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {event} from 'src/cloud/utils/reporting'
4040

4141
interface Props {
4242
formContent: Subscription
43-
updateForm: (any) => void
43+
updateForm: (any: Subscription) => void
4444
className: string
4545
edit: boolean
4646
}
@@ -54,7 +54,7 @@ const BrokerFormContent: FC<Props> = ({
5454
const mqttProtocol = 'MQTT'
5555
const protocolList = [mqttProtocol]
5656
const [protocol, setProtocol] = useState(mqttProtocol)
57-
const [security, setSecurity] = useState('none')
57+
5858
useEffect(() => {
5959
updateForm({...formContent, protocol: protocol.toLowerCase()})
6060
}, [protocol])
@@ -68,11 +68,6 @@ const BrokerFormContent: FC<Props> = ({
6868
})
6969
}, [])
7070

71-
useEffect(() => {
72-
if (formContent.brokerUsername) {
73-
setSecurity('user')
74-
}
75-
}, [])
7671
return (
7772
<Grid>
7873
<Grid.Row>
@@ -199,7 +194,7 @@ const BrokerFormContent: FC<Props> = ({
199194
}
200195
helpText={
201196
className !== 'create' && edit
202-
? 'Changing the hostname will require you to provide your password again.'
197+
? 'Changing the hostname will require you to provide your security credentials again.'
203198
: ''
204199
}
205200
>
@@ -301,16 +296,22 @@ const BrokerFormContent: FC<Props> = ({
301296
name="no-security"
302297
id="none"
303298
testID={`${className}-broker-form-no-security--button`}
304-
active={security === 'none'}
299+
active={formContent.authType === 'none'}
305300
onClick={() => {
306301
event(
307302
'broker security toggle',
308303
{method: 'none', step: 'broker'},
309304
{feature: 'subscriptions'}
310305
)
311-
setSecurity('none')
312-
formContent.brokerUsername = null
313-
formContent.brokerPassword = null
306+
updateForm({
307+
...formContent,
308+
authType: 'none',
309+
brokerUsername: null,
310+
brokerPassword: null,
311+
brokerCACert: null,
312+
brokerClientCert: null,
313+
brokerClientKey: null,
314+
})
314315
}}
315316
value="none"
316317
titleText="None"
@@ -322,14 +323,20 @@ const BrokerFormContent: FC<Props> = ({
322323
name="user"
323324
id="user"
324325
testID={`${className}-broker-form--user--button`}
325-
active={security === 'user'}
326+
active={formContent.authType === 'user'}
326327
onClick={() => {
327328
event(
328329
'broker security toggle',
329330
{method: 'user', step: 'broker'},
330331
{feature: 'subscriptions'}
331332
)
332-
setSecurity('user')
333+
updateForm({
334+
...formContent,
335+
authType: 'user',
336+
brokerCACert: null,
337+
brokerClientCert: null,
338+
brokerClientKey: null,
339+
})
333340
}}
334341
value="user"
335342
titleText="User"
@@ -341,14 +348,19 @@ const BrokerFormContent: FC<Props> = ({
341348
name="certificate"
342349
id="certificate"
343350
testID="certificate--button"
344-
active={security === 'certificate'}
351+
active={formContent.authType === 'certificate'}
345352
onClick={() => {
346353
event(
347354
'broker security toggle',
348355
{method: 'certificate', step: 'broker'},
349356
{feature: 'subscriptions'}
350357
)
351-
setSecurity('certificate')
358+
updateForm({
359+
...formContent,
360+
brokerUsername: null,
361+
brokerPassword: null,
362+
authType: 'certificate',
363+
})
352364
}}
353365
value="certificate"
354366
titleText="Certificate"
@@ -357,15 +369,21 @@ const BrokerFormContent: FC<Props> = ({
357369
Certificate
358370
</SelectGroup.Option>
359371
</SelectGroup>
360-
{security === 'user' && (
372+
{formContent.authType === 'user' && (
361373
<UserInput
362374
formContent={formContent}
363375
updateForm={updateForm}
364376
className={className}
365377
edit={edit}
366378
/>
367379
)}
368-
{security === 'certificate' && <CertificateInput />}
380+
{formContent.authType === 'certificate' && (
381+
<CertificateInput
382+
subscription={formContent}
383+
updateForm={updateForm}
384+
edit={edit}
385+
/>
386+
)}
369387
</Grid.Column>
370388
</Grid.Row>
371389
</Grid>

src/writeData/subscriptions/components/CertificateInput.tsx

Lines changed: 197 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Libraries
2-
import React, {FC, useContext} from 'react'
2+
import React, {FC, useCallback, useContext} from 'react'
33

44
// Contexts
55
import {AppSettingContext} from 'src/shared/contexts/app'
@@ -14,9 +14,89 @@ import {
1414
Button,
1515
Icon,
1616
IconFont,
17+
JustifyContent,
18+
ComponentColor,
19+
ButtonType,
20+
Overlay,
21+
FlexBoxChild,
22+
ClickOutside,
1723
} from '@influxdata/clockface'
24+
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
25+
import TextAreaWithLabel from 'src/writeData/subscriptions/components/TextAreaWithLabel'
26+
import {event} from 'src/cloud/utils/reporting'
27+
import {useDispatch} from 'react-redux'
28+
import {dismissOverlay, showOverlay} from 'src/overlays/actions/overlays'
29+
import {Subscription} from 'src/types'
30+
import {OverlayContext} from 'src/overlays/components/OverlayController'
1831

19-
const CertificateInput: FC = () => {
32+
interface OwnProps {
33+
edit?: boolean
34+
updateForm: (_: Subscription) => void
35+
subscription: Subscription
36+
}
37+
38+
const NewCertificateInput: FC<OwnProps> = ({updateForm, subscription}) => {
39+
const handleUpdateCACert = useCallback(
40+
e => {
41+
updateForm({...subscription, brokerCACert: e.target.value})
42+
},
43+
[subscription, updateForm]
44+
)
45+
const handleUpdatePrivateKey = useCallback(
46+
e => {
47+
updateForm({...subscription, brokerClientKey: e.target.value})
48+
},
49+
[subscription, updateForm]
50+
)
51+
const handleUpdateCert = useCallback(
52+
e => {
53+
updateForm({...subscription, brokerClientCert: e.target.value})
54+
},
55+
[subscription, updateForm]
56+
)
57+
58+
return (
59+
<FlexBox
60+
alignItems={AlignItems.FlexStart}
61+
justifyContent={JustifyContent.Center}
62+
direction={FlexDirection.Column}
63+
margin={ComponentSize.Large}
64+
>
65+
<InputLabel size={ComponentSize.Medium}>
66+
Enable secure communication for your subscription with SSL and TLS
67+
<br />
68+
certificates by configuring the following certificate information:
69+
</InputLabel>
70+
<TextAreaWithLabel
71+
name="Certificate"
72+
label="Certificate authority"
73+
onChange={handleUpdateCACert}
74+
value={subscription?.brokerCACert ?? ''}
75+
placeholder={CertificatePlaceholders.caCert}
76+
rows={4}
77+
required
78+
/>
79+
<TextAreaWithLabel
80+
name="PrivateKey"
81+
label="Private Key"
82+
onChange={handleUpdatePrivateKey}
83+
placeholder={CertificatePlaceholders.clientKey}
84+
value={subscription?.brokerClientKey ?? ''}
85+
rows={4}
86+
/>
87+
<TextAreaWithLabel
88+
name="CertificateAuthority"
89+
label="Certificate"
90+
onChange={handleUpdateCert}
91+
value={subscription?.brokerClientCert ?? ''}
92+
placeholder={CertificatePlaceholders.clientCert}
93+
rows={4}
94+
/>
95+
</FlexBox>
96+
)
97+
}
98+
99+
const OldCertificateInput: FC = () => {
20100
const {
21101
subscriptionsCertificateInterest,
22102
setSubscriptionsCertificateInterest,
@@ -51,4 +131,119 @@ const CertificateInput: FC = () => {
51131
</FlexBox>
52132
)
53133
}
134+
135+
const CertificatePlaceholders = {
136+
caCert: `-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----`,
137+
clientCert: `-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----`,
138+
clientKey: `-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----`,
139+
}
140+
141+
interface ReplaceCertificateModalProps {
142+
onClose: () => void
143+
}
144+
export const ReplaceCertificateOverlay: FC<ReplaceCertificateModalProps> = () => {
145+
const {
146+
onClose,
147+
params: {subscription, updateForm},
148+
} = useContext(OverlayContext)
149+
const handleReplaceCert = useCallback(() => {
150+
updateForm({...subscription, certProvidedAt: null})
151+
onClose()
152+
}, [subscription, updateForm, onClose])
153+
return (
154+
<Overlay visible={true} className="subscription-replace-cert-overlay">
155+
<ClickOutside onClickOutside={onClose}>
156+
<div>
157+
<Overlay.Container maxWidth={450} style={{paddingTop: '20px'}}>
158+
<Overlay.Body>
159+
<FlexBox
160+
direction={FlexDirection.Row}
161+
stretchToFitWidth={true}
162+
justifyContent={JustifyContent.SpaceBetween}
163+
alignItems={AlignItems.Center}
164+
>
165+
<div>
166+
Are you sure you want to replace your certificate? Once you
167+
save your changes, we cannot retrieve the previous
168+
certificate.
169+
</div>
170+
<Button
171+
color={ComponentColor.Primary}
172+
text="Yes, Continue"
173+
onClick={handleReplaceCert}
174+
testID="subs-replace-cert-confirm-btn"
175+
/>
176+
</FlexBox>
177+
</Overlay.Body>
178+
</Overlay.Container>
179+
</div>
180+
</ClickOutside>
181+
</Overlay>
182+
)
183+
}
184+
185+
const CertificateDetails: FC<OwnProps> = ({subscription, updateForm, edit}) => {
186+
const dispatch = useDispatch()
187+
return (
188+
<FlexBox
189+
direction={FlexDirection.Row}
190+
margin={ComponentSize.Large}
191+
stretchToFitWidth={true}
192+
justifyContent={JustifyContent.SpaceBetween}
193+
alignItems={AlignItems.Center}
194+
>
195+
<FlexBoxChild>
196+
<FlexBox
197+
direction={FlexDirection.Column}
198+
alignItems={AlignItems.Stretch}
199+
justifyContent={JustifyContent.Center}
200+
>
201+
<InputLabel size={ComponentSize.Medium} style={{fontWeight: 'bold'}}>
202+
Certificate
203+
</InputLabel>
204+
<InputLabel size={ComponentSize.Small}>
205+
{subscription.certProvidedAt}
206+
</InputLabel>
207+
</FlexBox>
208+
</FlexBoxChild>
209+
{edit && (
210+
<Button
211+
text="Replace Certificate"
212+
color={ComponentColor.Secondary}
213+
onClick={() => {
214+
event('replace certificate clicked', {}, {feature: 'subscriptions'})
215+
dispatch(
216+
showOverlay(
217+
'subscription-replace-certificate',
218+
{subscription, updateForm},
219+
() => dispatch(dismissOverlay)
220+
)
221+
)
222+
}}
223+
type={ButtonType.Button}
224+
titleText="Edit"
225+
testID="update-sub-form--edit"
226+
/>
227+
)}
228+
</FlexBox>
229+
)
230+
}
231+
232+
const CertificateInput: FC<OwnProps> = ({subscription, updateForm, edit}) => {
233+
if (subscription.authType === 'certificate' && subscription.certProvidedAt) {
234+
return (
235+
<CertificateDetails
236+
subscription={subscription}
237+
updateForm={updateForm}
238+
edit={edit}
239+
/>
240+
)
241+
}
242+
243+
return isFlagEnabled('enableCertificateSupport') ? (
244+
<NewCertificateInput subscription={subscription} updateForm={updateForm} />
245+
) : (
246+
<OldCertificateInput />
247+
)
248+
}
54249
export default CertificateInput

0 commit comments

Comments
 (0)