Compare commits

...

127 Commits

Author SHA1 Message Date
Johannes Zellner a50409bdca Also show errors above input fields for password reset 2017-03-20 16:50:31 +01:00
Johannes Zellner 60a722e6cc Remove superflous quote in html 2017-03-20 16:43:36 +01:00
Johannes Zellner 4d6cafa589 Show form errors on the top during user activation 2017-03-20 15:57:02 +01:00
Johannes Zellner 63e557430b ng-href takes a template string 2017-03-20 15:26:31 +01:00
Johannes Zellner 04acb4423d Add open registration rest api tests 2017-03-20 14:27:47 +01:00
Johannes Zellner ea813acf4c Add open registration default value and test 2017-03-20 14:27:39 +01:00
Johannes Zellner b1198dfdbf Add settingsdb tests for open registration 2017-03-20 14:22:11 +01:00
Johannes Zellner 4342de3747 Show error response on signup 2017-03-20 14:19:52 +01:00
Johannes Zellner ef8bc7e7e9 username must be null or non-empty string 2017-03-20 14:01:12 +01:00
Johannes Zellner e18e401f6b Improve signup form 2017-03-20 14:00:56 +01:00
Johannes Zellner ab998c47e8 Show user signup link if registration is open 2017-03-20 13:52:31 +01:00
Johannes Zellner 9fb830b2e1 add section to toggle open registration in settings view 2017-03-20 12:55:48 +01:00
Johannes Zellner 415c3f90a1 Always send an object with properties 2017-03-20 12:53:21 +01:00
Johannes Zellner 60c8ff7fb1 Add open_registration settings routes 2017-03-20 12:31:53 +01:00
Johannes Zellner 037816313c Remove newline 2017-03-20 12:29:15 +01:00
Johannes Zellner 3d285d1ac6 Better signup styling 2017-03-20 12:04:23 +01:00
Johannes Zellner 135338786f Protect user creation if open registration is not allowed 2017-03-20 12:00:58 +01:00
Johannes Zellner 661f1fce31 Angular uses double curly brackets 2017-03-20 11:58:01 +01:00
Johannes Zellner 03664ef784 Add open registration setting 2017-03-20 11:56:58 +01:00
Johannes Zellner d2111ef2b6 Add user signup ui 2017-03-20 11:52:11 +01:00
Johannes Zellner e0df19c888 Remove unused api wrapper for getAppLogStream() 2017-03-20 10:46:27 +01:00
Girish Ramakrishnan 6a523606ca Revert "Bump version to Nginx IPv6 support."
This reverts commit 5555321cf5.
This reverts commit f087ebbee0.
This reverts commit d04f64d3d4.

Part of #264
2017-03-19 14:25:30 -07:00
Girish Ramakrishnan b6cd40e63c Use latest manifestformat 2017-03-19 14:20:00 -07:00
Girish Ramakrishnan b421866bf5 Remove simpleauth
Simple Auth used to provide auth over HTTP. The original motivation
behind this was this was a simple way to add Cloudron Auth integration.
Back in the day, Cloudron Auth was a requirement for apps but this is
not the case anymore.

This is currently not used by any app and having this might encourage
people to make Cloudron specific un-upstreamable changes.
2017-03-19 01:31:38 -07:00
Girish Ramakrishnan fe06075816 more CHANGES 2017-03-17 13:49:47 -07:00
Girish Ramakrishnan 2b73eb90ec Merge branch 'ipv6' into 'master'
Add IPv6 Support

See merge request !3
2017-03-17 19:55:30 +00:00
Jonah Aragon 5555321cf5 Bump version to Nginx IPv6 support. 2017-03-17 19:43:54 +00:00
Jonah Aragon f087ebbee0 Add listen [::]:80; for IPv6 redirects. 2017-03-17 19:13:18 +00:00
Jonah Aragon d04f64d3d4 Add IPv6 listen directives 2017-03-17 19:12:25 +00:00
Girish Ramakrishnan 777a5a0929 Add 0.106.0 changes 2017-03-17 10:23:17 -07:00
Girish Ramakrishnan 6c297f890e Bump mail container 2017-03-17 10:23:17 -07:00
Johannes Zellner 3c8d0b1b37 Never hide the busy state on setup when it suceeded
In that case the whole page gets redirected and to avoid page flickering
we keep it at busy until the browser tears the whole page apart.
2017-03-16 09:58:21 +01:00
Johannes Zellner 74f2cd156f Only send setupToken on admin creation if it was actually specified 2017-03-16 09:37:28 +01:00
Girish Ramakrishnan a9fdffa9af 0.105.1 changes 2017-03-15 21:15:15 -07:00
Girish Ramakrishnan e6f8e8eb94 ami field is only required if shown 2017-03-15 21:10:22 -07:00
Girish Ramakrishnan 1bd89ca055 Wait for platform ready after box restarts
This is required for the case where the box restarts apptasks.
For example, the server can reboot mid-way when apptask is running
(as in cloudron-setup + appBundle case) and then when it comes back
up it doesn't wait for the platform to be ready. And the apps fail
to install (mysql takes a bit to startup)
2017-03-15 20:35:44 -07:00
Girish Ramakrishnan 0e226d0314 Download icon (for repair case) 2017-03-15 20:35:44 -07:00
Girish Ramakrishnan e8d4e2c792 send more logs 2017-03-15 19:35:42 -07:00
Girish Ramakrishnan 4cfbed8273 Use inline docker pgp key
The one from keyserver keeps failing sporadically

