QGIS API Documentation 4.1.0-Master (5bf3c20f3c9)
Loading...
Searching...
No Matches
qgsauthimportidentitydialog.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsauthimportidentitydialog.cpp
3 ---------------------
4 begin : May 9, 2015
5 copyright : (C) 2015 by Boundless Spatial, Inc. USA
6 author : Larry Shaffer
7 email : lshaffer at boundlessgeo dot com
8 ***************************************************************************
9 * *
10 * This program is free software; you can redistribute it and/or modify *
11 * it under the terms of the GNU General Public License as published by *
12 * the Free Software Foundation; either version 2 of the License, or *
13 * (at your option) any later version. *
14 * *
15 ***************************************************************************/
16
17#include "ui_qgsauthimportidentitydialog.h"
19
20#include "qgsapplication.h"
21#include "qgsauthcertutils.h"
22#include "qgsauthconfig.h"
23#include "qgsauthguiutils.h"
24#include "qgsauthmanager.h"
25#include "qgshelp.h"
26#include "qgslogger.h"
27#include "qgssettings.h"
28
29#include <QFile>
30#include <QFileDialog>
31#include <QPushButton>
32#include <QString>
33
34#include "moc_qgsauthimportidentitydialog.cpp"
35
36using namespace Qt::StringLiterals;
37
39 : QDialog( parent )
40 , mPkiBundle( QgsPkiBundle() )
41{
42 if ( QgsApplication::authManager()->isDisabled() )
43 {
44 mDisabled = true;
45 mAuthNotifyLayout = new QVBoxLayout;
46 this->setLayout( mAuthNotifyLayout );
47 mAuthNotify = new QLabel( QgsApplication::authManager()->disabledMessage(), this );
48 mAuthNotifyLayout->addWidget( mAuthNotify );
49 }
50 else
51 {
52 setupUi( this );
53 connect( lePkiPathsKeyPass, &QLineEdit::textChanged, this, &QgsAuthImportIdentityDialog::lePkiPathsKeyPass_textChanged );
54 connect( chkPkiPathsPassShow, &QCheckBox::stateChanged, this, &QgsAuthImportIdentityDialog::chkPkiPathsPassShow_stateChanged );
55 connect( btnPkiPathsCert, &QToolButton::clicked, this, &QgsAuthImportIdentityDialog::btnPkiPathsCert_clicked );
56 connect( btnPkiPathsKey, &QToolButton::clicked, this, &QgsAuthImportIdentityDialog::btnPkiPathsKey_clicked );
57 connect( lePkiPkcs12KeyPass, &QLineEdit::textChanged, this, &QgsAuthImportIdentityDialog::lePkiPkcs12KeyPass_textChanged );
58 connect( chkPkiPkcs12PassShow, &QCheckBox::stateChanged, this, &QgsAuthImportIdentityDialog::chkPkiPkcs12PassShow_stateChanged );
59 connect( btnPkiPkcs12Bundle, &QToolButton::clicked, this, &QgsAuthImportIdentityDialog::btnPkiPkcs12Bundle_clicked );
60 connect( buttonBox, &QDialogButtonBox::rejected, this, &QWidget::close );
61 connect( buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
62 connect( buttonBox, &QDialogButtonBox::helpRequested, this, [] { QgsHelp::openHelp( u"auth_system/auth_workflows.html#authentication-identities"_s ); } );
63 mIdentityType = identitytype;
64
65 populateIdentityType();
66 }
67}
68
70{
71 if ( mDisabled )
72 {
74 }
75 return mIdentityType;
76}
77
78const QPair<QSslCertificate, QSslKey> QgsAuthImportIdentityDialog::certBundleToImport()
79{
80 if ( mDisabled )
81 {
82 return qMakePair( QSslCertificate(), QSslKey() );
83 }
84 return mCertBundle;
85}
86
87void QgsAuthImportIdentityDialog::populateIdentityType()
88{
89 if ( mIdentityType == CertIdentity )
90 {
91 stkwBundleType->setVisible( true );
92
93 cmbIdentityTypes->addItem( tr( "PKI PEM/DER Certificate Paths" ), QVariant( QgsAuthImportIdentityDialog::PkiPaths ) );
94 cmbIdentityTypes->addItem( tr( "PKI PKCS#12 Certificate Bundle" ), QVariant( QgsAuthImportIdentityDialog::PkiPkcs12 ) );
95
96 connect( cmbIdentityTypes, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), stkwBundleType, &QStackedWidget::setCurrentIndex );
97 connect( stkwBundleType, &QStackedWidget::currentChanged, cmbIdentityTypes, &QComboBox::setCurrentIndex );
98
99 connect( cmbIdentityTypes, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, [this] { validateIdentity(); } );
100 connect( stkwBundleType, &QStackedWidget::currentChanged, this, &QgsAuthImportIdentityDialog::validateIdentity );
101
102 cmbIdentityTypes->setCurrentIndex( 0 );
103 stkwBundleType->setCurrentIndex( 0 );
104 stkwBundleType->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly );
105 }
106 // else switch stacked widget, and populate/connect according to that type and widget
107}
108
109void QgsAuthImportIdentityDialog::validateIdentity()
110{
111 bool ok = false;
112 if ( mIdentityType == CertIdentity )
113 {
114 ok = validateBundle();
115 }
116 okButton()->setEnabled( ok );
117}
118
119bool QgsAuthImportIdentityDialog::validateBundle()
120{
121 // clear out any previously set bundle
122 const QSslCertificate emptycert;
123 const QSslKey emptykey;
124 mCertBundle = qMakePair( emptycert, emptykey );
125 mPkiBundle = QgsPkiBundle();
126
127 QWidget *curpage = stkwBundleType->currentWidget();
128 if ( curpage == pagePkiPaths )
129 {
130 return validatePkiPaths();
131 }
132 else if ( curpage == pagePkiPkcs12 )
133 {
134 return validatePkiPkcs12();
135 }
136
137 return false;
138}
139
140void QgsAuthImportIdentityDialog::clearValidation()
141{
142 teValidation->clear();
143 teValidation->setStyleSheet( QString() );
144}
145
146void QgsAuthImportIdentityDialog::writeValidation( const QString &msg, QgsAuthImportIdentityDialog::Validity valid, bool append )
147{
148 QString ss;
149 QString txt( msg );
150 switch ( valid )
151 {
152 case Valid:
153 ss = QgsAuthGuiUtils::greenTextStyleSheet( u"QTextEdit"_s );
154 txt = tr( "Valid: %1" ).arg( msg );
155 break;
156 case Invalid:
157 ss = QgsAuthGuiUtils::redTextStyleSheet( u"QTextEdit"_s );
158 txt = tr( "Invalid: %1" ).arg( msg );
159 break;
160 case Unknown:
161 break;
162 }
163 teValidation->setStyleSheet( ss );
164 if ( append )
165 {
166 teValidation->append( txt );
167 }
168 else
169 {
170 teValidation->setText( txt );
171 }
172 teValidation->moveCursor( QTextCursor::Start );
173}
174
175void QgsAuthImportIdentityDialog::lePkiPathsKeyPass_textChanged( const QString &pass )
176{
177 Q_UNUSED( pass )
178 validateIdentity();
179}
180
181void QgsAuthImportIdentityDialog::chkPkiPathsPassShow_stateChanged( int state )
182{
183 lePkiPathsKeyPass->setEchoMode( ( state > 0 ) ? QLineEdit::Normal : QLineEdit::Password );
184}
185
186void QgsAuthImportIdentityDialog::btnPkiPathsCert_clicked()
187{
188 const QString &fn = getOpenFileName( tr( "Open Client Certificate File" ), tr( "All files (*.*);;PEM (*.pem);;DER (*.der)" ) );
189 if ( !fn.isEmpty() )
190 {
191 lePkiPathsCert->setText( fn );
192 validateIdentity();
193 }
194}
195
196void QgsAuthImportIdentityDialog::btnPkiPathsKey_clicked()
197{
198 const QString &fn = getOpenFileName( tr( "Open Private Key File" ), tr( "All files (*.*);;PEM (*.pem);;DER (*.der)" ) );
199 if ( !fn.isEmpty() )
200 {
201 lePkiPathsKey->setText( fn );
202 validateIdentity();
203 }
204}
205
206void QgsAuthImportIdentityDialog::lePkiPkcs12KeyPass_textChanged( const QString &pass )
207{
208 Q_UNUSED( pass )
209 validateIdentity();
210}
211
212void QgsAuthImportIdentityDialog::chkPkiPkcs12PassShow_stateChanged( int state )
213{
214 lePkiPkcs12KeyPass->setEchoMode( ( state > 0 ) ? QLineEdit::Normal : QLineEdit::Password );
215}
216
217void QgsAuthImportIdentityDialog::btnPkiPkcs12Bundle_clicked()
218{
219 const QString &fn = getOpenFileName( tr( "Open PKCS#12 Certificate Bundle" ), tr( "PKCS#12 (*.p12 *.pfx)" ) );
220 if ( !fn.isEmpty() )
221 {
222 lePkiPkcs12Bundle->setText( fn );
223 validateIdentity();
224 }
225}
226
227bool QgsAuthImportIdentityDialog::validatePkiPaths()
228{
229 bool isvalid = false;
230
231 // required components
232 const QString certpath( lePkiPathsCert->text() );
233 const QString keypath( lePkiPathsKey->text() );
234
235 const bool certfound = QFile::exists( certpath );
236 const bool keyfound = QFile::exists( keypath );
237
238 fileFound( certpath.isEmpty() || certfound, lePkiPathsCert );
239 fileFound( keypath.isEmpty() || keyfound, lePkiPathsKey );
240
241 if ( !certfound || !keyfound )
242 {
243 writeValidation( tr( "Missing components" ), Invalid );
244 return false;
245 }
246
247 // check for issue date validity
248 QSslCertificate clientcert;
249 QList<QSslCertificate> certs( QgsAuthCertUtils::certsFromFile( certpath ) );
250 QList<QSslCertificate> ca_certs;
251 if ( !certs.isEmpty() )
252 {
253 clientcert = certs.takeFirst();
254 }
255 else
256 {
257 writeValidation( tr( "Failed to read client certificate from file" ), Invalid );
258 return false;
259 }
260
261 if ( clientcert.isNull() )
262 {
263 writeValidation( tr( "Failed to load client certificate from file" ), Invalid );
264 return false;
265 }
266
267 if ( !certs.isEmpty() ) // Multiple certificates in file
268 {
269 teValidation->append( tr( "Extra certificates found with identity" ) );
270 ca_certs = certs;
271 }
272
273 isvalid = QgsAuthCertUtils::certIsViable( clientcert );
274
275 const QDateTime startdate( clientcert.effectiveDate() );
276 const QDateTime enddate( clientcert.expiryDate() );
277
278 writeValidation( tr( "%1 thru %2" ).arg( startdate.toString(), enddate.toString() ), ( QgsAuthCertUtils::certIsCurrent( clientcert ) ? Valid : Invalid ) );
279 //TODO: set enabled on cert info button, relative to cert validity
280
281 // check for valid private key and that any supplied password works
282 const QString keypass( lePkiPathsKeyPass->text() );
283 const QSslKey clientkey( QgsAuthCertUtils::keyFromFile( keypath, keypass ) );
284 if ( clientkey.isNull() )
285 {
286 writeValidation( tr( "Failed to load client private key from file" ), Invalid, true );
287 if ( !keypass.isEmpty() )
288 {
289 writeValidation( tr( "Private key password may not match" ), Invalid, true );
290 }
291 return false;
292 }
293
294 if ( isvalid )
295 {
296 mCertBundle = qMakePair( clientcert, clientkey );
297 mPkiBundle = QgsPkiBundle( clientcert, clientkey, ca_certs );
298 }
299
300 return isvalid;
301}
302
303bool QgsAuthImportIdentityDialog::validatePkiPkcs12()
304{
305 // required components
306 const QString bundlepath( lePkiPkcs12Bundle->text() );
307 const bool bundlefound = QFile::exists( bundlepath );
308 fileFound( bundlepath.isEmpty() || bundlefound, lePkiPkcs12Bundle );
309
310 if ( !bundlefound )
311 {
312 writeValidation( tr( "Missing components" ), Invalid );
313 return false;
314 }
315
316 if ( !QCA::isSupported( "pkcs12" ) )
317 {
318 writeValidation( tr( "QCA library has no PKCS#12 support" ), Invalid );
319 return false;
320 }
321
322 // load the bundle
323 QCA::SecureArray passarray;
324 QString keypass = QString();
325 if ( !lePkiPkcs12KeyPass->text().isEmpty() )
326 {
327 passarray = QCA::SecureArray( lePkiPkcs12KeyPass->text().toUtf8() );
328 keypass = lePkiPkcs12KeyPass->text();
329 }
330
331 QCA::ConvertResult res;
332 const QCA::KeyBundle bundle( QCA::KeyBundle::fromFile( bundlepath, passarray, &res, u"qca-ossl"_s ) );
333
334 if ( res == QCA::ErrorFile )
335 {
336 writeValidation( tr( "Failed to read bundle file" ), Invalid );
337 return false;
338 }
339 else if ( res == QCA::ErrorPassphrase )
340 {
341 writeValidation( tr( "Incorrect bundle password" ), Invalid );
342 lePkiPkcs12KeyPass->setPlaceholderText( u"Required passphrase"_s );
343 return false;
344 }
345 else if ( res == QCA::ErrorDecode )
346 {
347 writeValidation( tr( "Failed to decode (try entering password)" ), Invalid );
348 return false;
349 }
350
351 if ( bundle.isNull() )
352 {
353 writeValidation( tr( "Bundle empty or can not be loaded" ), Invalid );
354 return false;
355 }
356
357 // check for primary cert and that it is valid
358 const QCA::Certificate cert( bundle.certificateChain().primary() );
359 if ( cert.isNull() )
360 {
361 writeValidation( tr( "Bundle client cert can not be loaded" ), Invalid );
362 return false;
363 }
364
365 // TODO: add more robust validation, including cert chain resolution
366 const QDateTime startdate( cert.notValidBefore() );
367 const QDateTime enddate( cert.notValidAfter() );
368 const QDateTime now( QDateTime::currentDateTime() );
369 const bool bundlevalid = ( now >= startdate && now <= enddate );
370
371 writeValidation( tr( "%1 thru %2" ).arg( startdate.toString(), enddate.toString() ), ( bundlevalid ? Valid : Invalid ) );
372
373 if ( bundlevalid )
374 {
375 QSslCertificate clientcert;
376 QList<QSslCertificate> certs( QgsAuthCertUtils::certsFromString( cert.toPEM() ) );
377 if ( !certs.isEmpty() )
378 {
379 clientcert = certs.first();
380 }
381 if ( clientcert.isNull() )
382 {
383 writeValidation( tr( "Qt cert could not be created from QCA cert" ), Invalid, true );
384 return false;
385 }
386 QSslKey clientkey;
387 clientkey = QSslKey( bundle.privateKey().toRSA().toPEM().toLatin1(), QSsl::Rsa );
388 if ( clientkey.isNull() )
389 {
390 writeValidation( tr( "Qt private key could not be created from QCA key" ), Invalid, true );
391 return false;
392 }
393
394 const QCA::CertificateChain cert_chain( bundle.certificateChain() );
395 QList<QSslCertificate> ca_certs;
396 if ( cert_chain.size() > 1 )
397 {
398 const auto constCert_chain = cert_chain;
399 for ( const QCA::Certificate &ca_cert : constCert_chain )
400 {
401 if ( ca_cert != cert_chain.primary() )
402 {
403 ca_certs << QSslCertificate( ca_cert.toPEM().toLatin1() );
404 }
405 }
406 }
407
408 mCertBundle = qMakePair( clientcert, clientkey );
409 mPkiBundle = QgsPkiBundle( clientcert, clientkey, ca_certs );
410 }
411
412 return bundlevalid;
413}
414
415void QgsAuthImportIdentityDialog::fileFound( bool found, QWidget *widget )
416{
417 if ( !found )
418 {
419 widget->setStyleSheet( QgsAuthGuiUtils::redTextStyleSheet( u"QLineEdit"_s ) );
420 widget->setToolTip( tr( "File not found" ) );
421 }
422 else
423 {
424 widget->setStyleSheet( QString() );
425 widget->setToolTip( QString() );
426 }
427}
428
429QString QgsAuthImportIdentityDialog::getOpenFileName( const QString &title, const QString &extfilter )
430{
431 QgsSettings settings;
432 const QString recentdir = settings.value( u"UI/lastAuthImportBundleOpenFileDir"_s, QDir::homePath() ).toString();
433 QString f = QFileDialog::getOpenFileName( this, title, recentdir, extfilter );
434
435 // return dialog focus on Mac
436 this->raise();
437 this->activateWindow();
438
439 if ( !f.isEmpty() )
440 {
441 settings.setValue( u"UI/lastAuthImportBundleOpenFileDir"_s, QFileInfo( f ).absoluteDir().path() );
442 }
443 return f;
444}
445
446QPushButton *QgsAuthImportIdentityDialog::okButton()
447{
448 return buttonBox->button( QDialogButtonBox::Ok );
449}
static QgsAuthManager * authManager()
Returns the application's authentication manager instance.
static QString greenTextStyleSheet(const QString &selector="*")
Green text stylesheet representing valid, trusted, etc. certificate.
static QString redTextStyleSheet(const QString &selector="*")
Red text stylesheet representing invalid, untrusted, etc. certificate.
const QPair< QSslCertificate, QSslKey > certBundleToImport()
Gets certificate/key bundle to be imported.
Validity
Type of certificate/bundle validity output.
IdentityType
Type of identity being imported.
QgsAuthImportIdentityDialog::IdentityType identityType() const
Gets identity type.
QgsAuthImportIdentityDialog(QgsAuthImportIdentityDialog::IdentityType identitytype, QWidget *parent=nullptr)
Construct a dialog for importing identities.
static void openHelp(const QString &key)
Opens help topic for the given help key using default system web browser.
Definition qgshelp.cpp:41
Storage set for PKI bundle: SSL certificate, key, optional CA cert chain.
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
@ CurrentPageOnly
Only the size of the current page is considered when calculating the stacked widget size.