OIDC auth implemented, tests updated

This commit is contained in:
Vladimir D 2024-02-14 14:13:42 +04:00 committed by Johannes Zellner
parent fdc4e20c77
commit 146b5ac17e
6 changed files with 160 additions and 93 deletions

View file

@ -16,7 +16,7 @@
"postgresql": {},
"redis": {},
"sendmail": {},
"ldap": {},
"oidc": { "loginRedirectUri": "/auth/auth/openid_connect/callback" },
"scheduler": {
"cleanup": {
"schedule": "11 01 * * *",

View file

@ -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=

View file

@ -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 <<EOT
# SSO configuration
OIDC_ENABLED=
OIDC_DISPLAY_NAME=
OIDC_ISSUER=
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_REDIRECT_URI=
OIDC_DISCOVERY=
OIDC_SCOPE=
OIDC_UID_FIELD=
EOT
fi
if [[ -n "${CLOUDRON_OIDC_ISSUER:-}" ]]; then
echo "==> 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')"

74
test/package-lock.json generated
View file

@ -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",

View file

@ -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": {

View file

@ -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); });
});