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