https://github.com/docker/docker/issues/13555
https://github.com/docker/docker/issues/20022
http://askubuntu.com/questions/720517/key-server-times-out-while-installing-docker-on-ubuntu-14-04
2017-03-15 18:04:44 -07:00
Girish Ramakrishnan 0410ac9780 doc: activate api 2017-03-15 16:14:25 -07:00
Girish Ramakrishnan 82fcf6a770 setupToken is not required in activate 2017-03-15 15:55:31 -07:00
Girish Ramakrishnan a1332865c0 Fix wording (should be prove otherwise) 2017-03-15 15:42:06 -07:00
Girish Ramakrishnan ae0e4de93e No semicolons in bash code 2017-03-15 15:40:43 -07:00
Johannes Zellner 02a6525558 Add changes for 0.105.0 2017-03-15 14:56:35 +01:00
Girish Ramakrishnan 5afef14760 Actually send emails for responsive apps 2017-03-14 13:42:28 -07:00
Johannes Zellner 890d589a36 Do not show Route53 in dns setup for AMIs 2017-03-14 16:54:46 +01:00
Johannes Zellner 89a50c4b83 Use ami provider in ami creation script 2017-03-14 13:48:11 +01:00
Johannes Zellner da5cd2b62c Show instance id input on cloudron setup for amis 2017-03-14 13:45:18 +01:00
Johannes Zellner 57321624aa Add ami setupToken verification in auth route 2017-03-14 13:45:04 +01:00
Johannes Zellner 876ae822b2 Skip splash setup if cloudron domain was not yet setup
This is based on the existence of admin.conf nginx file.
The splash would create/overwrite that file, but it will depend on the
host.cert to be already created, which is only the case after domain
setup.
2017-03-14 10:58:24 +01:00
Johannes Zellner 1ceb75868b Remove last remainder of apidocs 2017-03-14 10:12:17 +01:00
Johannes Zellner 98ad16f943 Remove unused requires 2017-03-14 10:10:59 +01:00
Johannes Zellner 9363746c1a Use ec2 sysinfo for ami provider 2017-03-14 09:34:39 +01:00
Johannes Zellner 7a1b9ab94c Support ami provider for ssh authorized_keys api 2017-03-14 09:34:11 +01:00
Johannes Zellner 46d6b5b81f Add hidden 'ami' provider for pre-built amis 2017-03-14 09:32:51 +01:00
Girish Ramakrishnan 7e8757a78c grep quietly 2017-03-13 13:52:16 -07:00
Girish Ramakrishnan e508b25ecd Lower memory expectations 2017-03-13 13:05:59 -07:00
Girish Ramakrishnan 3fdc10c523 Parse free and fdisk output with C locale
some vps providers seem to set a different locale by default.
Settings LC_ALL overrides all the other LC_*
2017-03-13 10:36:05 -07:00
Johannes Zellner 717953c162 Half the backup progress polling 2017-03-13 13:28:14 +01:00
Johannes Zellner daa34c3b4d add some asserts in the ldap code 2017-03-13 11:10:08 +01:00
Johannes Zellner bf5c78d819 Refactor ldap user listing code to avoid pyramids 2017-03-13 11:09:12 +01:00
Johannes Zellner 1763144278 Only list users in ldap groups who have access to the app
Fixes #215
2017-03-13 11:06:29 +01:00
Johannes Zellner 2f598529fc Only list users who have access to the app in an ldap search
Part of #215
2017-03-13 11:02:45 +01:00
Girish Ramakrishnan 8264e69e2f remove unused require 2017-03-10 14:52:31 -08:00
Johannes Zellner b0638df94e Only show the remote support for admins 2017-03-10 17:21:01 +01:00
Johannes Zellner bb61eee557 Add missing quote in support view 2017-03-10 17:17:51 +01:00
Johannes Zellner 39c39b861d Require admins for authorized_keys route 2017-03-10 17:16:45 +01:00
Girish Ramakrishnan e3deda4ef3 Always show port 25 status 2017-03-09 16:21:47 -08:00
Girish Ramakrishnan 7e44e7de82 Check outbound port 25
Fixes #243
2017-03-09 16:20:53 -08:00
Girish Ramakrishnan 9dd0518c00 Show email settings for non-caas
This is because people can use route53/DO now and we can show them
the RDNS settings as well.
2017-03-09 15:21:43 -08:00
Girish Ramakrishnan 81313d1c40 reduce nxdomain caching timeout
the other option is to use "/usr/sbin/unbound-control flush_negative"
on demand
2017-03-09 15:03:14 -08:00
Girish Ramakrishnan 2ceccc4557 Add note for caas users about enabling email 2017-03-09 14:25:03 -08:00
Girish Ramakrishnan 1c36918e92 Done -> Almost done 2017-03-09 10:21:52 -08:00
Girish Ramakrishnan 8d93df23c1 doc: cnameTarget 2017-03-09 10:00:42 -08:00
Johannes Zellner 0c06b34a2c Add more changes for 0.104.0 2017-03-09 15:38:09 +01:00
Johannes Zellner fe980eab7f Show either cnameTarget or fqdn for CNAME setup hint
Fixes #101
2017-03-09 15:23:17 +01:00
Johannes Zellner 979b903bf2 Add cnameTarget for apps using an external domain
We have 4 properties related to the domain:
1) location, is the subdomain location without information how to craft
a fqdn on the client
2) fqdn, the intended domain to reach the app
3) altDomain, just the value for the external domain, merely a db record
value
4) cnameTarget, mostly for display purpose on the client, which
otherwise has no way to build the original cloudron local fqdn
2017-03-09 15:11:27 +01:00
Johannes Zellner 4b8ee0934a Add Cloudron cancel link to the settings view
Fixes #251
2017-03-09 13:36:29 +01:00
Girish Ramakrishnan 0439725790 Bump infra version 2017-03-08 22:27:41 -08:00
Girish Ramakrishnan 4b3ef33989 Add some basic secure headers
Part of #249
2017-03-08 22:14:44 -08:00
Girish Ramakrishnan 9e99d51853 do not remote support for caas 2017-03-08 16:15:13 -08:00
Girish Ramakrishnan 00a9fa8f34 fix wording a bit 2017-03-08 16:11:45 -08:00
Girish Ramakrishnan 84a35343d1 Display <sso> and <nosso> contents based on SSO 2017-03-08 15:16:15 -08:00
Girish Ramakrishnan 397bd17c55 update showdown to 1.6.4 2017-03-08 15:01:07 -08:00
Girish Ramakrishnan c8e377a9bd doc: scaleway may need reboot for security group to take effect 2017-03-08 10:34:12 -08:00
Johannes Zellner 90e3138bae Show the correct postInstall message after app installation
Fixes #255
2017-03-08 15:42:29 +01:00
Girish Ramakrishnan 24b32a763b Add comments for CLI tool 2017-03-07 12:44:17 -08:00
Johannes Zellner 69a12d36ef Also give lightsail the special user treatment 2017-03-07 16:51:58 +01:00
Johannes Zellner 1485718fa6 Special treatment for ec2 and authorized_key user 2017-03-07 16:44:04 +01:00
Johannes Zellner 750f03d9de Add the public key of our support ssh key 2017-03-07 16:13:48 +01:00
Johannes Zellner b5ddf1d24d Add ssh support toggle button in the support view 2017-03-07 16:12:00 +01:00
Johannes Zellner 043a35111d Remove unused requires in ssh test 2017-03-07 16:11:21 +01:00
Johannes Zellner 676457b589 Add authorized_key wrappers to client.js 2017-03-07 16:07:25 +01:00
Johannes Zellner e61f11be81 Since we need root to save the authorized_key file we do it via sudo script 2017-03-07 15:16:41 +01:00
Johannes Zellner 101a44affd Add authorized_keys.sh 2017-03-07 15:16:18 +01:00
Johannes Zellner 7995c664ed Add shell.sudoSync() 2017-03-07 15:14:37 +01:00
Johannes Zellner 6023c0e5dc Ensure the authorized_file permissions are correct 2017-03-07 14:39:14 +01:00
Johannes Zellner d49d76c1ee add ssh route tests and fixup the code accordingly 2017-03-07 14:12:25 +01:00
Johannes Zellner 77ef212daa Add SSH authorized_keys routes 2017-03-07 13:16:28 +01:00
Johannes Zellner 7aa80193c0 Add more changes 2017-03-06 10:47:36 +01:00
Johannes Zellner 5632c74556 Add isadmin ldap attribute
Fixes #241
2017-03-06 10:45:50 +01:00
Girish Ramakrishnan 7a08745af1 doc: add the 20gb requirement 2017-03-05 18:31:31 -08:00
Girish Ramakrishnan d9ba0858c7 Add 0.103.1 changes 2017-03-03 09:42:31 -08:00
Johannes Zellner 617e51d294 Adjust the oom notification email 2017-03-03 11:04:48 +01:00
Johannes Zellner c07d322fff Do not send ldap records for users without a username set
If an app relies on the attribute to be set, apps like owncloud would
fail internally.
2017-03-03 10:18:38 +01:00
Girish Ramakrishnan 9b8fa8a772 doc: sso 2017-03-02 15:15:15 -08:00
Girish Ramakrishnan c351242af7 lie about the time if it is ahead of us
Fixes #247
2017-03-02 14:34:18 -08:00
Johannes Zellner 55245557f5 Use the new app login event in the webadmin
Part of #247
2017-03-02 17:15:01 +01:00
Johannes Zellner ee1cef3ee8 Add new event type for app mailbox ldap login 2017-03-02 17:13:19 +01:00
Girish Ramakrishnan 5d51a7178f domain migrate: Add text that subdomains are not supported 2017-03-01 15:45:37 -08:00
Girish Ramakrishnan 9d52397bcc Move dhparam creation
Now that all cloudrons have the dhparams file, we can generate this
*after* restoring from backup and if required.
2017-03-01 15:25:20 -08:00
Girish Ramakrishnan 5098fbe061 Version 0.103.0 changes 2017-03-01 13:08:49 -08:00
Girish Ramakrishnan 7062aa4ac7 use test image 19.0.1 2017-02-28 20:21:02 -08:00
Girish Ramakrishnan d6fec4f2b9 alertsTo must be an array 2017-02-28 18:17:17 -08:00
Girish Ramakrishnan 86ef462c76 doc: add email/recvmail to allowed addon keys 2017-02-27 06:51:54 -08:00
Girish Ramakrishnan c76e7a3f63 randomize the cn in ip based cert
Fixes #224
2017-02-25 15:38:15 -08:00
Girish Ramakrishnan 2516a08659 remove reference to npm-demo from manifest 2017-02-25 13:36:27 -08:00
Girish Ramakrishnan 562fe30333 Update cloudron-manifestformat
adds the upcoming tls addon
2017-02-24 22:13:28 -08:00
Girish Ramakrishnan 4e0eed4bb2 make tests pass 2017-02-24 21:48:38 -08:00
Girish Ramakrishnan b604caec72 Get rid of x509 module
This is the last of the "native" modules. These modules take forever
to rebuild in low memory machines
2017-02-24 21:01:48 -08:00
Girish Ramakrishnan 6b409e9089 Do not send crash logs to support in self-hosted case
Fixes #242
2017-02-24 10:40:51 -08:00
Girish Ramakrishnan 015d434358 remove unused require 2017-02-24 10:39:03 -08:00
Girish Ramakrishnan c8e448cb84 Remove support@cloudron.io in app died mails
part of #242
2017-02-24 10:36:48 -08:00
Girish Ramakrishnan 03924be491 self-hosted: do not cc support for bounce mails from apps
part of #242
2017-02-24 10:34:07 -08:00
Girish Ramakrishnan 2729cecf4a self-hosting: remove support@cloudron.io frmo oom mails, cert renewal and backup failure mails
Part of #242
2017-02-24 10:25:20 -08:00
Girish Ramakrishnan 32e2377828 sysinfo: getIp -> getPublicIp 2017-02-23 22:03:48 -08:00
Girish Ramakrishnan fdb8139b03 createAMI: better help text 2017-02-23 15:44:56 -08:00
81 changed files with 1999 additions and 1814 deletions
+33
View File
@@ -772,3 +772,36 @@
* Fix issue where Cloudrons with many apps (> 35) were unable to backup
* Improve wording of DNS Setup
[0.103.0]
* Do not send crash logs and other notifications to support@cloudron.io for self-hosted instances
* Make auto-generated self-signed cert load quickly on Firefox (take 2)
[0.104.0]
* (mail) Fix crash when sending mails to groups with just 1 user
* (ldap) Add isadmin attribute to better map users in apps
* (ldap) Hide users which have not yet set a username in ldap searches
* (core) Add SSH authorized_keys management
* (core) Add additional security related headers to the nginx reverse proxy
* (ui) Add remote SSH support option
* (ui) Fix eventlog display
* (ui) Fix CNAME setup information
[0.105.0]
* Always show email related checks
* Show outbound SMTP port 25 status
* Hide remote feature for normal users
* Only list users via ldap searches who have access to the app
* Fix installation issue on servers with a differente locale set
[0.105.1]
* Fix crash when setupToken is not provided in activate API
* Add inline Docker GPG key
* Re-download icon when repairing app
* Fix issue where pre-installed apps were not installed correctly
* Fix issue where new cloudrons could not be activated
[0.106.0]
* (mail) Fix email forwarding to external domains
* (mail) Set maximum email size to 25MB
* Remove SimpleAuth addon
+2 -2
View File
@@ -69,7 +69,7 @@ if [[ ! -f "${ssh_keys}" ]]; then
fi
if [[ -z "${image_id}" ]]; then
echo "--region is required"
echo "--region is required (us-east-1 or eu-central-1)"
exit 1
fi
@@ -141,7 +141,7 @@ while true; do
done
echo "=> Running cloudron-setup"
$SSH ubuntu@${server_ip} sudo /bin/bash "cloudron-setup" --env "${deploy_env}" --provider "ec2" --skip-reboot
$SSH ubuntu@${server_ip} sudo /bin/bash "cloudron-setup" --env "${deploy_env}" --provider "ami" --skip-reboot
wait_for_ssh
+32 -1
View File
@@ -52,7 +52,38 @@ apt-get install -y python # Install python which is required for npm rebuild
# https://docs.docker.com/engine/installation/linux/ubuntulinux/
echo "==> Installing Docker"
apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
docker_key="-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1
mQINBFWln24BEADrBl5p99uKh8+rpvqJ48u4eTtjeXAWbslJotmC/CakbNSqOb9o
ddfzRvGVeJVERt/Q/mlvEqgnyTQy+e6oEYN2Y2kqXceUhXagThnqCoxcEJ3+KM4R
mYdoe/BJ/J/6rHOjq7Omk24z2qB3RU1uAv57iY5VGw5p45uZB4C4pNNsBJXoCvPn
TGAs/7IrekFZDDgVraPx/hdiwopQ8NltSfZCyu/jPpWFK28TR8yfVlzYFwibj5WK
dHM7ZTqlA1tHIG+agyPf3Rae0jPMsHR6q+arXVwMccyOi+ULU0z8mHUJ3iEMIrpT
X+80KaN/ZjibfsBOCjcfiJSB/acn4nxQQgNZigna32velafhQivsNREFeJpzENiG
HOoyC6qVeOgKrRiKxzymj0FIMLru/iFF5pSWcBQB7PYlt8J0G80lAcPr6VCiN+4c
NKv03SdvA69dCOj79PuO9IIvQsJXsSq96HB+TeEmmL+xSdpGtGdCJHHM1fDeCqkZ
hT+RtBGQL2SEdWjxbF43oQopocT8cHvyX6Zaltn0svoGs+wX3Z/H6/8P5anog43U
65c0A+64Jj00rNDr8j31izhtQMRo892kGeQAaaxg4Pz6HnS7hRC+cOMHUU4HA7iM
zHrouAdYeTZeZEQOA7SxtCME9ZnGwe2grxPXh/U/80WJGkzLFNcTKdv+rwARAQAB
tDdEb2NrZXIgUmVsZWFzZSBUb29sIChyZWxlYXNlZG9ja2VyKSA8ZG9ja2VyQGRv
Y2tlci5jb20+iQI4BBMBAgAiBQJVpZ9uAhsvBgsJCAcDAgYVCAIJCgsEFgIDAQIe
AQIXgAAKCRD3YiFXLFJgnbRfEAC9Uai7Rv20QIDlDogRzd+Vebg4ahyoUdj0CH+n
Ak40RIoq6G26u1e+sdgjpCa8jF6vrx+smpgd1HeJdmpahUX0XN3X9f9qU9oj9A4I
1WDalRWJh+tP5WNv2ySy6AwcP9QnjuBMRTnTK27pk1sEMg9oJHK5p+ts8hlSC4Sl
uyMKH5NMVy9c+A9yqq9NF6M6d6/ehKfBFFLG9BX+XLBATvf1ZemGVHQusCQebTGv
0C0V9yqtdPdRWVIEhHxyNHATaVYOafTj/EF0lDxLl6zDT6trRV5n9F1VCEh4Aal8
L5MxVPcIZVO7NHT2EkQgn8CvWjV3oKl2GopZF8V4XdJRl90U/WDv/6cmfI08GkzD
YBHhS8ULWRFwGKobsSTyIvnbk4NtKdnTGyTJCQ8+6i52s+C54PiNgfj2ieNn6oOR
7d+bNCcG1CdOYY+ZXVOcsjl73UYvtJrO0Rl/NpYERkZ5d/tzw4jZ6FCXgggA/Zxc
jk6Y1ZvIm8Mt8wLRFH9Nww+FVsCtaCXJLP8DlJLASMD9rl5QS9Ku3u7ZNrr5HWXP
HXITX660jglyshch6CWeiUATqjIAzkEQom/kEnOrvJAtkypRJ59vYQOedZ1sFVEL
MXg2UCkD/FwojfnVtjzYaTCeGwFQeqzHmM241iuOmBYPeyTY5veF49aBJA1gEJOQ
TvBR8Q==
=Fm3p
-----END PGP PUBLIC KEY BLOCK-----
"
echo "$docker_key" | apt-key add -
echo "deb https://apt.dockerproject.org/repo ubuntu-xenial main" > /etc/apt/sources.list.d/docker.list
apt-get -y update
+1 -5
View File
@@ -13,8 +13,7 @@ var appHealthMonitor = require('./src/apphealthmonitor.js'),
async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
server = require('./src/server.js'),
simpleauth = require('./src/simpleauth.js');
server = require('./src/server.js');
console.log();
console.log('==========================================');
@@ -33,7 +32,6 @@ console.log();
async.series([
server.start,
ldap.start,
simpleauth.start,
appHealthMonitor.start,
], function (error) {
if (error) {
@@ -48,13 +46,11 @@ var NOOP_CALLBACK = function () { };
process.on('SIGINT', function () {
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
simpleauth.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
process.on('SIGTERM', function () {
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
simpleauth.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
-64
View File
@@ -318,67 +318,3 @@ cloudron exec
> swaks --server "${MAIL_SMTP_SERVER}" -p "${MAIL_SMTP_PORT}" --from "${MAIL_SMTP_USERNAME}@${MAIL_DOMAIN}" --body "Test mail from cloudron app at $(hostname -f)" --auth-user "${MAIL_SMTP_USERNAME}" --auth-password "${MAIL_SMTP_PASSWORD}"
```
## simpleauth
Simple Auth can be used for authenticating users with a HTTP request. This method of authentication is targeted
at applications, which for whatever reason can't use the ldap addon.
The response contains an `accessToken` which can then be used to access the [Cloudron API](/references/api.html).
Exported environment variables:
```
SIMPLE_AUTH_SERVER= # the simple auth HTTP server
SIMPLE_AUTH_PORT= # the simple auth server port
SIMPLE_AUTH_URL= # the simple auth server URL. same as "http://SIMPLE_AUTH_SERVER:SIMPLE_AUTH_PORT
SIMPLE_AUTH_CLIENT_ID # a client id for identifying the request originator with the auth server
```
This addons provides two REST APIs:
**POST /api/v1/login**
Request JSON body:
```
{
"username": "<username> or <email>",
"password": "<password>"
}
```
Response 200 with JSON body:
```
{
"accessToken": "<accessToken>",
"user": {
"id": "<userId>",
"username": "<username>",
"email": "<email>",
"admin": <admin boolean>,
"displayName": "<display name>"
}
}
```
**GET /api/v1/logout**
Request params:
```
?access_token=<accessToken>
```
Response 200 with JSON body:
```
{}
```
For debugging, [cloudron exec](https://www.npmjs.com/package/cloudron) can be used to run the `curl` tool within the context of the app:
```
cloudron exec
> USERNAME=<enter username>
> PASSWORD=<enter password>
> PAYLOAD="{\"clientId\":\"${SIMPLE_AUTH_CLIENT_ID}\", \"username\":\"${USERNAME}\", \"password\":\"${PASSWORD}\"}"
> curl -H "Content-Type: application/json" -X POST -d "${PAYLOAD}" "${SIMPLE_AUTH_ORIGIN}/api/v1/login"
```
+44 -6
View File
@@ -62,7 +62,7 @@ curl -H "Content-Type: application/json" -H "Authorization: Bearer <token>" http
## OAuth
OAuth authentication is meant to be used by apps. An app can get an OAuth token using the
[oauth](addons.html#oauth) or [simpleauth](addons.html#simpleauth) addon.
[oauth](addons.html#oauth) addon.
Tokens obtained via OAuth have a restricted scope wherein they can only access the user's profile.
This restriction is so that apps cannot make undesired changes to the user's Cloudron.
@@ -199,7 +199,8 @@ Response (200):
health: <enum>, // health of the application
location: <string>, // subdomain on which app is installed
fqdn: <string>, // the FQDN of this app
altDomain: <string> // alternate domain from which this app can be reached
altDomain: <string>, // alternate domain from which this app can be reached
cnameTarget: <string> || null, // If altDomain is set, this contains the CNAME location for the app
accessRestriction: null || { // list of users and groups who can access this application
users: [ ],
groups: [ ]
@@ -209,7 +210,8 @@ Response (200):
portBindings: { // mapping from application ports to public ports
},
iconUrl: <url>, // a relative url providing the icon
memoryLimit: <number> // memory constraint in bytes
memoryLimit: <number>, // memory constraint in bytes
sso: <boolean> // Enable single sign-on
}
```
@@ -257,6 +259,8 @@ is integrated with Cloudron Authentication.
`manifest` is the [application manifest](/references/manifest.html).
For apps that support optional single sign-on, the `sso` field can be used to disable Cloudron authentication. By default, single sign-on is enabled.
### List apps
GET `/api/v1/apps/:appId` <scope>admin</scope>
@@ -278,7 +282,8 @@ Response (200):
health: <enum>, // health of the application
location: <string>, // subdomain on which app is installed
fqdn: <string>, // the FQDN of this app
altDomain: <string> // alternate domain from which this app can be reached
altDomain: <string>, // alternate domain from which this app can be reached
cnameTarget: <string> || null, // If altDomain is set, this contains the CNAME location for the app
accessRestriction: null || { // list of users and groups who can access this application
users: [ ],
groups: [ ]
@@ -654,6 +659,38 @@ curl -L <url> | openssl aes-256-cbc -d -pass "pass:$<backupKey>" | tar -zxf -
## Cloudron
### Activate the Cloudron
POST `/api/v1/cloudron/activate`
Activates the Cloudron with an admin username and password.
Request:
```
{
username: <string>, // the admin username
password: <string>, // the admin password
email: <email> // the admin email
}
```
Response (201):
```
{
"token": "771ee724a66aa557f95af06b4e6c27992f9230f6b1d65d5fbaa34cae9318d453",
"expires": 1490224113353
}
```
The `token` parameter can be used to make further API calls.
Curl example to activate the cloudron:
```
curl -X POST -H "Content-Type: application/json" -d '{"username": "girish", "password":"MySecret123#", "email": "girish@cloudron.io" }' https://my.cloudron.info/api/v1/cloudron/activate
```
### Update the Cloudron
POST `/api/v1/cloudron/update` <scope>admin</scope>
@@ -682,8 +719,9 @@ Gets information about an in-progress Cloudron update or backup.
`update` or `backup` is `null` when there is no such activity in progress.
```
Response (200):
```
{
update: null || { percent: <number>, message: <string> },
backup: null || { percent: <number>, message: <string> }
@@ -806,7 +844,7 @@ Response (200):
* user.remove
* user.update
`source` contains information on the originator of the action. For example, for user.login, this contains the IP address, the appId and the authType (ldap or simpleauth or oauth).
`source` contains information on the originator of the action. For example, for user.login, this contains the IP address, the appId and the authType (ldap or oauth).
`data` contains information on the event itself. For example, for user.login, this contains the userId that logged in. For app.install, it contains the manifest and location of the app that was installed.
-1
View File
@@ -29,7 +29,6 @@ Cloudron provides multiple authentication strategies.
* OAuth 2.0 provided by the [OAuth addon](/references/addons.html#oauth)
* LDAP provided by the [LDAP addon](/references/addons.html#ldap)
* Simple Auth provided by [Simple Auth addon](/references/addons.html#simpleauth)
# Choosing a strategy
+6
View File
@@ -47,12 +47,14 @@ Type: object
Required: no
Allowed keys
* [email](addons.html#email)
* [ldap](addons.html#ldap)
* [localstorage](addons.html#localstorage)
* [mongodb](addons.html#mongodb)
* [mysql](addons.html#mysql)
* [oauth](addons.html#oauth)
* [postgresql](addons.html#postgresql)
* [recvmail](addons.html#recvmail)
* [redis](addons.html#redis)
* [sendmail](addons.html#sendmail)
@@ -278,6 +280,10 @@ The intended use of this field is to display some post installation steps that t
complete the installation. For example, displaying the default admin credentials and informing the user to
to change it.
The message can have the following special tags:
* `<sso> ... </sso>` - Content in `sso` blocks are shown if SSO enabled.
* `<nosso> ... </nosso>`- Content in `nosso` blocks are shows when SSO is disabled.
## optionalSso
Type: boolean
+4 -4
View File
@@ -45,8 +45,9 @@ Please let us know if any of them requires tweaks or adjustments.
## Create server
Create an `Ubuntu 16.04 (Xenial)` server with at-least `1gb` RAM. Do not make any changes
to vanilla ubuntu. Be sure to allocate a static IPv4 address for your server.
Create an `Ubuntu 16.04 (Xenial)` server with at-least `1gb` RAM and 20GB disk space.
Do not make any changes to vanilla ubuntu. Be sure to allocate a static IPv4 address
for your server.
Cloudron has a built-in firewall and ports are opened and closed dynamically, as and when
apps are installed, re-configured or removed. For this reason, be sure to open all TCP and
@@ -265,8 +266,7 @@ reputation should be easy to get back.
* Linode - Follow this [guide](https://www.linode.com/docs/networking/dns/setting-reverse-dns).
* Scaleway - Edit your security group to allow email. You can also set a PTR record on the interface with your
`my.<domain>`.
* Scaleway - Edit your security group to allow email and [reboot the server](https://community.online.net/t/security-group-not-working/2096) for the change to take effect. You can also set a PTR record on the interface with your `my.<domain>`.
* Check if your IP is listed in any DNSBL list [here](http://multirbl.valli.org/). In most cases,
you can apply for removal of your IP by filling out a form at the DNSBL manager site.
+563 -749
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -17,7 +17,7 @@
"aws-sdk": "^2.1.46",
"body-parser": "^1.13.1",
"checksum": "^0.1.1",
"cloudron-manifestformat": "^2.6.0",
"cloudron-manifestformat": "^2.8.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^0.1.0",
"connect-timeout": "^1.5.0",
@@ -67,8 +67,7 @@
"tldjs": "^1.6.2",
"underscore": "^1.7.0",
"valid-url": "^1.0.9",
"validator": "^4.9.0",
"x509": "^0.2.4"
"validator": "^4.9.0"
},
"devDependencies": {
"bootstrap-sass": "^3.3.3",
+4 -3
View File
@@ -16,14 +16,14 @@ fi
readonly LOG_FILE="/var/log/cloudron-setup.log"
readonly DATA_FILE="/root/cloudron-install-data.json"
readonly MINIMUM_DISK_SIZE_GB="19" # this is the size of "/" and required to fit in docker images 19 is a safe bet for different reporting on 20GB min
readonly MINIMUM_MEMORY="980" # this is mostly reported for 1GB main memory (DO 992, EC2 990, Linode 989)
readonly MINIMUM_MEMORY="974" # this is mostly reported for 1GB main memory (DO 992, EC2 990, Linode 989, Serverdiscounter.com 974)
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
# copied from cloudron-resize-fs.sh
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
readonly disk_device="$(for d in $(find /dev -type b); do [ "$(mountpoint -d /)" = "$(mountpoint -x $d)" ] && echo $d && break; done)"
readonly disk_size_bytes=$(fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf $5 }')
readonly disk_size_bytes=$(LC_ALL=C fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf $5 }')
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024/1024))
# verify the system has minimum requirements met
@@ -97,6 +97,7 @@ if [[ -z "${dataJson}" ]]; then
echo "--provider is required (azure, digitalocean, ec2, lightsail, linode, ovh, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
+5
View File
@@ -11,6 +11,11 @@ readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
echo "Setting up nginx update page"
if [[ ! -f "${DATA_DIR}/nginx/applications/admin.conf" ]]; then
echo "No admin.conf found. This Cloudron has no domain yet. Skip splash setup"
exit
fi
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
# keep this is sync with config.js appFqdn()
+8 -7
View File
@@ -161,7 +161,7 @@ echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
# We listen on 0.0.0.0 because there is no way control ordering of docker (which creates the 172.18.0.0/16) and unbound
echo -e "server:\n\tinterface: 0.0.0.0\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow" > /etc/unbound/unbound.conf.d/cloudron-network.conf
echo -e "server:\n\tinterface: 0.0.0.0\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30" > /etc/unbound/unbound.conf.d/cloudron-network.conf
echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
@@ -194,15 +194,11 @@ mkdir -p "${DATA_DIR}/nginx/applications"
mkdir -p "${DATA_DIR}/nginx/cert"
cp "${script_dir}/start/nginx/nginx.conf" "${DATA_DIR}/nginx/nginx.conf"
cp "${script_dir}/start/nginx/mime.types" "${DATA_DIR}/nginx/mime.types"
if ! grep "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
# default nginx service file does not restart on crash
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
systemctl daemon-reload
fi
# This is here, since the splash screen needs this file to be present :-(
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
fi
systemctl start nginx
# bookkeep the version as part of data
@@ -320,9 +316,14 @@ if [[ ! -z "${arg_tls_config}" ]]; then
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
fi
echo "==> Generating dhparams (takes forever)"
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
fi
set_progress "60" "Starting Cloudron"
systemctl start cloudron.target
sleep 2 # give systemd sometime to start the processes
set_progress "90" "Done"
set_progress "90" "Almost done"
+2 -2
View File
@@ -13,10 +13,10 @@ disk_device="$(for d in $(find /dev -type b); do [ "$(mountpoint -d /)" = "$(mou
existing_swap=$(cat /proc/meminfo | grep SwapTotal | awk '{ printf "%.0f", $2/1024 }')
# all sizes are in mb
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
readonly swap_size=$((${physical_memory} - ${existing_swap})) # if you change this, fix enoughResourcesAvailable() in client.js
readonly app_count=$((${physical_memory} / 200)) # estimated app count
readonly disk_size_bytes=$(fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf $5 }') # can't rely on fdisk human readable units, using bytes instead
readonly disk_size_bytes=$(LC_ALL=C fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf $5 }') # can't rely on fdisk human readable units, using bytes instead
readonly disk_size=$((${disk_size_bytes}/1024/1024))
readonly system_size=10240 # 10 gigs for system libs, apps images, installer, box code, data and tmp
readonly ext4_reserved=$((disk_size * 5 / 100)) # this can be changes using tune2fs -m percent /dev/vda1
+8
View File
@@ -33,6 +33,14 @@ server {
# https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options
add_header X-Frame-Options "<%= xFrameOptions %>";
# https://github.com/twitter/secureheaders
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Compatibility_Matrix
# https://wiki.mozilla.org/Security/Guidelines/Web_Security
add_header X-XSS-Protection "1; mode=block";
add_header X-Download-Options "noopen";
add_header X-Content-Type-Options "nosniff";
add_header X-Permitted-Cross-Domain-Policies "none";
proxy_http_version 1.1;
proxy_intercept_errors on;
proxy_read_timeout 3500;
+3
View File
@@ -36,3 +36,6 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmbackup.sh
Defaults!/home/yellowtent/box/src/scripts/update.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/update.sh
Defaults!/home/yellowtent/box/src/scripts/authorized_keys.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/authorized_keys.sh
-51
View File
@@ -106,12 +106,6 @@ var KNOWN_ADDONS = {
teardown: NOOP,
backup: NOOP,
restore: NOOP
},
simpleauth: {
setup: setupSimpleAuth,
teardown: teardownSimpleAuth,
backup: NOOP,
restore: setupSimpleAuth
}
};
@@ -286,51 +280,6 @@ function teardownOauth(app, options, callback) {
});
}
function setupSimpleAuth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (!app.sso) return callback(null);
var appId = app.id;
var scope = 'profile';
clients.delByAppIdAndType(app.id, clients.TYPE_SIMPLE_AUTH, function (error) { // remove existing creds
if (error && error.reason !== ClientsError.NOT_FOUND) return callback(error);
clients.add(appId, clients.TYPE_SIMPLE_AUTH, '', scope, function (error, result) {
if (error) return callback(error);
var env = [
'SIMPLE_AUTH_SERVER=172.18.0.1',
'SIMPLE_AUTH_PORT=' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_URL=http://172.18.0.1:' + config.get('simpleAuthPort'), // obsolete, remove
'SIMPLE_AUTH_ORIGIN=http://172.18.0.1:' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_CLIENT_ID=' + result.id
];
debugApp(app, 'Setting simple auth addon config to %j', env);
appdb.setAddonConfig(appId, 'simpleauth', env, callback);
});
});
}
function teardownSimpleAuth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'teardownSimpleAuth');
clients.delByAppIdAndType(app.id, clients.TYPE_SIMPLE_AUTH, function (error) {
if (error && error.reason !== ClientsError.NOT_FOUND) debug(error);
appdb.unsetAddonConfig(app.id, 'simpleauth', callback);
});
}
function setupEmail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
+1 -1
View File
@@ -50,7 +50,7 @@ function setHealth(app, health, callback) {
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
if (app.debugMode) mailer.appDied(app); // do not send mails for dev apps
if (!app.debugMode) mailer.appDied(app); // do not send mails for dev apps
gHealthInfo[app.id].emailSent = true;
} else {
debugApp(app, 'waiting for sometime to update the app health');
+4 -2
View File
@@ -152,7 +152,6 @@ function validatePortBindings(portBindings, tcpPorts) {
config.get('sysadminPort'), /* sysadmin app server (lo) */
config.get('smtpPort'), /* internal smtp port (lo) */
config.get('ldapPort'), /* ldap server (lo) */
config.get('simpleAuthPort'), /* simple auth server (lo) */
3306, /* mysql (lo) */
4190, /* managesieve */
8000 /* graphite (lo) */
@@ -312,6 +311,7 @@ function get(appId, callback) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || config.appFqdn(app.location);
app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null;
callback(null, app);
});
@@ -330,6 +330,7 @@ function getByIpAddress(ip, callback) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || config.appFqdn(app.location);
app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null;
callback(null, app);
});
@@ -345,6 +346,7 @@ function getAll(callback) {
apps.forEach(function (app) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || config.appFqdn(app.location);
app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null;
});
callback(null, apps);
@@ -525,7 +527,7 @@ function install(data, auditSource, callback) {
if ('sso' in data && !('optionalSso' in manifest)) return callback(new AppsError(AppsError.BAD_FIELD, 'sso can only be specified for apps with optionalSso'));
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons['simpleauth'] || !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
+6 -3
View File
@@ -218,7 +218,7 @@ function registerSubdomain(app, overwrite, callback) {
assert.strictEqual(typeof overwrite, 'boolean');
assert.strictEqual(typeof callback, 'function');
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
@@ -257,7 +257,7 @@ function unregisterSubdomain(app, location, callback) {
return callback(null);
}
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
@@ -295,7 +295,7 @@ function waitForDnsPropagation(app, callback) {
return callback(null);
}
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
subdomains.waitForDns(config.appFqdn(app.location), ip, 'A', { interval: 5000, times: 120 }, callback);
@@ -523,6 +523,9 @@ function configure(app, callback) {
reserveHttpPort.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
registerSubdomain.bind(null, app, true /* overwrite */),
+21 -14
View File
@@ -43,8 +43,7 @@ var acme = require('./cert/acme.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
user = require('./user.js'),
util = require('util'),
x509 = require('x509');
util = require('util');
function CertificatesError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -268,16 +267,6 @@ function validateCertificate(cert, key, fqdn) {
if (!cert && key) return new Error('missing cert');
if (cert && !key) return new Error('missing key');
var content;
try {
content = x509.parseCert(cert);
} catch (e) {
return new Error('invalid cert: ' + e.message);
}
// check expiration
if (content.notAfter < new Date()) return new Error('cert expired');
function matchesDomain(domain) {
if (typeof domain !== 'string') return false;
if (domain === fqdn) return true;
@@ -286,8 +275,22 @@ function validateCertificate(cert, key, fqdn) {
return false;
}
// check domain
var domains = content.altNames.concat(content.subject.commonName);
// get commonName (http://stackoverflow.com/questions/17353122/parsing-strings-crt-files)
var result = safe.child_process.execSync('openssl x509 -noout -subject | sed -r "s|.*CN=(.*)|\\1|; s|/[^/]*=.*$||"', { encoding: 'utf8', input: cert });
if (!result) return new Error(util.format('could not get CN'));
var commonName = result.trim();
debug('validateCertificate: detected commonName as %s', commonName);
// https://github.com/drwetter/testssl.sh/pull/383
var cmd = `openssl x509 -noout -text | grep -A3 "Subject Alternative Name" | \
grep "DNS:" | \
sed -e "s/DNS://g" -e "s/ //g" -e "s/,/ /g" -e "s/othername:<unsupported>//g"`;
result = safe.child_process.execSync(cmd, { encoding: 'utf8', input: cert });
var altNames = result ? [ ] : result.trim().split(' '); // might fail if cert has no SAN
debug('validateCertificate: detected altNames as %j', altNames);
// check altNames
var domains = altNames.concat(commonName);
if (!domains.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, domains));
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
@@ -295,6 +298,10 @@ function validateCertificate(cert, key, fqdn) {
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (certModulus !== keyModulus) return new Error('key does not match the cert');
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
if (!result) return new Error('cert expired');
return null;
}
-2
View File
@@ -32,7 +32,6 @@ exports = module.exports = {
TYPE_EXTERNAL: 'external',
TYPE_BUILT_IN: 'built-in',
TYPE_OAUTH: 'addon-oauth',
TYPE_SIMPLE_AUTH: 'addon-simpleauth',
TYPE_PROXY: 'addon-proxy'
};
@@ -192,7 +191,6 @@ function getAll(callback) {
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
if (record.type === exports.TYPE_SIMPLE_AUTH) record.name = result.manifest.title + ' Simple Auth';
record.location = result.location;
+5 -4
View File
@@ -242,7 +242,8 @@ function configureDefaultServer(callback) {
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
debug('configureDefaultServer: create new cert');
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, 'cloudron');
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
safe.child_process.execSync(certCommand);
}
@@ -264,7 +265,7 @@ function configureAdmin(callback) {
debug('configureAdmin');
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
subdomains.waitForDns(config.adminFqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
@@ -621,7 +622,7 @@ function addDnsRecords(callback) {
var dkimKey = readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
var webadminRecord = { subdomain: constants.ADMIN_LOCATION, type: 'A', values: [ ip ] };
@@ -957,7 +958,7 @@ function migrate(options, callback) {
function refreshDNS(callback) {
callback = callback || NOOP_CALLBACK;
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('refreshDNS: current ip %s', ip);
-1
View File
@@ -84,7 +84,6 @@ function initConfig() {
data.smtpPort = 2525; // // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.simpleAuthPort = 3004;
data.provider = 'caas';
data.appBundle = [ ];
+2 -1
View File
@@ -8,7 +8,7 @@ exports = module.exports = {
getAllPaged: getAllPaged,
cleanup: cleanup,
// keep in sync with webadmin index.js filter
// keep in sync with webadmin index.js filter and CLI tool
ACTION_ACTIVATE: 'cloudron.activate',
ACTION_APP_CLONE: 'app.clone',
ACTION_APP_CONFIGURE: 'app.configure',
@@ -16,6 +16,7 @@ exports = module.exports = {
ACTION_APP_RESTORE: 'app.restore',
ACTION_APP_UNINSTALL: 'app.uninstall',
ACTION_APP_UPDATE: 'app.update',
ACTION_APP_LOGIN: 'app.login',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
+3 -3
View File
@@ -5,8 +5,8 @@
// Do not require anything here!
exports = module.exports = {
// a version bump means that all containers (apps and addons) are recreated
'version': 45,
// a version bump means that all app containers are recreated
'version': 46,
'baseImages': [ 'cloudron/base:0.10.0' ],
@@ -17,7 +17,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.16.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.12.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.30.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.30.3' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.11.0' }
}
};
+33 -7
View File
@@ -7,6 +7,7 @@ exports = module.exports = {
var assert = require('assert'),
apps = require('./apps.js'),
async = require('async'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:ldap'),
@@ -15,8 +16,7 @@ var assert = require('assert'),
UserError = user.UserError,
ldap = require('ldapjs'),
mailboxdb = require('./mailboxdb.js'),
safe = require('safetydance'),
util = require('util');
safe = require('safetydance');
var gServer = null;
@@ -26,6 +26,9 @@ var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
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 callback(new ldap.InsufficientAccessRightsError('Missing source identifier'));
@@ -38,14 +41,36 @@ function getAppByRequest(req, callback) {
});
}
function getUsersWithAccessToApp(req, callback) {
assert.strictEqual(typeof req, 'object');
assert.strictEqual(typeof callback, 'function');
getAppByRequest(req, function (error, app) {
if (error) return callback(error);
user.list(function (error, result){
if (error) return callback(new ldap.OperationsError(error.toString()));
async.filter(result, apps.hasAccessTo.bind(null, app), function (error, result) {
if (error) return callback(new ldap.OperationsError(error.toString()));
callback(null, result);
});
});
});
}
function userSearch(req, res, next) {
debug('user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
user.list(function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
getUsersWithAccessToApp(req, function (error, result) {
if (error) return next(error);
// send user objects
result.forEach(function (entry) {
// skip entries with empty username. Some apps like owncloud can't deal with this
if (!entry.username) return;
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
var groups = [ GROUP_USERS_DN ];
@@ -69,6 +94,7 @@ function userSearch(req, res, next) {
givenName: firstName,
username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
isadmin: entry.admin ? 1 : 0,
memberof: groups
}
};
@@ -93,8 +119,8 @@ function userSearch(req, res, next) {
function groupSearch(req, res, next) {
debug('group search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
user.list(function (error, result){
if (error) return next(new ldap.OperationsError(error.toString()));
getUsersWithAccessToApp(req, function (error, result) {
if (error) return next(error);
var groups = [{
name: 'users',
@@ -293,7 +319,7 @@ function authenticateMailbox(req, res, next) {
if (mailbox.ownerType === mailboxdb.TYPE_APP) {
if (req.credentials !== mailbox.ownerId) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId });
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId });
return res.end();
}
+5 -3
View File
@@ -2,10 +2,12 @@
Dear Cloudron Admin,
The <%= program %> on <%= fqdn %> exited unexpectedly!
<%= program %> on <%= fqdn %> exited unexpectedly using too much memory!
The program has been restarted but should this message appear repeatedly,
you should give the program more memory.
The app has been restarted now. Should this message appear repeatedly or
undefined behavior is observed, give the app more memory.
This can be done in the advanced settings in the app configuration dialog
in your Cloudron's web interface.
Please see some excerpt of the logs below.
+6 -5
View File
@@ -37,7 +37,6 @@ var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
debug = require('debug')('box:mailer'),
dns = require('native-dns'),
docker = require('./docker.js').connection,
ejs = require('ejs'),
nodemailer = require('nodemailer'),
@@ -346,7 +345,7 @@ function appDied(app) {
var mailOptions = {
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.concat('support@cloudron.io').join(', '),
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] App %s is down', config.fqdn(), app.fqdn),
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
};
@@ -442,7 +441,7 @@ function backupFailed(error) {
var mailOptions = {
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.concat('support@cloudron.io').join(', '),
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Failed to backup', config.fqdn()),
text: render('backup_failed.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
};
@@ -460,7 +459,7 @@ function certificateRenewalError(domain, message) {
var mailOptions = {
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.concat('support@cloudron.io').join(', '),
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Certificate renewal error', domain),
text: render('certificate_renewal_error.ejs', { domain: domain, message: message, format: 'text' })
};
@@ -478,7 +477,7 @@ function oomEvent(program, context) {
var mailOptions = {
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.concat('support@cloudron.io').join(', '),
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
text: render('oom_event.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
};
@@ -494,6 +493,8 @@ function unexpectedExit(program, context, callback) {
assert.strictEqual(typeof context, 'string');
assert.strictEqual(typeof callback, 'function');
if (config.provider() !== 'caas') return callback(); // no way to get admins without db access
var mailOptions = {
from: mailConfig().from,
to: 'support@cloudron.io',
+48
View File
@@ -0,0 +1,48 @@
<% include header %>
<!-- tester -->
<script>
'use strict';
// very basic angular app
var app = angular.module('Application', []);
app.controller('Controller', ['$scope', function ($scope) {
$scope.success = <%= success %>;
$scope.error = '<%= error %>';
}]);
</script>
<div class="container" ng-app="Application" ng-controller="Controller" ng-cloak>
<div class="row">
<div class="col-md-12 text-center">
<br/>
<h4 ng-hide="success">Hello there, welcome to <%= cloudronName %>.</h4>
<h2 ng-hide="success">Sign up with your email address.</h2>
<h3 ng-show="success">You have received an email invitation to this Cloudron to finish the signup.</h3>
<br/><br/>
</div>
</div>
<div class="row">
<div class="col-md-6 col-md-offset-3" ng-show="!success">
<form action="/api/v1/session/account/create" method="post" name="createForm" autocomplete="off" role="form" novalidate>
<input type="password" style="display: none;">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group" ng-class="{ 'has-error': (createForm.email.$dirty && createForm.email.$invalid) || (!createForm.email.$dirty && error) }">
<label class="control-label" for="inputEmail">Email</label>
<input type="email" class="form-control" id="inputEmail" ng-model="email" name="email" autofocus required>
<div class="control-label" ng-show="(createForm.email.$dirty && createForm.email.$invalid) || (!createForm.email.$dirty && error)">
<small ng-show="createForm.email.$dirty && createForm.email.$invalid">Must be a valid email address</small>
<small ng-show="!createForm.email.$dirty && error">{{ error }}</small>
</div>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="createForm.$invalid"/>
</form>
</div>
</div>
</div>
<% include footer %>
+4 -4
View File
@@ -32,19 +32,19 @@ app.controller('Controller', ['$scope', function ($scope) {
<center><p class="has-error"><%= error %></p></center>
<% if (user && user.username) { %>
<div class="form-group"">
<div class="form-group">
<label class="control-label">Username</label>
<input type="text" class="form-control" ng-model="username" name="username" readonly required>
</div>
<% } else { %>
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) }">
<label class="control-label">Username</label>
<input type="text" class="form-control" ng-model="username" name="username" required autofocus>
<div class="control-label" ng-show="setupForm.username.$dirty && setupForm.username.$invalid">
<small ng-show="setupForm.username.$error.minlength">The username is too short</small>
<small ng-show="setupForm.username.$error.maxlength">The username is too long</small>
<small ng-show="setupForm.username.$dirty && setupForm.username.$invalid">Not a valid username</small>
</div>
<input type="text" class="form-control" ng-model="username" name="username" required autofocus>
</div>
<% } %>
@@ -55,18 +55,18 @@ app.controller('Controller', ['$scope', function ($scope) {
<div class="form-group" ng-class="{ 'has-error': (setupForm.password.$dirty && setupForm.password.$invalid) }">
<label class="control-label">New Password</label>
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
</div>
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
<label class="control-label">Repeat Password</label>
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
<div class="control-label" ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
<small ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
</div>
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
+2 -2
View File
@@ -26,17 +26,17 @@ app.controller('Controller', [function () {}]);
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
<label class="control-label" for="inputPassword">New Password</label>
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
<div class="control-label" ng-show="resetForm.password.$dirty && resetForm.password.$invalid">
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
</div>
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
</div>
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
<div class="control-label" ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
<small ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
</div>
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>
</form>
+15 -11
View File
@@ -67,7 +67,7 @@ function start(callback) {
// short-circuit for the restart case
if (_.isEqual(infra, existingInfra)) {
debug('platform is uptodate at version %s', infra.version);
process.nextTick(function () { exports.events.emit(exports.EVENT_READY); });
emitPlatformReady();
return callback();
}
@@ -82,14 +82,7 @@ function start(callback) {
], function (error) {
if (error) return callback(error);
// give 30 seconds for the platform to "settle". For example, mysql might still be initing the
// database dir and we cannot call service scripts until that's done.
// TODO: make this smarter to not wait for 30secs for the crash-restart case
gPlatformReadyTimer = setTimeout(function () {
debug('emitting platform ready');
gPlatformReadyTimer = null;
exports.events.emit(exports.EVENT_READY);
}, 30000);
emitPlatformReady();
callback();
});
@@ -104,6 +97,17 @@ function uninitialize(callback) {
callback();
}
function emitPlatformReady() {
// give 30 seconds for the platform to "settle". For example, mysql might still be initing the
// database dir and we cannot call service scripts until that's done.
// TODO: make this smarter to not wait for 30secs for the crash-restart case
gPlatformReadyTimer = setTimeout(function () {
debug('emitting platform ready');
gPlatformReadyTimer = null;
exports.events.emit(exports.EVENT_READY);
}, 30000);
}
function removeOldImages(callback) {
debug('removing old addon images');
@@ -241,7 +245,8 @@ function createMailConfig(callback) {
const alertsFrom = 'no-reply@' + config.fqdn();
user.getOwner(function (error, owner) {
var alertsTo = [ 'webmaster@cloudron.io' ].concat(error ? [] : owner.email).join(',');
var alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
alertsTo.concat(error ? [] : owner.email).join(',');
if (!safe.fs.writeFileSync(paths.DATA_DIR + '/addons/mail/mail.ini',
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}`, 'utf8')) {
@@ -342,4 +347,3 @@ function startApps(existingInfra, callback) {
apps.configureInstalledApps(callback);
}
}
+1
View File
@@ -54,6 +54,7 @@ function removeInternalAppFields(app) {
fqdn: app.fqdn,
memoryLimit: app.memoryLimit,
altDomain: app.altDomain,
cnameTarget: app.cnameTarget,
xFrameOptions: app.xFrameOptions,
sso: app.sso,
debugMode: app.debugMode
+22 -22
View File
@@ -24,8 +24,6 @@ var assert = require('assert'),
HttpSuccess = require('connect-lastmile').HttpSuccess,
progress = require('../progress.js'),
mailer = require('../mailer.js'),
settings = require('../settings.js'),
SettingsError = settings.SettingsError,
superagent = require('superagent'),
updateChecker = require('../updatechecker.js'),
_ = require('underscore');
@@ -35,18 +33,8 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
/**
* Creating an admin user and activate the cloudron.
*
* @apiParam {string} username The administrator's user name
* @apiParam {string} password The administrator's password
* @apiParam {string} email The administrator's email address
*
* @apiSuccess (Created 201) {string} token A valid access token
*/
function activate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.query.setupToken, 'string');
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
@@ -101,21 +89,33 @@ function dnsSetup(req, res, next) {
function setupTokenAuth(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
// skip setupToken auth for non caas case
if (config.provider() !== 'caas') return next();
if (config.provider() === 'caas') {
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
if (typeof req.query.setupToken !== 'string') return next(new HttpError(400, 'no setupToken provided'));
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken })
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
next();
});
} else if (config.provider() === 'ami') {
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
superagent.get('http://169.254.169.254/latest/meta-data/instance-id').timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode !== 200) return next(new HttpError(500, 'Unable to get meta data'));
if (result.text !== req.query.setupToken) return next(new HttpError(403, 'Invalid token'));
next();
});
} else {
next();
});
}
}
function getStatus(req, res, next) {
+1
View File
@@ -13,5 +13,6 @@ exports = module.exports = {
profile: require('./profile.js'),
sysadmin: require('./sysadmin.js'),
settings: require('./settings.js'),
ssh: require('./ssh.js'),
user: require('./user.js')
};
+84 -3
View File
@@ -11,6 +11,7 @@ var appdb = require('../appdb'),
DatabaseError = require('../databaseerror'),
debug = require('debug')('box:routes/oauth2'),
eventlog = require('../eventlog.js'),
generatePassword = require('../password.js').generate,
hat = require('hat'),
HttpError = require('connect-lastmile').HttpError,
middleware = require('../middleware/index.js'),
@@ -233,7 +234,6 @@ function loginForm(req, res) {
switch (result.type) {
case clients.TYPE_BUILT_IN: return renderBuiltIn();
case clients.TYPE_EXTERNAL: return render(result.appId, '/api/v1/cloudron/avatar');
case clients.TYPE_SIMPLE_AUTH: return sendError(req, res, 'Unknown OAuth client');
default: break;
}
@@ -358,6 +358,87 @@ function accountSetup(req, res, next) {
});
}
// -> POST /api/v1/session/account/setup
function accountSetup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'Missing username'));
if (typeof req.body.displayName !== 'string') return next(new HttpError(400, 'Missing displayName'));
debug('accountSetup: with token %s.', req.body.resetToken);
user.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return sendError(req, res, 'Invalid Reset Token');
var data = _.pick(req.body, 'username', 'displayName');
user.update(userObject.id, data, auditSource(req), function (error) {
if (error && error.reason === UserError.ALREADY_EXISTS) return renderAccountSetupSite(res, req, userObject, 'Username already exists');
if (error && error.reason === UserError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error && error.reason === UserError.NOT_FOUND) return renderAccountSetupSite(res, req, userObject, 'No such user');
if (error) return next(new HttpError(500, error));
userObject.username = req.body.username;
userObject.displayName = req.body.displayName;
// setPassword clears the resetToken
user.setPassword(userObject.id, req.body.password, function (error, result) {
if (error && error.reason === UserError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error) return next(new HttpError(500, error));
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
});
});
});
}
function renderAccountCreateSite(res, req, error, success) {
renderTemplate(res, 'account_create', {
error: error,
success: !!success,
csrf: req.csrfToken(),
title: 'Account Create'
});
}
// -> GET /api/v1/session/account/create.html
function accountCreateSite(req, res, next) {
settings.getOpenRegistration(function (error, enabled) {
if (error) return next(new HttpError(500, error));
if (!enabled) return sendError(req, res, 'User creation is not allowed on this Cloudron');
renderAccountCreateSite(res, req, '', '');
});
}
// -> POST /api/v1/session/account/create
function accountCreate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'Missing email'));
debug('accountCreate: with email %s.', req.body.email);
settings.getOpenRegistration(function (error, enabled) {
if (error) return next(new HttpError(500, error));
if (!enabled) return sendError(req, res, 'User signup is not allowed on this Cloudron');
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
var auditSource = { ip: ip, username: req.body.email, userId: null };
user.create(null, generatePassword(), req.body.email, '', auditSource, { sendInvite: true }, function (error, result) {
if (error && error.reason === UserError.ALREADY_EXISTS) return renderAccountCreateSite(res, req, 'User with this email address already exists');
if (error) return sendError(req, res, 'Internal Error');
debug('accountCreate: success for email %s now with id %s', req.body.remail, result.id);
renderAccountCreateSite(res, req, '', true);
});
});
}
// -> GET /api/v1/session/password/reset.html
function passwordResetSite(req, res, next) {
if (!req.query.reset_token) return next(new HttpError(400, 'Missing reset_token'));
@@ -450,8 +531,6 @@ var authorization = [
if (type === clients.TYPE_EXTERNAL || type === clients.TYPE_BUILT_IN) {
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id });
return next();
} else if (type === clients.TYPE_SIMPLE_AUTH) {
return sendError(req, res, 'Unknown OAuth client.');
}
appdb.get(req.oauth2.client.appId, function (error, appObject) {
@@ -558,6 +637,8 @@ exports = module.exports = {
passwordReset: passwordReset,
accountSetupSite: accountSetupSite,
accountSetup: accountSetup,
accountCreateSite: accountCreateSite,
accountCreate: accountCreate,
authorization: authorization,
token: token,
validateRequestedScopes: validateRequestedScopes,
+27 -3
View File
@@ -10,7 +10,7 @@ exports = module.exports = {
getCloudronAvatar: getCloudronAvatar,
setCloudronAvatar: setCloudronAvatar,
getEmailDnsRecords: getEmailDnsRecords,
getEmailStatus: getEmailStatus,
getDnsConfig: getDnsConfig,
setDnsConfig: setDnsConfig,
@@ -27,6 +27,9 @@ exports = module.exports = {
getAppstoreConfig: getAppstoreConfig,
setAppstoreConfig: setAppstoreConfig,
getOpenRegistration: getOpenRegistration,
setOpenRegistration: setOpenRegistration,
setFallbackCertificate: setFallbackCertificate,
setAdminCertificate: setAdminCertificate
};
@@ -150,8 +153,8 @@ function getCloudronAvatar(req, res, next) {
});
}
function getEmailDnsRecords(req, res, next) {
settings.getEmailDnsRecords(function (error, records) {
function getEmailStatus(req, res, next) {
settings.getEmailStatus(function (error, records) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, records));
@@ -234,6 +237,27 @@ function setAppstoreConfig(req, res, next) {
});
}
function getOpenRegistration(req, res, next) {
settings.getOpenRegistration(function (error, enabled) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { enabled: enabled }));
});
}
function setOpenRegistration(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
settings.setOpenRegistration(req.body.enabled, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
// default fallback cert
function setFallbackCertificate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
+54
View File
@@ -0,0 +1,54 @@
'use strict';
exports = module.exports = {
getAuthorizedKeys: getAuthorizedKeys,
getAuthorizedKey: getAuthorizedKey,
addAuthorizedKey: addAuthorizedKey,
delAuthorizedKey: delAuthorizedKey
};
var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
ssh = require('../ssh.js'),
SshError = ssh.SshError;
function getAuthorizedKeys(req, res, next) {
ssh.getAuthorizedKeys(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { keys: result }));
});
}
function getAuthorizedKey(req, res, next) {
assert.strictEqual(typeof req.params.identifier, 'string');
ssh.getAuthorizedKey(req.params.identifier, function (error, result) {
if (error && error.reason === SshError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { identifier: result.identifier, key: result.key }));
});
}
function addAuthorizedKey(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.key !== 'string' || !req.body.key) return next(new HttpError(400, 'key must be a non empty'));
ssh.addAuthorizedKey(req.body.key, function (error) {
if (error && error.reason === SshError.INVALID_KEY) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function delAuthorizedKey(req, res, next) {
assert.strictEqual(typeof req.params.identifier, 'string');
ssh.delAuthorizedKey(req.params.identifier, function (error) {
if (error && error.reason === SshError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
+1 -4
View File
@@ -30,7 +30,6 @@ var appdb = require('../../appdb.js'),
safe = require('safetydance'),
server = require('../../server.js'),
settings = require('../../settings.js'),
simpleauth = require('../../simpleauth.js'),
superagent = require('superagent'),
taskmanager = require('../../taskmanager.js'),
tokendb = require('../../tokendb.js'),
@@ -42,7 +41,7 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '19.0.0';
var TEST_IMAGE_TAG = '20.0.0';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
@@ -174,7 +173,6 @@ function startBox(done) {
server.start.bind(server),
ldap.start,
simpleauth.start,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
@@ -257,7 +255,6 @@ function stopBox(done) {
appdb._clear,
server.stop,
ldap.stop,
simpleauth.stop,
config._reset
], done);
}
-59
View File
@@ -273,16 +273,6 @@ describe('OAuth2', function () {
scope: 'profile'
};
// simple auth client
var CLIENT_8 = {
id: 'cid-client8',
appId: APP_2.id,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'secret8',
redirectURI: 'http://redirect8',
scope: 'profile'
};
// app with accessRestriction allowing group
var CLIENT_9 = {
id: 'cid-client9',
@@ -311,7 +301,6 @@ describe('OAuth2', function () {
clientdb.add.bind(null, CLIENT_5.id, CLIENT_5.appId, CLIENT_5.type, CLIENT_5.clientSecret, CLIENT_5.redirectURI, CLIENT_5.scope),
clientdb.add.bind(null, CLIENT_6.id, CLIENT_6.appId, CLIENT_6.type, CLIENT_6.clientSecret, CLIENT_6.redirectURI, CLIENT_6.scope),
clientdb.add.bind(null, CLIENT_7.id, CLIENT_7.appId, CLIENT_7.type, CLIENT_7.clientSecret, CLIENT_7.redirectURI, CLIENT_7.scope),
clientdb.add.bind(null, CLIENT_8.id, CLIENT_8.appId, CLIENT_8.type, CLIENT_8.clientSecret, CLIENT_8.redirectURI, CLIENT_8.scope),
clientdb.add.bind(null, CLIENT_9.id, CLIENT_9.appId, CLIENT_9.type, CLIENT_9.clientSecret, CLIENT_9.redirectURI, CLIENT_9.scope),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1),
@@ -557,24 +546,6 @@ describe('OAuth2', function () {
});
});
});
it('fails when using simple auth credentials', function (done) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_8.redirectURI + '&client_id=' + CLIENT_8.id + '&response_type=code';
request.get(url, { jar: true }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body).to.eql('<script>window.location.href = "/api/v1/session/login?returnTo=' + CLIENT_8.redirectURI + '";</script>');
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_8.redirectURI, { jar: true, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(body.indexOf('Unknown OAuth client')).to.not.equal(-1);
done();
});
});
});
});
describe('loginForm submit', function () {
@@ -796,21 +767,6 @@ describe('OAuth2', function () {
});
});
it('fails for grant type code due to simple auth credentials', function (done) {
startAuthorizationFlow(CLIENT_7, 'code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_8.redirectURI + '&client_id=' + CLIENT_8.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(body.indexOf('Unknown OAuth client.')).to.not.equal(-1);
done();
});
});
});
it('succeeds for grant type code with accessRestriction', function (done) {
startAuthorizationFlow(CLIENT_7, 'code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_7.redirectURI + '&client_id=' + CLIENT_7.id + '&response_type=code';
@@ -858,21 +814,6 @@ describe('OAuth2', function () {
});
});
it('fails for grant type token due to simple auth credentials', function (done) {
startAuthorizationFlow(CLIENT_7, 'token', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_8.redirectURI + '&client_id=' + CLIENT_8.id + '&response_type=token';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(body.indexOf('Unknown OAuth client.')).to.not.equal(-1);
done();
});
});
});
it('succeeds for grant type token', function (done) {
startAuthorizationFlow(CLIENT_7, 'token', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_7.redirectURI + '&client_id=' + CLIENT_7.id + '&response_type=token';
+41
View File
@@ -770,4 +770,45 @@ describe('Settings API', function () {
});
});
});
describe('open_registration', function () {
it('get open_registration succeeds without being set', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/open_registration')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.enabled).to.equal(false);
done();
});
});
it('cannot set without data', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/open_registration')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/open_registration')
.query({ access_token: token })
.send({ enabled: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/open_registration')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.enabled).to.equal(true);
done();
});
});
});
});
-512
View File
@@ -1,512 +0,0 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var appdb = require('../../appdb.js'),
async = require('async'),
clientdb = require('../../clientdb.js'),
clients = require('../../clients.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
server = require('../../server.js'),
simpleauth = require('../../simpleauth.js'),
nock = require('nock'),
settings = require('../../settings.js');
describe('SimpleAuth API', function () {
var SERVER_URL = 'http://localhost:' + config.get('port');
var SIMPLE_AUTH_ORIGIN = 'http://localhost:' + config.get('simpleAuthPort');
var USERNAME = 'superaDMin', PASSWORD = 'Foobar?1337', EMAIL ='silly@ME.com';
var APP_0 = {
id: 'app0',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test0',
portBindings: {},
accessRestriction: { users: [ 'foobar', 'someone'] },
memoryLimit: 0,
altDomain: null
};
var APP_1 = {
id: 'app1',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test1',
portBindings: {},
accessRestriction: { users: [ 'foobar', 'someone' ] },
memoryLimit: 0,
altDomain: null
};
var APP_2 = {
id: 'app2',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test2',
portBindings: {},
accessRestriction: null,
memoryLimit: 0,
altDomain: null
};
var APP_3 = {
id: 'app3',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test3',
portBindings: {},
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
memoryLimit: 0,
altDomain: null
};
var CLIENT_0 = {
id: 'someclientid',
appId: 'someappid',
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_1 = {
id: 'someclientid1',
appId: APP_0.id,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret1',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_2 = {
id: 'someclientid2',
appId: APP_1.id,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret2',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_3 = {
id: 'someclientid3',
appId: APP_2.id,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret3',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_4 = {
id: 'someclientid4',
appId: APP_2.id,
type: clients.TYPE_OAUTH,
clientSecret: 'someclientsecret4',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_5 = {
id: 'someclientid5',
appId: APP_3.id,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret5',
redirectURI: '',
scope: 'user,profile'
};
before(function (done) {
async.series([
server.start.bind(server),
simpleauth.start.bind(simpleauth),
database._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: result.body.token}).end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.eql(200);
APP_1.accessRestriction.users.push(result.body.id);
callback();
});
});
},
clientdb.add.bind(null, CLIENT_0.id, CLIENT_0.appId, CLIENT_0.type, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope),
clientdb.add.bind(null, CLIENT_1.id, CLIENT_1.appId, CLIENT_1.type, CLIENT_1.clientSecret, CLIENT_1.redirectURI, CLIENT_1.scope),
clientdb.add.bind(null, CLIENT_2.id, CLIENT_2.appId, CLIENT_2.type, CLIENT_2.clientSecret, CLIENT_2.redirectURI, CLIENT_2.scope),
clientdb.add.bind(null, CLIENT_3.id, CLIENT_3.appId, CLIENT_3.type, CLIENT_3.clientSecret, CLIENT_3.redirectURI, CLIENT_3.scope),
clientdb.add.bind(null, CLIENT_4.id, CLIENT_4.appId, CLIENT_4.type, CLIENT_4.clientSecret, CLIENT_4.redirectURI, CLIENT_4.scope),
clientdb.add.bind(null, CLIENT_5.id, CLIENT_5.appId, CLIENT_5.type, CLIENT_5.clientSecret, CLIENT_5.redirectURI, CLIENT_5.scope),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3),
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
], done);
});
after(function (done) {
async.series([
database._clear,
simpleauth.stop.bind(simpleauth),
server.stop.bind(server)
], done);
});
describe('login', function () {
it('cannot login without clientId', function (done) {
var body = {};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot login without username', function (done) {
var body = {
clientId: 'someclientid'
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot login without password', function (done) {
var body = {
clientId: 'someclientid',
username: USERNAME
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot login with unkown clientId', function (done) {
var body = {
clientId: CLIENT_0.id+CLIENT_0.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot login with unkown user', function (done) {
var body = {
clientId: CLIENT_0.id,
username: USERNAME+USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot login with empty password', function (done) {
var body = {
clientId: CLIENT_0.id,
username: USERNAME,
password: ''
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot login with wrong password', function (done) {
var body = {
clientId: CLIENT_0.id,
username: USERNAME,
password: PASSWORD+PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails for unkown app', function (done) {
var body = {
clientId: CLIENT_0.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails for disallowed app', function (done) {
var body = {
clientId: CLIENT_1.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds for allowed app', function (done) {
var body = {
clientId: CLIENT_2.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.displayName).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME.toLowerCase());
expect(result.body.email).to.eql(EMAIL.toLowerCase());
done();
});
});
});
it('succeeds for allowed app with email', function (done) {
var body = {
clientId: CLIENT_2.id,
username: EMAIL,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.displayName).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME.toLowerCase());
expect(result.body.email).to.eql(EMAIL.toLowerCase());
done();
});
});
});
it('succeeds for app without accessRestriction', function (done) {
var body = {
clientId: CLIENT_3.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.displayName).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME.toLowerCase());
expect(result.body.email).to.eql(EMAIL.toLowerCase());
done();
});
});
});
it('succeeds for app with group accessRestriction', function (done) {
var body = {
clientId: CLIENT_5.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.displayName).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME.toLowerCase());
expect(result.body.email).to.eql(EMAIL.toLowerCase());
done();
});
});
});
it('fails for wrong client credentials', function (done) {
var body = {
clientId: CLIENT_4.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
});
describe('logout', function () {
var accessToken;
before(function (done) {
var body = {
clientId: CLIENT_3.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
accessToken = result.body.accessToken;
done();
});
});
it('fails without access_token', function (done) {
superagent.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with unkonwn access_token', function (done) {
superagent.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.query({ access_token: accessToken+accessToken })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.query({ access_token: accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: accessToken })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
});
});
});
+240
View File
@@ -0,0 +1,240 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var ssh = require('../../ssh.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
server = require('../../server.js'),
nock = require('nock');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var INVALID_KEY_TYPE = 'ssh-foobar AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N nebulon@nebulon';
var INVALID_KEY_VALUE = 'ssh-rsa foobar nebulon@nebulon';
var INVALID_KEY_IDENTIFIER = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N';
var VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N nebulon@nebulon';
var VALID_KEY_1 = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N muchmore';
var token = null;
var server;
function setup(done) {
config.set('fqdn', 'foobar.com');
async.series([
server.start.bind(server),
ssh._clear,
database._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('SSH API', function () {
this.timeout(10000);
before(setup);
after(cleanup);
describe('add authorized_keys', function () {
it('fails due to missing key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to empty key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: 'foobar' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key type', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_TYPE })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key value', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_VALUE })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key identifier', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_IDENTIFIER })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: VALID_KEY })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
describe('get authorized_keys', function () {
it('fails for non existing key', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.identifier).to.be.a('string');
expect(res.body.identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.key).to.equal(VALID_KEY);
done();
});
});
});
describe('list authorized_keys', function () {
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.keys).to.be.an('array');
expect(res.body.keys.length).to.equal(1);
expect(res.body.keys[0]).to.be.an('object');
expect(res.body.keys[0].identifier).to.be.a('string');
expect(res.body.keys[0].identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.keys[0].key).to.equal(VALID_KEY);
done();
});
});
it('succeeds with two keys', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: VALID_KEY_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.keys).to.be.an('array');
expect(res.body.keys.length).to.equal(2);
expect(res.body.keys[0]).to.be.an('object');
expect(res.body.keys[0].identifier).to.be.a('string');
expect(res.body.keys[0].identifier).to.equal(VALID_KEY_1.split(' ')[2]);
expect(res.body.keys[0].key).to.equal(VALID_KEY_1);
expect(res.body.keys[1]).to.be.an('object');
expect(res.body.keys[1].identifier).to.be.a('string');
expect(res.body.keys[1].identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.keys[1].key).to.equal(VALID_KEY);
done();
});
});
});
});
describe('delete authorized_keys', function () {
it('fails for non existing key', function (done) {
superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
});
});
});
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
# verify argument count
if [[ $# -lt 3 ]]; then
echo "Usage: authorized_keys.sh <user> <source> <destination>"
exit 1
fi
if [[ -f "$2" ]]; then
cp "$2" "$3"
chown "$1":"$1" "$3"
fi
+1 -1
View File
@@ -21,7 +21,7 @@ readonly program_name=$1
echo "${program_name}.log"
echo "-------------------"
journalctl --all --no-pager -u ${program_name} -n 300
journalctl --all --no-pager -u ${program_name} -n 800
echo
echo
echo "dmesg"
+9 -2
View File
@@ -105,6 +105,10 @@ function initializeExpressSync() {
router.post('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.reboot);
router.post('/api/v1/cloudron/migrate', cloudronScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate);
router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.graphs.getGraphs);
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.user.requireAdmin, routes.ssh.getAuthorizedKeys);
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.user.requireAdmin, routes.ssh.addAuthorizedKey);
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.user.requireAdmin, routes.ssh.getAuthorizedKey);
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.user.requireAdmin, routes.ssh.delAuthorizedKey);
// feedback
router.post('/api/v1/cloudron/feedback', usersScope, routes.cloudron.feedback);
@@ -144,6 +148,8 @@ function initializeExpressSync() {
router.post('/api/v1/session/password/reset', csrf, routes.oauth2.passwordReset);
router.get ('/api/v1/session/account/setup.html', csrf, routes.oauth2.accountSetupSite);
router.post('/api/v1/session/account/setup', csrf, routes.oauth2.accountSetup);
router.get ('/api/v1/session/account/create.html', csrf, routes.oauth2.accountCreateSite);
router.post('/api/v1/session/account/create', csrf, routes.oauth2.accountCreate);
// oauth2 routes
router.get ('/api/v1/oauth/dialog/authorize', routes.oauth2.authorization);
@@ -184,13 +190,12 @@ function initializeExpressSync() {
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.user.requireAdmin, routes.settings.setCloudronName);
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, routes.settings.getCloudronAvatar);
router.post('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, multipart, routes.settings.setCloudronAvatar);
router.get ('/api/v1/settings/email_dns_records', settingsScope, routes.user.requireAdmin, routes.settings.getEmailDnsRecords);
router.get ('/api/v1/settings/email_status', settingsScope, routes.user.requireAdmin, routes.settings.getEmailStatus);
router.get ('/api/v1/settings/dns_config', settingsScope, routes.user.requireAdmin, routes.settings.getDnsConfig);
router.post('/api/v1/settings/dns_config', settingsScope, routes.user.requireAdmin, routes.settings.setDnsConfig);
router.get ('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.getBackupConfig);
router.post('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.setBackupConfig);
router.post('/api/v1/settings/certificate', settingsScope, routes.user.requireAdmin, routes.settings.setFallbackCertificate);
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.user.requireAdmin, routes.settings.setAdminCertificate);
router.get ('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.getTimeZone);
router.post('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.setTimeZone);
@@ -198,6 +203,8 @@ function initializeExpressSync() {
router.post('/api/v1/settings/appstore_config', settingsScope, routes.user.requireAdmin, routes.settings.setAppstoreConfig);
router.get ('/api/v1/settings/mail_config', settingsScope, routes.user.requireAdmin, routes.settings.getMailConfig);
router.post('/api/v1/settings/mail_config', settingsScope, routes.user.requireAdmin, routes.settings.setMailConfig);
router.get ('/api/v1/settings/open_registration', settingsScope, routes.user.requireAdmin, routes.settings.getOpenRegistration);
router.post('/api/v1/settings/open_registration', settingsScope, routes.user.requireAdmin, routes.settings.setOpenRegistration);
// eventlog route
router.get('/api/v1/eventlog', settingsScope, routes.user.requireAdmin, routes.eventlog.get);
+77 -7
View File
@@ -6,7 +6,7 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
getEmailDnsRecords: getEmailDnsRecords,
getEmailStatus: getEmailStatus,
getAutoupdatePattern: getAutoupdatePattern,
setAutoupdatePattern: setAutoupdatePattern,
@@ -44,6 +44,9 @@ exports = module.exports = {
getMailConfig: getMailConfig,
setMailConfig: setMailConfig,
getOpenRegistration: getOpenRegistration,
setOpenRegistration: setOpenRegistration,
getDefaultSync: getDefaultSync,
getAll: getAll,
@@ -58,6 +61,7 @@ exports = module.exports = {
UPDATE_CONFIG_KEY: 'update_config',
APPSTORE_CONFIG_KEY: 'appstore_config',
MAIL_CONFIG_KEY: 'mail_config',
OPEN_REGISTRATION_KEY: 'open_registration',
events: null
};
@@ -74,6 +78,7 @@ var assert = require('assert'),
cloudron = require('./cloudron.js'),
CloudronError = cloudron.CloudronError,
moment = require('moment-timezone'),
net = require('net'),
paths = require('./paths.js'),
safe = require('safetydance'),
settingsdb = require('./settingsdb.js'),
@@ -101,6 +106,7 @@ var gDefaults = (function () {
result[exports.UPDATE_CONFIG_KEY] = { prerelease: false };
result[exports.APPSTORE_CONFIG_KEY] = {};
result[exports.MAIL_CONFIG_KEY] = { enabled: false };
result[exports.OPEN_REGISTRATION_KEY] = false;
return result;
})();
@@ -143,10 +149,10 @@ function uninitialize(callback) {
callback();
}
function getEmailDnsRecords(callback) {
function getEmailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
var records = {};
var records = {}, outboundPort25 = {};
var dkimKey = cloudron.readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
@@ -260,7 +266,7 @@ function getEmailDnsRecords(callback) {
status: false
};
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
records.ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
@@ -279,6 +285,44 @@ function getEmailDnsRecords(callback) {
});
}
function checkOutbound25(callback) {
var smtpServer = _.sample([
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.o2.ie',
'smtp.comcast.net',
'outgoing.verizon.net'
]);
outboundPort25 = {
value: 'OK',
status: false
};
var client = new net.Socket();
client.setTimeout(5000);
client.connect(25, smtpServer);
client.on('connect', function () {
outboundPort25.status = true;
outboundPort25.value = 'OK';
client.end();
callback();
});
client.on('timeout', function () {
outboundPort25.status = false;
outboundPort25.value = 'Connect to ' + smtpServer + ' timed out';
client.destroy();
callback(new Error('Timeout'));
});
client.on('error', function (error) {
outboundPort25.status = false;
outboundPort25.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
client.destroy();
callback(error);
});
}
function ignoreError(what, func) {
return function (callback) {
func(function (error) {
@@ -298,9 +342,10 @@ function getEmailDnsRecords(callback) {
ignoreError('spf', checkSpf),
ignoreError('dmarc', checkDmarc),
ignoreError('dkim', checkDkim),
ignoreError('ptr', checkPtr)
ignoreError('ptr', checkPtr),
ignoreError('port25', checkOutbound25)
], function () {
callback(null, records);
callback(null, { dns: records, outboundPort25: outboundPort25 } );
});
}
@@ -453,7 +498,7 @@ function setDnsConfig(dnsConfig, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
subdomains.verifyDnsConfig(dnsConfig, domain, ip, function (error, result) {
@@ -605,6 +650,31 @@ function setMailConfig(mailConfig, callback) {
});
}
function getOpenRegistration(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.OPEN_REGISTRATION_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.OPEN_REGISTRATION_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
// settingsdb holds string values only
callback(null, !!value);
});
}
function setOpenRegistration(enabled, callback) {
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.OPEN_REGISTRATION_KEY, enabled ? 'enabled' : '', function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.OPEN_REGISTRATION_KEY, enabled);
return callback(null);
});
}
function getAppstoreConfig(callback) {
assert.strictEqual(typeof callback, 'function');
+19 -1
View File
@@ -3,7 +3,8 @@
exports = module.exports = {
exec: exec,
execSync: execSync,
sudo: sudo
sudo: sudo,
sudoSync: sudoSync
};
var assert = require('assert'),
@@ -70,3 +71,20 @@ function sudo(tag, args, callback) {
var cp = exec(tag, SUDO, [ '-S' ].concat(args), callback);
cp.stdin.end();
}
function sudoSync(tag, cmd, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof cmd, 'string');
// -S makes sudo read stdin for password
cmd = 'sudo -S ' + cmd;
debug(cmd);
try {
child_process.execSync(cmd, { stdio: 'inherit' });
} catch (e) {
if (callback) return callback(e);
throw e;
}
if (callback) callback();
}
-163
View File
@@ -1,163 +0,0 @@
'use strict';
exports = module.exports = {
start: start,
stop: stop
};
var apps = require('./apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
clients = require('./clients.js'),
ClientsError = clients.ClientsError,
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:src/simpleauth'),
eventlog = require('./eventlog.js'),
express = require('express'),
http = require('http'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
middleware = require('./middleware'),
tokendb = require('./tokendb.js'),
user = require('./user.js'),
UserError = require('./user.js').UserError;
var gHttpServer = null;
function loginLogic(clientId, username, password, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
debug('login: client %s and user %s', clientId, username);
clients.get(clientId, function (error, clientObject) {
if (error) return callback(error);
// only allow simple auth clients
if (clientObject.type !== clients.TYPE_SIMPLE_AUTH) return callback(new ClientsError(ClientsError.INVALID_CLIENT));
var authFunction = (username.indexOf('@') === -1) ? user.verifyWithUsername : user.verifyWithEmail;
authFunction(username, password, function (error, userObject) {
if (error) return callback(error);
apps.get(clientObject.appId, function (error, appObject) {
if (error) return callback(error);
apps.hasAccessTo(appObject, userObject, function (error, access) {
if (error) return callback(error);
if (!access) return callback(new AppsError(AppsError.ACCESS_DENIED));
var accessToken = tokendb.generateToken();
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
tokendb.add(accessToken, userObject.id, clientId, expires, clientObject.scope, function (error) {
if (error) return callback(error);
debug('login: new access token for client %s and user %s: %s', clientId, username, accessToken);
callback(null, { accessToken: accessToken, user: userObject });
});
});
});
});
});
}
function logoutLogic(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
debug('logout: %s', accessToken);
tokendb.del(accessToken, function (error) {
if (error) return callback(error);
callback(null);
});
}
function login(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.clientId !== 'string') return next(new HttpError(400, 'clientId is required'));
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username is required'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required'));
loginLogic(req.body.clientId, req.body.username, req.body.password, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(401, 'Unknown client'));
if (error && error.reason === ClientsError.INVALID_CLIENT) return next(new HttpError(401, 'Unknown client'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(401, 'Forbidden'));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(401, 'Unknown app'));
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(401, 'Forbidden'));
if (error && error.reason === AppsError.ACCESS_DENIED) return next(new HttpError(401, 'Forbidden'));
if (error) return next(new HttpError(500, error));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'simpleauth', clientId: req.body.clientId }, { userId: result.user.id });
var tmp = {
accessToken: result.accessToken,
user: {
id: result.user.id,
username: result.user.username,
email: result.user.email,
admin: !!result.user.admin,
displayName: result.user.displayName
}
};
next(new HttpSuccess(200, tmp));
});
}
function logout(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
if (typeof req.query.access_token !== 'string') return next(new HttpError(400, 'access_token in query required'));
logoutLogic(req.query.access_token, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(401, 'Forbidden'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
function initializeExpressSync() {
var app = express();
var httpServer = http.createServer(app);
httpServer.on('error', console.error);
var json = middleware.json({ strict: true, limit: '100kb' });
var router = new express.Router();
// basic auth
router.post('/api/v1/login', login);
router.get ('/api/v1/logout', logout);
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('SimpleAuth :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
app
.use(middleware.timeout(10000))
.use(json)
.use(router)
.use(middleware.lastMile());
return httpServer;
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
gHttpServer = initializeExpressSync();
gHttpServer.listen(config.get('simpleAuthPort'), '0.0.0.0', callback);
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
if (gHttpServer) gHttpServer.close(callback);
}
+149
View File
@@ -0,0 +1,149 @@
'use strict';
exports = module.exports = {
SshError: SshError,
getAuthorizedKeys: getAuthorizedKeys,
getAuthorizedKey: getAuthorizedKey,
addAuthorizedKey: addAuthorizedKey,
delAuthorizedKey: delAuthorizedKey,
_clear: clear
};
var assert = require('assert'),
config = require('./config.js'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
shell = require('./shell.js'),
util = require('util');
var AUTHORIZED_KEYS_FILEPATH = config.TEST ? path.join(config.baseDir(), 'authorized_keys') : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? '/home/ubuntu/.ssh/authorized_keys' : '/root/.ssh/authorized_keys');
var AUTHORIZED_KEYS_TMP_FILEPATH = '/tmp/.authorized_keys';
var AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/authorized_keys.sh');
var VALID_KEY_TYPES = ['ssh-rsa']; // TODO add all supported ones
var VALID_MIN_KEY_LENGTH = 370; // TODO verify this length requirement
function SshError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(SshError, Error);
SshError.NOT_FOUND = 'Not found';
SshError.INVALID_KEY = 'Invalid key';
SshError.INTERNAL_ERROR = 'Internal Error';
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
fs.unlink(AUTHORIZED_KEYS_FILEPATH, function (error) {
if (error && error.code !== 'ENOENT') return callback(error);
callback();
});
}
function saveKeys(keys) {
assert(Array.isArray(keys));
if (!safe.fs.writeFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, keys.map(function (k) { return k.key; }).join('\n'))) {
console.error(safe.error);
return false;
}
try {
// 600 = rw-------
fs.chmodSync(AUTHORIZED_KEYS_TMP_FILEPATH, '600');
} catch (e) {
console.error('Failed to adjust permissions of %s', AUTHORIZED_KEYS_TMP_FILEPATH, e);
return false;
}
var user = config.TEST ? process.env.USER : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root');
shell.sudoSync('authorized_keys', util.format('%s %s %s %s', AUTHORIZED_KEYS_CMD, user, AUTHORIZED_KEYS_TMP_FILEPATH, AUTHORIZED_KEYS_FILEPATH));
return true;
}
function getKeys() {
shell.sudoSync('authorized_keys', util.format('%s %s %s %s', AUTHORIZED_KEYS_CMD, process.env.USER, AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_TMP_FILEPATH));
var content = safe.fs.readFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, 'utf8');
if (!content) return [];
var keys = content.split('\n')
.filter(function (k) { return !!k.trim(); })
.map(function (k) { return { identifier: k.split(' ')[2], key: k }; })
.filter(function (k) { return k.identifier && k.key; });
return keys;
}
function getAuthorizedKeys(callback) {
assert.strictEqual(typeof callback, 'function');
return callback(null, getKeys().sort(function (a, b) { return a.identifier.localeCompare(b.identifier); }));
}
function getAuthorizedKey(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
var keys = getKeys();
if (keys.length === 0) return callback(new SshError(SshError.NOT_FOUND));
var key = keys.find(function (k) { return k.identifier === identifier; });
if (!key) return callback(new SshError(SshError.NOT_FOUND));
callback(null, key);
}
function addAuthorizedKey(key, callback) {
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
var tmp = key.split(' ');
if (tmp.length !== 3) return callback(new SshError(SshError.INVALID_KEY));
if (!VALID_KEY_TYPES.some(function (t) { return tmp[0] === t; })) return callback(new SshError(SshError.INVALID_KEY, 'Invalid key type'));
if (tmp[1].length < VALID_MIN_KEY_LENGTH) return callback(new SshError(SshError.INVALID_KEY));
var identifier = tmp[2];
var keys = getKeys();
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index !== -1) keys[index] = { identifier: identifier, key: key };
else keys.push({ identifier: identifier, key: key });
if (!saveKeys(keys)) return callback(new SshError(SshError.INTERNAL_ERROR));
callback();
}
function delAuthorizedKey(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
var keys = getKeys();
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index === -1) return callback(new SshError(SshError.NOT_FOUND));
// now remove the key
keys.splice(index, 1);
if (!saveKeys(keys)) return callback(new SshError(SshError.INTERNAL_ERROR));
callback();
}
+4 -3
View File
@@ -3,7 +3,7 @@
exports = module.exports = {
SysInfoError: SysInfoError,
getIp: getIp
getPublicIp: getPublicIp
};
var assert = require('assert'),
@@ -44,17 +44,18 @@ function getApi(callback) {
case 'digitalocean': return callback(null, generic);
case 'ec2': return callback(null, ec2);
case 'lightsail': return callback(null, ec2);
case 'ami': return callback(null, ec2);
case 'scaleway': return callback(null, scaleway);
default: return callback(null, generic);
}
}
function getIp(callback) {
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
getApi(function (error, api) {
if (error) return callback(error);
api.getIp(callback);
api.getPublicIp(callback);
});
}
+2 -2
View File
@@ -1,7 +1,7 @@
'use strict';
exports = module.exports = {
getIp: getIp
getPublicIp: getPublicIp
};
var assert = require('assert'),
@@ -9,7 +9,7 @@ var assert = require('assert'),
safe = require('safetydance'),
SysInfoError = require('../sysinfo.js').SysInfoError;
function getIp(callback) {
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV === 'test') return callback(null, '127.0.0.1');
+2 -2
View File
@@ -1,7 +1,7 @@
'use strict';
exports = module.exports = {
getIp: getIp
getPublicIp: getPublicIp
};
var assert = require('assert'),
@@ -9,7 +9,7 @@ var assert = require('assert'),
SysInfoError = require('../sysinfo.js').SysInfoError,
util = require('util');
function getIp(callback) {
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
superagent.get('http://169.254.169.254/latest/meta-data/public-ipv4').timeout(30 * 1000).end(function (error, result) {
+2 -2
View File
@@ -1,7 +1,7 @@
'use strict';
exports = module.exports = {
getIp: getIp
getPublicIp: getPublicIp
};
var assert = require('assert'),
@@ -10,7 +10,7 @@ var assert = require('assert'),
superagent = require('superagent'),
SysInfoError = require('../sysinfo.js').SysInfoError;
function getIp(callback) {
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
async.retry({ times: 10, interval: 5000 }, function (callback) {
+2 -2
View File
@@ -7,12 +7,12 @@
// -------------------------------------------
exports = module.exports = {
getIp: getIp
getPublicIp: getPublicIp
};
var assert = require('assert');
function getIp(callback) {
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
callback(new Error('not implemented'));
+2 -2
View File
@@ -1,13 +1,13 @@
'use strict';
exports = module.exports = {
getIp: getIp
getPublicIp: getPublicIp
};
var assert = require('assert'),
superagent = require('superagent');
function getIp(callback) {
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
superagent.get('http://169.254.42.42/conf').timeout(30 * 1000).end(function (error, result) {
+2
View File
@@ -15,6 +15,8 @@ var async = require('async'),
function setup(done) {
async.series([
database.initialize,
settings.initialize,
certificates.initialize,
database._clear
], done);
}
+2 -2
View File
@@ -19,7 +19,8 @@ scripts=("${SOURCE_DIR}/src/scripts/rmappdir.sh" \
"${SOURCE_DIR}/src/scripts/reboot.sh" \
"${SOURCE_DIR}/src/scripts/update.sh" \
"${SOURCE_DIR}/src/scripts/collectlogs.sh" \
"${SOURCE_DIR}/src/scripts/reloadcollectd.sh")
"${SOURCE_DIR}/src/scripts/reloadcollectd.sh" \
"${SOURCE_DIR}/src/scripts/authorized_keys.sh")
for script in "${scripts[@]}"; do
if [[ $(sudo -n "${script}" --check 2>/dev/null) != "OK" ]]; then
@@ -49,4 +50,3 @@ if [[ "${image_missing}" == "true" ]]; then
echo "Pull above images before running tests"
exit 1
fi
+1
View File
@@ -20,6 +20,7 @@ describe('dns provider', function () {
before(function (done) {
async.series([
database.initialize,
settings.initialize,
config._reset
], done);
});
+115 -3
View File
@@ -38,6 +38,12 @@ var USER_1 = {
email: 'USER1@email.com',
displayName: 'User 1'
};
var USER_2 = {
username: 'Username2',
password: 'Username2pass?12345',
email: 'USER2@email.com',
displayName: 'User 2'
};
var GROUP_ID, GROUP_NAME = 'developers';
@@ -98,6 +104,15 @@ function setup(done) {
callback(null);
});
},
function (callback) {
user.create(USER_2.username, USER_2.password, USER_2.email, USER_0.displayName, AUDIT_SOURCE, { invitor: USER_0 }, function (error, result) {
if (error) return callback(error);
USER_2.id = result.id;
callback(null);
});
},
function (callback) {
groups.create(GROUP_NAME, function (error, result) {
if (error) return callback(error);
@@ -409,6 +424,66 @@ describe('Ldap', function () {
});
});
});
it ('does not list users who have no access', function (done) {
appdb.update(APP_0.id, { accessRestriction: { users: [], groups: [] } }, 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(0);
appdb.update(APP_0.id, { accessRestriction: null }, done);
});
});
});
});
it ('does only list users who have access', function (done) {
appdb.update(APP_0.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());
appdb.update(APP_0.id, { accessRestriction: null }, done);
});
});
});
});
});
describe('search groups', function () {
@@ -435,9 +510,10 @@ describe('Ldap', function () {
entries.sort(function (a, b) { return a.username < b.username; });
expect(entries[0].cn).to.equal('users');
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid.length).to.equal(3);
expect(entries[0].memberuid[0]).to.equal(USER_0.id);
expect(entries[0].memberuid[1]).to.equal(USER_1.id);
expect(entries[0].memberuid[2]).to.equal(USER_2.id);
expect(entries[1].cn).to.equal('admins');
// if only one entry, the array becomes a string :-/
expect(entries[1].memberuid).to.equal(USER_0.id);
@@ -465,9 +541,10 @@ describe('Ldap', function () {
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.length).to.equal(3);
expect(entries[0].memberuid[0]).to.equal(USER_0.id);
expect(entries[0].memberuid[1]).to.equal(USER_1.id);
expect(entries[0].memberuid[2]).to.equal(USER_2.id);
expect(entries[1].cn).to.equal('admins');
// if only one entry, the array becomes a string :-/
expect(entries[1].memberuid).to.equal(USER_0.id);
@@ -495,11 +572,46 @@ describe('Ldap', function () {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(1);
expect(entries[0].cn).to.equal('users');
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid.length).to.equal(3);
done();
});
});
});
it ('does only list users who have access', function (done) {
appdb.update(APP_0.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');
// if only one entry, the array becomes a string :-/
expect(entries[1].memberuid).to.equal(USER_0.id);
appdb.update(APP_0.id, { accessRestriction: null }, done);
});
});
});
});
});
function ldapSearch(dn, filter, callback) {
+23
View File
@@ -188,5 +188,28 @@ describe('Settings', function () {
done();
});
});
it('can get open registration default value', function (done) {
settings.getOpenRegistration(function (error, enabled) {
expect(error).to.be(null);
expect(enabled).to.equal(false);
done();
});
});
it('can set open registration', function (done) {
settings.setOpenRegistration(true, function (error) {
expect(error).to.be(null);
done();
});
});
it('can get open registration', function (done) {
settings.getOpenRegistration(function (error, enabled) {
expect(error).to.be(null);
expect(enabled).to.equal(true);
done();
});
});
});
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -52,7 +52,7 @@
<script src="3rdparty/js/ansi_up.js"></script>
<!-- Showdown (markdown converter) -->
<script src="3rdparty/js/showdown-1.1.0.min.js"></script>
<script src="3rdparty/js/showdown-1.6.4.min.js"></script>
<script src="3rdparty/js/showdown-target-blank.min.js"></script>
<!-- Bootstrap slider -->
+41 -9
View File
@@ -1,7 +1,6 @@
'use strict';
/* global angular */
/* global EventSource */
angular.module('Application').service('Client', ['$http', 'md5', 'Notification', function ($http, md5, Notification) {
var client = null;
@@ -440,8 +439,22 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.getExpectedDnsRecords = function (callback) {
get('/api/v1/settings/email_dns_records').success(function(data, status) {
Client.prototype.setOpenRegistration = function (enabled, callback) {
post('/api/v1/settings/open_registration', { enabled: enabled }).success(function(data, status) {
if (status !== 200) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getOpenRegistration = function (callback) {
get('/api/v1/settings/open_registration').success(function(data, status) {
if (status !== 200) return callback(new ClientError(status, data));
callback(null, data.enabled);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getEmailStatus = function (callback) {
get('/api/v1/settings/email_status').success(function(data, status) {
if (status !== 200) return callback(new ClientError(status, data));
callback(null, data);
}).error(defaultErrorHandler(callback));
@@ -477,6 +490,27 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.addAuthorizedKey = function (key, callback) {
put('/api/v1/cloudron/ssh/authorized_keys', { key: key }).success(function (data, status) {
if (status !== 201) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.delAuthorizedKey = function (identifier, callback) {
del('/api/v1/cloudron/ssh/authorized_keys/' + identifier).success(function (data, status) {
if (status !== 202) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getAuthorizedKeys = function (callback) {
get('/api/v1/cloudron/ssh/authorized_keys').success(function (data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data.keys);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getBackups = function (callback) {
get('/api/v1/backups').success(function (data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
@@ -592,11 +626,6 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
else return callback(new Error('App not found'));
};
Client.prototype.getAppLogStream = function (appId) {
var source = new EventSource(client.apiOrigin + '/api/v1/apps/' + appId + '/logstream');
return source;
};
Client.prototype.getAppIconUrls = function (app) {
return {
cloudron: app.iconUrl ? (this.apiOrigin + app.iconUrl + '?access_token=' + token) : null,
@@ -628,7 +657,10 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
displayName: displayName
};
post('/api/v1/cloudron/activate?setupToken=' + setupToken, data).success(function(data, status) {
var query = '';
if (setupToken) query = '?setupToken=' + setupToken;
post('/api/v1/cloudron/activate' + query, data).success(function(data, status) {
if (status !== 201 || typeof data !== 'object') return callback(new ClientError(status, data));
that.setToken(data.token);
+10 -3
View File
@@ -216,7 +216,7 @@ app.filter('prettyDate', function () {
day_diff = Math.floor(diff / 86400);
if (isNaN(day_diff) || day_diff < 0)
return;
return 'just now';
return day_diff === 0 && (
diff < 60 && 'just now' ||
@@ -253,7 +253,11 @@ app.filter('postInstallMessage', function () {
if (!app) return text;
var parts = text.split(SSO_MARKER);
if (parts.length === 1) return text;
if (parts.length === 1) {
// [^] matches even newlines. '?' makes it non-greedy
if (app.sso) return text.replace(/\<nosso\>[^]*?\<\/nosso\>/g, '');
else return text.replace(/\<sso\>[^]*?\<\/sso\>/g, '');
}
if (app.sso) return parts[1];
else return parts[0];
@@ -261,7 +265,7 @@ app.filter('postInstallMessage', function () {
});
// keep this in sync with eventlog.js
// keep this in sync with eventlog.js and CLI tool
var ACTION_ACTIVATE = 'cloudron.activate';
var ACTION_APP_CONFIGURE = 'app.configure';
var ACTION_APP_INSTALL = 'app.install';
@@ -269,6 +273,7 @@ var ACTION_APP_RESTORE = 'app.restore';
var ACTION_APP_UNINSTALL = 'app.uninstall';
var ACTION_APP_UPDATE = 'app.update';
var ACTION_APP_UPDATE = 'app.update';
var ACTION_APP_LOGIN = 'app.login';
var ACTION_BACKUP_FINISH = 'backup.finish';
var ACTION_BACKUP_START = 'backup.start';
var ACTION_CERTIFICATE_RENEWAL = 'certificate.renew';
@@ -281,6 +286,7 @@ var ACTION_USER_REMOVE = 'user.remove';
var ACTION_USER_UPDATE = 'user.update';
app.filter('eventLogDetails', function() {
// NOTE: if you change this, the CLI tool (cloudron machine eventlog) probably needs fixing as well
return function(eventLog) {
var source = eventLog.source;
var data = eventLog.data;
@@ -293,6 +299,7 @@ app.filter('eventLogDetails', function() {
case ACTION_APP_RESTORE: return 'App ' + data.appId + ' restored';
case ACTION_APP_UNINSTALL: return 'App ' + data.appId + ' uninstalled';
case ACTION_APP_UPDATE: return 'App ' + data.appId + ' updated to version ' + data.toManifest.id + '@' + data.toManifest.version;
case ACTION_APP_LOGIN: return 'App ' + data.appId + ' logged in';
case ACTION_BACKUP_START: return 'Backup started';
case ACTION_BACKUP_FINISH: return 'Backup finished. ' + (errorMessage ? ('error: ' + errorMessage) : ('id: ' + data.filename));
case ACTION_CERTIFICATE_RENEWAL: return 'Certificate renewal for ' + data.domain + (errorMessage ? ' failed' : 'succeeded');
+3 -3
View File
@@ -74,12 +74,12 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
Client.getDnsConfig(function (error, result) {
if (error) return console.error(error);
if (result.provider !== 'manual' && result.provider !== 'noop') return;
if (result.provider === 'caas') return;
Client.getExpectedDnsRecords(function (error, result) {
Client.getEmailStatus(function (error, result) {
if (error) return console.error(error);
if (!result.spf.status || !result.dkim.status) {
if (!result.dns.spf.status || !result.dns.dkim.status || !result.dns.ptr.status || !result.outboundPort25.status) {
var actionScope = $scope.$new(true);
actionScope.action = '/#/settings';
+9 -3
View File
@@ -20,15 +20,20 @@ app.controller('SetupController', ['$scope', '$http', 'Client', function ($scope
$scope.provider = '';
$scope.apiServerOrigin = '';
$scope.setupToken = '';
$scope.instanceId = '';
$scope.activateCloudron = function () {
$scope.busy = true;
Client.createAdmin($scope.account.username, $scope.account.password, $scope.account.email, $scope.account.displayName, $scope.setupToken, function (error) {
if (error) {
Client.createAdmin($scope.account.username, $scope.account.password, $scope.account.email, $scope.account.displayName, $scope.setupToken || $scope.instanceId, function (error) {
if (error && error.statusCode === 403) {
$scope.busy = false;
$scope.error = $scope.provider === 'ami' ? 'Wrong instance id' : 'Wrong setup token';
return;
} else if (error) {
$scope.busy = false;
console.error('Internal error', error);
$scope.error = error;
$scope.busy = false;
return;
}
@@ -77,6 +82,7 @@ app.controller('SetupController', ['$scope', '$http', 'Client', function ($scope
$scope.account.displayName = search.displayName || $scope.account.displayName;
$scope.account.requireEmail = !search.email;
$scope.provider = status.provider;
$scope.instanceId = search.instanceId;
$scope.apiServerOrigin = status.apiServerOrigin;
$scope.initialized = true;
+5
View File
@@ -78,6 +78,11 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', 'ngTld', func
if (status.adminFqdn) return waitForDnsSetup();
if (status.provider === 'digitalocean') $scope.dnsCredentials.provider = 'digitalocean';
if (status.provider === 'ami') {
// remove route53 on ami
$scope.dnsProvider.shift();
$scope.dnsCredentials.provider = 'wildcard';
}
$scope.provider = status.provider;
$scope.initialized = true;
+5
View File
@@ -77,6 +77,11 @@
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': setupForm.instanceId.$dirty && (setupForm.instanceId.$invalid || error) }" ng-show="provider === 'ami'">
<p>Provide the EC2 instance id to verify you are the owner</p>
<input type="text" class="form-control" ng-model="instanceId" id="inputInstanceId" name="instanceId" placeholder="AWS EC2 instance id" ng-maxlength="20" ng-minlength="10" ng-required="provider === 'ami'" autocomplete="off">
<p ng-show="error" class="has-error">{{ error }}</p>
</div>
</div>
</div>
<div class="row">
+1
View File
@@ -14,6 +14,7 @@ angular.module('Application').controller('ActivityController', ['$scope', '$loca
{ name: 'app.restore', value: 'app.restore' },
{ name: 'app.uninstall', value: 'app.uninstall' },
{ name: 'app.update', value: 'app.update' },
{ name: 'app.login', value: 'app.login' },
{ name: 'backup.finish', value: 'backup.finish' },
{ name: 'backup.start', value: 'backup.start' },
{ name: 'certificate.renew', value: 'certificate.renew' },
+1 -1
View File
@@ -51,7 +51,7 @@
</div>
<p class="text-center" ng-show="appConfigure.usingAltDomain && appConfigure.location && appConfigure.isAltDomainValid()">
Add a CNAME record for {{ appConfigure.location }} to {{ appConfigure.app.fqdn }}
Add a CNAME record for <b>{{ appConfigure.location }}</b> to <b>{{ appConfigure.app.cnameTarget || appConfigure.app.fqdn }}</b>
<br>
</p>
+1 -1
View File
@@ -188,7 +188,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.accessRestriction = app.accessRestriction || { users: [], groups: [] };
$scope.appConfigure.memoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
$scope.appConfigure.xFrameOptions = app.xFrameOptions.indexOf('ALLOW-FROM') === 0 ? app.xFrameOptions.split(' ')[1] : '';
$scope.appConfigure.customAuth = !(app.manifest.addons['simpleauth'] || app.manifest.addons['ldap'] || app.manifest.addons['oauth']);
$scope.appConfigure.customAuth = !(app.manifest.addons['ldap'] || app.manifest.addons['oauth']);
// create ticks starting from manifest memory limit
$scope.appConfigure.memoryTicks = [
+4 -1
View File
@@ -118,7 +118,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
var manifest = app.manifest;
$scope.appInstall.optionalSso = !!manifest.optionalSso;
$scope.appInstall.customAuth = !(manifest.addons['simpleauth'] || manifest.addons['ldap'] || manifest.addons['oauth']);
$scope.appInstall.customAuth = !(manifest.addons['ldap'] || manifest.addons['oauth']);
$scope.appInstall.accessRestrictionOption = 'any';
// set default ports
@@ -156,6 +156,9 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
sso: !$scope.appInstall.optionalSso ? undefined : ($scope.appInstall.accessRestrictionOption !== 'nosso')
};
// add sso property for the postInstall message to be shown correctly
$scope.appInstall.app.sso = data.sso;
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, data, function (error) {
if (error) {
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
+1
View File
@@ -12,6 +12,7 @@
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.customDomainId.$invalid }" uib-tooltip="{{ config.provider === 'caas' ? '' : 'Changing the domain is not yet supported' }}">
<label class="control-label" for="customDomainId">Domain name</label>
<input type="text" class="form-control" ng-model="dnsCredentials.customDomain" id="customDomainId" name="customDomainId" ng-disabled="dnsCredentials.busy || config.provider !== 'caas'" check-tld placeholder="example.com" required autofocus>
<p>&nbsp;<span ng-show="dnsCredentialsForm.customDomainId.$error.invalidSubdomain" class="text-danger">Subdomains are <a href="https://cloudron.io/references/selfhosting.html#domain-setup" target="_blank" title="Domain documentation">not supported</a></span></p>
</div>
<div class="form-group">
+50 -11
View File
@@ -61,16 +61,16 @@
<div class="modal-header">
<h4 class="modal-title">Cloudron Email Server</h4>
</div>
<div class="modal-body" ng-show="dnsConfig.provider === 'noop'">
No DNS provider is setup. Required DNS records will be displayed and have to be manually setup.<br/>
<br/>
Contact us for help on how to configure DNS manually by <a href="mailto:support@cloudron.io">Email</a> or <a href="https://chat.cloudron.io" target="_blank">Chat</a>
<div class="modal-body" ng-show="dnsConfig.provider === 'noop' || dnsConfig.provider === 'manual'">
No DNS provider is setup. Displayed DNS records will have to be setup manually.<br/>
</div>
<div class="modal-body" ng-show="dnsConfig.provider !== 'noop'">
<div class="modal-body" ng-show="dnsConfig.provider === 'route53' || dnsConfig.provider === 'digitalocean'">
The Cloudron will setup Email related DNS records automatically.
If this domain is already configured to handle email with some other provider, it will <b>overwrite</b> those records.
<br/><br/>
Disabling Cloudron Email later will <b>not</b> put the old records back.
<br/>
<br/><br/>
Status of DNS Records will show an error when DNS is propagating (~5 minutes).
<br/>
</div>
<div class="modal-footer">
@@ -258,6 +258,8 @@
<div class="row">
<div class="col-xs-12 text-right">
<a href="{{ config.webServerOrigin }}/console.html#/userprofile" target="_blank">Change payment method</a>
or
<a href="{{ config.webServerOrigin }}/console.html" target="_blank">Cancel this Cloudron</a>
</div>
</div>
<div class="row">
@@ -291,15 +293,12 @@
<div class="col-md-12">
Cloudron has a built-in email server that allows users to send and receive email for your domain.
The <a href="https://cloudron.io/references/usermanual.html#email" target="_blank">User manual</a> has information on how to setup email clients.
<br/>
<br/>
Apps can send email regardless of this setting. Enable this option to allow apps to receive emails.
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12" ng-show="(dnsConfig.provider === 'noop' || dnsConfig.provider === 'manual')">
Set the following DNS records to guarantee email functionality.
<div class="col-md-12" ng-show="(dnsConfig.provider !== 'caas')">
Set the following DNS records to guarantee email delivery.
<div ng-repeat="record in expectedDnsRecordsTypes">
<div class="row" ng-if="mailConfig.enabled || (record.name !== 'DMARC' && record.name !== 'MX')">
@@ -320,6 +319,21 @@
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<h4 class="text-muted">
Outbound SMTP (Port 25) <i ng-class="outboundPort25.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'" aria-hidden="true"></i>
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!outboundPort25.status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
</h4>
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">Advanced</a>
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
<div class="panel-body">
<p><b> {{ outboundPort25.value }} </b> </p>
</div>
</div>
</div>
</div>
</div>
</div>
<br/>
@@ -327,6 +341,9 @@
<div class="col-md-12" ng-show="dnsConfig.provider !== 'caas'">
<button ng-class="mailConfig.enabled ? 'btn btn-danger pull-right' : 'btn btn-primary pull-right'" ng-click="email.toggle()" ng-enabled="mailConfig">{{ mailConfig.enabled ? "Disable Email" : "Enable Email" }}</button>
</div>
<div class="col-md-12" ng-show="dnsConfig.provider === 'caas'">
<span class="text-danger text-bold">This feature requires the Cloudron to be on <a href="https://cloudron.io/references/usermanual.html#entire-cloudron-on-a-custom-domain" target="_blank">custom domain</a>.</span>
</div>
</div>
</div>
@@ -433,5 +450,27 @@
</div>
</div>
<div class="section-header">
<div class="text-left">
<h3>User Registration</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;">
<div class="row">
<div class="col-md-12">
<p>
By default the Cloudron only allows admins to invite other users.
You may enable user registration, allowing users to signup without such an invite.
</p>
<p ng-show="openRegistrationEnabled">
The user signup link is: <a ng-href="{{ signupLink }}" target="_blank">{{ signupLink }}</a>
</p>
<br/>
<button class="btn btn-primary pull-right" ng-click="toggleOpenRegistration()">{{ openRegistrationEnabled ? 'Disable user registration' : 'Enable user registration' }}</button>
</div>
</div>
</div>
<!-- Offset the footer -->
<br/><br/>
+25 -3
View File
@@ -6,8 +6,11 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$scope.client = Client;
$scope.user = Client.getUserInfo();
$scope.config = Client.getConfig();
$scope.openRegistrationEnabled = false;
$scope.signupLink = '';
$scope.backupConfig = {};
$scope.dnsConfig = {};
$scope.outboundPort25 = {};
$scope.expectedDnsRecords = {};
$scope.expectedDnsRecordsTypes = [
{ name: 'MX', value: 'mx' },
@@ -147,7 +150,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$scope.createBackup.percent = data.backup.percent;
$scope.createBackup.message = data.backup.message;
window.setTimeout(checkIfDone, 250);
window.setTimeout(checkIfDone, 500);
});
}
@@ -506,13 +509,24 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
});
}
function getOpenRegistration() {
Client.getOpenRegistration(function (error, enabled) {
if (error) return console.error(error);
$scope.openRegistrationEnabled = enabled;
$scope.signupLink = window.location.origin + '/api/v1/session/account/create.html';
});
}
function showExpectedDnsRecords(callback) {
callback = callback || function (error) { if (error) console.error(error); };
Client.getExpectedDnsRecords(function (error, dnsRecords) {
Client.getEmailStatus(function (error, result) {
if (error) return callback(error);
$scope.expectedDnsRecords = dnsRecords;
$scope.expectedDnsRecords = result.dns;
$scope.outboundPort25 = result.outboundPort25;
callback(null);
});
@@ -611,12 +625,20 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
}
};
$scope.toggleOpenRegistration = function () {
Client.setOpenRegistration(!$scope.openRegistrationEnabled, function (error) {
if (error) return console.error(error);
$scope.openRegistrationEnabled = !$scope.openRegistrationEnabled;
});
};
Client.onReady(function () {
fetchBackups();
getMailConfig();
getBackupConfig();
getDnsConfig();
getAutoupdatePattern();
getOpenRegistration();
if ($scope.config.provider === 'caas') {
getPlans();
+25 -18
View File
@@ -11,24 +11,12 @@
<div class="grid-item-top">
<div class="row animateMeOpacity">
<div class="col-lg-12">
<h3>Community</h3>
Chat with us live at <a href="https://chat.cloudron.io/" target="_blank">chat.cloudron.io</a>.
</div>
</div>
</div>
</div>
<br/>
<div class="card card-large">
<div class="grid-item-top">
<div class="row animateMeOpacity">
<div class="col-lg-12">
<h3>Docs</h3>
For user manuals and developer related questions, please refer to our <a href="{{ config.webServerOrigin }}/documentation.html" target="_blank"> documentation</a>.
<br/>
<br/>
Cloudron is open source. To report issues, <a href="https://git.cloudron.io/cloudron/box/issues" target="_blank">open a ticket</a>.
<h3>Documentation and Chat</h3>
For user manuals and app development related questions, please refer to our <a href="{{ config.webServerOrigin }}/documentation.html" target="_blank"> documentation</a>.
Cloudron is <a href="https://git.cloudron.io" target="_blank">open source</a> - use the <a href="https://git.cloudron.io/cloudron/box/issues" target="_blank">issue tracker</a>
to report bugs and raise feature requests.
<br/><br/>
For any other questions, chat with us live at <a href="https://chat.cloudron.io/" target="_blank">chat.cloudron.io</a>.
</div>
</div>
</div>
@@ -67,6 +55,25 @@
</div>
</div>
</div>
<br/>
<div class="card card-large" ng-show="config.provider !== 'caas' && user.admin">
<div class="grid-item-top">
<div class="row animateMeOpacity">
<div class="col-lg-12">
<h3>Remote Support</h3>
Enable this option to allow Cloudron engineers to connect to this server via SSH.
<br/>
<br/>
Do not enable this option before contacting us first at <a href="https://chat.cloudron.io/" target="_blank">chat.cloudron.io</a>.
<br/>
<br/>
<button class="btn" ng-class="{ 'btn-danger': !sshSupportEnabled, 'btn-primary': sshSupportEnabled }" ng-click="toggleSshSupport()">{{ sshSupportEnabled ? 'Disable SSH support access' : 'Enable SSH support access' }}</button>
</div>
</div>
</div>
</div>
</div>
<!-- Offset the footer -->
+28
View File
@@ -2,6 +2,7 @@
angular.module('Application').controller('SupportController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
$scope.feedback = {
error: null,
@@ -12,6 +13,8 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat
description: ''
};
$scope.sshSupportEnabled = false;
function resetFeedback() {
$scope.feedback.subject = '';
$scope.feedback.description = '';
@@ -38,5 +41,30 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat
});
};
var CLOUDRON_SUPPORT_PUBLIC_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io';
var CLOUDRON_SUPPORT_PUBLIC_KEY_IDENTIFIER = 'support@cloudron.io';
$scope.toggleSshSupport = function () {
if ($scope.sshSupportEnabled) {
Client.delAuthorizedKey(CLOUDRON_SUPPORT_PUBLIC_KEY_IDENTIFIER, function (error) {
if (error) return console.error(error);
$scope.sshSupportEnabled = false;
});
} else {
Client.addAuthorizedKey(CLOUDRON_SUPPORT_PUBLIC_KEY, function (error) {
if (error) return console.error(error);
$scope.sshSupportEnabled = true;
});
}
};
Client.onReady(function () {
Client.getAuthorizedKeys(function (error, keys) {
if (error) return console.error(error);
$scope.sshSupportEnabled = keys.some(function (k) { return k.key === CLOUDRON_SUPPORT_PUBLIC_KEY; });
});
});
$('.modal-backdrop').remove();
}]);