From e176d6c705fc0eb5c6f6be25e193a967e1bf1ffc Mon Sep 17 00:00:00 2001 From: Vladimir D Date: Tue, 26 Sep 2023 15:30:53 +0400 Subject: [PATCH] LDAP to OIDC auth migration, tests refactored --- CloudronManifest.json | 2 +- app.ini.template | 5 +++++ start.sh | 29 +++++++++++++++++++++++++++- test/package-lock.json | 44 +++++++++++++++++++++--------------------- test/package.json | 6 +++--- test/test.js | 40 +++++++++++++++++++++++++++++++++----- 6 files changed, 94 insertions(+), 32 deletions(-) diff --git a/CloudronManifest.json b/CloudronManifest.json index f5b4cff..2ea2671 100644 --- a/CloudronManifest.json +++ b/CloudronManifest.json @@ -13,7 +13,7 @@ "mysql": { }, "sendmail": { "supportsDisplayName": true }, "localstorage": { }, - "ldap": { } + "oidc": { "loginRedirectUri": "/user/oauth2/cloudron/callback" } }, "tcpPorts": { "SSH_PORT": { diff --git a/app.ini.template b/app.ini.template index e1ef8bc..3a2d4eb 100644 --- a/app.ini.template +++ b/app.ini.template @@ -48,6 +48,11 @@ ENABLED = true ; APP_DATA_PATH/attachments PATH = +[oauth2_client] +ENABLE_AUTO_REGISTRATION = true +USERNAME = sub +UPDATE_AVATAR = false +ACCOUNT_LINKING = auto [mailer] ENABLED = true diff --git a/start.sh b/start.sh index e9bfb36..842b0ef 100755 --- a/start.sh +++ b/start.sh @@ -25,6 +25,25 @@ setup_ldap_source() { fi } +migrate_ldap_users_to_oidc() { + set -eu + + echo "==> migrate LDAP to OIDC" + mysql -u"${CLOUDRON_MYSQL_USERNAME}" -p"${CLOUDRON_MYSQL_PASSWORD}" -h mysql --database="${CLOUDRON_MYSQL_DATABASE}" -N -B -e \ + "UPDATE user u, (select id from login_source WHERE name='cloudron' and type='6') ls SET u.login_type=6, u.login_source=u.id WHERE u.login_type=2 AND u.login_source=1" +} + +setup_oidc_source() { + set -eu + + echo "==> Setup OIDC source" + + now=$(date +%s) + mysql -u"${CLOUDRON_MYSQL_USERNAME}" -p"${CLOUDRON_MYSQL_PASSWORD}" -h mysql --database="${CLOUDRON_MYSQL_DATABASE}" -e \ + "REPLACE INTO login_source (id, type, name, is_active, cfg, created_unix, updated_unix) VALUES (1,6,'cloudron', 1,'{\"Provider\":\"openidConnect\",\"ClientID\":\"${CLOUDRON_OIDC_CLIENT_ID}\",\"ClientSecret\":\"${CLOUDRON_OIDC_CLIENT_SECRET}\",\"OpenIDConnectAutoDiscoveryURL\":\"${CLOUDRON_OIDC_ISSUER}/.well-known/openid-configuration\",\"CustomURLMapping\":null,\"IconURL\":\"\",\"Scopes\":[\"openid email profile\"],\"RequiredClaimName\":\"\",\"RequiredClaimValue\":\"\",\"GroupClaimName\":\"\",\"AdminGroup\":\"\",\"GroupTeamMap\":\"\",\"GroupTeamMapRemoval\":false,\"RestrictedGroup\":\"\"}','${now}','${now}')" + +} + setup_root_user() { set -eu @@ -51,7 +70,15 @@ setup_auth() { setup_ldap_source fi - user_count=$(mysql -u"${CLOUDRON_MYSQL_USERNAME}" -p"${CLOUDRON_MYSQL_PASSWORD}" -h mysql --database="${CLOUDRON_MYSQL_DATABASE}" -N -B -e "SELECT count(*) FROM user;") + if [[ -n "${CLOUDRON_OIDC_ISSUER:-}" ]]; then + setup_oidc_source + ldap_users_to_migrate=$(mysql -u"${CLOUDRON_MYSQL_USERNAME}" -p"${CLOUDRON_MYSQL_PASSWORD}" -h mysql --database="${CLOUDRON_MYSQL_DATABASE}" -N -B -e "select count(*) from user WHERE login_type=2 AND login_source=1") + if [ "${ldap_users_to_migrate:0}" -gt 0 ]; then + migrate_ldap_users_to_oidc + fi + fi + + user_count=$(mysql -u"${CLOUDRON_MYSQL_USERNAME}" -p"${CLOUDRON_MYSQL_PASSWORD}" -h mysql --database="${CLOUDRON_MYSQL_DATABASE}" -N -B -e "SELECT count(*) FROM user") # be careful, not to create root user for existing LDAP based installs if [[ "${user_count}" == "0" ]]; then echo "==> Setting up root user for first run" diff --git a/test/package-lock.json b/test/package-lock.json index fd8e26e..1127f81 100644 --- a/test/package-lock.json +++ b/test/package-lock.json @@ -9,11 +9,11 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "chromedriver": "^115.0.0", + "chromedriver": "^117.0.3", "expect.js": "^0.3.1", "mocha": "^10.2.0", - "selenium-webdriver": "^4.10.0", - "superagent": "^8.0.9" + "selenium-webdriver": "^4.13.0", + "superagent": "^8.1.2" } }, "node_modules/@testim/chrome-version": { @@ -236,9 +236,9 @@ } }, "node_modules/chromedriver": { - "version": "115.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-115.0.0.tgz", - "integrity": "sha512-mkPL+sXMLMUenoXCiKREw+cBl3ibfhiWxkiv9ByIPpqtrrInCt9zKdOolAsbmN/ndlH51WtT5ukUKbeRdrpikg==", + "version": "117.0.3", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-117.0.3.tgz", + "integrity": "sha512-c2rk2eGK5zZFBJMdviUlAJfQEBuPNIKfal4+rTFVYAmrWbMPYAqPozB+rIkc1lDP/Ryw44lPiqKglrI01ILhTQ==", "hasInstallScript": true, "dependencies": { "@testim/chrome-version": "^1.1.3", @@ -253,7 +253,7 @@ "chromedriver": "bin/chromedriver" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/cliui": { @@ -1171,9 +1171,9 @@ ] }, "node_modules/selenium-webdriver": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.10.0.tgz", - "integrity": "sha512-hSQPw6jgc+ej/UEcdQPG/iBwwMeCEgZr9HByY/J8ToyXztEqXzU9aLsIyrlj1BywBcStO4JQK/zMUWWrV8+riA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.13.0.tgz", + "integrity": "sha512-8JS0h5E0Sq7gNfbGg8LVaQ+Eqek97tvOONn3Jmy+NiWfb12WYpftz4VTC4D2JT4wakdG6VUzGKpA8cFGg0IjkA==", "dependencies": { "jszip": "^3.10.1", "tmp": "^0.2.1", @@ -1272,9 +1272,9 @@ } }, "node_modules/superagent": { - "version": "8.0.9", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz", - "integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", "dependencies": { "component-emitter": "^1.3.0", "cookiejar": "^2.1.4", @@ -1657,9 +1657,9 @@ } }, "chromedriver": { - "version": "115.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-115.0.0.tgz", - "integrity": "sha512-mkPL+sXMLMUenoXCiKREw+cBl3ibfhiWxkiv9ByIPpqtrrInCt9zKdOolAsbmN/ndlH51WtT5ukUKbeRdrpikg==", + "version": "117.0.3", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-117.0.3.tgz", + "integrity": "sha512-c2rk2eGK5zZFBJMdviUlAJfQEBuPNIKfal4+rTFVYAmrWbMPYAqPozB+rIkc1lDP/Ryw44lPiqKglrI01ILhTQ==", "requires": { "@testim/chrome-version": "^1.1.3", "axios": "^1.4.0", @@ -2323,9 +2323,9 @@ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "selenium-webdriver": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.10.0.tgz", - "integrity": "sha512-hSQPw6jgc+ej/UEcdQPG/iBwwMeCEgZr9HByY/J8ToyXztEqXzU9aLsIyrlj1BywBcStO4JQK/zMUWWrV8+riA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.13.0.tgz", + "integrity": "sha512-8JS0h5E0Sq7gNfbGg8LVaQ+Eqek97tvOONn3Jmy+NiWfb12WYpftz4VTC4D2JT4wakdG6VUzGKpA8cFGg0IjkA==", "requires": { "jszip": "^3.10.1", "tmp": "^0.2.1", @@ -2402,9 +2402,9 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, "superagent": { - "version": "8.0.9", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz", - "integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", "requires": { "component-emitter": "^1.3.0", "cookiejar": "^2.1.4", diff --git a/test/package.json b/test/package.json index 2b8c9a3..f7b02c1 100644 --- a/test/package.json +++ b/test/package.json @@ -9,10 +9,10 @@ "author": "", "license": "ISC", "dependencies": { - "chromedriver": "^115.0.0", + "chromedriver": "^117.0.3", "expect.js": "^0.3.1", "mocha": "^10.2.0", - "selenium-webdriver": "^4.10.0", - "superagent": "^8.0.9" + "selenium-webdriver": "^4.13.0", + "superagent": "^8.1.2" } } diff --git a/test/test.js b/test/test.js index 2e59cc8..a3feaff 100755 --- a/test/test.js +++ b/test/test.js @@ -33,6 +33,7 @@ describe('Application life cycle test', function () { const SSH_PORT = 29420; let app, browser; + var athenticated_by_oidc = false; const repodir = '/tmp/testrepo'; const reponame = 'testrepo'; @@ -56,6 +57,11 @@ describe('Application life cycle test', function () { expect(app).to.be.an('object'); } + async function waitForElement(elem) { + await browser.wait(until.elementLocated(elem), TIMEOUT); + await browser.wait(until.elementIsVisible(browser.findElement(elem)), TIMEOUT); + } + function sleep(millis) { return new Promise(resolve => setTimeout(resolve, millis)); } @@ -93,6 +99,29 @@ describe('Application life cycle test', function () { await login('root', 'changeme'); } + async function loginOIDC(username, password) { + browser.manage().deleteAllCookies(); + await browser.get(`https://${app.fqdn}/user/login`); + await browser.sleep(2000); + + + await browser.findElement(By.xpath('//a[contains(@class, "openidConnect") and contains(., "Sign in with cloudron")]')).click(); + await browser.sleep(2000); + + if (!athenticated_by_oidc) { + await waitForElement(By.xpath('//input[@name="username"]')); + await browser.findElement(By.xpath('//input[@name="username"]')).sendKeys(username); + await browser.findElement(By.xpath('//input[@name="password"]')).sendKeys(password); + await browser.sleep(2000); + await browser.findElement(By.xpath('//button[@type="submit" and contains(text(), "Sign in")]')).click(); + await browser.sleep(2000); + + athenticated_by_oidc = true; + } + + await waitForElement(By.xpath('//img[contains(@class, "avatar")]')); + } + async function logout() { await browser.get('https://' + app.fqdn); @@ -182,7 +211,7 @@ describe('Application life cycle test', function () { it('can send mail', sendMail); it('can logout', logout); - it('can login', login.bind(null, username, password)); + it('can login', loginOIDC.bind(null, username, password)); it('can set avatar', setAvatar); it('can get avatar', checkAvatar); @@ -198,7 +227,7 @@ describe('Application life cycle test', function () { it('can restart app', function () { execSync('cloudron restart --app ' + app.id); }); - xit('can login', login.bind(null, username, password)); // no need to relogin since session persists + xit('can login', loginOIDC.bind(null, username, password)); // no need to relogin since session persists it('displays correct clone url', checkCloneUrl); it('can clone the url', cloneRepo); it('file exists in repo', fileExists); @@ -206,7 +235,7 @@ describe('Application life cycle test', function () { it('backup app', function () { execSync('cloudron backup create --app ' + app.id, EXEC_ARGS); }); it('restore app', function () { execSync('cloudron restore --app ' + app.id, EXEC_ARGS); }); - it('can login', login.bind(null, username, password)); + it('can login', loginOIDC.bind(null, username, password)); it('can get avatar', checkAvatar); it('can clone the url', cloneRepo); it('file exists in repo', function () { expect(fs.existsSync(repodir + '/newfile')).to.be(true); }); @@ -220,7 +249,7 @@ describe('Application life cycle test', function () { }); it('can get app information', getAppInfo); - it('can login', login.bind(null, username, password)); + it('can login', loginOIDC.bind(null, username, password)); it('can get avatar', checkAvatar); it('displays correct clone url', checkCloneUrl); it('can clone the url', cloneRepo); @@ -248,6 +277,7 @@ describe('Application life cycle test', function () { it('can install app', function () { execSync(`cloudron install --appstore-id ${app.manifest.id} --location ${LOCATION} -p SSH_PORT=${SSH_PORT}`, EXEC_ARGS); }); it('can get app information', getAppInfo); + // to be replaced with loginOIDC in the next release it('can login', login.bind(null, username, password)); it('can set avatar', setAvatar); it('can get avatar', checkAvatar); @@ -263,7 +293,7 @@ describe('Application life cycle test', function () { it('can send mail', sendMail); it('can logout', logout); - it('can login', login.bind(null, username, password)); + it('can login', loginOIDC.bind(null, username, password)); it('can get avatar', checkAvatar); it('can clone the url', cloneRepo); it('file exists in cloned repo', fileExists);