From 7417ce44e5f0cb054e77baa6beed402e6deb8ff8 Mon Sep 17 00:00:00 2001 From: Vladimir D Date: Mon, 19 Feb 2024 17:13:57 +0400 Subject: [PATCH] OIDC auth implemented, tests updated --- CloudronManifest.json | 3 +- POSTINSTALL.md | 12 ++++++ start.sh | 13 +++++++ test/package-lock.json | 14 +++---- test/package.json | 2 +- test/test.js | 85 ++++++++++++++++++++++++++++++++++++++---- 6 files changed, 113 insertions(+), 16 deletions(-) diff --git a/CloudronManifest.json b/CloudronManifest.json index 36a5d92..7a2e213 100644 --- a/CloudronManifest.json +++ b/CloudronManifest.json @@ -19,7 +19,8 @@ } }, "addons": { - "localstorage": {} + "localstorage": {}, + "oidc": { "loginRedirectUri": "/oauth_callback" } }, "manifestVersion": 2, "website": "http://www.minio.io", diff --git a/POSTINSTALL.md b/POSTINSTALL.md index 501474d..ea489de 100644 --- a/POSTINSTALL.md +++ b/POSTINSTALL.md @@ -5,3 +5,15 @@ Please use the following credentials to login: Please change the credentials immediately by following this [guide](https://cloudron.io/documentation/apps/minio/#admin-credentials). +By default, Cloudron users have `readwrite` access policy. +If you'd like to change it, you should create a respective policy by following [Minio documentation](https://min.io/docs/minio/linux/administration/identity-access-management/policy-based-access-control.html) + +After that you should add the variable MINIO_IDENTITY_OPENID_ROLE_POLICY in /app/data/env.sh, e.g. + +``` +export MINIO_IDENTITY_OPENID_ROLE_POLICY="new-policy-name" +``` + +Where `new-policy-name` is the policy you have created. + +Be sure to restart the app after making changes. diff --git a/start.sh b/start.sh index fb37fc2..f8f8657 100755 --- a/start.sh +++ b/start.sh @@ -17,6 +17,19 @@ if [[ ! -d /app/data/mc_config ]]; then /app/code/mc --config-dir /app/data/mc_config &> /dev/null || true fi +if [[ -n "${CLOUDRON_OIDC_ISSUER:-}" ]]; then + export MINIO_IDENTITY_OPENID_DISPLAY_NAME="Cloudron" + export MINIO_IDENTITY_OPENID_CONFIG_URL="${CLOUDRON_OIDC_DISCOVERY_URL}" + export MINIO_IDENTITY_OPENID_CLIENT_ID="${CLOUDRON_OIDC_CLIENT_ID}" + export MINIO_IDENTITY_OPENID_CLIENT_SECRET="${CLOUDRON_OIDC_CLIENT_SECRET}" + export MINIO_IDENTITY_OPENID_SCOPES="openid profile email" + if [[ -z "${MINIO_IDENTITY_OPENID_ROLE_POLICY:-}" ]]; then + export MINIO_IDENTITY_OPENID_ROLE_POLICY="readwrite" + fi + + export MINIO_IDENTITY_OPENID_COMMENT="Cloudron OIDC" +fi + # minio is used for backups at times and has a large number of files. optimize by checking if files are actually in correct chown state echo "==> Changing ownership" [[ $(stat --format '%U' /app/data/data) != "cloudron" ]] && chown -R cloudron:cloudron /app/data diff --git a/test/package-lock.json b/test/package-lock.json index 58e09d3..5b5ff42 100644 --- a/test/package-lock.json +++ b/test/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "chromedriver": "^121.0.1", + "chromedriver": "^121.0.2", "expect.js": "^0.3.1", "mocha": "^10.3.0", "selenium-webdriver": "^4.17.0", @@ -228,9 +228,9 @@ } }, "node_modules/chromedriver": { - "version": "121.0.1", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-121.0.1.tgz", - "integrity": "sha512-7y/RLV3tKNpNf/Ye74eOF7gyrCA78qq3i6JjrMJ4xovc2XZaw4a3cZA6+2PflGX/0HttYiKJV2WO611JROGNaw==", + "version": "121.0.2", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-121.0.2.tgz", + "integrity": "sha512-58MUSCEE3oB3G3Y/Jo3URJ2Oa1VLHcVBufyYt7vNfGrABSJm7ienQLF9IQ8LPDlPVgLUXt2OBfggK3p2/SlEBg==", "hasInstallScript": true, "dependencies": { "@testim/chrome-version": "^1.1.4", @@ -1710,9 +1710,9 @@ } }, "chromedriver": { - "version": "121.0.1", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-121.0.1.tgz", - "integrity": "sha512-7y/RLV3tKNpNf/Ye74eOF7gyrCA78qq3i6JjrMJ4xovc2XZaw4a3cZA6+2PflGX/0HttYiKJV2WO611JROGNaw==", + "version": "121.0.2", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-121.0.2.tgz", + "integrity": "sha512-58MUSCEE3oB3G3Y/Jo3URJ2Oa1VLHcVBufyYt7vNfGrABSJm7ienQLF9IQ8LPDlPVgLUXt2OBfggK3p2/SlEBg==", "requires": { "@testim/chrome-version": "^1.1.4", "axios": "^1.6.5", diff --git a/test/package.json b/test/package.json index a4a95bc..f59e080 100644 --- a/test/package.json +++ b/test/package.json @@ -9,7 +9,7 @@ "author": "", "license": "ISC", "dependencies": { - "chromedriver": "^121.0.1", + "chromedriver": "^121.0.2", "expect.js": "^0.3.1", "mocha": "^10.3.0", "selenium-webdriver": "^4.17.0", diff --git a/test/test.js b/test/test.js index 7bb871c..4917146 100644 --- a/test/test.js +++ b/test/test.js @@ -20,6 +20,11 @@ const execSync = require('child_process').execSync, { Builder, By, until } = require('selenium-webdriver'), { Options } = require('selenium-webdriver/chrome'); +if (!process.env.USERNAME || !process.env.PASSWORD) { + console.log('USERNAME and PASSWORD env vars need to be set'); + process.exit(1); +} + describe('Application life cycle test', function () { this.timeout(0); @@ -29,6 +34,9 @@ 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; before(function () { browser = new Builder().forBrowser('chrome').setChromeOptions(new Options().windowSize({ width: 1280, height: 1024 })).build(); @@ -51,6 +59,14 @@ describe('Application life cycle test', function () { async function login(accessKey='minioadmin', secretKey='minioadmin') { await browser.get(`https://${app.fqdn}/login`); + await browser.sleep(2000); + + if (await browser.findElements(By.id('accessKey')).then(found => !found.length) && await browser.findElements(By.id('alternativeMethods-select')).then(found => !!found.length)) { + await browser.findElement(By.xpath('//div[@id="alternativeMethods-select"]/div[contains(., "Other Authentication Methods")]')).click(); + await browser.sleep(2000); + await browser.findElement(By.xpath('//li[contains(., "Use Credentials")] | //div[@label="Use Credentials"]')).click(); + await browser.sleep(2000); + } await waitForElement(By.id('accessKey')); await browser.findElement(By.id('accessKey')).sendKeys(accessKey); await browser.findElement(By.id('secretKey')).sendKeys(secretKey); @@ -59,13 +75,35 @@ describe('Application life cycle test', function () { await timers.setTimeout(5000); } + async function loginOIDC(username, password) { + browser.manage().deleteAllCookies(); + await browser.get(`https://${app.fqdn}/login`); + await browser.sleep(4000); + + await browser.findElement(By.xpath('//button[contains(., "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.id('loginSubmitButton')).click(); + await browser.sleep(2000); + + athenticated_by_oidc = true; + } + + await waitForElement(By.xpath('//span[contains(text(), "Buckets")]')); + } + async function logout() { await browser.get(`https://${app.fqdn}/`); await waitForElement(By.xpath('//span[contains(text(), "Buckets")]')); const button = await browser.findElement(By.xpath('//button[@id="sign-out"]')); await browser.executeScript('arguments[0].scrollIntoView(false)', button); await button.click(); - await waitForElement(By.id('accessKey')); + await waitForElement(By.xpath('//*[@id="accessKey"] | //button[contains(., "Cloudron")]')); } async function addBucket() { @@ -104,13 +142,17 @@ describe('Application life cycle test', function () { it('can get app information', getAppInfo); - it('can login', login.bind(null, 'minioadmin', 'minioadmin')); + it('can Admin login', login.bind(null, 'minioadmin', 'minioadmin')); it('can add bucket', addBucket); it('can logout', logout); it('does redirect', checkRedirect); it('check api', checkApi); - it('can change credentials', async function () { + it('can OIDC login', loginOIDC.bind(null, username, password)); + it('has bucket', checkBucket); + it('can logout', logout); + + it('can change Admin credentials', async function () { let data = fs.readFileSync(path.join(__dirname, '../env.sh'), 'utf8'); data = data .replace(/MINIO_ROOT_USER=.*/, 'MINIO_ROOT_USER=minioakey') @@ -123,12 +165,16 @@ describe('Application life cycle test', function () { it('can restart app', function () { execSync(`cloudron restart --app ${app.id}`, EXEC_ARGS); }); - it('can login', login.bind(null, 'minioakey', 'minioskey')); + it('can Admin login', login.bind(null, 'minioakey', 'minioskey')); it('has bucket', checkBucket); it('can logout', logout); it('does redirect', checkRedirect); it('check api', checkApi); + it('can OIDC login', loginOIDC.bind(null, username, password)); + it('has bucket', checkBucket); + it('can logout', logout); + it('backup app', function () { execSync('cloudron backup create --app ' + app.id, EXEC_ARGS); }); it('restore app', async function () { const backups = JSON.parse(execSync(`cloudron backup list --raw --app ${app.id}`)); @@ -139,9 +185,15 @@ describe('Application life cycle test', function () { await timers.setTimeout(10000); }); - it('can login', login.bind(null, 'minioakey', 'minioskey')); + it('can get app information', getAppInfo); + it('can Admin login', login.bind(null, 'minioakey', 'minioskey')); it('has bucket', checkBucket); it('can logout', logout); + + it('can OIDC login', loginOIDC.bind(null, username, password)); + it('has bucket', checkBucket); + it('can logout', logout); + it('does redirect', checkRedirect); it('check api', checkApi); @@ -152,9 +204,14 @@ describe('Application life cycle test', function () { }); it('can get app information', getAppInfo); - it('can login', login.bind(null, 'minioakey', 'minioskey')); + it('can Admin login', login.bind(null, 'minioakey', 'minioskey')); it('has bucket', checkBucket); it('can logout', logout); + + it('can OIDC login', loginOIDC.bind(null, username, password)); + it('has bucket', checkBucket); + it('can logout', logout); + it('does redirect', checkRedirect); it('check api', checkApi); @@ -167,13 +224,27 @@ describe('Application life cycle test', function () { it('can login', login.bind(null, 'minioadmin', 'minioadmin')); it('can add buckets', addBucket); it('can logout', logout); + + /* should be added on the next release + it('can OIDC login', loginOIDC.bind(null, username, password)); + it('has bucket', checkBucket); + it('can logout', logout); + */ + it('can update', function () { execSync(`cloudron update --app ${LOCATION}`, EXEC_ARGS); }); it('can configure', function () { execSync(`cloudron configure --app ${LOCATION} --location ${LOCATION} --secondary-domains API_SERVER_DOMAIN=${LOCATION}-api`, EXEC_ARGS); }); it('can get app information', getAppInfo); - it('can login', login.bind(null, 'minioadmin', 'minioadmin')); + it('can Admin login', login.bind(null, 'minioadmin', 'minioadmin')); it('has bucket', checkBucket); it('can logout', logout); + + /* should be added on the next release + it('can OIDC login', loginOIDC.bind(null, username, password)); + it('has bucket', checkBucket); + it('can logout', logout); + */ + it('does redirect', checkRedirect); it('check api', checkApi);