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