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