github.com/argoproj/argo-cd/v2@v2.10.9/ui/src/app/settings/components/certs-list/certs-list.tsx (about) 1 import {DropDownMenu, FormField, NotificationType, SlidingPanel} from 'argo-ui'; 2 import * as PropTypes from 'prop-types'; 3 import * as React from 'react'; 4 import {Form, FormApi, Text, TextArea} from 'react-form'; 5 import {RouteComponentProps} from 'react-router'; 6 7 import {DataLoader, EmptyState, ErrorNotification, Page} from '../../../shared/components'; 8 import {AppContext} from '../../../shared/context'; 9 import * as models from '../../../shared/models'; 10 import {services} from '../../../shared/services'; 11 12 require('./certs-list.scss'); 13 14 interface NewTLSCertParams { 15 serverName: string; 16 certType: string; 17 certData: string; 18 } 19 20 interface NewSSHKnownHostParams { 21 certData: string; 22 } 23 24 export class CertsList extends React.Component<RouteComponentProps<any>> { 25 public static contextTypes = { 26 router: PropTypes.object, 27 apis: PropTypes.object, 28 history: PropTypes.object 29 }; 30 31 private formApiTLS: FormApi; 32 private formApiSSH: FormApi; 33 private loader: DataLoader; 34 35 public render() { 36 return ( 37 <Page 38 title='Repository certificates and known hosts' 39 toolbar={{ 40 breadcrumbs: [{title: 'Settings', path: '/settings'}, {title: 'Repository certificates and known hosts'}], 41 actionMenu: { 42 className: 'fa fa-plus', 43 items: [ 44 { 45 title: 'Add TLS certificate', 46 iconClassName: 'fa fa-plus', 47 action: () => (this.showAddTLSCertificate = true) 48 }, 49 { 50 title: 'Add SSH known hosts', 51 iconClassName: 'fa fa-plus', 52 action: () => (this.showAddSSHKnownHosts = true) 53 } 54 ] 55 } 56 }}> 57 <div className='certs-list'> 58 <div className='argo-container'> 59 <DataLoader load={() => services.certs.list()} ref={loader => (this.loader = loader)}> 60 {(certs: models.RepoCert[]) => 61 (certs.length > 0 && ( 62 <div className='argo-table-list'> 63 <div className='argo-table-list__head'> 64 <div className='row'> 65 <div className='columns small-3'>SERVER NAME</div> 66 <div className='columns small-3'>CERT TYPE</div> 67 <div className='columns small-6'>CERT INFO</div> 68 </div> 69 </div> 70 {certs.map(cert => ( 71 <div className='argo-table-list__row' key={cert.certType + '_' + cert.certSubType + '_' + cert.serverName}> 72 <div className='row'> 73 <div className='columns small-3'> 74 <i className='icon argo-icon-git' /> {cert.serverName} 75 </div> 76 <div className='columns small-3'> 77 {cert.certType} {cert.certSubType} 78 </div> 79 <div className='columns small-6'> 80 {cert.certInfo} 81 <DropDownMenu 82 anchor={() => ( 83 <button className='argo-button argo-button--light argo-button--lg argo-button--short'> 84 <i className='fa fa-ellipsis-v' /> 85 </button> 86 )} 87 items={[ 88 { 89 title: 'Remove', 90 action: () => this.removeCert(cert.serverName, cert.certType, cert.certSubType) 91 } 92 ]} 93 /> 94 </div> 95 </div> 96 </div> 97 ))} 98 </div> 99 )) || ( 100 <EmptyState icon='argo-icon-git'> 101 <h4>No certificates configured</h4> 102 <h5>You can add further certificates below..</h5> 103 <button className='argo-button argo-button--base' onClick={() => (this.showAddTLSCertificate = true)}> 104 Add TLS certificates 105 </button>{' '} 106 <button className='argo-button argo-button--base' onClick={() => (this.showAddSSHKnownHosts = true)}> 107 Add SSH known hosts 108 </button> 109 </EmptyState> 110 ) 111 } 112 </DataLoader> 113 </div> 114 </div> 115 <SlidingPanel 116 isShown={this.showAddTLSCertificate} 117 onClose={() => (this.showAddTLSCertificate = false)} 118 header={ 119 <div> 120 <button className='argo-button argo-button--base' onClick={() => this.formApiTLS.submitForm(null)}> 121 Create 122 </button>{' '} 123 <button onClick={() => (this.showAddTLSCertificate = false)} className='argo-button argo-button--base-o'> 124 Cancel 125 </button> 126 </div> 127 }> 128 <Form 129 onSubmit={params => this.addTLSCertificate(params as NewTLSCertParams)} 130 getApi={api => (this.formApiTLS = api)} 131 preSubmit={(params: NewTLSCertParams) => ({ 132 serverName: params.serverName, 133 certData: btoa(params.certData) 134 })} 135 validateError={(params: NewTLSCertParams) => ({ 136 serverName: !params.serverName && 'Repository Server Name is required', 137 certData: !params.certData && 'TLS Certificate is required' 138 })}> 139 {formApiTLS => ( 140 <form onSubmit={formApiTLS.submitForm} role='form' className='certs-list width-control' encType='multipart/form-data'> 141 <div className='white-box'> 142 <p>CREATE TLS REPOSITORY CERTIFICATE</p> 143 <div className='argo-form-row'> 144 <FormField formApi={formApiTLS} label='Repository Server Name' field='serverName' component={Text} /> 145 </div> 146 <div className='argo-form-row'> 147 <FormField formApi={formApiTLS} label='TLS Certificate (PEM format)' field='certData' component={TextArea} /> 148 </div> 149 </div> 150 </form> 151 )} 152 </Form> 153 </SlidingPanel> 154 <SlidingPanel 155 isShown={this.showAddSSHKnownHosts} 156 onClose={() => (this.showAddSSHKnownHosts = false)} 157 header={ 158 <div> 159 <button className='argo-button argo-button--base' onClick={() => this.formApiSSH.submitForm(null)}> 160 Create 161 </button>{' '} 162 <button onClick={() => (this.showAddSSHKnownHosts = false)} className='argo-button argo-button--base-o'> 163 Cancel 164 </button> 165 </div> 166 }> 167 <Form 168 onSubmit={params => this.addSSHKnownHosts(params as NewSSHKnownHostParams)} 169 getApi={api => (this.formApiSSH = api)} 170 preSubmit={(params: NewSSHKnownHostParams) => ({ 171 certData: btoa(params.certData) 172 })} 173 validateError={(params: NewSSHKnownHostParams) => ({ 174 certData: !params.certData && 'SSH known hosts data is required' 175 })}> 176 {formApiSSH => ( 177 <form onSubmit={formApiSSH.submitForm} role='form' className='certs-list width-control' encType='multipart/form-data'> 178 <div className='white-box'> 179 <p>CREATE SSH KNOWN HOST ENTRIES</p> 180 <p> 181 Paste SSH known hosts data in the text area below, one entry per line. You can use output from <code>ssh-keyscan</code> or the contents on 182 an <code>ssh_known_hosts</code> file verbatim. Lines starting with <code>#</code> will be treated as comments and ignored. 183 </p> 184 <p> 185 <strong>Make sure there are no linebreaks in the keys.</strong> 186 </p> 187 <div className='argo-form-row'> 188 <FormField formApi={formApiSSH} label='SSH known hosts data' field='certData' component={TextArea} /> 189 </div> 190 </div> 191 </form> 192 )} 193 </Form> 194 </SlidingPanel> 195 </Page> 196 ); 197 } 198 199 private clearForms() { 200 this.formApiSSH.resetAll(); 201 this.formApiTLS.resetAll(); 202 } 203 204 private async addTLSCertificate(params: NewTLSCertParams) { 205 try { 206 await services.certs.create({items: [{serverName: params.serverName, certType: 'https', certData: params.certData, certSubType: '', certInfo: ''}], metadata: null}); 207 this.showAddTLSCertificate = false; 208 this.loader.reload(); 209 } catch (e) { 210 this.appContext.apis.notifications.show({ 211 content: <ErrorNotification title='Unable to add TLS certificate' e={e} />, 212 type: NotificationType.Error 213 }); 214 } 215 } 216 217 private async addSSHKnownHosts(params: NewSSHKnownHostParams) { 218 try { 219 let knownHostEntries: models.RepoCert[] = []; 220 atob(params.certData) 221 .split('\n') 222 .forEach(function processEntry(item, index) { 223 const trimmedLine = item.trimLeft(); 224 if (trimmedLine.startsWith('#') === false) { 225 const knownHosts = trimmedLine.split(' ', 3); 226 if (knownHosts.length === 3) { 227 // Perform a little sanity check on the data - server 228 // checks too, but let's not send it invalid data in 229 // the first place. 230 const subType = knownHosts[1].match(/^(ssh\-[a-z0-9]+|ecdsa-[a-z0-9\-]+)$/gi); 231 if (subType != null) { 232 // Key could be valid for multiple hosts 233 const hostnames = knownHosts[0].split(','); 234 for (const hostname of hostnames) { 235 knownHostEntries = knownHostEntries.concat({ 236 serverName: hostname, 237 certType: 'ssh', 238 certSubType: knownHosts[1], 239 certData: btoa(knownHosts[2]), 240 certInfo: '' 241 }); 242 } 243 } else { 244 throw new Error('Invalid SSH subtype: ' + subType); 245 } 246 } 247 } 248 }); 249 if (knownHostEntries.length === 0) { 250 throw new Error('No valid known hosts data entered'); 251 } 252 await services.certs.create({items: knownHostEntries, metadata: null}); 253 this.showAddSSHKnownHosts = false; 254 this.loader.reload(); 255 } catch (e) { 256 this.appContext.apis.notifications.show({ 257 content: <ErrorNotification title='Unable to add SSH known hosts data' e={e} />, 258 type: NotificationType.Error 259 }); 260 } 261 } 262 263 private async removeCert(serverName: string, certType: string, certSubType: string) { 264 const confirmed = await this.appContext.apis.popup.confirm('Remove certificate', 'Are you sure you want to remove ' + certType + ' certificate for ' + serverName + '?'); 265 if (confirmed) { 266 await services.certs.delete(serverName, certType, certSubType); 267 this.loader.reload(); 268 } 269 } 270 271 private get showAddTLSCertificate() { 272 return new URLSearchParams(this.props.location.search).get('addTLSCert') === 'true'; 273 } 274 275 private set showAddTLSCertificate(val: boolean) { 276 this.clearForms(); 277 this.appContext.router.history.push(`${this.props.match.url}?addTLSCert=${val}`); 278 } 279 280 private get showAddSSHKnownHosts() { 281 return new URLSearchParams(this.props.location.search).get('addSSHKnownHosts') === 'true'; 282 } 283 284 private set showAddSSHKnownHosts(val: boolean) { 285 this.clearForms(); 286 this.appContext.router.history.push(`${this.props.match.url}?addSSHKnownHosts=${val}`); 287 } 288 289 private get appContext(): AppContext { 290 return this.context as AppContext; 291 } 292 }