Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51aaa8f304 | |||
| 0c2e200176 | |||
| 8d7ba5cc26 |
@@ -1351,32 +1351,5 @@
|
||||
|
||||
[3.0.2]
|
||||
* Fix issue where normal users are shown apps they don't have access to
|
||||
* Re-configure email apps when email is enabled/disabled
|
||||
|
||||
[3.1.0]
|
||||
* Add UDP support
|
||||
* Clicking invite button does not send an invite immediately
|
||||
* Implement docker addon
|
||||
* Automatically login after password reset and account setup
|
||||
* Make backup interval configurable
|
||||
* Fix alternate domain certificate renewal
|
||||
|
||||
[3.1.1]
|
||||
* Fix caas domain migration
|
||||
|
||||
[3.1.2]
|
||||
* Add UDP support
|
||||
* Clicking invite button does not send an invite immediately
|
||||
* Implement docker addon
|
||||
* Automatically login after password reset and account setup
|
||||
* Make backup interval configurable
|
||||
* Fix alternate domain certificate renewal
|
||||
* API token can now have a name
|
||||
|
||||
[3.1.3]
|
||||
* Prevent dashboard domain from being deleted
|
||||
* Add alternateDomains to app install route
|
||||
|
||||
[3.1.4]
|
||||
* Fix issue where support tab was redirecting
|
||||
* Re-configure mail apps when mail is enabled/disabled
|
||||
|
||||
|
||||
@@ -14,11 +14,8 @@ function die {
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
|
||||
apt-mark hold grub* >/dev/null
|
||||
apt-get -o Dpkg::Options::="--force-confdef" update -y
|
||||
apt-get -o Dpkg::Options::="--force-confdef" upgrade -y
|
||||
apt-mark unhold grub* >/dev/null
|
||||
apt-get -o Dpkg::Options::="--force-confdef" dist-upgrade -y
|
||||
|
||||
echo "==> Installing required packages"
|
||||
|
||||
@@ -75,9 +72,8 @@ if [[ "${storage_driver}" != "overlay2" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# do not upgrade grub because it might prompt user and break this script
|
||||
echo "==> Enable memory accounting"
|
||||
apt-get -y --no-upgrade install grub2-common
|
||||
apt-get -y install grub2
|
||||
sed -e 's/^GRUB_CMDLINE_LINUX="\(.*\)"$/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
|
||||
update-grub
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ var appHealthMonitor = require('./src/apphealthmonitor.js'),
|
||||
async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
dockerProxy = require('./src/dockerproxy.js'),
|
||||
server = require('./src/server.js');
|
||||
|
||||
console.log();
|
||||
@@ -26,9 +25,6 @@ console.log(' Version: ', config.version());
|
||||
console.log(' Admin Origin: ', config.adminOrigin());
|
||||
console.log(' Appstore API server origin: ', config.apiServerOrigin());
|
||||
console.log(' Appstore Web server origin: ', config.webServerOrigin());
|
||||
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
|
||||
console.log(' LDAP Server Port: ', config.get('ldapPort'));
|
||||
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
@@ -36,7 +32,6 @@ console.log();
|
||||
async.series([
|
||||
server.start,
|
||||
ldap.start,
|
||||
dockerProxy.start,
|
||||
appHealthMonitor.start,
|
||||
], function (error) {
|
||||
if (error) {
|
||||
@@ -51,13 +46,11 @@ var NOOP_CALLBACK = function () { };
|
||||
process.on('SIGINT', function () {
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings ADD COLUMN type VARCHAR(8) NOT NULL DEFAULT "tcp"'),
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP INDEX hostPort'), // this drops the unique constraint
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort, type)')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort)'),
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP COLUMN type')
|
||||
], callback);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
var backupConfig = JSON.parse(results[0].value);
|
||||
backupConfig.intervalSecs = 24 * 60 * 60;
|
||||
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
// first check precondtion of domain entry in settings
|
||||
db.all('SELECT * FROM domains', [ ], function (error, domains) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let caasDomains = domains.filter(function (d) { return d.provider === 'caas'; });
|
||||
|
||||
async.eachSeries(caasDomains, function (domain, iteratorCallback) {
|
||||
let config = JSON.parse(domain.configJson);
|
||||
config.hyphenatedSubdomains = true;
|
||||
|
||||
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tokens ADD COLUMN name VARCHAR(64) DEFAULT ""', [], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tokens DROP COLUMN name', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -41,7 +41,6 @@ CREATE TABLE IF NOT EXISTS groupMembers(
|
||||
FOREIGN KEY(userId) REFERENCES users(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens(
|
||||
name VARCHAR(64) DEFAULT "", // description
|
||||
accessToken VARCHAR(128) NOT NULL UNIQUE,
|
||||
identifier VARCHAR(128) NOT NULL,
|
||||
clientId VARCHAR(128),
|
||||
@@ -51,7 +50,7 @@ CREATE TABLE IF NOT EXISTS tokens(
|
||||
|
||||
CREATE TABLE IF NOT EXISTS clients(
|
||||
id VARCHAR(128) NOT NULL UNIQUE, // prefixed with cid- to identify token easily in auth routes
|
||||
appId VARCHAR(128) NOT NULL, // name of the client (for external apps) or id of app (for built-in apps)
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(16) NOT NULL,
|
||||
clientSecret VARCHAR(512) NOT NULL,
|
||||
redirectURI VARCHAR(512) NOT NULL,
|
||||
@@ -93,7 +92,6 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appPortBindings(
|
||||
hostPort INTEGER NOT NULL UNIQUE,
|
||||
type VARCHAR(8) NOT NULL DEFAULT "tcp",
|
||||
environmentVariable VARCHAR(128) NOT NULL,
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
|
||||
Generated
+9
-79
@@ -1632,15 +1632,15 @@
|
||||
}
|
||||
},
|
||||
"cloudron-manifestformat": {
|
||||
"version": "2.13.1",
|
||||
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.13.1.tgz",
|
||||
"integrity": "sha512-KvWaUw0q2U+EL+y7LJ+Q8YZfERYgyGRwj48ZRfsREaIjckS8mG03Oa2UIf2x/uGLfw0frWvTNdQXe3ZpC94N9g==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.11.0.tgz",
|
||||
"integrity": "sha512-t4KR2KmK1JDtxw1n6IVNg0+xxspk/Cpb6m1WimE9hJ0KJYsIgZNnkce47uYiG9/nWrgUSV4xcdzsS91OOvWgig==",
|
||||
"requires": {
|
||||
"cron": "1.3.0",
|
||||
"java-packagename-regex": "1.0.0",
|
||||
"java-packagename-regex": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
|
||||
"safetydance": "0.0.15",
|
||||
"semver": "4.3.6",
|
||||
"tv4": "1.3.0",
|
||||
"tv4": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
|
||||
"validator": "3.43.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -1708,41 +1708,6 @@
|
||||
"typedarray": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
|
||||
}
|
||||
},
|
||||
"connect": {
|
||||
"version": "3.6.6",
|
||||
"resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
|
||||
"integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"finalhandler": "1.1.0",
|
||||
"parseurl": "1.3.2",
|
||||
"utils-merge": "1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
|
||||
"integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "1.0.2",
|
||||
"escape-html": "1.0.3",
|
||||
"on-finished": "2.3.0",
|
||||
"parseurl": "1.3.2",
|
||||
"statuses": "1.3.1",
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"connect-ensure-login": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz",
|
||||
@@ -2717,11 +2682,6 @@
|
||||
"safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz"
|
||||
}
|
||||
},
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"ejs": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz",
|
||||
@@ -3232,13 +3192,13 @@
|
||||
"dependencies": {
|
||||
"mkdirp": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
|
||||
"integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=",
|
||||
"dev": true
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "http://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
|
||||
"integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=",
|
||||
"dev": true
|
||||
}
|
||||
@@ -4530,8 +4490,7 @@
|
||||
}
|
||||
},
|
||||
"java-packagename-regex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
|
||||
"version": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
|
||||
"integrity": "sha1-lR9he9WhlCIO0GcLm4KowOxcYiQ="
|
||||
},
|
||||
"js-base64": {
|
||||
@@ -5296,11 +5255,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"multiparty": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.1.4.tgz",
|
||||
@@ -6010,14 +5964,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
|
||||
"requires": {
|
||||
"ee-first": "1.1.1"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -6167,11 +6113,6 @@
|
||||
"resolved": "https://registry.npmjs.org/parse-links/-/parse-links-0.1.0.tgz",
|
||||
"integrity": "sha1-afpighugBBX+c2MyNVIeRUe36CE="
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
|
||||
"integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
|
||||
},
|
||||
"passport": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz",
|
||||
@@ -7932,11 +7873,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
|
||||
"integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
|
||||
},
|
||||
"stdout-stream": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz",
|
||||
@@ -8543,8 +8479,7 @@
|
||||
"dev": true
|
||||
},
|
||||
"tv4": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
|
||||
"version": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
|
||||
"integrity": "sha1-0CDIRvrdUMhVq7JeuuzGj8EPeWM="
|
||||
},
|
||||
"tweetnacl": {
|
||||
@@ -8601,11 +8536,6 @@
|
||||
"version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
|
||||
|
||||
+3
-3
@@ -20,8 +20,7 @@
|
||||
"async": "^2.6.1",
|
||||
"aws-sdk": "^2.253.1",
|
||||
"body-parser": "^1.18.3",
|
||||
"cloudron-manifestformat": "^2.13.1",
|
||||
"connect": "^3.6.6",
|
||||
"cloudron-manifestformat": "^2.11.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^1.0.2",
|
||||
"connect-timeout": "^1.9.0",
|
||||
@@ -92,7 +91,8 @@
|
||||
"scripts": {
|
||||
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
|
||||
"migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
|
||||
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
|
||||
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test/[^a]*",
|
||||
"test_all": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
|
||||
"postmerge": "/bin/true",
|
||||
"precommit": "/bin/true",
|
||||
"prepush": "npm test",
|
||||
|
||||
+5
-12
@@ -44,7 +44,6 @@ fi
|
||||
initBaseImage="true"
|
||||
# provisioning data
|
||||
provider=""
|
||||
edition=""
|
||||
requestedVersion=""
|
||||
apiServerOrigin="https://api.cloudron.io"
|
||||
webServerOrigin="https://cloudron.io"
|
||||
@@ -53,16 +52,13 @@ sourceTarballUrl=""
|
||||
rebootServer="true"
|
||||
baseDataDir=""
|
||||
|
||||
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
|
||||
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,edition:,skip-reboot" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,skip-reboot" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
|
||||
--provider) provider="$2"; shift 2;;
|
||||
--edition) edition="$2"; shift 2;;
|
||||
--version) requestedVersion="$2"; shift 2;;
|
||||
--env)
|
||||
if [[ "$2" == "dev" ]]; then
|
||||
@@ -98,7 +94,7 @@ fi
|
||||
|
||||
# validate arguments in the absence of data
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "ami" && \
|
||||
@@ -108,8 +104,6 @@ elif [[ \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "exoscale" && \
|
||||
"${provider}" != "galaxygate" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "gce" && \
|
||||
"${provider}" != "hetzner" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
@@ -120,7 +114,7 @@ elif [[ \
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -143,12 +137,12 @@ echo ""
|
||||
if [[ "${initBaseImage}" == "true" ]]; then
|
||||
echo "=> Updating apt and installing script dependencies"
|
||||
if ! apt-get update &>> "${LOG_FILE}"; then
|
||||
echo "Could not update package repositories. See ${LOG_FILE}"
|
||||
echo "Could not update package repositories"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! apt-get install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
|
||||
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
|
||||
echo "Could not install setup dependencies (curl)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -175,7 +169,6 @@ fi
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"provider": "${provider}",
|
||||
"edition": "${edition}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"version": "${version}"
|
||||
|
||||
@@ -14,7 +14,6 @@ arg_version=""
|
||||
arg_web_server_origin=""
|
||||
arg_provider=""
|
||||
arg_is_demo="false"
|
||||
arg_edition=""
|
||||
|
||||
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
@@ -56,9 +55,6 @@ while true; do
|
||||
arg_provider=$(echo "$2" | $json provider)
|
||||
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
|
||||
|
||||
arg_edition=$(echo "$2" | $json edition)
|
||||
[[ "${arg_edition}" == "" ]] && arg_edition=""
|
||||
|
||||
shift 2
|
||||
;;
|
||||
--) break;;
|
||||
@@ -73,4 +69,3 @@ echo "fqdn: ${arg_fqdn}"
|
||||
echo "version: ${arg_version}"
|
||||
echo "web server: ${arg_web_server_origin}"
|
||||
echo "provider: ${arg_provider}"
|
||||
echo "edition: ${arg_edition}"
|
||||
|
||||
+1
-2
@@ -218,8 +218,7 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
"adminFqdn": "${arg_admin_fqdn}",
|
||||
"adminLocation": "${arg_admin_location}",
|
||||
"provider": "${arg_provider}",
|
||||
"isDemo": ${arg_is_demo},
|
||||
"edition": "${arg_edition}"
|
||||
"isDemo": ${arg_is_demo}
|
||||
}
|
||||
CONF_END
|
||||
|
||||
|
||||
@@ -90,8 +90,8 @@ server {
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade";
|
||||
proxy_hide_header Referrer-Policy;
|
||||
|
||||
<% if ( endpoint === 'admin' ) { -%>
|
||||
# CSP headers for the admin/dashboard resources
|
||||
<% if ( endpoint === 'admin' ) { -%>
|
||||
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
|
||||
<% } -%>
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:accesscontrol'),
|
||||
settings = require('./settings.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
@@ -114,7 +114,11 @@ function scopesForUser(user, callback) {
|
||||
|
||||
if (user.admin) return callback(null, exports.VALID_SCOPES);
|
||||
|
||||
callback(null, config.isSpacesEnabled() ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
|
||||
settings.getSpacesConfig(function (error, spaces) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, spaces.enabled ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
|
||||
});
|
||||
}
|
||||
|
||||
function validateToken(accessToken, callback) {
|
||||
|
||||
@@ -109,12 +109,6 @@ var KNOWN_ADDONS = {
|
||||
teardown: NOOP,
|
||||
backup: NOOP,
|
||||
restore: NOOP
|
||||
},
|
||||
docker: {
|
||||
setup: NOOP,
|
||||
teardown: NOOP,
|
||||
backup: NOOP,
|
||||
restore: NOOP
|
||||
}
|
||||
};
|
||||
|
||||
@@ -205,8 +199,6 @@ function getEnvironment(app, callback) {
|
||||
appdb.getAddonConfigByAppId(app.id, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${config.get('dockerProxyPort')}` });
|
||||
|
||||
return callback(null, result.map(function (e) { return e.name + '=' + e.value; }));
|
||||
});
|
||||
}
|
||||
|
||||
+23
-28
@@ -71,9 +71,7 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.ts' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
const SUBDOMAIN_FIELDS = [ 'appId', 'domain', 'subdomain', 'type' ].join(',');
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
function postProcess(result) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
@@ -98,16 +96,14 @@ function postProcess(result) {
|
||||
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
|
||||
|
||||
result.portBindings = { };
|
||||
let hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
|
||||
let environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
|
||||
let portTypes = result.portTypes === null ? [ ] : result.portTypes.split(',');
|
||||
var hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
|
||||
var environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
|
||||
|
||||
delete result.hostPorts;
|
||||
delete result.environmentVariables;
|
||||
delete result.portTypes;
|
||||
|
||||
for (var i = 0; i < environmentVariables.length; i++) {
|
||||
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] };
|
||||
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
|
||||
}
|
||||
|
||||
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
|
||||
@@ -137,15 +133,15 @@ function get(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
|
||||
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
@@ -162,15 +158,15 @@ function getByHttpPort(httpPort, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
|
||||
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
@@ -186,15 +182,15 @@ function getByContainerId(containerId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
|
||||
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
@@ -209,14 +205,14 @@ function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
|
||||
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
database.query('SELECT * FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
alternateDomains.forEach(function (d) {
|
||||
@@ -275,8 +271,8 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId) VALUES (?, ?, ?, ?)',
|
||||
args: [ env, portBindings[env].hostPort, portBindings[env].type, id ]
|
||||
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, appId) VALUES (?, ?, ?)',
|
||||
args: [ env, portBindings[env], id ]
|
||||
});
|
||||
});
|
||||
|
||||
@@ -326,19 +322,18 @@ function getPortBindings(id, callback) {
|
||||
|
||||
var portBindings = { };
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
portBindings[results[i].environmentVariable] = { hostPort: results[i].hostPort, type: results[i].type };
|
||||
portBindings[results[i].environmentVariable] = results[i].hostPort;
|
||||
}
|
||||
|
||||
callback(null, portBindings);
|
||||
});
|
||||
}
|
||||
|
||||
function delPortBinding(hostPort, type, callback) {
|
||||
function delPortBinding(hostPort, callback) {
|
||||
assert.strictEqual(typeof hostPort, 'number');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ], function (error, result) {
|
||||
database.query('DELETE FROM appPortBindings WHERE hostPort=?', [ hostPort ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -399,8 +394,8 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
// replace entries by app id
|
||||
queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] });
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
var values = [ portBindings[env].hostPort, portBindings[env].type, env, id ];
|
||||
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId) VALUES(?, ?, ?, ?)', args: values });
|
||||
var values = [ portBindings[env], env, id ];
|
||||
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, environmentVariable, appId) VALUES(?, ?, ?)', args: values });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+78
-99
@@ -45,13 +45,10 @@ exports = module.exports = {
|
||||
setOwner: setOwner,
|
||||
transferOwnership: transferOwnership,
|
||||
|
||||
PORT_TYPE_TCP: 'tcp',
|
||||
PORT_TYPE_UDP: 'udp',
|
||||
|
||||
// exported for testing
|
||||
_validateHostname: validateHostname,
|
||||
_validatePortBindings: validatePortBindings,
|
||||
_validateAccessRestriction: validateAccessRestriction,
|
||||
_translatePortBindings: translatePortBindings
|
||||
_validateAccessRestriction: validateAccessRestriction
|
||||
};
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
@@ -84,6 +81,7 @@ var appdb = require('./appdb.js'),
|
||||
split = require('split'),
|
||||
superagent = require('superagent'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
tld = require('tldjs'),
|
||||
TransformStream = require('stream').Transform,
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
url = require('url'),
|
||||
@@ -125,10 +123,43 @@ AppsError.BILLING_REQUIRED = 'Billing Required';
|
||||
AppsError.ACCESS_DENIED = 'Access denied';
|
||||
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
|
||||
|
||||
// Hostname validation comes from RFC 1123 (section 2.1)
|
||||
// Domain name validation comes from RFC 2181 (Name syntax)
|
||||
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
||||
// We are validating the validity of the location-fqdn as host name (and not dns name)
|
||||
function validateHostname(location, domain, hostname) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
|
||||
const RESERVED_LOCATIONS = [
|
||||
constants.API_LOCATION,
|
||||
constants.SMTP_LOCATION,
|
||||
constants.IMAP_LOCATION
|
||||
];
|
||||
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
if (hostname === config.adminFqdn()) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
// workaround https://github.com/oncletom/tld.js/issues/73
|
||||
var tmp = hostname.replace('_', '-');
|
||||
if (!tld.isValid(tmp)) return new AppsError(AppsError.BAD_FIELD, 'Hostname is not a valid domain name');
|
||||
|
||||
if (hostname.length > 253) return new AppsError(AppsError.BAD_FIELD, 'Hostname length exceeds 253 characters');
|
||||
|
||||
if (location) {
|
||||
// label validation
|
||||
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new AppsError(AppsError.BAD_FIELD, 'Invalid subdomain length');
|
||||
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new AppsError(AppsError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot');
|
||||
if (/^[-.]/.test(location)) return new AppsError(AppsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// validate the port bindings
|
||||
function validatePortBindings(portBindings, manifest) {
|
||||
function validatePortBindings(portBindings, tcpPorts) {
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof manifest, 'object');
|
||||
|
||||
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
|
||||
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
|
||||
@@ -159,58 +190,26 @@ function validatePortBindings(portBindings, manifest) {
|
||||
|
||||
if (!portBindings) return null;
|
||||
|
||||
for (let portName in portBindings) {
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(portName)) return new AppsError(AppsError.BAD_FIELD, `${portName} is not a valid environment variable`);
|
||||
var env;
|
||||
for (env in portBindings) {
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(env)) return new AppsError(AppsError.BAD_FIELD, env + ' is not valid environment variable');
|
||||
|
||||
const hostPort = portBindings[portName];
|
||||
if (!Number.isInteger(hostPort)) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not an integer`);
|
||||
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(hostPort));
|
||||
if (hostPort <= 1023 || hostPort > 65535) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not in permitted range`);
|
||||
if (!Number.isInteger(portBindings[env])) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not an integer');
|
||||
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
|
||||
if (portBindings[env] <= 1023 || portBindings[env] > 65535) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not in permitted range');
|
||||
|
||||
}
|
||||
|
||||
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
|
||||
// that the user wants the service disabled
|
||||
const tcpPorts = manifest.tcpPorts || { };
|
||||
const udpPorts = manifest.udpPorts || { };
|
||||
for (let portName in portBindings) {
|
||||
if (!(portName in tcpPorts) && !(portName in udpPorts)) return new AppsError(AppsError.BAD_FIELD, `Invalid portBindings ${portName}`);
|
||||
tcpPorts = tcpPorts || { };
|
||||
for (env in portBindings) {
|
||||
if (!(env in tcpPorts)) return new AppsError(AppsError.BAD_FIELD, 'Invalid portBindings ' + env);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function translatePortBindings(portBindings, manifest) {
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof manifest, 'object');
|
||||
|
||||
if (!portBindings) return null;
|
||||
|
||||
let result = {};
|
||||
const tcpPorts = manifest.tcpPorts || { };
|
||||
|
||||
for (let portName in portBindings) {
|
||||
const portType = portName in tcpPorts ? exports.PORT_TYPE_TCP : exports.PORT_TYPE_UDP;
|
||||
result[portName] = { hostPort: portBindings[portName], type: portType };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function postProcess(app) {
|
||||
let result = {};
|
||||
for (let portName in app.portBindings) {
|
||||
result[portName] = app.portBindings[portName].hostPort;
|
||||
}
|
||||
app.portBindings = result;
|
||||
}
|
||||
|
||||
function addSpacesSuffix(location, user) {
|
||||
if (user.admin || !config.isSpacesEnabled()) return location;
|
||||
|
||||
const spacesSuffix = user.username.replace(/\./g, '-');
|
||||
return location === '' ? spacesSuffix : `${location}-${spacesSuffix}`;
|
||||
}
|
||||
|
||||
function validateAccessRestriction(accessRestriction) {
|
||||
assert.strictEqual(typeof accessRestriction, 'object');
|
||||
|
||||
@@ -306,8 +305,8 @@ function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS);
|
||||
|
||||
// check if any of the port bindings conflict
|
||||
for (let portName in portBindings) {
|
||||
if (portBindings[portName] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
|
||||
for (var env in portBindings) {
|
||||
if (portBindings[env] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
|
||||
}
|
||||
|
||||
return new AppsError(AppsError.ALREADY_EXISTS);
|
||||
@@ -376,13 +375,11 @@ function get(appId, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
postProcess(app);
|
||||
|
||||
domaindb.get(app.domain, function (error, domainObject) {
|
||||
domaindb.get(app.domain, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, domainObject);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
@@ -406,13 +403,11 @@ function getByIpAddress(ip, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
postProcess(app);
|
||||
|
||||
domaindb.get(app.domain, function (error, domainObject) {
|
||||
domaindb.get(app.domain, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, domainObject);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
@@ -432,14 +427,12 @@ function getAll(callback) {
|
||||
appdb.getAll(function (error, apps) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
apps.forEach(postProcess);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
domaindb.get(app.domain, function (error, domainObject) {
|
||||
domaindb.get(app.domain, function (error, result) {
|
||||
if (error) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, domainObject);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
@@ -494,9 +487,8 @@ function mailboxNameForLocation(location, manifest) {
|
||||
return (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
}
|
||||
|
||||
function install(data, user, auditSource, callback) {
|
||||
function install(data, auditSource, callback) {
|
||||
assert(data && typeof data === 'object');
|
||||
assert(user && typeof user === 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -515,8 +507,7 @@ function install(data, user, auditSource, callback) {
|
||||
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
|
||||
backupId = data.backupId || null,
|
||||
backupFormat = data.backupFormat || 'tgz',
|
||||
ownerId = data.ownerId,
|
||||
alternateDomains = data.alternateDomains || [];
|
||||
ownerId = data.ownerId;
|
||||
|
||||
assert(data.appStoreId || data.manifest); // atleast one of them is required
|
||||
|
||||
@@ -529,7 +520,7 @@ function install(data, user, auditSource, callback) {
|
||||
error = checkManifestConstraints(manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(portBindings, manifest);
|
||||
error = validatePortBindings(portBindings, manifest.tcpPorts);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateAccessRestriction(accessRestriction);
|
||||
@@ -568,14 +559,12 @@ function install(data, user, auditSource, callback) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
|
||||
location = addSpacesSuffix(location, user);
|
||||
alternateDomains.forEach(function (ad) { ad.subdomain = addSpacesSuffix(ad.subdomain, user); }); // TODO: validate these
|
||||
var fqdn = domains.fqdn(location, domain, domainObject.provider);
|
||||
|
||||
error = domains.validateHostname(location, domainObject);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
error = validateHostname(location, domain, fqdn);
|
||||
if (error) return callback(error);
|
||||
|
||||
if (cert && key) {
|
||||
let fqdn = domains.fqdn(location, domain, domainObject);
|
||||
error = reverseProxy.validateCertificate(fqdn, cert, key);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
}
|
||||
@@ -591,11 +580,10 @@ function install(data, user, auditSource, callback) {
|
||||
mailboxName: mailboxNameForLocation(location, manifest),
|
||||
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
|
||||
enableBackup: enableBackup,
|
||||
robotsTxt: robotsTxt,
|
||||
alternateDomains: alternateDomains
|
||||
robotsTxt: robotsTxt
|
||||
};
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
|
||||
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, portBindings, data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
@@ -618,7 +606,6 @@ function install(data, user, auditSource, callback) {
|
||||
|
||||
// save cert to boxdata/certs
|
||||
if (cert && key) {
|
||||
let fqdn = domains.fqdn(location, domainObject);
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
}
|
||||
@@ -639,10 +626,9 @@ function install(data, user, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function configure(appId, data, user, auditSource, callback) {
|
||||
function configure(appId, data, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert(data && typeof data === 'object');
|
||||
assert(user && typeof user === 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -663,10 +649,9 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
}
|
||||
|
||||
if ('portBindings' in data) {
|
||||
error = validatePortBindings(data.portBindings, app.manifest);
|
||||
portBindings = values.portBindings = data.portBindings;
|
||||
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
|
||||
if (error) return callback(error);
|
||||
values.portBindings = translatePortBindings(data.portBindings, app.manifest);
|
||||
portBindings = data.portBindings;
|
||||
} else {
|
||||
portBindings = app.portBindings;
|
||||
}
|
||||
@@ -703,22 +688,19 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
if ('alternateDomains' in data) {
|
||||
// TODO validate all subdomains [{ domain: '', subdomain: ''}]
|
||||
values.alternateDomains = data.alternateDomains;
|
||||
values.alternateDomains.forEach(function (ad) { ad.subdomain = addSpacesSuffix(ad.subdomain, user); }); // TODO: validate these
|
||||
}
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
|
||||
location = addSpacesSuffix(location, user);
|
||||
var fqdn = domains.fqdn(location, domain, domainObject.provider);
|
||||
|
||||
error = domains.validateHostname(location, domainObject);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
error = validateHostname(location, domain, fqdn);
|
||||
if (error) return callback(error);
|
||||
|
||||
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
|
||||
if ('cert' in data && 'key' in data) {
|
||||
let fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
if (data.cert && data.key) {
|
||||
error = reverseProxy.validateCertificate(fqdn, data.cert, data.key);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
@@ -934,10 +916,9 @@ function restore(appId, data, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function clone(appId, data, user, auditSource, callback) {
|
||||
function clone(appId, data, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert(user && typeof user === 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -969,16 +950,15 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
error = checkManifestConstraints(backupInfo.manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(portBindings, backupInfo.manifest);
|
||||
error = validatePortBindings(portBindings, backupInfo.manifest.tcpPorts);
|
||||
if (error) return callback(error);
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
|
||||
location = addSpacesSuffix(location, user);
|
||||
error = domains.validateHostname(location, domainObject);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
error = validateHostname(location, domain, domains.fqdn(location, domain, domainObject.provider));
|
||||
if (error) return callback(error);
|
||||
|
||||
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
|
||||
|
||||
@@ -994,7 +974,7 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
robotsTxt: app.robotsTxt
|
||||
};
|
||||
|
||||
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
|
||||
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, portBindings, data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -1179,12 +1159,11 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
|
||||
function canAutoupdateApp(app, newManifest) {
|
||||
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
|
||||
|
||||
const newTcpPorts = newManifest.tcpPorts || { };
|
||||
const newUdpPorts = newManifest.udpPorts || { };
|
||||
const portBindings = app.portBindings; // this is never null
|
||||
var newTcpPorts = newManifest.tcpPorts || { };
|
||||
var portBindings = app.portBindings; // this is never null
|
||||
|
||||
for (let portName in portBindings) {
|
||||
if (!(portName in newTcpPorts) && !(portName in newUdpPorts)) return new Error(`${portName} was in use but new update removes it`);
|
||||
for (var env in portBindings) {
|
||||
if (!(env in newTcpPorts)) return new Error(env + ' was in use but new update removes it');
|
||||
}
|
||||
|
||||
// it's fine if one or more (unused) keys got removed
|
||||
|
||||
+31
-11
@@ -95,7 +95,6 @@ function isFreePlan(subscription) {
|
||||
return !subscription || subscription.plan.id === 'free';
|
||||
}
|
||||
|
||||
// See app.js install it will create a db record first but remove it again if appstore purchase fails
|
||||
function purchase(appId, appstoreId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof appstoreId, 'string');
|
||||
@@ -103,20 +102,41 @@ function purchase(appId, appstoreId, callback) {
|
||||
|
||||
if (appstoreId === '') return callback(null);
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
function doThePurchase() {
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
|
||||
var data = { appstoreId: appstoreId };
|
||||
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getSubscription(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
|
||||
var data = { appstoreId: appstoreId };
|
||||
// only check for app install count if on the free plan
|
||||
if (result.id !== 'free') return doThePurchase();
|
||||
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
appdb.getAppStoreIds(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
var count = result.filter(function (a) { return !!a.appStoreId; }).length;
|
||||
|
||||
// we only allow max of 2 app installations without a subscription
|
||||
// WARNING install and clone in apps.js will first add the db record and then call purchase() so we test for more than 2 here
|
||||
if (count > 2) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, 'Too many apps installed'));
|
||||
|
||||
doThePurchase();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+9
-22
@@ -46,6 +46,7 @@ var addons = require('./addons.js'),
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tld = require('tldjs'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -134,20 +135,6 @@ function createContainer(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// Only delete the main container of the app, not destroy any docker addon created ones
|
||||
function deleteMainContainer(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'deleting main app container');
|
||||
|
||||
docker.deleteContainer(app.containerId, function (error) {
|
||||
if (error) return callback(new Error('Error deleting container: ' + error));
|
||||
|
||||
updateApp(app, { containerId: null }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteContainers(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -457,7 +444,7 @@ function install(app, callback) {
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteMainContainer.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
function teardownAddons(next) {
|
||||
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
|
||||
var addonsToRemove = !isRestoring
|
||||
@@ -570,7 +557,7 @@ function configure(app, callback) {
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteMainContainer.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
unregisterAlternateDomains.bind(null, app, false /* all */),
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
@@ -671,7 +658,7 @@ function update(app, callback) {
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteMainContainer.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
function deleteImageIfChanged(done) {
|
||||
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
|
||||
|
||||
@@ -683,14 +670,14 @@ function update(app, callback) {
|
||||
|
||||
// free unused ports
|
||||
function (next) {
|
||||
const currentPorts = app.portBindings || {};
|
||||
const newTcpPorts = app.updateConfig.manifest.tcpPorts || {};
|
||||
const newUdpPorts = app.updateConfig.manifest.udpPorts || {};
|
||||
// make sure we always have objects
|
||||
var currentPorts = app.portBindings || {};
|
||||
var newPorts = app.updateConfig.manifest.tcpPorts || {};
|
||||
|
||||
async.each(Object.keys(currentPorts), function (portName, callback) {
|
||||
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(); // port still in use
|
||||
if (newPorts[portName]) return callback(); // port still in use
|
||||
|
||||
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
|
||||
appdb.delPortBinding(currentPorts[portName], function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) console.error('Portbinding does not exist in database.');
|
||||
else if (error) return next(error);
|
||||
|
||||
|
||||
+6
-13
@@ -124,9 +124,6 @@ function testConfig(backupConfig, callback) {
|
||||
|
||||
if (backupConfig.format !== 'tgz' && backupConfig.format !== 'rsync') return callback(new BackupsError(BackupsError.BAD_FIELD, 'unknown format'));
|
||||
|
||||
// remember to adjust the cron ensureBackup task interval accordingly
|
||||
if (backupConfig.intervalSecs < 6 * 60 * 60) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Interval must be atleast 6 hours'));
|
||||
|
||||
api(backupConfig.provider).testConfig(backupConfig, callback);
|
||||
}
|
||||
|
||||
@@ -984,19 +981,15 @@ function ensureBackup(auditSource, callback) {
|
||||
getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, backups) {
|
||||
if (error) {
|
||||
debug('Unable to list backups', error);
|
||||
return callback(error);
|
||||
return callback(error); // no point trying to backup if appstore is down
|
||||
}
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(error);
|
||||
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
|
||||
debug('Previous backup was %j, no need to backup now', backups[0]);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < (backupConfig.intervalSecs - 3600) * 1000)) { // adjust 1 hour
|
||||
debug('Previous backup was %j, no need to backup now', backups[0]);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
backup(auditSource, callback);
|
||||
});
|
||||
backup(auditSource, callback);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+8
-21
@@ -68,7 +68,7 @@ ClientsError.NOT_FOUND = 'Not found';
|
||||
ClientsError.INTERNAL_ERROR = 'Internal Error';
|
||||
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
|
||||
|
||||
function validateClientName(name) {
|
||||
function validateName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
if (name.length < 1) return new ClientsError(ClientsError.BAD_FIELD, 'Name must be atleast 1 character');
|
||||
@@ -79,14 +79,6 @@ function validateClientName(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateTokenName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
if (name.length > 64) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function add(appId, type, redirectURI, scope, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
@@ -97,7 +89,7 @@ function add(appId, type, redirectURI, scope, callback) {
|
||||
var error = accesscontrol.validateScopeString(scope);
|
||||
if (error) return callback(new ClientsError(ClientsError.INVALID_SCOPE, error.message));
|
||||
|
||||
error = validateClientName(appId);
|
||||
error = validateName(appId);
|
||||
if (error) return callback(error);
|
||||
|
||||
var id = 'cid-' + uuid.v4();
|
||||
@@ -252,17 +244,12 @@ function delByAppIdAndType(appId, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
|
||||
function addTokenByUserId(clientId, userId, expiresAt, callback) {
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof expiresAt, 'number');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const name = options.name || '';
|
||||
let error = validateTokenName(name);
|
||||
if (error) return callback(error);
|
||||
|
||||
get(clientId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -278,7 +265,7 @@ function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
|
||||
tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), name, function (error) {
|
||||
tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), function (error) {
|
||||
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
@@ -295,17 +282,17 @@ function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
|
||||
}
|
||||
|
||||
// this issues a cid-cli token that does not require a password in various routes
|
||||
function issueDeveloperToken(userObject, auditSource, callback) {
|
||||
function issueDeveloperToken(userObject, ip, callback) {
|
||||
assert.strictEqual(typeof userObject, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
|
||||
addTokenByUserId('cid-cli', userObject.id, expiresAt, {}, function (error, result) {
|
||||
addTokenByUserId('cid-cli', userObject.id, expiresAt, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: userObject.id, user: users.removePrivateFields(userObject) });
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'cli', ip: ip }, { userId: userObject.id, user: users.removePrivateFields(userObject) });
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
|
||||
+2
-2
@@ -145,10 +145,10 @@ function getConfig(callback) {
|
||||
version: config.version(),
|
||||
progress: progress.getAll(),
|
||||
isDemo: config.isDemo(),
|
||||
edition: config.edition(),
|
||||
memory: os.totalmem(),
|
||||
provider: config.provider(),
|
||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY]
|
||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
|
||||
spaces: allSettings[settings.SPACES_CONFIG_KEY] // here because settings route cannot be accessed by spaces users
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+1
-30
@@ -24,7 +24,6 @@ exports = module.exports = {
|
||||
version: version,
|
||||
setVersion: setVersion,
|
||||
database: database,
|
||||
edition: edition,
|
||||
|
||||
// these values are derived
|
||||
adminOrigin: adminOrigin,
|
||||
@@ -39,12 +38,6 @@ exports = module.exports = {
|
||||
|
||||
isDemo: isDemo,
|
||||
|
||||
// feature flags based on editions (these have a separate license from standard edition)
|
||||
isSpacesEnabled: isSpacesEnabled,
|
||||
allowHyphenatedSubdomains: allowHyphenatedSubdomains,
|
||||
allowOperatorActions: allowOperatorActions,
|
||||
isAdminDomainLocked: isAdminDomainLocked,
|
||||
|
||||
// for testing resets to defaults
|
||||
_reset: _reset
|
||||
};
|
||||
@@ -83,8 +76,7 @@ function saveSync() {
|
||||
adminFqdn: data.adminFqdn,
|
||||
adminLocation: data.adminLocation,
|
||||
provider: data.provider,
|
||||
isDemo: data.isDemo,
|
||||
edition: data.edition
|
||||
isDemo: data.isDemo
|
||||
};
|
||||
|
||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
|
||||
@@ -111,8 +103,6 @@ function initConfig() {
|
||||
data.smtpPort = 2525; // this value comes from mail container
|
||||
data.sysadminPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
data.dockerProxyPort = 3003;
|
||||
data.edition = '';
|
||||
|
||||
// keep in sync with start.sh
|
||||
data.database = {
|
||||
@@ -229,22 +219,6 @@ function isDemo() {
|
||||
return get('isDemo') === true;
|
||||
}
|
||||
|
||||
function isSpacesEnabled() {
|
||||
return get('edition') === 'education';
|
||||
}
|
||||
|
||||
function allowHyphenatedSubdomains() {
|
||||
return get('edition') === 'hostingprovider';
|
||||
}
|
||||
|
||||
function allowOperatorActions() {
|
||||
return get('edition') !== 'hostingprovider';
|
||||
}
|
||||
|
||||
function isAdminDomainLocked() {
|
||||
return get('edition') === 'hostingprovider';
|
||||
}
|
||||
|
||||
function provider() {
|
||||
return get('provider');
|
||||
}
|
||||
@@ -261,6 +235,3 @@ function dkimSelector() {
|
||||
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
|
||||
}
|
||||
|
||||
function edition() {
|
||||
return get('edition');
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ exports = module.exports = {
|
||||
|
||||
ADMIN_NAME: 'Settings',
|
||||
|
||||
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
|
||||
|
||||
NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf',
|
||||
|
||||
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ function recreateJobs(tz) {
|
||||
|
||||
if (gJobs.backup) gJobs.backup.stop();
|
||||
gJobs.backup = new CronJob({
|
||||
cronTime: '00 00 */6 * * *', // check every 6 hours
|
||||
cronTime: '00 00 */6 * * *', // every 6 hours. backups.ensureBackup() will only trigger a backup once per day
|
||||
onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
|
||||
+1
-2
@@ -130,8 +130,7 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
fqdn: domain,
|
||||
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
|
||||
fqdn: domain
|
||||
};
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
|
||||
@@ -28,7 +28,7 @@ function translateRequestError(result, callback) {
|
||||
if (result.statusCode === 422) return callback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
|
||||
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
|
||||
let error = result.body.errors[0];
|
||||
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
|
||||
let message = error.message;
|
||||
if (error.code === 6003) {
|
||||
if (error.error_chain[0] && error.error_chain[0].code === 6103) message = 'Invalid API Key';
|
||||
else message = 'Invalid credentials';
|
||||
@@ -231,12 +231,10 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
|
||||
if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'email must be a non-empty string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
email: dnsConfig.email,
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
email: dnsConfig.email
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
@@ -201,11 +201,9 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
token: dnsConfig.token
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
+1
-3
@@ -113,11 +113,9 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
token: dnsConfig.token
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
+1
-3
@@ -24,8 +24,7 @@ function getDnsCredentials(dnsConfig) {
|
||||
credentials: {
|
||||
client_email: dnsConfig.credentials.client_email,
|
||||
private_key: dnsConfig.credentials.private_key
|
||||
},
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -168,7 +167,6 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
if (!dnsConfig.credentials || typeof dnsConfig.credentials !== 'object') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials must be an object'));
|
||||
if (typeof dnsConfig.credentials.client_email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.client_email must be a string'));
|
||||
if (typeof dnsConfig.credentials.private_key !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.private_key must be a string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var credentials = getDnsCredentials(dnsConfig);
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
+1
-3
@@ -148,12 +148,10 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
|
||||
if (!dnsConfig.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiKey must be a non-empty string'));
|
||||
if (!dnsConfig.apiSecret || typeof dnsConfig.apiSecret !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiSecret must be a non-empty string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var credentials = {
|
||||
apiKey: dnsConfig.apiKey,
|
||||
apiSecret: dnsConfig.apiSecret,
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
apiSecret: dnsConfig.apiSecret
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
+1
-9
@@ -55,19 +55,11 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if ('wildcard' in dnsConfig && typeof dnsConfig.wildcard !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'wildcard must be a boolean'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var config = {
|
||||
wildcard: !!dnsConfig.wildcard,
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
};
|
||||
|
||||
// Very basic check if the nameservers can be fetched
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
callback(null, config);
|
||||
callback(null, { wildcard: !!dnsConfig.wildcard });
|
||||
});
|
||||
}
|
||||
|
||||
+1
-6
@@ -208,14 +208,9 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a string'));
|
||||
if (typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var credentials = {
|
||||
username: dnsConfig.username,
|
||||
token: dnsConfig.token,
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
token: dnsConfig.token
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
+2
-4
@@ -114,7 +114,7 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
};
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.changeResourceRecordSets(params, function(error) {
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
if (error && error.code === 'AccessDenied') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'PriorRequestNotComplete') return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
|
||||
@@ -235,15 +235,13 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
|
||||
if (!dnsConfig.accessKeyId || typeof dnsConfig.accessKeyId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'accessKeyId must be a non-empty string'));
|
||||
if (!dnsConfig.secretAccessKey || typeof dnsConfig.secretAccessKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'secretAccessKey must be a non-empty string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var credentials = {
|
||||
accessKeyId: dnsConfig.accessKeyId,
|
||||
secretAccessKey: dnsConfig.secretAccessKey,
|
||||
region: dnsConfig.region || 'us-east-1',
|
||||
endpoint: dnsConfig.endpoint || null,
|
||||
listHostedZonesByName: true, // new/updated creds require this perm
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
listHostedZonesByName: true // new/updated creds require this perm
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
+6
-9
@@ -127,17 +127,14 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ];
|
||||
|
||||
var portEnv = [];
|
||||
for (let portName in app.portBindings) {
|
||||
const hostPort = app.portBindings[portName];
|
||||
const portType = portName in manifest.tcpPorts ? 'tcp' : 'udp';
|
||||
const ports = portType == 'tcp' ? manifest.tcpPorts : manifest.udpPorts;
|
||||
for (var e in app.portBindings) {
|
||||
var hostPort = app.portBindings[e];
|
||||
var containerPort = manifest.tcpPorts[e].containerPort || hostPort;
|
||||
|
||||
var containerPort = ports[portName].containerPort || hostPort;
|
||||
exposedPorts[containerPort + '/tcp'] = {};
|
||||
portEnv.push(e + '=' + hostPort);
|
||||
|
||||
exposedPorts[`${containerPort}/${portType}`] = {};
|
||||
portEnv.push(`${portName}=${hostPort}`);
|
||||
|
||||
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
|
||||
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
|
||||
}
|
||||
|
||||
// first check db record, then manifest
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
AppsError = apps.AppsError,
|
||||
assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
express = require('express'),
|
||||
debug = require('debug')('box:dockerproxy'),
|
||||
http = require('http'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
middleware = require('./middleware'),
|
||||
net = require('net'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
_ = require('underscore');
|
||||
|
||||
var gHttpServer = null;
|
||||
|
||||
function authorizeApp(req, res, next) {
|
||||
// TODO add here some authorization
|
||||
// - block apps not using the docker addon
|
||||
// - block calls regarding platform containers
|
||||
// - only allow managing and inspection of containers belonging to the app
|
||||
|
||||
// make the tests pass for now
|
||||
if (config.TEST) {
|
||||
req.app = { id: 'testappid' };
|
||||
return next();
|
||||
}
|
||||
|
||||
apps.getByIpAddress(req.connection.remoteAddress, function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
if (!('docker' in app.manifest.addons)) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
req.app = app;
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function attachDockerRequest(req, res, next) {
|
||||
var options = {
|
||||
socketPath: '/var/run/docker.sock',
|
||||
method: req.method,
|
||||
path: req.url,
|
||||
headers: req.headers
|
||||
};
|
||||
|
||||
req.dockerRequest = http.request(options, function (dockerResponse) {
|
||||
res.writeHead(dockerResponse.statusCode, dockerResponse.headers);
|
||||
|
||||
// Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed
|
||||
res.write(' ');
|
||||
|
||||
dockerResponse.on('error', function (error) { console.error('dockerResponse error:', error); });
|
||||
dockerResponse.pipe(res, { end: true });
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
function containersCreate(req, res, next) {
|
||||
safe.set(req.body, 'HostConfig.NetworkMode', 'cloudron'); // overwrite the network the container lives in
|
||||
safe.set(req.body, 'NetworkingConfig', {}); // drop any custom network configs
|
||||
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id })); // overwrite the app id to track containers of an app
|
||||
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
|
||||
|
||||
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
|
||||
dockerDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'docker');
|
||||
|
||||
debug('Original volume binds:', req.body.HostConfig.Binds);
|
||||
|
||||
let binds = [];
|
||||
for (let bind of (req.body.HostConfig.Binds || [])) {
|
||||
if (bind.startsWith(appDataDir)) binds.push(bind); // eclipse will inspect docker to find out the host folders and pass that to child containers
|
||||
else if (bind.startsWith('/app/data')) binds.push(bind.replace(new RegExp('^/app/data'), appDataDir));
|
||||
else binds.push(`${dockerDataDir}/${bind}`);
|
||||
}
|
||||
|
||||
// cleanup the paths from potential double slashes
|
||||
binds = binds.map(function (bind) { return bind.replace(/\/+/g, '/'); });
|
||||
|
||||
debug('Rewritten volume binds:', binds);
|
||||
safe.set(req.body, 'HostConfig.Binds', binds);
|
||||
|
||||
let plainBody = JSON.stringify(req.body);
|
||||
|
||||
req.dockerRequest.setHeader('Content-Length', Buffer.byteLength(plainBody));
|
||||
req.dockerRequest.end(plainBody);
|
||||
}
|
||||
|
||||
function process(req, res, next) {
|
||||
// we have to rebuild the body since we consumed in in the parser
|
||||
if (Object.keys(req.body).length !== 0) {
|
||||
let plainBody = JSON.stringify(req.body);
|
||||
req.dockerRequest.setHeader('Content-Length', Buffer.byteLength(plainBody));
|
||||
req.dockerRequest.end(plainBody);
|
||||
} else if (!req.readable) {
|
||||
req.dockerRequest.end();
|
||||
} else {
|
||||
req.pipe(req.dockerRequest, { end: true });
|
||||
}
|
||||
}
|
||||
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert(gHttpServer === null, 'Already started');
|
||||
|
||||
let json = middleware.json({ strict: true });
|
||||
let router = new express.Router();
|
||||
router.post('/:version/containers/create', containersCreate);
|
||||
|
||||
let proxyServer = express();
|
||||
|
||||
if (config.TEST) {
|
||||
proxyServer.use(function (req, res, next) {
|
||||
console.log('Proxying: ' + req.method, req.url);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
proxyServer.use(authorizeApp)
|
||||
.use(attachDockerRequest)
|
||||
.use(json)
|
||||
.use(router)
|
||||
.use(process)
|
||||
.use(middleware.lastMile());
|
||||
|
||||
gHttpServer = http.createServer(proxyServer);
|
||||
gHttpServer.listen(config.get('dockerProxyPort'), '0.0.0.0', callback);
|
||||
|
||||
debug(`startDockerProxy: started proxy on port ${config.get('dockerProxyPort')}`);
|
||||
|
||||
gHttpServer.on('upgrade', function (req, client, head) {
|
||||
// Create a new tcp connection to the TCP server
|
||||
var remote = net.connect('/var/run/docker.sock', function () {
|
||||
var upgradeMessage = req.method + ' ' + req.url + ' HTTP/1.1\r\n' +
|
||||
`Host: ${req.headers.host}\r\n` +
|
||||
'Connection: Upgrade\r\n' +
|
||||
'Upgrade: tcp\r\n';
|
||||
|
||||
if (req.headers['content-type'] === 'application/json') {
|
||||
// TODO we have to parse the immediate upgrade request body, but I don't know how
|
||||
let plainBody = '{"Detach":false,"Tty":false}\r\n';
|
||||
upgradeMessage += `Content-Type: application/json\r\n`;
|
||||
upgradeMessage += `Content-Length: ${Buffer.byteLength(plainBody)}\r\n`;
|
||||
upgradeMessage += '\r\n';
|
||||
upgradeMessage += plainBody;
|
||||
}
|
||||
|
||||
upgradeMessage += '\r\n';
|
||||
|
||||
// resend the upgrade event to the docker daemon, so it responds with the proper message through the pipes
|
||||
remote.write(upgradeMessage);
|
||||
|
||||
// two-way pipes between client and docker daemon
|
||||
client.pipe(remote).pipe(client);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gHttpServer) gHttpServer.close();
|
||||
|
||||
gHttpServer = null;
|
||||
|
||||
callback();
|
||||
}
|
||||
+19
-86
@@ -6,7 +6,6 @@ module.exports = exports = {
|
||||
getAll: getAll,
|
||||
update: update,
|
||||
del: del,
|
||||
isLocked: isLocked,
|
||||
|
||||
fqdn: fqdn,
|
||||
setAdmin: setAdmin,
|
||||
@@ -20,15 +19,12 @@ module.exports = exports = {
|
||||
removePrivateFields: removePrivateFields,
|
||||
removeRestrictedFields: removeRestrictedFields,
|
||||
|
||||
validateHostname: validateHostname,
|
||||
|
||||
DomainsError: DomainsError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
caas = require('./caas.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:domains'),
|
||||
domaindb = require('./domaindb.js'),
|
||||
@@ -108,54 +104,12 @@ function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
|
||||
api(provider).verifyDnsConfig(config, domain, zoneName, ip, callback);
|
||||
}
|
||||
|
||||
function fqdn(location, domainObject) {
|
||||
return location + (location ? (domainObject.config.hyphenatedSubdomains ? '-' : '.') : '') + domainObject.domain;
|
||||
}
|
||||
|
||||
// Hostname validation comes from RFC 1123 (section 2.1)
|
||||
// Domain name validation comes from RFC 2181 (Name syntax)
|
||||
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
||||
// We are validating the validity of the location-fqdn as host name (and not dns name)
|
||||
function validateHostname(location, domainObject) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
|
||||
const hostname = fqdn(location, domainObject);
|
||||
|
||||
const RESERVED_LOCATIONS = [
|
||||
constants.API_LOCATION,
|
||||
constants.SMTP_LOCATION,
|
||||
constants.IMAP_LOCATION
|
||||
];
|
||||
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
if (hostname === config.adminFqdn()) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
// workaround https://github.com/oncletom/tld.js/issues/73
|
||||
var tmp = hostname.replace('_', '-');
|
||||
if (!tld.isValid(tmp)) return new DomainsError(DomainsError.BAD_FIELD, 'Hostname is not a valid domain name');
|
||||
|
||||
if (hostname.length > 253) return new DomainsError(DomainsError.BAD_FIELD, 'Hostname length exceeds 253 characters');
|
||||
|
||||
if (location) {
|
||||
// label validation
|
||||
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new DomainsError(DomainsError.BAD_FIELD, 'Invalid subdomain length');
|
||||
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot');
|
||||
if (/^[-.]/.test(location)) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot');
|
||||
}
|
||||
|
||||
if (domainObject.config.hyphenatedSubdomains) {
|
||||
if (location.indexOf('.') !== -1) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain cannot contain a dot');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function add(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConfig, callback) {
|
||||
function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof fallbackCertificate, 'object');
|
||||
assert.strictEqual(typeof tlsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -179,12 +133,10 @@ function add(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConf
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or le-*'));
|
||||
}
|
||||
|
||||
if (dnsConfig.hyphenatedSubdomains && !config.allowHyphenatedSubdomains()) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Not allowed in this edition'));
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
|
||||
verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, function (error, result) {
|
||||
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, result) {
|
||||
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
|
||||
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record: ' + error.message));
|
||||
@@ -206,10 +158,6 @@ function add(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConf
|
||||
});
|
||||
}
|
||||
|
||||
function isLocked(domain) {
|
||||
return domain === config.adminDomain() && config.isAdminDomainLocked();
|
||||
}
|
||||
|
||||
function get(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -219,8 +167,6 @@ function get(domain, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
result.locked = isLocked(domain);
|
||||
|
||||
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
|
||||
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -242,17 +188,15 @@ function getAll(callback) {
|
||||
domaindb.getAll(function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
result.forEach(function (r) { r.locked = isLocked(r.domain); });
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function update(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConfig, callback) {
|
||||
function update(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof fallbackCertificate, 'object');
|
||||
assert.strictEqual(typeof tlsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -276,12 +220,10 @@ function update(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsC
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or letsencrypt-*'));
|
||||
}
|
||||
|
||||
if (dnsConfig.hyphenatedSubdomains && !config.allowHyphenatedSubdomains()) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Not allowed in this edition'));
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
|
||||
verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, function (error, result) {
|
||||
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, result) {
|
||||
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
|
||||
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record:' + error.message));
|
||||
@@ -310,8 +252,6 @@ function del(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (domain === config.adminDomain()) return callback(new DomainsError(DomainsError.IN_USE));
|
||||
|
||||
domaindb.del(domain, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error && error.reason === DatabaseError.IN_USE) return callback(new DomainsError(DomainsError.IN_USE));
|
||||
@@ -321,8 +261,7 @@ function del(domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// returns the 'name' that needs to be inserted into zone
|
||||
function getName(domain, subdomain, type) {
|
||||
function getName(domain, subdomain) {
|
||||
// support special caas domains
|
||||
if (domain.provider === 'caas') return subdomain;
|
||||
|
||||
@@ -330,13 +269,7 @@ function getName(domain, subdomain, type) {
|
||||
|
||||
var part = domain.domain.slice(0, -domain.zoneName.length - 1);
|
||||
|
||||
if (subdomain === '') {
|
||||
return part;
|
||||
} else if (type === 'TXT') {
|
||||
return `${subdomain}.${part}`;
|
||||
} else {
|
||||
return subdomain + (domain.config.hyphenatedSubdomains ? '-' : '.') + part;
|
||||
}
|
||||
return subdomain === '' ? part : subdomain + '.' + part;
|
||||
}
|
||||
|
||||
function getDnsRecords(subdomain, domain, type, callback) {
|
||||
@@ -348,7 +281,7 @@ function getDnsRecords(subdomain, domain, type, callback) {
|
||||
get(domain, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain, type), type, function (error, values) {
|
||||
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain), type, function (error, values) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, values);
|
||||
@@ -368,7 +301,7 @@ function upsertDnsRecords(subdomain, domain, type, values, callback) {
|
||||
get(domain, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
|
||||
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain), type, values, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
@@ -388,7 +321,7 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
|
||||
get(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
|
||||
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain), type, values, function (error) {
|
||||
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(error);
|
||||
|
||||
callback(null);
|
||||
@@ -427,7 +360,7 @@ function setAdmin(domain, callback) {
|
||||
|
||||
config.setAdminDomain(result.domain);
|
||||
config.setAdminLocation('my');
|
||||
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
|
||||
config.setAdminFqdn('my' + (result.provider === 'caas' ? '-' : '.') + result.domain);
|
||||
|
||||
callback();
|
||||
|
||||
@@ -436,19 +369,19 @@ function setAdmin(domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function fqdn(location, domain, provider) {
|
||||
return location + (location ? (provider !== 'caas' ? '.' : '-') : '') + domain;
|
||||
}
|
||||
|
||||
// removes all fields that are strictly private and should never be returned by API calls
|
||||
function removePrivateFields(domain) {
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'locked');
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate');
|
||||
if (result.fallbackCertificate) delete result.fallbackCertificate.key; // do not return the 'key'. in caas, this is private
|
||||
return result;
|
||||
}
|
||||
|
||||
// removes all fields that are not accessible by a normal user
|
||||
function removeRestrictedFields(domain) {
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'locked');
|
||||
|
||||
// always ensure config object
|
||||
result.config = { hyphenatedSubdomains: !!domain.config.hyphenatedSubdomains };
|
||||
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+38
-30
@@ -10,6 +10,7 @@ var assert = require('assert'),
|
||||
apps = require('./apps.js'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:ldap'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
@@ -28,32 +29,37 @@ var NOOP = function () {};
|
||||
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
|
||||
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
|
||||
|
||||
// Will attach req.app if successful
|
||||
function authenticateApp(req, res, next) {
|
||||
function getAppByRequest(req, callback) {
|
||||
assert.strictEqual(typeof req, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var sourceIp = req.connection.ldap.id.split(':')[0];
|
||||
if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier'));
|
||||
if (sourceIp.split('.').length !== 4) return callback(new ldap.InsufficientAccessRightsError('Missing source identifier'));
|
||||
|
||||
apps.getByIpAddress(sourceIp, function (error, app) {
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
if (!app) return next(new ldap.OperationsError('Could not detect app source'));
|
||||
if (error) return callback(new ldap.OperationsError(error.message));
|
||||
|
||||
req.app = app;
|
||||
if (!app) return callback(new ldap.OperationsError('Could not detect app source'));
|
||||
|
||||
next();
|
||||
callback(null, app);
|
||||
});
|
||||
}
|
||||
|
||||
function getUsersWithAccessToApp(req, callback) {
|
||||
assert.strictEqual(typeof req.app, 'object');
|
||||
assert.strictEqual(typeof req, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
users.list(function (error, result) {
|
||||
if (error) return callback(new ldap.OperationsError(error.toString()));
|
||||
getAppByRequest(req, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.filter(result, apps.hasAccessTo.bind(null, req.app), function (error, allowedUsers) {
|
||||
users.list(function (error, result) {
|
||||
if (error) return callback(new ldap.OperationsError(error.toString()));
|
||||
|
||||
callback(null, allowedUsers);
|
||||
async.filter(result, apps.hasAccessTo.bind(null, app), function (error, allowedUsers) {
|
||||
if (error) return callback(new ldap.OperationsError(error.toString()));
|
||||
|
||||
callback(null, allowedUsers);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -134,7 +140,7 @@ function userSearch(req, res, next) {
|
||||
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
|
||||
|
||||
var groups = [ GROUP_USERS_DN ];
|
||||
if (entry.admin || req.app.ownerId === entry.id) groups.push(GROUP_ADMINS_DN);
|
||||
if (entry.admin) groups.push(GROUP_ADMINS_DN);
|
||||
|
||||
var displayName = entry.displayName || entry.username || ''; // displayName can be empty and username can be null
|
||||
var nameParts = displayName.split(' ');
|
||||
@@ -154,7 +160,7 @@ function userSearch(req, res, next) {
|
||||
givenName: firstName,
|
||||
username: entry.username,
|
||||
samaccountname: entry.username, // to support ActiveDirectory clients
|
||||
isadmin: (entry.admin || req.app.ownerId === entry.id) ? 1 : 0,
|
||||
isadmin: entry.admin ? 1 : 0,
|
||||
memberof: groups
|
||||
}
|
||||
};
|
||||
@@ -194,7 +200,7 @@ function groupSearch(req, res, next) {
|
||||
|
||||
groups.forEach(function (group) {
|
||||
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
|
||||
var members = group.admin ? result.filter(function (entry) { return entry.admin || req.app.ownerId === entry.id; }) : result;
|
||||
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
|
||||
|
||||
var obj = {
|
||||
dn: dn.toString(),
|
||||
@@ -243,7 +249,7 @@ function groupAdminsCompare(req, res, next) {
|
||||
// we only support memberuid here, if we add new group attributes later add them here
|
||||
if (req.attribute === 'memberuid') {
|
||||
var found = result.find(function (u) { return u.id === req.value; });
|
||||
if (found && (found.admin || req.app.ownerId == found.id)) return res.end(true);
|
||||
if (found && found.admin) return res.end(true);
|
||||
}
|
||||
|
||||
res.end(false);
|
||||
@@ -408,7 +414,6 @@ function mailingListSearch(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
// Will attach req.user if successful
|
||||
function authenticateUser(req, res, next) {
|
||||
debug('user bind: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
|
||||
|
||||
@@ -440,18 +445,21 @@ function authenticateUser(req, res, next) {
|
||||
}
|
||||
|
||||
function authorizeUserForApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
assert.strictEqual(typeof req.app, 'object');
|
||||
assert(req.user);
|
||||
|
||||
apps.hasAccessTo(req.app, req.user, function (error, result) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
getAppByRequest(req, function (error, app) {
|
||||
if (error) return next(error);
|
||||
|
||||
// we return no such object, to avoid leakage of a users existence
|
||||
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
apps.hasAccessTo(app, req.user, function (error, result) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id, app: req.app }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
|
||||
// we return no such object, to avoid leakage of a users existence
|
||||
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
res.end();
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id, app: app }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
|
||||
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -518,9 +526,9 @@ function start(callback) {
|
||||
|
||||
gServer = ldap.createServer({ log: logger });
|
||||
|
||||
gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch);
|
||||
gServer.search('ou=groups,dc=cloudron', authenticateApp, groupSearch);
|
||||
gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp);
|
||||
gServer.search('ou=users,dc=cloudron', userSearch);
|
||||
gServer.search('ou=groups,dc=cloudron', groupSearch);
|
||||
gServer.bind('ou=users,dc=cloudron', authenticateUser, authorizeUserForApp);
|
||||
|
||||
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
|
||||
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch);
|
||||
@@ -531,8 +539,8 @@ function start(callback) {
|
||||
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
|
||||
|
||||
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
|
||||
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
|
||||
gServer.compare('cn=users,ou=groups,dc=cloudron', groupUsersCompare);
|
||||
gServer.compare('cn=admins,ou=groups,dc=cloudron', groupAdminsCompare);
|
||||
|
||||
// this is the bind for addons (after bind, they might search and authenticate)
|
||||
gServer.bind('ou=addons,dc=cloudron', function(req, res /*, next */) {
|
||||
|
||||
+5
-22
@@ -278,15 +278,6 @@ function checkMx(domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function txtToDict(txt) {
|
||||
var dict = {};
|
||||
txt.split(';').forEach(function(v) {
|
||||
var p = v.trim().split('=');
|
||||
dict[p[0]]=p[1];
|
||||
});
|
||||
return dict;
|
||||
}
|
||||
|
||||
function checkDmarc(domain, callback) {
|
||||
var dmarc = {
|
||||
domain: '_dmarc.' + domain,
|
||||
@@ -302,9 +293,7 @@ function checkDmarc(domain, callback) {
|
||||
|
||||
if (txtRecords.length !== 0) {
|
||||
dmarc.value = txtRecords[0].join('');
|
||||
// allow extra fields in dmarc like rua
|
||||
const actual = txtToDict(dmarc.value), expected = txtToDict(dmarc.expected);
|
||||
dmarc.status = Object.keys(expected).every(k => expected[k] === actual[k]);
|
||||
dmarc.status = (dmarc.value === dmarc.expected);
|
||||
}
|
||||
|
||||
callback(null, dmarc);
|
||||
@@ -625,7 +614,7 @@ function txtRecordsWithSpf(domain, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) {
|
||||
if (error) return new MailError(MailError.EXTERNAL_ERROR, error.message);
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
|
||||
|
||||
@@ -741,14 +730,10 @@ function setDnsRecords(domain, callback) {
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
domains.upsertDnsRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
|
||||
}, function (error, changeIds) {
|
||||
if (error) {
|
||||
debug(`addDnsRecords: failed to update: ${error}`);
|
||||
return callback(new MailError(MailError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
|
||||
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
|
||||
|
||||
debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
|
||||
|
||||
callback(null);
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -776,8 +761,6 @@ function removeDomain(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (domain === config.adminDomain()) return callback(new MailError(MailError.IN_USE));
|
||||
|
||||
maildb.del(domain, function (error) {
|
||||
if (error && error.reason === DatabaseError.IN_USE) return callback(new MailError(MailError.IN_USE));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, error.message));
|
||||
|
||||
@@ -4,6 +4,15 @@ Dear <%= cloudronName %> Admin,
|
||||
|
||||
A new user with email <%= user.email %> was added to <%= cloudronName %>.
|
||||
|
||||
<% if (inviteLink) { %>
|
||||
As requested, this user has not been sent an invitation email.
|
||||
|
||||
To set a password and perform any configuration on behalf of the user, please use this link:
|
||||
<%= inviteLink %>
|
||||
|
||||
<% } %>
|
||||
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
<% } else { %>
|
||||
@@ -18,6 +27,14 @@ Powered by https://cloudron.io
|
||||
A new user with email <%= user.email %> was added to <%= cloudronName %>.
|
||||
</p>
|
||||
|
||||
<% if (inviteLink) { %>
|
||||
<p>
|
||||
As requested, this user has not been sent an invitation email.<br/>
|
||||
<br/>
|
||||
<a href="<%= inviteLink %>">Set a password and perform any configuration on behalf of the user</a>
|
||||
</p>
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
|
||||
+4
-2
@@ -226,10 +226,11 @@ function sendInvite(user, invitor) {
|
||||
});
|
||||
}
|
||||
|
||||
function userAdded(user) {
|
||||
function userAdded(user, inviteSent) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof inviteSent, 'boolean');
|
||||
|
||||
debug('Sending mail for userAdded');
|
||||
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
@@ -238,6 +239,7 @@ function userAdded(user) {
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
inviteLink: inviteSent ? null : `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
+38
-50
@@ -77,22 +77,22 @@ ReverseProxyError.INTERNAL_ERROR = 'Internal Error';
|
||||
ReverseProxyError.INVALID_CERT = 'Invalid certificate';
|
||||
ReverseProxyError.NOT_FOUND = 'Not Found';
|
||||
|
||||
function getApi(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
function getApi(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.get(domain, function (error, result) {
|
||||
domains.get(app.domain, function (error, domain) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
|
||||
if (domain.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
|
||||
|
||||
var api = result.tlsConfig.provider === 'caas' ? caas : acme;
|
||||
var api = domain.tlsConfig.provider === 'caas' ? caas : acme;
|
||||
|
||||
var options = { };
|
||||
if (result.tlsConfig.provider === 'caas') {
|
||||
if (domain.tlsConfig.provider === 'caas') {
|
||||
options.prod = true;
|
||||
} else { // acme
|
||||
options.prod = result.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
|
||||
options.prod = domain.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
|
||||
}
|
||||
|
||||
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
||||
@@ -127,6 +127,14 @@ function validateCertificate(domain, cert, key) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
|
||||
function matchesDomain(candidate) {
|
||||
if (typeof candidate !== 'string') return false;
|
||||
if (candidate === domain) return true;
|
||||
if (candidate.indexOf('*') === 0 && candidate.slice(2) === domain.slice(domain.indexOf('.') + 1)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for empty cert and key strings
|
||||
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
|
||||
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
|
||||
@@ -221,14 +229,12 @@ function getCertificate(app, callback) {
|
||||
return getFallbackCertificate(app.domain, callback);
|
||||
}
|
||||
|
||||
function ensureCertificate(appDomain, auditSource, callback) {
|
||||
assert.strictEqual(typeof appDomain, 'object');
|
||||
assert.strictEqual(typeof appDomain.fqdn, 'string');
|
||||
assert.strictEqual(typeof appDomain.domain, 'string');
|
||||
function ensureCertificate(app, auditSource, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const vhost = appDomain.fqdn;
|
||||
const vhost = app.fqdn;
|
||||
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
|
||||
@@ -250,7 +256,7 @@ function ensureCertificate(appDomain, auditSource, callback) {
|
||||
debug('ensureCertificate: %s cert does not exist', vhost);
|
||||
}
|
||||
|
||||
getApi(appDomain.domain, function (error, api, apiOptions) {
|
||||
getApi(app, function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
|
||||
@@ -266,14 +272,14 @@ function ensureCertificate(appDomain, auditSource, callback) {
|
||||
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: vhost, errorMessage: errorMessage });
|
||||
|
||||
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
|
||||
if (!certFilePath || !keyFilePath) return getFallbackCertificate(appDomain.domain, callback);
|
||||
if (!certFilePath || !keyFilePath) return getFallbackCertificate(app.domain, callback);
|
||||
|
||||
callback(null, { certFilePath, keyFilePath, reason: 'new-le' });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function writeAdminConfig(bundle, configFileName, vhost, callback) {
|
||||
function configureAdminInternal(bundle, configFileName, vhost, callback) {
|
||||
assert.strictEqual(typeof bundle, 'object');
|
||||
assert.strictEqual(typeof configFileName, 'string');
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
@@ -302,15 +308,15 @@ function configureAdmin(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var adminAppDomain = { domain: config.adminDomain(), fqdn: config.adminFqdn() };
|
||||
ensureCertificate(adminAppDomain, auditSource, function (error, bundle) {
|
||||
var adminApp = { domain: config.adminDomain(), fqdn: config.adminFqdn() };
|
||||
ensureCertificate(adminApp, auditSource, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
writeAdminConfig(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
|
||||
configureAdminInternal(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
|
||||
});
|
||||
}
|
||||
|
||||
function writeAppConfig(app, bundle, callback) {
|
||||
function configureAppInternal(app, bundle, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof bundle, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -343,7 +349,7 @@ function writeAppConfig(app, bundle, callback) {
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
function writeAppRedirectConfig(app, fqdn, bundle, callback) {
|
||||
function configureAppRedirect(app, fqdn, bundle, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof bundle, 'object');
|
||||
@@ -382,7 +388,7 @@ function configureApp(app, auditSource, callback) {
|
||||
ensureCertificate({ fqdn: app.fqdn, domain: app.domain }, auditSource, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
writeAppConfig(app, bundle, function (error) {
|
||||
configureAppInternal(app, bundle, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// now setup alternateDomain redirects if any
|
||||
@@ -392,7 +398,7 @@ function configureApp(app, auditSource, callback) {
|
||||
ensureCertificate({ fqdn: fqdn, domain: domain.domain }, auditSource, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
writeAppRedirectConfig(app, fqdn, bundle, callback);
|
||||
configureAppRedirect(app, fqdn, bundle, callback);
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
@@ -420,42 +426,24 @@ function renewAll(auditSource, callback) {
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var allDomains = [];
|
||||
allApps.push({ domain: config.adminDomain(), fqdn: config.adminFqdn() }); // inject fake webadmin app
|
||||
|
||||
// add webadmin domain
|
||||
allDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin' });
|
||||
|
||||
// add app main
|
||||
allApps.forEach(function (app) {
|
||||
allDomains.push({ domain: app.domain, fqdn: app.fqdn, type: 'main', app: app });
|
||||
|
||||
// and alternate domains
|
||||
app.alternateDomains.forEach(function (domain) {
|
||||
// TODO support hyphenated domains here as well
|
||||
var fqdn = (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain;
|
||||
|
||||
allDomains.push({ domain: domain.domain, fqdn: fqdn, type: 'alternate', app: app });
|
||||
});
|
||||
});
|
||||
|
||||
async.eachSeries(allDomains, function (domain, iteratorCallback) {
|
||||
ensureCertificate(domain, auditSource, function (error, bundle) {
|
||||
async.eachSeries(allApps, function (app, iteratorCallback) {
|
||||
ensureCertificate(app, auditSource, function (error, bundle) {
|
||||
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
|
||||
if (bundle.reason !== 'new-le' && bundle.reason !== 'fallback') return iteratorCallback();
|
||||
|
||||
// reconfigure for the case where we got a renewed cert after fallback
|
||||
var configureFunc;
|
||||
if (domain.type === 'webadmin') configureFunc = writeAdminConfig.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn());
|
||||
else if (domain.type === 'main') configureFunc = writeAppConfig.bind(null, domain.app, bundle);
|
||||
else if (domain.type === 'alternate') configureFunc = writeAppRedirectConfig.bind(null, domain.app, domain.fqdn, bundle);
|
||||
else return callback(new Error(`Unknown domain type for ${domain.fqdn}. This should never happen`));
|
||||
var configureFunc = app.fqdn === config.adminFqdn() ?
|
||||
configureAdminInternal.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
|
||||
: configureAppInternal.bind(null, app, bundle);
|
||||
|
||||
configureFunc(function (ignoredError) {
|
||||
if (ignoredError) debug('renewAll: error reconfiguring app', ignoredError);
|
||||
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
|
||||
|
||||
platform.handleCertChanged(domain.fqdn);
|
||||
platform.handleCertChanged(app.fqdn);
|
||||
|
||||
iteratorCallback(); // move to next domain
|
||||
iteratorCallback(); // move to next app
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -482,7 +470,7 @@ function configureDefaultServer(callback) {
|
||||
safe.child_process.execSync(certCommand);
|
||||
}
|
||||
|
||||
writeAdminConfig({ certFilePath, keyFilePath }, 'default.conf', '', function (error) {
|
||||
configureAdminInternal({ certFilePath, keyFilePath }, 'default.conf', '', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('configureDefaultServer: done');
|
||||
|
||||
@@ -5,10 +5,13 @@ exports = module.exports = {
|
||||
uninitialize: uninitialize,
|
||||
|
||||
scope: scope,
|
||||
websocketAuth: websocketAuth
|
||||
websocketAuth: websocketAuth,
|
||||
verifyAppOwnership: verifyAppOwnership
|
||||
};
|
||||
|
||||
var accesscontrol = require('../accesscontrol.js'),
|
||||
apps = require('../apps.js'),
|
||||
AppsError = apps.AppsError,
|
||||
assert = require('assert'),
|
||||
BasicStrategy = require('passport-http').BasicStrategy,
|
||||
BearerStrategy = require('passport-http-bearer').Strategy,
|
||||
@@ -18,6 +21,7 @@ var accesscontrol = require('../accesscontrol.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
LocalStrategy = require('passport-local').Strategy,
|
||||
passport = require('passport'),
|
||||
settings = require('../settings.js'),
|
||||
users = require('../users.js'),
|
||||
UsersError = users.UsersError;
|
||||
|
||||
@@ -138,3 +142,25 @@ function websocketAuth(requiredScopes, req, res, next) {
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function verifyAppOwnership(req, res, next) {
|
||||
if (req.user.admin) return next();
|
||||
|
||||
const appCreate = !('id' in req.params);
|
||||
|
||||
settings.getSpacesConfig(function (error, spaces) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (!spaces.enabled) return next();
|
||||
|
||||
if (appCreate) return next(); // ok to install app
|
||||
|
||||
apps.get(req.params.id, function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
if (app.ownerId !== req.user.id) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+3
-30
@@ -1,8 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
verifyOwnership: verifyOwnership,
|
||||
|
||||
getApp: getApp,
|
||||
getApps: getApps,
|
||||
getAppIcon: getAppIcon,
|
||||
@@ -32,7 +30,6 @@ exports = module.exports = {
|
||||
var apps = require('../apps.js'),
|
||||
AppsError = apps.AppsError,
|
||||
assert = require('assert'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:routes/apps'),
|
||||
fs = require('fs'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
@@ -47,25 +44,6 @@ function auditSource(req) {
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function verifyOwnership(req, res, next) {
|
||||
if (req.user.admin) return next();
|
||||
|
||||
if (!config.isSpacesEnabled()) return next();
|
||||
|
||||
const appCreate = !('id' in req.params);
|
||||
|
||||
if (appCreate) return next(); // ok to install app
|
||||
|
||||
apps.get(req.params.id, function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
if (app.ownerId !== req.user.id) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function getApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
@@ -139,14 +117,9 @@ function installApp(req, res, next) {
|
||||
|
||||
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
|
||||
|
||||
if ('alternateDomains' in data) {
|
||||
if (!Array.isArray(data.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array'));
|
||||
if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
|
||||
}
|
||||
|
||||
debug('Installing app :%j', data);
|
||||
|
||||
apps.install(data, req.user, auditSource(req), function (error, app) {
|
||||
apps.install(data, auditSource(req), function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
@@ -196,7 +169,7 @@ function configureApp(req, res, next) {
|
||||
|
||||
debug('Configuring app id:%s data:%j', req.params.id, data);
|
||||
|
||||
apps.configure(req.params.id, data, req.user, auditSource(req), function (error) {
|
||||
apps.configure(req.params.id, data, auditSource(req), function (error) {
|
||||
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
@@ -246,7 +219,7 @@ function cloneApp(req, res, next) {
|
||||
if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required'));
|
||||
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
|
||||
apps.clone(req.params.id, data, req.user, auditSource(req), function (error, result) {
|
||||
apps.clone(req.params.id, data, auditSource(req), function (error, result) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
|
||||
@@ -80,9 +80,8 @@ function addToken(req, res, next) {
|
||||
var data = req.body;
|
||||
var expiresAt = data.expiresAt ? parseInt(data.expiresAt, 10) : Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
if (isNaN(expiresAt) || expiresAt <= Date.now()) return next(new HttpError(400, 'expiresAt must be a timestamp in the future'));
|
||||
if ('name' in req.body && typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
|
||||
|
||||
clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, { name: req.body.name || '' }, function (error, result) {
|
||||
clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, function (error, result) {
|
||||
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(201, { token: result }));
|
||||
|
||||
@@ -24,8 +24,7 @@ function login(req, res, next) {
|
||||
if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
|
||||
}
|
||||
|
||||
const auditSource = { authType: 'cli', ip: ip };
|
||||
clients.issueDeveloperToken(user, auditSource, function (error, result) {
|
||||
clients.issueDeveloperToken(user, ip, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, result));
|
||||
|
||||
+1
-11
@@ -5,9 +5,7 @@ exports = module.exports = {
|
||||
get: get,
|
||||
getAll: getAll,
|
||||
update: update,
|
||||
del: del,
|
||||
|
||||
verifyDomainLock: verifyDomainLock
|
||||
del: del
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -16,14 +14,6 @@ var assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function verifyDomainLock(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
if (domains.isLocked(req.params.domain)) return next(new HttpError(423, 'This domain is locked'));
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
function add(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
|
||||
@@ -88,7 +88,6 @@ function setDnsRecords(req, res, next) {
|
||||
|
||||
mail.setDnsRecords(req.params.domain, function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === MailError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(201));
|
||||
|
||||
+5
-12
@@ -91,7 +91,7 @@ function initialize() {
|
||||
authcodedb.del(code, function (error) {
|
||||
if(error) return callback(error);
|
||||
|
||||
clients.addTokenByUserId(client.id, authCode.userId, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||
clients.addTokenByUserId(client.id, authCode.userId, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('exchange: new access token for client %s user %s token %s', client.id, authCode.userId, result.accessToken.slice(0, 6)); // partial token for security
|
||||
@@ -104,7 +104,7 @@ function initialize() {
|
||||
|
||||
// implicit token grant that skips issuing auth codes. this is used by our webadmin
|
||||
gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client, user, ares, callback) {
|
||||
clients.addTokenByUserId(client.id, user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||
clients.addTokenByUserId(client.id, user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('grant token: new access token for client %s user %s token %s', client.id, user.id, result.accessToken.slice(0, 6)); // partial token for security
|
||||
@@ -362,13 +362,10 @@ function accountSetup(req, res, next) {
|
||||
// setPassword clears the resetToken
|
||||
users.setPassword(userObject.id, req.body.password, function (error) {
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
|
||||
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
|
||||
});
|
||||
res.redirect(config.adminOrigin());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -412,11 +409,7 @@ function passwordReset(req, res, next) {
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(406, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
|
||||
});
|
||||
res.redirect(config.adminOrigin());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+24
-2
@@ -23,7 +23,10 @@ exports = module.exports = {
|
||||
setAppstoreConfig: setAppstoreConfig,
|
||||
|
||||
getPlatformConfig: getPlatformConfig,
|
||||
setPlatformConfig: setPlatformConfig
|
||||
setPlatformConfig: setPlatformConfig,
|
||||
|
||||
setSpacesConfig: setSpacesConfig,
|
||||
getSpacesConfig: getSpacesConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -155,7 +158,6 @@ function setBackupConfig(req, res, next) {
|
||||
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
|
||||
if (typeof req.body.intervalSecs !== 'number') return next(new HttpError(400, 'intervalSecs is required'));
|
||||
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
if ('syncConcurrency' in req.body) {
|
||||
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
|
||||
@@ -204,6 +206,26 @@ function setPlatformConfig(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function getSpacesConfig(req, res, next) {
|
||||
settings.getSpacesConfig(function (error, config) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, config));
|
||||
});
|
||||
}
|
||||
|
||||
function setSpacesConfig(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
settings.setSpacesConfig(req.body, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getAppstoreConfig(req, res, next) {
|
||||
settings.getAppstoreConfig(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -216,7 +216,7 @@ function startBox(done) {
|
||||
token_1 = tokendb.generateToken();
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, user_1_id, 'test-client-id', Date.now() + 1000000, accesscontrol.SCOPE_APPS_READ, '', callback);
|
||||
tokendb.add(token_1, user_1_id, 'test-client-id', Date.now() + 1000000, accesscontrol.SCOPE_ANY, callback);
|
||||
},
|
||||
|
||||
function (callback) {
|
||||
@@ -359,7 +359,7 @@ describe('App API', function () {
|
||||
.send({ manifest: APP_MANIFEST, location: 'my', accessRestriction: null, domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.contain('my is reserved');
|
||||
expect(res.body.message).to.eql('my is reserved');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -370,7 +370,7 @@ describe('App API', function () {
|
||||
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null, domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.contain(constants.API_LOCATION + ' is reserved');
|
||||
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -381,7 +381,7 @@ describe('App API', function () {
|
||||
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: 23, accessRestriction: null, domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.contain('portBindings must be an object');
|
||||
expect(res.body.message).to.eql('portBindings must be an object');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -392,7 +392,7 @@ describe('App API', function () {
|
||||
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.contain('accessRestriction is required');
|
||||
expect(res.body.message).to.eql('accessRestriction is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -403,7 +403,7 @@ describe('App API', function () {
|
||||
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, accessRestriction: '', domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.contain('accessRestriction is required');
|
||||
expect(res.body.message).to.eql('accessRestriction is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -598,11 +598,11 @@ describe('App API', function () {
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
|
||||
expect(result.body.accessToken).to.be.a('string');
|
||||
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
|
||||
expect(result.body.token).to.be.a('string');
|
||||
|
||||
// overwrite non dev token
|
||||
token = result.body.accessToken;
|
||||
token = result.body.token;
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
@@ -630,7 +630,6 @@ describe('App installation', function () {
|
||||
this.timeout(100000);
|
||||
|
||||
var apiHockInstance = hock.createHock({ throwOnUnmatched: false });
|
||||
var apiHockServer;
|
||||
|
||||
var validCert1, validKey1;
|
||||
|
||||
@@ -700,7 +699,7 @@ describe('App installation', function () {
|
||||
});
|
||||
});
|
||||
|
||||
xit('installation - image created', function (done) {
|
||||
it('installation - image created', function (done) {
|
||||
expect(imageCreated).to.be.ok();
|
||||
done();
|
||||
});
|
||||
@@ -916,7 +915,7 @@ describe('App installation', function () {
|
||||
if (!err || err.code !== 'ECONNREFUSED') return setTimeout(waitForAppToDie, 500);
|
||||
|
||||
// wait for app status to be updated
|
||||
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID).query({ access_token: token }).end(function (error, result) {
|
||||
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID).query({ access_token: token_1 }).end(function (error, result) {
|
||||
if (error || result.statusCode !== 200 || result.body.runState !== 'stopped') return setTimeout(waitForAppToDie, 500);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -167,7 +167,7 @@ describe('Cloudron', function () {
|
||||
userId_1 = result.body.id;
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, 'cloudron', '', callback);
|
||||
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, 'cloudron', callback);
|
||||
});
|
||||
}
|
||||
], done);
|
||||
|
||||
@@ -63,7 +63,7 @@ function setup(done) {
|
||||
token_1 = tokendb.generateToken();
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, '', callback);
|
||||
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, callback);
|
||||
}
|
||||
|
||||
], done);
|
||||
@@ -141,7 +141,7 @@ describe('Eventlog API', function () {
|
||||
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.eventlogs.length).to.equal(2);
|
||||
expect(result.body.eventlogs.length).to.equal(1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -70,7 +70,7 @@ function setup(done) {
|
||||
userId_1 = result.body.id;
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, '', callback);
|
||||
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, callback);
|
||||
});
|
||||
}
|
||||
], done);
|
||||
|
||||
@@ -408,25 +408,6 @@ describe('Mail API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with modified DMARC1 values', function (done) {
|
||||
clearDnsAnswerQueue();
|
||||
|
||||
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC1; p=reject; rua=mailto:rua@example.com; pct=100']];
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
|
||||
expect(res.body.dns.dmarc).to.be.an('object');
|
||||
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
|
||||
expect(res.body.dns.dmarc.status).to.eql(true);
|
||||
expect(res.body.dns.dmarc.value).to.eql('v=DMARC1; p=reject; rua=mailto:rua@example.com; pct=100');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with all correct records', function (done) {
|
||||
clearDnsAnswerQueue();
|
||||
|
||||
|
||||
@@ -1481,11 +1481,9 @@ describe('Password', function () {
|
||||
it('succeeds', function (done) {
|
||||
var scope = nock(config.adminOrigin())
|
||||
.filteringPath(function (path) {
|
||||
path = path.replace(/accessToken=[^&]*/, 'accessToken=token');
|
||||
path = path.replace(/expiresAt=[^&]*/, 'expiresAt=1234');
|
||||
return path;
|
||||
})
|
||||
.get('/?accessToken=token&expiresAt=1234').reply(200, {});
|
||||
.get('/').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: '12345678', email: USER_0.email, resetToken: USER_0.resetToken })
|
||||
|
||||
@@ -115,7 +115,7 @@ describe('Profile API', function () {
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() - 2000; // 1 sec
|
||||
|
||||
tokendb.add(token, user_0.id, null, expires, 'profile', 'tokenname', function (error) {
|
||||
tokendb.add(token, user_0.id, null, expires, 'profile', function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
var accesscontrol = require('../../accesscontrol.js'),
|
||||
async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
constants = require('../../constants.js'),
|
||||
database = require('../../database.js'),
|
||||
domains = require('../../domains.js'),
|
||||
tokendb = require('../../tokendb.js'),
|
||||
@@ -16,8 +17,7 @@ var accesscontrol = require('../../accesscontrol.js'),
|
||||
mail = require('../../mail.js'),
|
||||
mailer = require('../../mailer.js'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
users = require('../../users.js');
|
||||
server = require('../../server.js');
|
||||
|
||||
const SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('Users API', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
var user_0, user_1, user_2, user_4;
|
||||
var token = null, userToken = null;
|
||||
var token = null;
|
||||
var token_1 = tokendb.generateToken();
|
||||
|
||||
before(setup);
|
||||
@@ -176,7 +176,7 @@ describe('Users API', function () {
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() + 2000; // 1 sec
|
||||
|
||||
tokendb.add(token, user_0.id, null, expires, accesscontrol.SCOPE_PROFILE, 'tokenname', function (error) {
|
||||
tokendb.add(token, user_0.id, null, expires, accesscontrol.SCOPE_PROFILE, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
setTimeout(function () {
|
||||
@@ -266,9 +266,11 @@ describe('Users API', function () {
|
||||
});
|
||||
|
||||
it('cannot create user without email', function (done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1 })
|
||||
.send({ username: USERNAME_1, invite: true })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
@@ -277,63 +279,46 @@ describe('Users API', function () {
|
||||
});
|
||||
|
||||
it('create second user succeeds', function (done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1 })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1, invite: true })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
|
||||
user_1 = result.body;
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, user_1.id, 'test-client-id', Date.now() + 10000, accesscontrol.SCOPE_PROFILE, 'fromtest', done);
|
||||
checkMails(2, function () {
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, user_1.id, 'test-client-id', Date.now() + 10000, accesscontrol.SCOPE_PROFILE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('reinvite unknown user fails', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1+USERNAME_1 + '/create_invite')
|
||||
mailer._clearMailQueue();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1+USERNAME_1 + '/invite')
|
||||
.query({ access_token: token })
|
||||
.send({})
|
||||
.end(function (err, res) {
|
||||
expect(err).to.be.an(Error);
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done();
|
||||
checkMails(0, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('send invite without creating invite fails succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/send_invite')
|
||||
.query({ access_token: token })
|
||||
.send({})
|
||||
.end(function (err, res) {
|
||||
expect(err).to.be.an(Error);
|
||||
expect(res.statusCode).to.equal(409);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('reinvite second user succeeds', function (done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
it('create invite second user succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/create_invite')
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/invite')
|
||||
.query({ access_token: token })
|
||||
.send({})
|
||||
.end(function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.resetToken).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can send invite', function (done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/send_invite')
|
||||
.query({ access_token: token })
|
||||
.send({})
|
||||
.end(function (err, res) {
|
||||
expect(err).to.be(null);
|
||||
expect(res.statusCode).to.equal(200);
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
@@ -399,12 +384,12 @@ describe('Users API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('create user missing username succeeds', function (done) {
|
||||
it('create user missing username fails', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ email: `unnamed${EMAIL_2}` })
|
||||
.send({ email: EMAIL_2 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -419,6 +404,16 @@ describe('Users API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('create user missing invite fails', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2, email: EMAIL_2 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create user reserved name fails', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
@@ -441,9 +436,10 @@ describe('Users API', function () {
|
||||
|
||||
it('create second and third user', function (done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2, email: EMAIL_2 })
|
||||
.send({ username: USERNAME_2, email: EMAIL_2, invite: false })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
|
||||
@@ -451,12 +447,12 @@ describe('Users API', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_3, email: EMAIL_3 })
|
||||
.send({ username: USERNAME_3, email: EMAIL_3, invite: true })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
|
||||
// two mails for user creation
|
||||
checkMails(2, done);
|
||||
// one mail for first user creation, two mails for second user creation (see 'invite' flag)
|
||||
checkMails(3, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -500,13 +496,13 @@ describe('Users API', function () {
|
||||
expect(error).to.be(null);
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.users).to.be.an('array');
|
||||
expect(res.body.users.length).to.equal(5);
|
||||
expect(res.body.users.length).to.equal(4);
|
||||
|
||||
res.body.users.forEach(function (user) {
|
||||
expect(user).to.be.an('object');
|
||||
expect(user.id).to.be.ok();
|
||||
expect(user.username).to.be.ok();
|
||||
expect(user.email).to.be.ok();
|
||||
if (!user.email.startsWith('unnamed')) expect(user.username).to.be.ok();
|
||||
expect(user.password).to.not.be.ok();
|
||||
expect(user.salt).to.not.be.ok();
|
||||
expect(user.groupIds).to.not.be.ok();
|
||||
@@ -680,7 +676,7 @@ describe('Users API', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_4, email: EMAIL_4, password: 'tooweak' })
|
||||
.send({ username: USERNAME_4, email: EMAIL_4, invite: false, password: 'tooweak' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
@@ -691,23 +687,23 @@ describe('Users API', function () {
|
||||
it('can create user with a password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_4, email: EMAIL_4, password: 'Secret1#' })
|
||||
.send({ username: USERNAME_4, email: EMAIL_4, invite: false, password: 'Secret1#' })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(201);
|
||||
|
||||
user_4 = result.body;
|
||||
|
||||
userToken = tokendb.generateToken();
|
||||
token = tokendb.generateToken();
|
||||
var expires = Date.now() + 2000; // 1 sec
|
||||
|
||||
tokendb.add(userToken, user_4.id, null, expires, accesscontrol.SCOPE_PROFILE, '', done);
|
||||
tokendb.add(token, user_4.id, null, expires, accesscontrol.SCOPE_PROFILE, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('can get profile of user with pre-set password', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: userToken })
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
|
||||
@@ -716,42 +712,5 @@ describe('Users API', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// Change password
|
||||
it('change password fails due to missing token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/password')
|
||||
.send({ password: 'youdontsay' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change password fails due to small password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: 'small' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change password succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: 'bigenough' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('did change the user password', function (done) {
|
||||
users.verify(user_0.id, 'bigenough', function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+7
-43
@@ -6,17 +6,13 @@ exports = module.exports = {
|
||||
list: list,
|
||||
create: create,
|
||||
remove: remove,
|
||||
changePassword: changePassword,
|
||||
verifyPassword: verifyPassword,
|
||||
createInvite: createInvite,
|
||||
sendInvite: sendInvite,
|
||||
setGroups: setGroups,
|
||||
transferOwnership: transferOwnership,
|
||||
verifyOperator: verifyOperator
|
||||
transferOwnership: transferOwnership
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('../config.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
users = require('../users.js'),
|
||||
@@ -27,27 +23,22 @@ function auditSource(req) {
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function verifyOperator(req, res, next) {
|
||||
if (config.allowOperatorActions()) return next();
|
||||
|
||||
next(new HttpError(401, 'Not allowed in this edition'));
|
||||
}
|
||||
|
||||
function create(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
|
||||
if (typeof req.body.invite !== 'boolean') return next(new HttpError(400, 'invite must be boolean'));
|
||||
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
|
||||
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
|
||||
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
|
||||
if ('admin' in req.body && typeof req.body.admin !== 'boolean') return next(new HttpError(400, 'admin flag must be a boolean'));
|
||||
|
||||
var password = req.body.password || null;
|
||||
var email = req.body.email;
|
||||
var sendInvite = req.body.invite;
|
||||
var username = 'username' in req.body ? req.body.username : null;
|
||||
var displayName = req.body.displayName || '';
|
||||
|
||||
users.create(username, password, email, displayName, { invitor: req.user, admin: req.body.admin }, auditSource(req), function (error, user) {
|
||||
users.create(username, password, email, displayName, { invitor: req.user, sendInvite: sendInvite }, auditSource(req), function (error, user) {
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -146,26 +137,14 @@ function verifyPassword(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function createInvite(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.userId, 'string');
|
||||
|
||||
users.createInvite(req.params.userId, function (error, resetToken) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { resetToken: resetToken }));
|
||||
});
|
||||
}
|
||||
|
||||
function sendInvite(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.userId, 'string');
|
||||
|
||||
users.sendInvite(req.params.userId, { invitor: req.user }, function (error) {
|
||||
users.sendInvite(req.params.userId, { invitor: req.user }, function (error, result) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(409, 'Call createInvite API first'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { }));
|
||||
next(new HttpSuccess(200, { resetToken: result }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -195,19 +174,4 @@ function transferOwnership(req, res, next) {
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function changePassword(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.userId, 'string');
|
||||
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
|
||||
|
||||
users.setPassword(req.params.userId, req.body.password, function (error) {
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
}
|
||||
+19
-22
@@ -94,7 +94,7 @@ function initializeExpressSync() {
|
||||
var usersReadScope = routes.accesscontrol.scope(accesscontrol.SCOPE_USERS_READ);
|
||||
var usersManageScope = routes.accesscontrol.scope(accesscontrol.SCOPE_USERS_MANAGE);
|
||||
var appsReadScope = routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_READ);
|
||||
var appsManageScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_MANAGE), routes.apps.verifyOwnership ];
|
||||
var appsManageScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_MANAGE), routes.accesscontrol.verifyAppOwnership ];
|
||||
var settingsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_SETTINGS);
|
||||
var mailScope = routes.accesscontrol.scope(accesscontrol.SCOPE_MAIL);
|
||||
var clientsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_CLIENTS);
|
||||
@@ -102,9 +102,6 @@ function initializeExpressSync() {
|
||||
var domainsManageScope = routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_MANAGE);
|
||||
var appstoreScope = routes.accesscontrol.scope(accesscontrol.SCOPE_APPSTORE);
|
||||
|
||||
const verifyOperator = routes.users.verifyOperator;
|
||||
const verifyDomainLock = routes.domains.verifyDomainLock;
|
||||
|
||||
// csrf protection
|
||||
var csrf = routes.oauth2.csrf();
|
||||
|
||||
@@ -129,10 +126,10 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/cloudron/disks', cloudronScope, routes.cloudron.getDisks);
|
||||
router.get ('/api/v1/cloudron/logs/:unit', cloudronScope, routes.cloudron.getLogs);
|
||||
router.get ('/api/v1/cloudron/logstream/:unit', cloudronScope, routes.cloudron.getLogStream);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, verifyOperator, routes.ssh.getAuthorizedKeys);
|
||||
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, verifyOperator, routes.ssh.addAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, verifyOperator, routes.ssh.getAuthorizedKey);
|
||||
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, verifyOperator, routes.ssh.delAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.ssh.getAuthorizedKeys);
|
||||
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.ssh.addAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.ssh.getAuthorizedKey);
|
||||
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.ssh.delAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.get);
|
||||
|
||||
// config route (for dashboard)
|
||||
@@ -152,10 +149,8 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/users/:userId', usersManageScope, routes.users.get); // this is manage scope because it returns non-restricted fields
|
||||
router.del ('/api/v1/users/:userId', usersManageScope, routes.users.verifyPassword, routes.users.remove);
|
||||
router.post('/api/v1/users/:userId', usersManageScope, routes.users.update);
|
||||
router.post('/api/v1/users/:userId/password', usersManageScope, routes.users.changePassword);
|
||||
router.put ('/api/v1/users/:userId/groups', usersManageScope, routes.users.setGroups);
|
||||
router.post('/api/v1/users/:userId/send_invite', usersManageScope, routes.users.sendInvite);
|
||||
router.post('/api/v1/users/:userId/create_invite', usersManageScope, routes.users.createInvite);
|
||||
router.post('/api/v1/users/:userId/invite', usersManageScope, routes.users.sendInvite);
|
||||
router.post('/api/v1/users/:userId/transfer', usersManageScope, routes.users.transferOwnership);
|
||||
|
||||
// Group management
|
||||
@@ -212,7 +207,7 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/apps/:id/logs', appsManageScope, routes.apps.getLogs);
|
||||
router.get ('/api/v1/apps/:id/exec', appsManageScope, routes.apps.exec);
|
||||
// websocket cannot do bearer authentication
|
||||
router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, [ accesscontrol.SCOPE_APPS_MANAGE ]), routes.apps.verifyOwnership, routes.apps.execWebSocket);
|
||||
router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, [ accesscontrol.SCOPE_APPS_MANAGE ]), routes.accesscontrol.verifyAppOwnership, routes.apps.execWebSocket);
|
||||
router.post('/api/v1/apps/:id/clone', appsManageScope, routes.apps.cloneApp);
|
||||
router.get ('/api/v1/apps/:id/download', appsManageScope, routes.apps.downloadFile);
|
||||
router.post('/api/v1/apps/:id/upload', appsManageScope, multipart, routes.apps.uploadFile);
|
||||
@@ -227,15 +222,17 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.settings.setCloudronName);
|
||||
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.settings.getCloudronAvatar);
|
||||
router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar);
|
||||
router.get ('/api/v1/settings/backup_config', settingsScope, verifyOperator, routes.settings.getBackupConfig);
|
||||
router.post('/api/v1/settings/backup_config', settingsScope, verifyOperator, routes.settings.setBackupConfig);
|
||||
router.get ('/api/v1/settings/platform_config', settingsScope, verifyOperator, routes.settings.getPlatformConfig);
|
||||
router.post('/api/v1/settings/platform_config', settingsScope, verifyOperator, routes.settings.setPlatformConfig);
|
||||
router.get ('/api/v1/settings/backup_config', settingsScope, routes.settings.getBackupConfig);
|
||||
router.post('/api/v1/settings/backup_config', settingsScope, routes.settings.setBackupConfig);
|
||||
router.get ('/api/v1/settings/platform_config', settingsScope, routes.settings.getPlatformConfig);
|
||||
router.post('/api/v1/settings/platform_config', settingsScope, routes.settings.setPlatformConfig);
|
||||
router.get ('/api/v1/settings/spaces_config', settingsScope, routes.settings.getSpacesConfig);
|
||||
router.post('/api/v1/settings/spaces_config', settingsScope, routes.settings.setSpacesConfig);
|
||||
|
||||
router.get ('/api/v1/settings/time_zone', settingsScope, routes.settings.getTimeZone);
|
||||
router.post('/api/v1/settings/time_zone', settingsScope, routes.settings.setTimeZone);
|
||||
router.get ('/api/v1/settings/appstore_config', appstoreScope, verifyOperator, routes.settings.getAppstoreConfig);
|
||||
router.post('/api/v1/settings/appstore_config', appstoreScope, verifyOperator, routes.settings.setAppstoreConfig);
|
||||
router.get ('/api/v1/settings/appstore_config', appstoreScope, routes.settings.getAppstoreConfig);
|
||||
router.post('/api/v1/settings/appstore_config', appstoreScope, routes.settings.setAppstoreConfig);
|
||||
|
||||
// email routes
|
||||
router.get ('/api/v1/mail/:domain', mailScope, routes.mail.getDomain);
|
||||
@@ -264,7 +261,7 @@ function initializeExpressSync() {
|
||||
router.del ('/api/v1/mail/:domain/lists/:name', mailScope, routes.mail.removeList);
|
||||
|
||||
// feedback
|
||||
router.post('/api/v1/feedback', cloudronScope, verifyOperator, routes.cloudron.feedback);
|
||||
router.post('/api/v1/feedback', cloudronScope, routes.cloudron.feedback);
|
||||
|
||||
// backup routes
|
||||
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
|
||||
@@ -273,9 +270,9 @@ function initializeExpressSync() {
|
||||
// domain routes
|
||||
router.post('/api/v1/domains', domainsManageScope, routes.domains.add);
|
||||
router.get ('/api/v1/domains', domainsReadScope, routes.domains.getAll);
|
||||
router.get ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.get); // this is manage scope because it returns non-restricted fields
|
||||
router.put ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.update);
|
||||
router.del ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.users.verifyPassword, routes.domains.del);
|
||||
router.get ('/api/v1/domains/:domain', domainsManageScope, routes.domains.get); // this is manage scope because it returns non-restricted fields
|
||||
router.put ('/api/v1/domains/:domain', domainsManageScope, routes.domains.update);
|
||||
router.del ('/api/v1/domains/:domain', domainsManageScope, routes.users.verifyPassword, routes.domains.del);
|
||||
|
||||
// caas routes
|
||||
router.get('/api/v1/caas/config', cloudronScope, routes.caas.getConfig);
|
||||
|
||||
+33
-3
@@ -38,6 +38,9 @@ exports = module.exports = {
|
||||
getPlatformConfig: getPlatformConfig,
|
||||
setPlatformConfig: setPlatformConfig,
|
||||
|
||||
getSpacesConfig: getSpacesConfig,
|
||||
setSpacesConfig: setSpacesConfig,
|
||||
|
||||
getAll: getAll,
|
||||
|
||||
// booleans. if you add an entry here, be sure to fix getAll
|
||||
@@ -50,6 +53,7 @@ exports = module.exports = {
|
||||
APPSTORE_CONFIG_KEY: 'appstore_config',
|
||||
CAAS_CONFIG_KEY: 'caas_config',
|
||||
PLATFORM_CONFIG_KEY: 'platform_config',
|
||||
SPACES_CONFIG_KEY: 'spaces_config',
|
||||
|
||||
// strings
|
||||
APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern',
|
||||
@@ -86,14 +90,14 @@ var gDefaults = (function () {
|
||||
provider: 'filesystem',
|
||||
key: '',
|
||||
backupFolder: '/var/backups',
|
||||
retentionSecs: 2 * 24 * 60 * 60, // 2 days
|
||||
intervalSecs: 24 * 60 * 60 // ~1 day
|
||||
retentionSecs: 172800
|
||||
};
|
||||
result[exports.UPDATE_CONFIG_KEY] = { prerelease: false };
|
||||
result[exports.APPSTORE_CONFIG_KEY] = {};
|
||||
result[exports.CAAS_CONFIG_KEY] = {};
|
||||
result[exports.EMAIL_DIGEST] = true;
|
||||
result[exports.PLATFORM_CONFIG_KEY] = {};
|
||||
result[exports.SPACES_CONFIG_KEY] = { enabled: false };
|
||||
|
||||
return result;
|
||||
})();
|
||||
@@ -353,6 +357,32 @@ function setEmailDigest(enabled, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getSpacesConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.SPACES_CONFIG_KEY, function (error, value) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.SPACES_CONFIG_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, JSON.parse(value));
|
||||
});
|
||||
}
|
||||
|
||||
function setSpacesConfig(value, callback) {
|
||||
assert.strictEqual(typeof value, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if ('enabled' in value && typeof value.enabled !== 'boolean') return callback(new SettingsError(SettingsError.BAD_FIELD, 'enabled must be a boolean'));
|
||||
|
||||
settingsdb.set(exports.SPACES_CONFIG_KEY, JSON.stringify(value), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.SPACES_CONFIG_KEY, value);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getCaasConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -475,7 +505,7 @@ function getAll(callback) {
|
||||
result[exports.DYNAMIC_DNS_KEY] = !!result[exports.DYNAMIC_DNS_KEY];
|
||||
|
||||
// convert JSON objects
|
||||
[exports.BACKUP_CONFIG_KEY, exports.UPDATE_CONFIG_KEY, exports.APPSTORE_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY ].forEach(function (key) {
|
||||
[exports.BACKUP_CONFIG_KEY, exports.UPDATE_CONFIG_KEY, exports.APPSTORE_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY, exports.SPACES_CONFIG_KEY ].forEach(function (key) {
|
||||
result[key] = typeof result[key] === 'object' ? result[key] : safe.JSON.parse(result[key]);
|
||||
});
|
||||
|
||||
|
||||
+1
-2
@@ -247,7 +247,7 @@ function activate(username, password, email, displayName, ip, auditSource, callb
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
|
||||
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) {
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
|
||||
@@ -322,7 +322,6 @@ function getStatus(callback) {
|
||||
cloudronName: cloudronName,
|
||||
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
|
||||
activated: count !== 0,
|
||||
edition: config.edition(),
|
||||
webadminStatus: gWebadminStatus // only valid when !activated
|
||||
});
|
||||
});
|
||||
|
||||
+50
-12
@@ -162,9 +162,9 @@ describe('Apps', function () {
|
||||
groupdb.add.bind(null, GROUP_0.id, GROUP_0.name),
|
||||
groupdb.add.bind(null, GROUP_1.id, GROUP_1.name),
|
||||
groups.addMember.bind(null, GROUP_0.id, USER_1.id),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0),
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.domain, APP_1.ownerId, apps._translatePortBindings(APP_1.portBindings, APP_1.manifest), APP_1),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.domain, APP_2.ownerId, apps._translatePortBindings(APP_2.portBindings, APP_2.manifest), APP_2),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0),
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.domain, APP_1.ownerId, APP_1.portBindings, APP_1),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.domain, APP_2.ownerId, APP_2.portBindings, APP_2),
|
||||
settingsdb.set.bind(null, settings.BACKUP_CONFIG_KEY, JSON.stringify({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }))
|
||||
], done);
|
||||
});
|
||||
@@ -176,28 +176,66 @@ describe('Apps', function () {
|
||||
], done);
|
||||
});
|
||||
|
||||
describe('validateHostname', function () {
|
||||
it('does not allow admin subdomain', function () {
|
||||
expect(apps._validateHostname('my', DOMAIN_0.domain, 'my.' + DOMAIN_0.domain)).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('cannot have >63 length subdomains', function () {
|
||||
var s = Array(64).fill('s').join('');
|
||||
expect(apps._validateHostname(s, 'example.com', s + '.example.com')).to.be.an(Error);
|
||||
expect(apps._validateHostname(`dev.${s}`, 'example.com', `dev.${s}.example.com`)).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows only alphanumerics and hypen', function () {
|
||||
expect(apps._validateHostname('#2r', 'example.com', '#2r.example.com')).to.be.an(Error);
|
||||
expect(apps._validateHostname('a%b', 'example.com', 'a%b.example.com')).to.be.an(Error);
|
||||
expect(apps._validateHostname('ab_', 'example.com', 'ab_.example.com')).to.be.an(Error);
|
||||
expect(apps._validateHostname('ab.', 'example.com', 'ab.example.com')).to.be.an(Error);
|
||||
expect(apps._validateHostname('ab..c', 'example.com', 'ab..c.example.com')).to.be.an(Error);
|
||||
expect(apps._validateHostname('.ab', 'example.com', '.ab.example.com')).to.be.an(Error);
|
||||
expect(apps._validateHostname('-ab', 'example.com', '-ab.example.com')).to.be.an(Error);
|
||||
expect(apps._validateHostname('ab-', 'example.com', 'ab-.example.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('total length cannot exceed 255', function () {
|
||||
var s = '';
|
||||
for (var i = 0; i < (255 - 'example.com'.length); i++) s += 's';
|
||||
|
||||
expect(apps._validateHostname(s, 'example.com', s + '.example.com')).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allow valid domains', function () {
|
||||
expect(apps._validateHostname('a', 'example.com', 'a.example.com')).to.be(null);
|
||||
expect(apps._validateHostname('a0-x', 'example.com', 'a0-x.example.com')).to.be(null);
|
||||
expect(apps._validateHostname('a0.x', 'example.com', 'a0-x.example.com')).to.be(null);
|
||||
expect(apps._validateHostname('a0.x.y', 'example.com', 'a0.x.y.example.com')).to.be(null);
|
||||
expect(apps._validateHostname('01', 'example.com', '01.example.com')).to.be(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePortBindings', function () {
|
||||
it('does not allow invalid host port', function () {
|
||||
expect(apps._validatePortBindings({ port: -1 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 0 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 'text' }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 65536 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 470 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: -1 }, { port: 5000 })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 0 }, { port: 5000 })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 'text' }, { port: 5000 })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 65536 }, { port: 5000 })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 470 }, { port: 5000 })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('does not allow ports not as part of manifest', function () {
|
||||
expect(apps._validatePortBindings({ port: 1567 }, { tcpPorts: { } })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 1567 }, { tcpPorts: { port3: null } })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 1567 }, { })).to.be.an(Error);
|
||||
expect(apps._validatePortBindings({ port: 1567 }, { port3: null })).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows valid bindings', function () {
|
||||
expect(apps._validatePortBindings({ port: 1024 }, { tcpPorts: { port: 5000 } })).to.be(null);
|
||||
expect(apps._validatePortBindings({ port: 1024 }, { port: 5000 })).to.be(null);
|
||||
|
||||
expect(apps._validatePortBindings({
|
||||
port1: 4033,
|
||||
port2: 3242,
|
||||
port3: 1234
|
||||
}, { tcpPorts: { port1: null, port2: null, port3: null } })).to.be(null);
|
||||
}, { port1: null, port2: null, port3: null })).to.be(null);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -129,9 +129,14 @@ describe('Appstore', function () {
|
||||
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
|
||||
.reply(201, {});
|
||||
|
||||
var scope2 = nock('http://localhost:6060')
|
||||
.get(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/subscription?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
|
||||
.reply(200, { subscription: { id: 'basic' }});
|
||||
|
||||
appstore.purchase(APP_ID, APPSTORE_APP_ID, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -221,7 +221,7 @@ describe('database', function () {
|
||||
manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' },
|
||||
httpPort: null,
|
||||
containerId: null,
|
||||
portBindings: { port: { hostPort: 5678, type: 'tcp' } },
|
||||
portBindings: { port: 5678 },
|
||||
health: null,
|
||||
accessRestriction: null,
|
||||
lastBackupId: null,
|
||||
@@ -553,7 +553,6 @@ describe('database', function () {
|
||||
|
||||
describe('token', function () {
|
||||
var TOKEN_0 = {
|
||||
name: 'token0',
|
||||
accessToken: tokendb.generateToken(),
|
||||
identifier: '0',
|
||||
clientId: 'clientid-0',
|
||||
@@ -561,7 +560,6 @@ describe('database', function () {
|
||||
scope: 'clients'
|
||||
};
|
||||
var TOKEN_1 = {
|
||||
name: 'token1',
|
||||
accessToken: tokendb.generateToken(),
|
||||
identifier: '1',
|
||||
clientId: 'clientid-1',
|
||||
@@ -569,7 +567,6 @@ describe('database', function () {
|
||||
scope: 'settings'
|
||||
};
|
||||
var TOKEN_2 = {
|
||||
name: 'token2',
|
||||
accessToken: tokendb.generateToken(),
|
||||
identifier: '2',
|
||||
clientId: 'clientid-2',
|
||||
@@ -585,14 +582,14 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('add succeeds', function (done) {
|
||||
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name, function (error) {
|
||||
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('add of same token fails', function (done) {
|
||||
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name, function (error) {
|
||||
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
|
||||
done();
|
||||
@@ -645,7 +642,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('delByIdentifier succeeds', function (done) {
|
||||
tokendb.add(TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope, '', function (error) {
|
||||
tokendb.add(TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
tokendb.delByIdentifier(TOKEN_1.identifier, function (error) {
|
||||
@@ -664,7 +661,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('getByIdentifierAndClientId succeeds', function (done) {
|
||||
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name, function (error) {
|
||||
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
tokendb.getByIdentifierAndClientId(TOKEN_0.identifier, TOKEN_0.clientId, function (error, result) {
|
||||
@@ -678,7 +675,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('delExpired succeeds', function (done) {
|
||||
tokendb.add(TOKEN_2.accessToken, TOKEN_2.identifier, TOKEN_2.clientId, TOKEN_2.expires, TOKEN_2.scope, TOKEN_2.name, function (error) {
|
||||
tokendb.add(TOKEN_2.accessToken, TOKEN_2.identifier, TOKEN_2.clientId, TOKEN_2.expires, TOKEN_2.scope, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
tokendb.delExpired(function (error, result) {
|
||||
@@ -709,7 +706,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('delByClientId succeeds', function (done) {
|
||||
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name, function (error) {
|
||||
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
tokendb.delByClientId(TOKEN_0.clientId, function (error) {
|
||||
@@ -738,7 +735,7 @@ describe('database', function () {
|
||||
manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' },
|
||||
httpPort: null,
|
||||
containerId: null,
|
||||
portBindings: { port: { hostPort: 5678, type: 'tcp' } },
|
||||
portBindings: { port: 5678 },
|
||||
health: null,
|
||||
accessRestriction: null,
|
||||
restoreConfig: null,
|
||||
@@ -824,7 +821,7 @@ describe('database', function () {
|
||||
appdb.getPortBindings(APP_0.id, function (error, bindings) {
|
||||
expect(error).to.be(null);
|
||||
expect(bindings).to.be.an(Object);
|
||||
expect(bindings).to.be.eql({ port: { hostPort: '5678', type: 'tcp' } });
|
||||
expect(bindings).to.be.eql({ port: '5678' });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -506,7 +506,6 @@ describe('dns provider', function () {
|
||||
before(function (done) {
|
||||
DOMAIN_0.provider = 'namecom';
|
||||
DOMAIN_0.config = {
|
||||
username: 'fake',
|
||||
token: TOKEN
|
||||
};
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var dockerProxy = require('../dockerproxy.js'),
|
||||
config = require('../config.js'),
|
||||
exec = require('child_process').exec,
|
||||
expect = require('expect.js');
|
||||
|
||||
const DOCKER = `docker -H tcp://localhost:${config.get('dockerProxyPort')} `;
|
||||
|
||||
describe('Dockerproxy', function () {
|
||||
var containerId;
|
||||
|
||||
// create a container to test against
|
||||
before(function (done) {
|
||||
dockerProxy.start(function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
exec(`${DOCKER} run -d ubuntu "bin/bash" "-c" "while true; do echo 'perpetual walrus'; sleep 1; done"`, function (error, stdout, stderr) {
|
||||
expect(error).to.be(null);
|
||||
expect(stderr).to.be.empty();
|
||||
|
||||
containerId = stdout.slice(0, -1); // removes the trailing \n
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
exec(`${DOCKER} rm -f ${containerId}`, function (error, stdout, stderr) {
|
||||
expect(error).to.be(null);
|
||||
expect(stderr).to.be.empty();
|
||||
|
||||
dockerProxy.stop(done);
|
||||
});
|
||||
});
|
||||
|
||||
// uncomment this to run the proxy for manual testing
|
||||
// this.timeout(1000000);
|
||||
// it('wait', function (done) {} );
|
||||
|
||||
it('can get info', function (done) {
|
||||
exec(DOCKER + ' info', function (error, stdout, stderr) {
|
||||
expect(error).to.be(null);
|
||||
expect(stdout).to.contain('Containers:');
|
||||
// expect(stderr).to.be.empty(); // on some machines, i get 'No swap limit support'
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can create container', function (done) {
|
||||
var cmd = DOCKER + ` run ubuntu "/bin/bash" "-c" "echo 'hello'"`;
|
||||
exec(cmd, function (error, stdout, stderr) {
|
||||
expect(error).to.be(null);
|
||||
expect(stdout).to.contain('hello');
|
||||
expect(stderr).to.be.empty();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('proxy overwrites the container network option', function (done) {
|
||||
var cmd = `${DOCKER} run --network ifnotrewritethiswouldfail ubuntu "/bin/bash" "-c" "echo 'hello'"`;
|
||||
exec(cmd, function (error, stdout, stderr) {
|
||||
expect(error).to.be(null);
|
||||
expect(stdout).to.contain('hello');
|
||||
expect(stderr).to.be.empty();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot see logs through docker logs, since syslog is configured', function (done) {
|
||||
exec(`${DOCKER} logs ${containerId}`, function (error, stdout, stderr) {
|
||||
expect(error.message).to.contain('configured logging driver does not support reading');
|
||||
expect(stderr).to.contain('configured logging driver does not support reading');
|
||||
expect(stdout).to.be.empty();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can use PUT to upload archive into a container', function (done) {
|
||||
exec(`${DOCKER} cp -a ${__dirname}/proxytestarchive.tar ${containerId}:/tmp/`, function (error, stdout, stderr) {
|
||||
expect(error).to.be(null);
|
||||
expect(stderr).to.be.empty();
|
||||
expect(stdout).to.be.empty();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can exec into a container', function (done) {
|
||||
exec(`${DOCKER} exec ${containerId} ls`, function (error, stdout, stderr) {
|
||||
expect(error).to.be(null);
|
||||
expect(stderr).to.be.empty();
|
||||
expect(stdout).to.equal('bin\nboot\ndev\netc\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
config = require('../config.js'),
|
||||
database = require('../database.js'),
|
||||
domains = require('../domains.js'),
|
||||
expect = require('expect.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
describe('Domains', function () {
|
||||
before(function (done) {
|
||||
config._reset();
|
||||
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear
|
||||
], done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
async.series([
|
||||
database._clear,
|
||||
database.uninitialize
|
||||
], done);
|
||||
});
|
||||
|
||||
const domain = {
|
||||
domain: 'example.com',
|
||||
zoneName: 'example.com',
|
||||
config: {}
|
||||
};
|
||||
|
||||
describe('validateHostname', function () {
|
||||
it('does not allow admin subdomain', function () {
|
||||
config.setFqdn('example.com');
|
||||
config.setAdminFqdn('my.example.com');
|
||||
|
||||
expect(domains.validateHostname('my', domain)).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('cannot have >63 length subdomains', function () {
|
||||
var s = Array(64).fill('s').join('');
|
||||
expect(domains.validateHostname(s, domain)).to.be.an(Error);
|
||||
domain.zoneName = `dev.${s}.example.com`;
|
||||
expect(domains.validateHostname(`dev.${s}`, domain)).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allows only alphanumerics and hypen', function () {
|
||||
expect(domains.validateHostname('#2r', domain)).to.be.an(Error);
|
||||
expect(domains.validateHostname('a%b', domain)).to.be.an(Error);
|
||||
expect(domains.validateHostname('ab_', domain)).to.be.an(Error);
|
||||
expect(domains.validateHostname('ab.', domain)).to.be.an(Error);
|
||||
expect(domains.validateHostname('ab..c', domain)).to.be.an(Error);
|
||||
expect(domains.validateHostname('.ab', domain)).to.be.an(Error);
|
||||
expect(domains.validateHostname('-ab', domain)).to.be.an(Error);
|
||||
expect(domains.validateHostname('ab-', domain)).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('total length cannot exceed 255', function () {
|
||||
var s = '';
|
||||
for (var i = 0; i < (255 - 'example.com'.length); i++) s += 's';
|
||||
|
||||
expect(domains.validateHostname(s, domain)).to.be.an(Error);
|
||||
});
|
||||
|
||||
it('allow valid domains', function () {
|
||||
expect(domains.validateHostname('a', domain)).to.be(null);
|
||||
expect(domains.validateHostname('a0-x', domain)).to.be(null);
|
||||
expect(domains.validateHostname('a0.x', domain)).to.be(null);
|
||||
expect(domains.validateHostname('a0.x.y', domain)).to.be(null);
|
||||
expect(domains.validateHostname('01', domain)).to.be(null);
|
||||
});
|
||||
|
||||
it('hyphenatedSubdomains', function () {
|
||||
let domainCopy = _.extend({}, domain);
|
||||
domainCopy.config.hyphenatedSubdomains = true;
|
||||
|
||||
expect(domains.validateHostname('a', domain)).to.be(null);
|
||||
expect(domains.validateHostname('a0-x', domain)).to.be(null);
|
||||
expect(domains.validateHostname('a0.x', domain)).to.be.an(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -33,8 +33,7 @@ describe('janitor', function () {
|
||||
identifier: '0',
|
||||
clientId: 'clientid-0',
|
||||
expires: Date.now() + 60 * 60 * 1000,
|
||||
scope: 'settings',
|
||||
name: 'clientid0'
|
||||
scope: 'settings'
|
||||
};
|
||||
var TOKEN_1 = {
|
||||
accessToken: tokendb.generateToken(),
|
||||
@@ -42,7 +41,6 @@ describe('janitor', function () {
|
||||
clientId: 'clientid-1',
|
||||
expires: Date.now() - 1000,
|
||||
scope: 'apps',
|
||||
name: 'clientid1'
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
@@ -51,8 +49,8 @@ describe('janitor', function () {
|
||||
database._clear,
|
||||
authcodedb.add.bind(null, AUTHCODE_0.authCode, AUTHCODE_0.clientId, AUTHCODE_0.userId, AUTHCODE_0.expiresAt),
|
||||
authcodedb.add.bind(null, AUTHCODE_1.authCode, AUTHCODE_1.clientId, AUTHCODE_1.userId, AUTHCODE_1.expiresAt),
|
||||
tokendb.add.bind(null, TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name),
|
||||
tokendb.add.bind(null, TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope, TOKEN_1.name)
|
||||
tokendb.add.bind(null, TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope),
|
||||
tokendb.add.bind(null, TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope)
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
+1
-77
@@ -7,7 +7,6 @@
|
||||
'use strict';
|
||||
|
||||
var appdb = require('../appdb.js'),
|
||||
apps = require('../apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
database = require('../database.js'),
|
||||
@@ -106,7 +105,7 @@ function setup(done) {
|
||||
|
||||
USER_0.id = APP_0.ownerId = result.id;
|
||||
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, callback);
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0, callback);
|
||||
});
|
||||
},
|
||||
appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }),
|
||||
@@ -540,42 +539,6 @@ describe('Ldap', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it ('lists the owner as admin', function (done) {
|
||||
// make a normal user the owner
|
||||
appdb.update(APP_0.id, { ownerId: USER_1.id, accessRestriction: { users: [], groups: [ GROUP_ID ] } }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: 'objectcategory=person'
|
||||
};
|
||||
|
||||
client.search('ou=users,dc=cloudron', opts, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(EventEmitter);
|
||||
|
||||
var entries = [];
|
||||
|
||||
result.on('searchEntry', function (entry) { entries.push(entry.object); });
|
||||
result.on('error', done);
|
||||
result.on('end', function (result) {
|
||||
expect(result.status).to.equal(0);
|
||||
expect(entries.length).to.equal(2);
|
||||
entries.sort(function (a, b) { return a.username > b.username; });
|
||||
|
||||
expect(entries[0].username).to.equal(USER_0.username.toLowerCase());
|
||||
expect(entries[1].username).to.equal(USER_1.username.toLowerCase());
|
||||
expect(entries[1].isadmin).to.equal('1');
|
||||
|
||||
client.unbind();
|
||||
|
||||
appdb.update(APP_0.id, { ownerId: USER_0.id, accessRestriction: null }, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('search groups', function () {
|
||||
@@ -742,45 +705,6 @@ describe('Ldap', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it ('shows owner as admin', function (done) {
|
||||
appdb.update(APP_0.id, { ownerId: USER_1.id, accessRestriction: { users: [], groups: [ GROUP_ID ] } }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: '&(objectclass=group)(cn=*)'
|
||||
};
|
||||
|
||||
client.search('ou=groups,dc=cloudron', opts, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(EventEmitter);
|
||||
|
||||
var entries = [];
|
||||
|
||||
result.on('searchEntry', function (entry) { entries.push(entry.object); });
|
||||
result.on('error', done);
|
||||
result.on('end', function (result) {
|
||||
expect(result.status).to.equal(0);
|
||||
expect(entries.length).to.equal(2);
|
||||
expect(entries[0].cn).to.equal('users');
|
||||
expect(entries[0].memberuid.length).to.equal(2);
|
||||
expect(entries[0].memberuid[0]).to.equal(USER_0.id);
|
||||
expect(entries[0].memberuid[1]).to.equal(USER_1.id);
|
||||
expect(entries[1].cn).to.equal('admins');
|
||||
|
||||
expect(entries[1].memberuid.length).to.equal(2);
|
||||
expect(entries[1].memberuid[0]).to.equal(USER_0.id);
|
||||
expect(entries[1].memberuid[1]).to.equal(USER_1.id);
|
||||
|
||||
client.unbind();
|
||||
|
||||
appdb.update(APP_0.id, { ownerId: USER_0.id, accessRestriction: null }, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function ldapSearch(dn, filter, callback) {
|
||||
|
||||
Binary file not shown.
@@ -128,7 +128,7 @@ describe('Certificates', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('returns prod caas for prod cloudron', function (done) {
|
||||
reverseProxy._getApi(DOMAIN_0.domain, function (error, api, options) {
|
||||
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
|
||||
expect(error).to.be(null);
|
||||
expect(api._name).to.be('caas');
|
||||
expect(options.prod).to.be(true);
|
||||
@@ -137,7 +137,7 @@ describe('Certificates', function () {
|
||||
});
|
||||
|
||||
it('returns prod caas for dev cloudron', function (done) {
|
||||
reverseProxy._getApi(DOMAIN_0.domain, function (error, api, options) {
|
||||
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
|
||||
expect(error).to.be(null);
|
||||
expect(api._name).to.be('caas');
|
||||
expect(options.prod).to.be(true);
|
||||
@@ -159,7 +159,7 @@ describe('Certificates', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('returns prod acme in prod cloudron', function (done) {
|
||||
reverseProxy._getApi(DOMAIN_0.domain, function (error, api, options) {
|
||||
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
|
||||
expect(error).to.be(null);
|
||||
expect(api._name).to.be('acme');
|
||||
expect(options.prod).to.be(true);
|
||||
@@ -168,7 +168,7 @@ describe('Certificates', function () {
|
||||
});
|
||||
|
||||
it('returns prod acme in dev cloudron', function (done) {
|
||||
reverseProxy._getApi(DOMAIN_0.domain, function (error, api, options) {
|
||||
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
|
||||
expect(error).to.be(null);
|
||||
expect(api._name).to.be('acme');
|
||||
expect(options.prod).to.be(true);
|
||||
@@ -190,7 +190,7 @@ describe('Certificates', function () {
|
||||
after(cleanup);
|
||||
|
||||
it('returns staging acme in prod cloudron', function (done) {
|
||||
reverseProxy._getApi(DOMAIN_0.domain, function (error, api, options) {
|
||||
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
|
||||
expect(error).to.be(null);
|
||||
expect(api._name).to.be('acme');
|
||||
expect(options.prod).to.be(false);
|
||||
@@ -199,7 +199,7 @@ describe('Certificates', function () {
|
||||
});
|
||||
|
||||
it('returns staging acme in dev cloudron', function (done) {
|
||||
reverseProxy._getApi(DOMAIN_0.domain, function (error, api, options) {
|
||||
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
|
||||
expect(error).to.be(null);
|
||||
expect(api._name).to.be('acme');
|
||||
expect(options.prod).to.be(false);
|
||||
|
||||
@@ -120,6 +120,21 @@ describe('Settings', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('can set spaces config', function (done) {
|
||||
settings.setSpacesConfig({ enabled: true }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get backup config', function (done) {
|
||||
settings.getSpacesConfig(function (error, spacesConfig) {
|
||||
expect(error).to.be(null);
|
||||
expect(spacesConfig.enabled).to.be(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can enable mail digest', function (done) {
|
||||
settings.setEmailDigest(true, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
'use strict';
|
||||
|
||||
var appdb = require('../appdb.js'),
|
||||
apps = require('../apps.js'),
|
||||
async = require('async'),
|
||||
config = require('../config.js'),
|
||||
constants = require('../constants.js'),
|
||||
@@ -301,7 +300,7 @@ describe('updatechecker - app - manual (email)', function () {
|
||||
if (error) return next(error);
|
||||
|
||||
APP_0.ownerId = userObject.id;
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, next);
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0, next);
|
||||
});
|
||||
},
|
||||
settings.setAppAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
|
||||
@@ -424,7 +423,7 @@ describe('updatechecker - app - automatic (no email)', function () {
|
||||
if (error) return next(error);
|
||||
|
||||
APP_0.ownerId = userObject.id;
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, next);
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0, next);
|
||||
});
|
||||
},
|
||||
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
|
||||
@@ -497,7 +496,7 @@ describe('updatechecker - app - automatic free (email)', function () {
|
||||
if (error) return next(error);
|
||||
|
||||
APP_0.ownerId = userObject.id;
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, next);
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0, next);
|
||||
});
|
||||
},
|
||||
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
|
||||
|
||||
+7
-21
@@ -176,7 +176,7 @@ describe('User', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
it('succeeds and attempts to send invite', function (done) {
|
||||
users.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).not.to.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
@@ -184,7 +184,8 @@ describe('User', function () {
|
||||
expect(result.email).to.equal(EMAIL.toLowerCase());
|
||||
expect(result.fallbackEmail).to.equal(EMAIL.toLowerCase());
|
||||
|
||||
done();
|
||||
// first user is owner, do not send mail to admins
|
||||
checkMails(0, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -221,7 +222,7 @@ describe('User', function () {
|
||||
expect(result.fallbackEmail).to.equal(EMAIL_1.toLowerCase());
|
||||
|
||||
// first user is owner, do not send mail to admins
|
||||
checkMails(1, { sentTo: EMAIL_1.toLowerCase() }, function (error) {
|
||||
checkMails(2, { sentTo: EMAIL_1.toLowerCase() }, function (error) {
|
||||
expect(error).not.to.be.ok();
|
||||
|
||||
maildb.update(DOMAIN_0.domain, { enabled: false }, done);
|
||||
@@ -829,7 +830,7 @@ describe('User', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('invite', function () {
|
||||
describe('send invite', function () {
|
||||
before(createOwner);
|
||||
after(cleanupUsers);
|
||||
|
||||
@@ -842,24 +843,9 @@ describe('User', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails as expected', function (done) {
|
||||
it('succeeds', function (done) {
|
||||
users.sendInvite(userObject.id, { }, function (error) {
|
||||
expect(error).to.be.ok(); // have to create resetToken first
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can create token', function (done) {
|
||||
users.createInvite(userObject.id, function (error, resetToken) {
|
||||
expect(error).to.be(null);
|
||||
expect(resetToken).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('send invite', function (done) {
|
||||
users.sendInvite(userObject.id, { }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
expect(error).to.eql(null);
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
|
||||
+4
-5
@@ -22,7 +22,7 @@ var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
hat = require('./hat.js');
|
||||
|
||||
var TOKENS_FIELDS = [ 'accessToken', 'identifier', 'clientId', 'scope', 'expires', 'name' ].join(',');
|
||||
var TOKENS_FIELDS = [ 'accessToken', 'identifier', 'clientId', 'scope', 'expires' ].join(',');
|
||||
|
||||
function generateToken() {
|
||||
return hat(8 * 32); // TODO: make this stronger
|
||||
@@ -40,17 +40,16 @@ function get(accessToken, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(accessToken, identifier, clientId, expires, scope, name, callback) {
|
||||
function add(accessToken, identifier, clientId, expires, scope, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert(typeof clientId === 'string' || clientId === null);
|
||||
assert.strictEqual(typeof expires, 'number');
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO tokens (accessToken, identifier, clientId, expires, scope, name) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[ accessToken, identifier, clientId, expires, scope, name ], function (error, result) {
|
||||
database.query('INSERT INTO tokens (accessToken, identifier, clientId, expires, scope) VALUES (?, ?, ?, ?, ?)',
|
||||
[ accessToken, identifier, clientId, expires, scope ], function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
|
||||
@@ -185,7 +185,6 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
adminFqdn: config.adminFqdn(),
|
||||
adminLocation: config.adminLocation(),
|
||||
isDemo: config.isDemo(),
|
||||
edition: config.edition(),
|
||||
|
||||
appstore: {
|
||||
apiServerOrigin: config.apiServerOrigin()
|
||||
|
||||
+1
-2
@@ -162,13 +162,12 @@ function del(userId, callback) {
|
||||
// also cleanup the groupMembers table
|
||||
var queries = [];
|
||||
queries.push({ query: 'DELETE FROM groupMembers WHERE userId = ?', args: [ userId ] });
|
||||
queries.push({ query: 'DELETE FROM tokens WHERE identifier = ?', args: [ userId ] });
|
||||
queries.push({ query: 'DELETE FROM users WHERE id = ?', args: [ userId ] });
|
||||
|
||||
database.transaction(queries, function (error, result) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, error));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result[2].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(error);
|
||||
});
|
||||
|
||||
+12
-26
@@ -21,7 +21,6 @@ exports = module.exports = {
|
||||
update: updateUser,
|
||||
createOwner: createOwner,
|
||||
getOwner: getOwner,
|
||||
createInvite: createInvite,
|
||||
sendInvite: sendInvite,
|
||||
setMembership: setMembership,
|
||||
setTwoFactorAuthenticationSecret: setTwoFactorAuthenticationSecret,
|
||||
@@ -151,9 +150,9 @@ function create(username, password, email, displayName, options, auditSource, ca
|
||||
assert(options && typeof options === 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
const isOwner = !!options.owner;
|
||||
const isAdmin = !!options.admin;
|
||||
const invitor = options.invitor || null;
|
||||
var invitor = options.invitor || null,
|
||||
sendInvite = !!options.sendInvite,
|
||||
owner = !!options.owner;
|
||||
|
||||
var error;
|
||||
|
||||
@@ -193,9 +192,9 @@ function create(username, password, email, displayName, options, auditSource, ca
|
||||
salt: salt.toString('hex'),
|
||||
createdAt: now,
|
||||
modifiedAt: now,
|
||||
resetToken: '',
|
||||
resetToken: hat(256),
|
||||
displayName: displayName,
|
||||
admin: isOwner || isAdmin
|
||||
admin: owner
|
||||
};
|
||||
|
||||
userdb.add(user.id, user, function (error) {
|
||||
@@ -204,9 +203,10 @@ function create(username, password, email, displayName, options, auditSource, ca
|
||||
|
||||
callback(null, user);
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_ADD, auditSource, { userId: user.id, email: user.email, user: removePrivateFields(user), invitor: invitor });
|
||||
eventlog.add(eventlog.ACTION_USER_ADD, auditSource, { userId: user.id, email: user.email, user: removePrivateFields(user) });
|
||||
|
||||
if (!isOwner) mailer.userAdded(user);
|
||||
if (!owner) mailer.userAdded(user, sendInvite);
|
||||
if (sendInvite) mailer.sendInvite(user, invitor);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -523,8 +523,9 @@ function getOwner(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function createInvite(userId, callback) {
|
||||
function sendInvite(userId, options, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.get(userId, function (error, userObject) {
|
||||
@@ -537,28 +538,13 @@ function createInvite(userId, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UsersError(UsersError.NOT_FOUND));
|
||||
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
|
||||
|
||||
mailer.sendInvite(userObject, options.invitor || null);
|
||||
|
||||
callback(null, userObject.resetToken);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendInvite(userId, options, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.get(userId, function (error, userObject) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UsersError(UsersError.NOT_FOUND));
|
||||
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!userObject.resetToken) return callback(new UsersError(UsersError.BAD_FIELD, 'Must generate resetToken to send inivitation'));
|
||||
|
||||
mailer.sendInvite(userObject, options.invitor || null);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function setTwoFactorAuthenticationSecret(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
Reference in New Issue
Block a user