Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a50409bdca | |||
| 60a722e6cc | |||
| 4d6cafa589 | |||
| 63e557430b | |||
| 04acb4423d | |||
| ea813acf4c | |||
| b1198dfdbf | |||
| 4342de3747 | |||
| ef8bc7e7e9 | |||
| e18e401f6b | |||
| ab998c47e8 | |||
| 9fb830b2e1 | |||
| 415c3f90a1 | |||
| 60c8ff7fb1 | |||
| 037816313c | |||
| 3d285d1ac6 | |||
| 135338786f | |||
| 661f1fce31 | |||
| 03664ef784 | |||
| d2111ef2b6 | |||
| e0df19c888 | |||
| 6a523606ca | |||
| b6cd40e63c | |||
| b421866bf5 | |||
| fe06075816 | |||
| 2b73eb90ec | |||
| 5555321cf5 | |||
| f087ebbee0 | |||
| d04f64d3d4 | |||
| 777a5a0929 | |||
| 6c297f890e | |||
| 3c8d0b1b37 | |||
| 74f2cd156f | |||
| a9fdffa9af | |||
| e6f8e8eb94 | |||
| 1bd89ca055 | |||
| 0e226d0314 | |||
| e8d4e2c792 | |||
| 4cfbed8273 | |||
| 0410ac9780 | |||
| 82fcf6a770 | |||
| a1332865c0 | |||
| ae0e4de93e | |||
| 02a6525558 | |||
| 5afef14760 | |||
| 890d589a36 | |||
| 89a50c4b83 | |||
| da5cd2b62c | |||
| 57321624aa | |||
| 876ae822b2 | |||
| 1ceb75868b | |||
| 98ad16f943 | |||
| 9363746c1a | |||
| 7a1b9ab94c | |||
| 46d6b5b81f | |||
| 7e8757a78c | |||
| e508b25ecd | |||
| 3fdc10c523 | |||
| 717953c162 | |||
| daa34c3b4d | |||
| bf5c78d819 | |||
| 1763144278 | |||
| 2f598529fc | |||
| 8264e69e2f | |||
| b0638df94e | |||
| bb61eee557 | |||
| 39c39b861d | |||
| e3deda4ef3 | |||
| 7e44e7de82 | |||
| 9dd0518c00 | |||
| 81313d1c40 | |||
| 2ceccc4557 | |||
| 1c36918e92 | |||
| 8d93df23c1 | |||
| 0c06b34a2c | |||
| fe980eab7f | |||
| 979b903bf2 | |||
| 4b8ee0934a | |||
| 0439725790 | |||
| 4b3ef33989 | |||
| 9e99d51853 | |||
| 00a9fa8f34 | |||
| 84a35343d1 | |||
| 397bd17c55 | |||
| c8e377a9bd | |||
| 90e3138bae | |||
| 24b32a763b | |||
| 69a12d36ef | |||
| 1485718fa6 | |||
| 750f03d9de | |||
| b5ddf1d24d | |||
| 043a35111d | |||
| 676457b589 | |||
| e61f11be81 | |||
| 101a44affd | |||
| 7995c664ed | |||
| 6023c0e5dc | |||
| d49d76c1ee | |||
| 77ef212daa | |||
| 7aa80193c0 | |||
| 5632c74556 | |||
| 7a08745af1 | |||
| d9ba0858c7 | |||
| 617e51d294 | |||
| c07d322fff | |||
| 9b8fa8a772 | |||
| c351242af7 | |||
| 55245557f5 | |||
| ee1cef3ee8 | |||
| 5d51a7178f | |||
| 9d52397bcc | |||
| 5098fbe061 | |||
| 7062aa4ac7 | |||
| d6fec4f2b9 | |||
| 86ef462c76 | |||
| c76e7a3f63 | |||
| 2516a08659 | |||
| 562fe30333 | |||
| 4e0eed4bb2 | |||
| b604caec72 | |||
| 6b409e9089 | |||
| 015d434358 | |||
| c8e448cb84 | |||
| 03924be491 | |||
| 2729cecf4a | |||
| 32e2377828 | |||
| fdb8139b03 |
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Generated
+563
-749
File diff suppressed because it is too large
Load Diff
+2
-3
@@ -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",
|
||||
|
||||
@@ -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" && \
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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 %>
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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');
|
||||
|
||||
@@ -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, {}));
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Executable
+29
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -15,6 +15,8 @@ var async = require('async'),
|
||||
function setup(done) {
|
||||
async.series([
|
||||
database.initialize,
|
||||
settings.initialize,
|
||||
certificates.initialize,
|
||||
database._clear
|
||||
], done);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ describe('dns provider', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
database.initialize,
|
||||
settings.initialize,
|
||||
config._reset
|
||||
], done);
|
||||
});
|
||||
|
||||
+115
-3
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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 -->
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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> <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">
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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();
|
||||
}]);
|
||||
|
||||
Reference in New Issue
Block a user