diff --git a/CloudronManifest.json b/CloudronManifest.json index dfb7689..0f96bd5 100644 --- a/CloudronManifest.json +++ b/CloudronManifest.json @@ -16,7 +16,7 @@ "postgresql": {}, "redis": {}, "sendmail": {}, - "ldap": {}, + "oidc": { "loginRedirectUri": "/auth/auth/openid_connect/callback" }, "scheduler": { "cleanup": { "schedule": "11 01 * * *", diff --git a/env.template b/env.template index 4547b51..e807045 100644 --- a/env.template +++ b/env.template @@ -32,15 +32,15 @@ SMTP_AUTH_METHOD=plain SMTP_OPENSSL_VERIFY_MODE=none # SSO configuration -LDAP_ENABLED= -LDAP_HOST= -LDAP_PORT= -LDAP_BASE= -LDAP_BIND_DN= -LDAP_PASSWORD= -LDAP_UID=username -LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(mail=%{email})) -LDAP_METHOD=plain +OIDC_ENABLED= +OIDC_DISPLAY_NAME= +OIDC_ISSUER= +OIDC_CLIENT_ID= +OIDC_CLIENT_SECRET= +OIDC_REDIRECT_URI= +OIDC_DISCOVERY= +OIDC_SCOPE= +OIDC_UID_FIELD= # Application secrets SECRET_KEY_BASE= diff --git a/start.sh b/start.sh index 3bc3ddc..fd9a9e2 100755 --- a/start.sh +++ b/start.sh @@ -30,16 +30,40 @@ sed -e "s/DB_HOST=.*/DB_HOST=${CLOUDRON_POSTGRESQL_HOST}/g" \ -e "s/WEB_DOMAIN=.*/WEB_DOMAIN=${CLOUDRON_APP_DOMAIN}/g" \ -i /app/data/env.production -if [[ -n "${CLOUDRON_LDAP_SERVER:-}" ]]; then - sed -e "s/LDAP_ENABLED=.*/LDAP_ENABLED=true/g" \ - -e "s/LDAP_HOST=.*/LDAP_HOST=${CLOUDRON_LDAP_SERVER}/g" \ - -e "s/LDAP_PORT=.*/LDAP_PORT=${CLOUDRON_LDAP_PORT}/g" \ - -e "s/LDAP_BASE=.*/LDAP_BASE=${CLOUDRON_LDAP_USERS_BASE_DN}/g" \ - -e "s/LDAP_BIND_DN=.*/LDAP_BIND_DN=${CLOUDRON_LDAP_BIND_DN}/g" \ - -e "s/LDAP_PASSWORD=.*/LDAP_PASSWORD=${CLOUDRON_LDAP_BIND_PASSWORD}/g" \ +# migrate LDAP settings to OIDC +if grep -q "^LDAP_ENABLED" /app/data/env.production; then + # get rid LDAP settings + sed -e "s/LDAP_.*//g" \ + -e "s/# SSO configuration//g" \ + -i /app/data/env.production + + cat >> /app/data/env.production < Setting up OIDC" + sed -e "s/OIDC_ENABLED=.*/OIDC_ENABLED=true/g" \ + -e "s/OIDC_DISPLAY_NAME=.*/OIDC_DISPLAY_NAME=Cloudron/g" \ + -e "s/OIDC_ISSUER=.*/OIDC_ISSUER=${CLOUDRON_OIDC_ISSUER//\//\\\/}/g" \ + -e "s/OIDC_CLIENT_ID=.*/OIDC_CLIENT_ID=${CLOUDRON_OIDC_CLIENT_ID}/g" \ + -e "s/OIDC_CLIENT_SECRET=.*/OIDC_CLIENT_SECRET=${CLOUDRON_OIDC_CLIENT_SECRET}/g" \ + -e "s/OIDC_REDIRECT_URI=.*/OIDC_REDIRECT_URI=${CLOUDRON_APP_ORIGIN//\//\\\/}\/auth\/auth\/openid_connect\/callback/g" \ + -e "s/OIDC_DISCOVERY=.*/OIDC_DISCOVERY=true/g" \ + -e "s/OIDC_SCOPE=.*/OIDC_SCOPE=openid,profile,email/g" \ + -e "s/OIDC_UID_FIELD=.*/OIDC_UID_FIELD=sub/g" \ -i /app/data/env.production -else - sed -e "s/LDAP_ENABLED=.*/LDAP_ENABLED=false/g" -i /app/data/env.production fi rm -f /run/mastodon/Gemfile.lock && cp /app/code/Gemfile.lock.original /run/mastodon/Gemfile.lock @@ -57,7 +81,7 @@ if grep -q "^SECRET_KEY_BASE=$" /app/data/env.production; then echo "==> Init database" HOME=/app/data SAFETY_ASSURED=1 bundle exec rails db:schema:load db:seed - if [[ -n "${CLOUDRON_LDAP_SERVER:-}" ]]; then + if [[ -n "${CLOUDRON_OIDC_ISSUER:-}" ]]; then echo "Disabling registration by default" PGPASSWORD=${CLOUDRON_POSTGRESQL_PASSWORD} psql -h ${CLOUDRON_POSTGRESQL_HOST} -p ${CLOUDRON_POSTGRESQL_PORT} -U ${CLOUDRON_POSTGRESQL_USERNAME} -d ${CLOUDRON_POSTGRESQL_DATABASE} \ -c "INSERT INTO settings (var, value) VALUES ('registrations_mode', 'none')" diff --git a/test/package-lock.json b/test/package-lock.json index a591699..e3bc20e 100644 --- a/test/package-lock.json +++ b/test/package-lock.json @@ -13,7 +13,7 @@ }, "devDependencies": { "expect.js": "^0.3.1", - "mocha": "^10.2.0", + "mocha": "^10.3.0", "selenium-webdriver": "^4.17.0" } }, @@ -825,9 +825,9 @@ } }, "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.3.0.tgz", + "integrity": "sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==", "dev": true, "dependencies": { "ansi-colors": "4.1.1", @@ -837,13 +837,12 @@ "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", - "glob": "7.2.0", + "glob": "8.1.0", "he": "1.2.0", "js-yaml": "4.1.0", "log-symbols": "4.1.0", "minimatch": "5.0.1", "ms": "2.1.3", - "nanoid": "3.3.3", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", @@ -858,10 +857,6 @@ }, "engines": { "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" } }, "node_modules/mocha/node_modules/brace-expansion": { @@ -873,6 +868,25 @@ "balanced-match": "^1.0.0" } }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mocha/node_modules/minimatch": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", @@ -896,18 +910,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1967,9 +1969,9 @@ } }, "mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.3.0.tgz", + "integrity": "sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==", "dev": true, "requires": { "ansi-colors": "4.1.1", @@ -1979,13 +1981,12 @@ "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", - "glob": "7.2.0", + "glob": "8.1.0", "he": "1.2.0", "js-yaml": "4.1.0", "log-symbols": "4.1.0", "minimatch": "5.0.1", "ms": "2.1.3", - "nanoid": "3.3.3", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", @@ -2004,6 +2005,19 @@ "balanced-match": "^1.0.0" } }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, "minimatch": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", @@ -2026,12 +2040,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/test/package.json b/test/package.json index c843fa6..63dfbd8 100644 --- a/test/package.json +++ b/test/package.json @@ -10,7 +10,7 @@ "license": "ISC", "devDependencies": { "expect.js": "^0.3.1", - "mocha": "^10.2.0", + "mocha": "^10.3.0", "selenium-webdriver": "^4.17.0" }, "dependencies": { diff --git a/test/test.js b/test/test.js index 93bb5d4..3ab2310 100755 --- a/test/test.js +++ b/test/test.js @@ -30,6 +30,7 @@ describe('Application life cycle test', function () { const EXEC_ARGS = { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' }; let browser, app; + var athenticated_by_oidc = false; let username = process.env.USERNAME; let password = process.env.PASSWORD; let manifest = require('../CloudronManifest.json'); @@ -42,6 +43,15 @@ describe('Application life cycle test', function () { browser.quit(); }); + function sleep(millis) { + return new Promise(resolve => setTimeout(resolve, millis)); + } + + async function waitForElement(elem) { + await browser.wait(until.elementLocated(elem), TEST_TIMEOUT); + await browser.wait(until.elementIsVisible(browser.findElement(elem)), TEST_TIMEOUT); + } + async function exists(selector) { await browser.wait(until.elementLocated(selector), TEST_TIMEOUT); } @@ -55,7 +65,7 @@ describe('Application life cycle test', function () { if (mode === 'none') { await browser.get('https://' + app.fqdn); await browser.sleep(2000); - await browser.findElement(By.xpath('//div[@class="sign-in-banner"]/descendant::button/span[contains(text(), "Create account")]')).click(); + await browser.findElement(By.xpath('//div[@class="sign-in-banner"]/descendant::button/span[contains(text(), "Create account")] | //div[@class="sign-in-banner"]/descendant::a/span[contains(text(), "Create account")]')).click(); await visible(By.xpath('//span[contains(text()[2], "is currently not possible")]')); } else if (mode === 'open') { await browser.get('https://' + app.fqdn + '/auth/sign_up'); @@ -72,6 +82,28 @@ describe('Application life cycle test', function () { await browser.sleep(3000); // can be wizard or timeline at this point } + async function loginOIDC(username, password) { + browser.manage().deleteAllCookies(); + await browser.get(`https://${app.fqdn}/auth/sign_in`); + await browser.sleep(4000); + + await browser.findElement(By.xpath('//a[contains(@class, "button") and text()="Cloudron"]')).click(); + await browser.sleep(4000); + + 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('//a[contains(., "Edit profile")] | //strong[text()="Successfully authenticated from Cloudron account."]')); + } + async function logout() { await browser.get('https://' + app.fqdn + '/settings/preferences/appearance'); // there is also separate login page at /users/sign_in await browser.wait(until.elementLocated(By.id('logout')), TEST_TIMEOUT); @@ -97,43 +129,6 @@ describe('Application life cycle test', function () { } xit('build app', function () { execSync('cloudron build', EXEC_ARGS); }); - it('install app', function () { execSync('cloudron install --location ' + LOCATION, EXEC_ARGS); }); - - it('can get app information', getAppInfo); - it('registration is disabled', checkRegistration.bind(null, 'none')); - it('can LDAP login', login.bind(null, username, password)); - it('can dismiss help', dismissHelp); - it('can see timeline', checkTimeline); - it('can logout', logout); - - it('backup app', function () { execSync('cloudron backup create --app ' + app.id, EXEC_ARGS); }); - it('restore app', function () { - const backups = JSON.parse(execSync('cloudron backup list --raw')); - execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS); - execSync('cloudron install --location ' + LOCATION, EXEC_ARGS); - getAppInfo(); - execSync(`cloudron restore --backup ${backups[0].id} --app ${app.id}`, EXEC_ARGS); - }); - - it('can LDAP login', login.bind(null, username, password)); - it('can see timeline', checkTimeline); - - it('can restart app', function () { execSync('cloudron restart --app ' + app.id, EXEC_ARGS); }); - it('can see timeline', checkTimeline); - - it('move to different location', async function () { - await browser.get('about:blank'); - execSync('cloudron configure --location ' + LOCATION + '2 --app ' + app.id, EXEC_ARGS); - }); - it('can get app information', getAppInfo); - - it('can LDAP login', login.bind(null, username, password)); - it('can see timeline', checkTimeline); - - it('uninstall app', async function () { - await browser.get('about:blank'); - execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS); - }); // No SSO it('install app (no sso)', function () { execSync('cloudron install --no-sso --location ' + LOCATION, EXEC_ARGS); }); @@ -161,9 +156,49 @@ describe('Application life cycle test', function () { execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS); }); + // SSO + it('install app (sso)', function () { execSync('cloudron install --location ' + LOCATION, EXEC_ARGS); }); + + it('can get app information', getAppInfo); + it('registration is disabled', checkRegistration.bind(null, 'none')); + it('can OIDC login', loginOIDC.bind(null, username, password)); + it('can dismiss help', dismissHelp); + it('can see timeline', checkTimeline); + it('can logout', logout); + + it('backup app', function () { execSync('cloudron backup create --app ' + app.id, EXEC_ARGS); }); + it('restore app', function () { + const backups = JSON.parse(execSync('cloudron backup list --raw')); + execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS); + execSync('cloudron install --location ' + LOCATION, EXEC_ARGS); + getAppInfo(); + execSync(`cloudron restore --backup ${backups[0].id} --app ${app.id}`, EXEC_ARGS); + }); + + it('can OIDC login', loginOIDC.bind(null, username, password)); + it('can see timeline', checkTimeline); + + it('can restart app', function () { execSync('cloudron restart --app ' + app.id, EXEC_ARGS); }); + it('can see timeline', checkTimeline); + + it('move to different location', async function () { + await browser.get('about:blank'); + execSync('cloudron configure --location ' + LOCATION + '2 --app ' + app.id, EXEC_ARGS); + }); + it('can get app information', getAppInfo); + + it('can OIDC login', loginOIDC.bind(null, username, password)); + it('can see timeline', checkTimeline); + + it('uninstall app', async function () { + await browser.get('about:blank'); + execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS); + }); + // test update it('can install app', function () { execSync('cloudron install --appstore-id ' + manifest.id + ' --location ' + LOCATION, EXEC_ARGS); }); it('can get app information', getAppInfo); + // needs to be changed to loginOIDC on the next release it('can LDAP login', login.bind(null, username, password)); it('can logout', logout); @@ -172,7 +207,7 @@ describe('Application life cycle test', function () { execSync('cloudron update --app ' + LOCATION, EXEC_ARGS); }); - it('can LDAP login', login.bind(null, username, password)); + it('can OIDC login', loginOIDC.bind(null, username, password)); it('uninstall app', function () { execSync('cloudron uninstall --app ' + app.id, EXEC_ARGS); }); });