Rebase opt-in fingerprint workaround onto KScreenLocker 6.7.5
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
#include "pamauthenticators.h"
|
||||
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
#include <security/pam_appl.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
// Test-only PAM implementation. No real authentication or system files are used.
|
||||
struct pam_handle {
|
||||
QByteArray service;
|
||||
pam_conv conversation;
|
||||
int attempts = 0;
|
||||
};
|
||||
|
||||
int pam_start(const char *service, const char *, const pam_conv *conversation, pam_handle_t **handle)
|
||||
{
|
||||
*handle = new pam_handle{service, *conversation};
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
int pam_end(pam_handle_t *handle, int)
|
||||
{
|
||||
delete handle;
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
int pam_set_item(pam_handle_t *, int, const void *) { return PAM_SUCCESS; }
|
||||
int pam_setcred(pam_handle_t *, int) { return PAM_SUCCESS; }
|
||||
const char *pam_strerror(pam_handle_t *, int) { return "simulated PAM result"; }
|
||||
|
||||
int pam_authenticate(pam_handle_t *handle, int)
|
||||
{
|
||||
++handle->attempts;
|
||||
if (handle->service == "missing") {
|
||||
return PAM_MODULE_UNKNOWN;
|
||||
}
|
||||
if (handle->service == "transient" && handle->attempts == 1) {
|
||||
return PAM_AUTHINFO_UNAVAIL;
|
||||
}
|
||||
if (handle->service == "timeout" && handle->attempts == 1) {
|
||||
return PAM_MAXTRIES;
|
||||
}
|
||||
pam_message message{PAM_PROMPT_ECHO_OFF, "Test credential:"};
|
||||
const pam_message *messages = &message;
|
||||
pam_response *response = nullptr;
|
||||
const auto &conversation = handle->conversation;
|
||||
const int result = conversation.conv(1, &messages, &response, conversation.appdata_ptr);
|
||||
if (result != PAM_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
const bool accepted = response && response->resp && std::strcmp(response->resp, "correct") == 0;
|
||||
if (response) {
|
||||
std::free(response->resp);
|
||||
std::free(response);
|
||||
}
|
||||
return accepted ? PAM_SUCCESS : PAM_AUTH_ERR;
|
||||
}
|
||||
|
||||
class RetryTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private Q_SLOTS:
|
||||
void retryWithActivePassword_data()
|
||||
{
|
||||
QTest::addColumn<QString>("service");
|
||||
QTest::newRow("temporary unavailability") << QStringLiteral("transient");
|
||||
QTest::newRow("attempt timeout") << QStringLiteral("timeout");
|
||||
}
|
||||
|
||||
void retryWithActivePassword()
|
||||
{
|
||||
QFETCH(QString, service);
|
||||
auto password = std::make_unique<PamAuthenticator>(QStringLiteral("password"), QStringLiteral("test"));
|
||||
auto fingerprint = std::make_unique<PamAuthenticator>(service, QStringLiteral("test"), PamAuthenticator::Fingerprint);
|
||||
auto *fingerprintPtr = fingerprint.get();
|
||||
QSignalSpy passwordPrompts(password.get(), &PamAuthenticator::promptForSecret);
|
||||
QSignalSpy fingerprintPrompts(fingerprintPtr, &PamAuthenticator::promptForSecret);
|
||||
QSignalSpy fingerprintFailures(fingerprintPtr, &PamAuthenticator::failed);
|
||||
std::vector<std::unique_ptr<PamAuthenticator>> others;
|
||||
others.push_back(std::move(fingerprint));
|
||||
PamAuthenticators authenticators(std::move(password), std::move(others));
|
||||
QSignalSpy successes(&authenticators, &PamAuthenticators::succeeded);
|
||||
|
||||
authenticators.startAuthenticating();
|
||||
QTRY_COMPARE(passwordPrompts.count(), 1);
|
||||
QTRY_COMPARE_WITH_TIMEOUT(fingerprintFailures.count(), 1, 2000);
|
||||
QTRY_VERIFY(!fingerprintPtr->isAvailable());
|
||||
QCOMPARE(authenticators.state(), PamAuthenticators::Authenticating);
|
||||
QVERIFY(!authenticators.isUnlocked());
|
||||
|
||||
authenticators.startAuthenticating();
|
||||
QTRY_COMPARE_WITH_TIMEOUT(fingerprintPrompts.count(), 1, 2000);
|
||||
QTRY_VERIFY(fingerprintPtr->isAvailable());
|
||||
// Repeated starts must not duplicate either active conversation.
|
||||
authenticators.startAuthenticating();
|
||||
QTest::qWait(100);
|
||||
QCOMPARE(passwordPrompts.count(), 1);
|
||||
QCOMPARE(fingerprintPrompts.count(), 1);
|
||||
QCOMPARE(successes.count(), 0);
|
||||
|
||||
// The original password conversation still accepts a response.
|
||||
authenticators.respond("correct");
|
||||
QTRY_COMPARE(successes.count(), 1);
|
||||
QVERIFY(authenticators.isUnlocked());
|
||||
}
|
||||
|
||||
void permanentUnavailability_data()
|
||||
{
|
||||
QTest::addColumn<QString>("service");
|
||||
QTest::addColumn<int>("type");
|
||||
QTest::newRow("absent smartcard") << QStringLiteral("transient") << int(PamAuthenticator::Smartcard);
|
||||
QTest::newRow("missing fingerprint module") << QStringLiteral("missing") << int(PamAuthenticator::Fingerprint);
|
||||
}
|
||||
|
||||
void permanentUnavailability()
|
||||
{
|
||||
QFETCH(QString, service);
|
||||
QFETCH(int, type);
|
||||
PamAuthenticator auth(service, QStringLiteral("test"), PamAuthenticator::NoninteractiveAuthenticatorType(type));
|
||||
QSignalSpy availability(&auth, &PamAuthenticator::availableChanged);
|
||||
QSignalSpy prompts(&auth, &PamAuthenticator::promptForSecret);
|
||||
auth.tryUnlock();
|
||||
QTRY_VERIFY(availability.count() >= 3);
|
||||
QVERIFY(!auth.isAvailable());
|
||||
availability.clear();
|
||||
auth.tryUnlock();
|
||||
QTest::qWait(100);
|
||||
QCOMPARE(availability.count(), 0);
|
||||
QCOMPARE(prompts.count(), 0);
|
||||
QVERIFY(!auth.isUnlocked());
|
||||
}
|
||||
|
||||
void graceAndIncorrectPassword()
|
||||
{
|
||||
auto password = std::make_unique<PamAuthenticator>(QStringLiteral("password"), QStringLiteral("test"));
|
||||
PamAuthenticators authenticators(std::move(password), {});
|
||||
QSignalSpy prompts(&authenticators, &PamAuthenticators::promptForSecretChanged);
|
||||
QSignalSpy failures(&authenticators, &PamAuthenticators::failed);
|
||||
QSignalSpy successes(&authenticators, &PamAuthenticators::succeeded);
|
||||
authenticators.setGraceLocked(true);
|
||||
authenticators.startAuthenticating();
|
||||
QTest::qWait(100);
|
||||
QCOMPARE(prompts.count(), 0);
|
||||
authenticators.setGraceLocked(false);
|
||||
authenticators.startAuthenticating();
|
||||
QTRY_COMPARE(prompts.count(), 1);
|
||||
authenticators.respond("incorrect");
|
||||
QTRY_COMPARE(failures.count(), 1);
|
||||
QCOMPARE(successes.count(), 0);
|
||||
QVERIFY(!authenticators.isUnlocked());
|
||||
authenticators.startAuthenticating();
|
||||
QTRY_COMPARE(prompts.count(), 2);
|
||||
authenticators.respond("correct");
|
||||
QTRY_COMPARE(successes.count(), 1);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(RetryTest)
|
||||
#include "retrytest.moc"
|
||||
Reference in New Issue
Block a user