Compare commits

..

235 Commits

Author SHA1 Message Date
Girish Ramakrishnan 8b2e4ce700 do another infra bump for existing cloudrons 2016-05-10 16:53:13 -07:00
Girish Ramakrishnan 776f184dbc wait randomly instead 2016-05-10 16:51:20 -07:00
Girish Ramakrishnan a54466f8c2 Fix apptask error because of multiple collectd restarts
Everyone gets in a rush to restart collectd and apptask update/restore
fails during infra updates
2016-05-10 09:52:55 -07:00
Girish Ramakrishnan f36641b443 better error message 2016-05-10 09:50:57 -07:00
Girish Ramakrishnan 36eb107b83 0.13.4 changes 2016-05-10 09:16:16 -07:00
Girish Ramakrishnan fa16ae9a0c Use mail container 0.12.0 that contains the restart fix 2016-05-10 09:05:06 -07:00
Girish Ramakrishnan 517b36b3f0 fix typo 2016-05-09 23:58:35 -07:00
Girish Ramakrishnan 83a28afc8f addons are started by box code now 2016-05-09 23:56:02 -07:00
Girish Ramakrishnan e76c7de259 bump test app version 2016-05-09 23:40:59 -07:00
Girish Ramakrishnan fcda4a771c play around with some text 2016-05-09 18:47:11 -07:00
Girish Ramakrishnan 76d8f16e22 0.13.3 changes 2016-05-08 01:17:03 -07:00
Girish Ramakrishnan 1b3cd1f373 remove tls since server does not offer it anymore 2016-05-08 01:15:24 -07:00
Girish Ramakrishnan 62b020e96d add note 2016-05-07 02:34:52 -07:00
Girish Ramakrishnan bc78f4a6d8 fix user to match adminEmail 2016-05-07 01:32:14 -07:00
Girish Ramakrishnan a8e458e935 Load certs into etc 2016-05-06 21:46:22 -07:00
Johannes Zellner e4747ef50c Rework the tutorial 2016-05-06 21:32:34 +02:00
Johannes Zellner 0d6637de27 Avoid circular dependencies with apps and certificates 2016-05-06 18:44:37 +02:00
Johannes Zellner 28e513a434 Fix eventlog tests 2016-05-06 18:05:28 +02:00
Girish Ramakrishnan e73174685b add note on settings route 2016-05-06 08:42:27 -07:00
Johannes Zellner 3af95508f5 eventlog getAllPaged is now getByQueryPaged 2016-05-06 17:27:52 +02:00
Johannes Zellner 4fa8ab596b Show busy state in eventlogs 2016-05-06 17:23:39 +02:00
Johannes Zellner 54c9bb7409 Add filter bar for event log view 2016-05-06 17:18:47 +02:00
Johannes Zellner 4c7dc5056d Add more query options for eventlog api 2016-05-06 16:49:17 +02:00
Johannes Zellner e986a67d39 Fixup all the unit tests 2016-05-06 15:16:22 +02:00
Johannes Zellner da8de173a6 Remove appdb.getBySubdomain() 2016-05-06 14:52:33 +02:00
Johannes Zellner cbc906f8d1 Remove apps.getBySubdomain() 2016-05-06 14:52:06 +02:00
Johannes Zellner c7958f8e1d Remove unused /api/v1/subdomains/:subdomain 2016-05-06 14:51:02 +02:00
Johannes Zellner b88ee8143a Rename welcome -> tutorial 2016-05-06 14:41:20 +02:00
Johannes Zellner e413f7ba9b Handle tutorial walkthrough 2016-05-06 14:38:17 +02:00
Johannes Zellner 7e1055ae44 Show tutorial for admins 2016-05-06 14:23:21 +02:00
Johannes Zellner c61ce40362 Add /api/v1/profile/tutorial route tests 2016-05-06 14:06:54 +02:00
Johannes Zellner e48156dceb postprocess showTutorial to ensure we deal with a boolean 2016-05-06 14:05:47 +02:00
Johannes Zellner f3811e3df9 Remove all unused vars in profile test 2016-05-06 14:03:24 +02:00
Johannes Zellner 0d40b1b80d Fix the test wording in profile password change tests 2016-05-06 14:02:21 +02:00
Johannes Zellner 8b92c8f7ae Remove unused checkMails() in profile tests 2016-05-06 13:57:46 +02:00
Johannes Zellner d41eb81b3d Add new profile/ route to set the showTutorial field 2016-05-06 13:56:40 +02:00
Johannes Zellner 3adf91afed Add setShowTutorial() api to users.js 2016-05-06 13:56:26 +02:00
Johannes Zellner 18f05de8ae use users.showTutorial field in userdb 2016-05-06 13:56:05 +02:00
Johannes Zellner b0f4396389 Add showTutorial field to users table 2016-05-06 13:55:03 +02:00
Johannes Zellner bf99475dbd Introduce basic welcome tutorial flow 2016-05-06 13:06:12 +02:00
Girish Ramakrishnan d50fa70f47 pass -out 2016-05-05 21:26:13 -07:00
Girish Ramakrishnan 0e655cadb0 generate dkim keys before dns setup
Two things require DKIM keys
1. the mail addon
2. the DNS TXT record
2016-05-05 21:15:10 -07:00
Girish Ramakrishnan 496e1c3dc1 fix path to INFRA_VERSION 2016-05-05 18:37:17 -07:00
Girish Ramakrishnan 325252699e set MAIL_FROM more smartly 2016-05-05 18:33:22 -07:00
Girish Ramakrishnan 2d43e22285 fix typo 2016-05-05 15:26:32 -07:00
Girish Ramakrishnan 9e673c3890 supply a bogus username/password 2016-05-05 15:24:52 -07:00
Girish Ramakrishnan c3c18e8a4b reserve more ports 2016-05-05 15:00:07 -07:00
Girish Ramakrishnan cb1bd58cb9 do not export submission port just yet 2016-05-05 14:53:07 -07:00
Girish Ramakrishnan 0bdff14c9f r -> ro 2016-05-05 13:54:57 -07:00
Girish Ramakrishnan c4ae9526af look for fallback cert in nginx cert dir 2016-05-05 13:52:08 -07:00
Girish Ramakrishnan 8d79ac9ae0 provide tls cert and key to mail server
haraka requires tls certs for:
1. supporting AUTH
2. port 587 support (MSA)

currently, we just reuse the cert for the admin domain. Otherwise,
we have to setup dns etc to get a new cert. While doable, its' not
necessary right now.
2016-05-05 13:18:17 -07:00
Girish Ramakrishnan 7d4ed5bafc Revert "x"
This reverts commit a1554b9cc1642037e9fbd0d261722c908f499aab.

committed by mistake
2016-05-05 12:57:04 -07:00
Johannes Zellner 85db8f398b Ensure we require an admin for settings routes
The cloudron name and avatar already have public routes.
2016-05-05 12:48:21 +02:00
Girish Ramakrishnan 636b71ce6f Add MAIL_FROM env 2016-05-05 00:28:08 -07:00
Girish Ramakrishnan b46008f0b1 add sendmail ou bind
this will be used by haraka to authenticate the apps
2016-05-05 00:26:43 -07:00
Girish Ramakrishnan 9d9bd42cd2 x 2016-05-05 00:09:16 -07:00
Girish Ramakrishnan 0f6a2a42f2 use latest mysql image 2016-05-04 23:09:30 -07:00
Girish Ramakrishnan fc8bf82993 Add getters for fallback and admin cert 2016-05-04 17:37:21 -07:00
Girish Ramakrishnan e56192913d reserved smtp and imap locations 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 2d08ce441f rename arg_fqdn to fqdn 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 87497c2047 pass fqdn as arg 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 32a0bf6fd2 add --check arg to setup_infra.sh 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 291e625785 skip infra setup for tests (the test does it itself) 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 6bcfd33e10 boot2docker is dead 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan b4c15b1719 Let the box code initialize the infrastructure
This is done because:
1. The box code can install certs for addons (like mail addon) when
   required.

2. The box code initialize/teardown addons on demand. This is not planned
   currently.
2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 920626192c fix error message 2016-05-04 09:28:50 -07:00
Johannes Zellner 778371b818 Only send out mails if the admin group has changed 2016-05-04 13:55:14 +02:00
Girish Ramakrishnan d2a1cea1e8 0.13.2 changes 2016-05-03 23:48:45 -07:00
Girish Ramakrishnan 5683cefe89 provide auditSource when autoupdating app 2016-05-03 23:36:27 -07:00
Girish Ramakrishnan 126d64ffa8 move backup event in backupBoxAndApps
the updater uses this code route
2016-05-03 18:36:52 -07:00
Girish Ramakrishnan 7262eb208f add route to get the timezone 2016-05-03 12:10:21 -07:00
Girish Ramakrishnan f57f8f5e58 fix typo 2016-05-03 12:09:58 -07:00
Girish Ramakrishnan 00ad1308aa handle empty time_zone 2016-05-03 12:09:22 -07:00
Girish Ramakrishnan 2cc37a9c31 0.13.1 changes 2016-05-03 11:50:52 -07:00
Girish Ramakrishnan 13b0093f20 telize has been discontinued. use freegeoip 2016-05-03 11:50:31 -07:00
Girish Ramakrishnan 5617baea50 eventlog tests 2016-05-02 14:54:25 -07:00
Girish Ramakrishnan da79e4f229 only admin can view activity logs 2016-05-02 14:54:20 -07:00
Girish Ramakrishnan 6c1bd522e6 add pagination 2016-05-02 11:50:15 -07:00
Girish Ramakrishnan 7fca9decc1 fixes to eventlog 2016-05-02 11:07:55 -07:00
Girish Ramakrishnan 7cf08b6a4d add start event
this signals that an update worked
2016-05-02 10:01:23 -07:00
Girish Ramakrishnan ffedbdfa13 various minor fixes to eventlog 2016-05-02 10:01:23 -07:00
Johannes Zellner 43e207a301 Merge branch 'v0.12.7-hotfix' 2016-05-02 17:34:56 +02:00
Johannes Zellner bea6019dc4 Changes for 0.12.7 2016-05-02 14:20:59 +02:00
Johannes Zellner 9042e9e1a9 Fix usage of the password change route in webadmin 2016-05-02 14:14:44 +02:00
Johannes Zellner b855dee4cb Fix crash by providing required eventsource 2016-05-02 13:43:12 +02:00
Girish Ramakrishnan b322f6805f move authType into source 2016-05-01 21:53:44 -07:00
Girish Ramakrishnan ccc119ddec add appLocation to user login 2016-05-01 21:47:35 -07:00
Girish Ramakrishnan 994cbaa22a add event log in model code 2016-05-01 21:38:20 -07:00
Girish Ramakrishnan d7a34bbf68 remove profile action 2016-05-01 20:14:21 -07:00
Girish Ramakrishnan 1f31fe6f8f make user.remove and user.update add eventlog 2016-05-01 20:11:11 -07:00
Girish Ramakrishnan 37bdd2672b make user.create take auditSource 2016-05-01 20:01:34 -07:00
Girish Ramakrishnan 63702f836a move developer eventlogs to model code 2016-05-01 13:46:33 -07:00
Girish Ramakrishnan 5898428a6c use req.connection.remoteAddress 2016-05-01 13:29:11 -07:00
Girish Ramakrishnan f4a6c64956 make cloudron.activate take an auditSource 2016-05-01 13:27:57 -07:00
Girish Ramakrishnan f9d4d3014d remove reboot (user cannot initiate anyway) 2016-05-01 13:23:31 -07:00
Girish Ramakrishnan 8254337552 make cloudron.updateToLatest take an auditSource 2016-05-01 13:17:35 -07:00
Girish Ramakrishnan d811115f21 update schema.sql 2016-05-01 13:17:27 -07:00
Girish Ramakrishnan fec388b648 move backup eventlog to model 2016-05-01 13:17:23 -07:00
Girish Ramakrishnan a969e323a6 what if cron was a username 2016-05-01 11:48:29 -07:00
Girish Ramakrishnan 09584ac29c fix failing test 2016-04-30 23:26:45 -07:00
Girish Ramakrishnan 7967610f3f add user login to event log 2016-04-30 23:18:14 -07:00
Girish Ramakrishnan 727332fe66 save the userId as well 2016-04-30 23:04:54 -07:00
Girish Ramakrishnan 09595d1c43 make tests pass 2016-04-30 22:35:46 -07:00
Girish Ramakrishnan c4ad6c803f add certificate renew event 2016-04-30 22:27:33 -07:00
Girish Ramakrishnan fd1a00d280 better text for action 2016-04-30 20:25:20 -07:00
Girish Ramakrishnan 5c2a650681 add activity view 2016-04-30 20:25:20 -07:00
Girish Ramakrishnan 43051cea3b add app update event 2016-04-30 20:25:20 -07:00
Girish Ramakrishnan 3d50a251ee store email in USER_ADD event 2016-04-30 20:25:20 -07:00
Girish Ramakrishnan 219df8babd pick up correct ip 2016-04-30 19:08:24 -07:00
Girish Ramakrishnan d3d9706a70 make response plural 2016-04-30 18:57:43 -07:00
Girish Ramakrishnan 8a3ad6c964 store the activation username 2016-04-30 14:32:35 -07:00
Girish Ramakrishnan 90719cd4d9 do not store entire manifest 2016-04-30 14:28:59 -07:00
Girish Ramakrishnan 30445ddab9 more changes 2016-04-30 14:14:02 -07:00
Girish Ramakrishnan 157fbc89b8 add eventlog route 2016-04-30 14:08:44 -07:00
Girish Ramakrishnan 71219c6af7 add eventlog hooks 2016-04-30 14:05:19 -07:00
Girish Ramakrishnan 934abafbd4 make actions seem like commands 2016-04-30 13:34:06 -07:00
Girish Ramakrishnan bc6e896507 add eventlog test 2016-04-30 13:02:57 -07:00
Girish Ramakrishnan ca8731c282 add source to events table 2016-04-30 12:56:23 -07:00
Girish Ramakrishnan c511019d79 remove jslint hint 2016-04-30 11:53:46 -07:00
Girish Ramakrishnan 992c4ee847 add some event enums 2016-04-30 11:49:51 -07:00
Girish Ramakrishnan 8c427553ba add eventlogdb tests 2016-04-30 10:16:27 -07:00
Girish Ramakrishnan c1df22f079 use JSON type (nice for querying) 2016-04-30 10:15:33 -07:00
Girish Ramakrishnan 7673ecde2f Add eventlog model file 2016-04-29 23:58:29 -07:00
Girish Ramakrishnan a9d0cf66fd Add eventlogdb 2016-04-29 23:58:24 -07:00
Girish Ramakrishnan db89784af8 add eventlog table 2016-04-29 23:58:20 -07:00
Girish Ramakrishnan 4143f903ad 0.13.0 changes 2016-04-29 20:50:55 -07:00
Girish Ramakrishnan 12820db4a5 fix typo in mysql config 2016-04-29 20:20:52 -07:00
Girish Ramakrishnan 8837cc5a3c update nginx version 2016-04-29 19:38:06 -07:00
Girish Ramakrishnan f2545e3def bump systemd to 229 2016-04-29 19:18:31 -07:00
Girish Ramakrishnan 7a72bf3f78 select mysql 5.7 2016-04-29 19:12:20 -07:00
Girish Ramakrishnan 87351f04ef use 16.04
among other things this will bump the mysql version bringing us
json type
2016-04-29 19:11:00 -07:00
Girish Ramakrishnan 4a04e0b52f use recommendation from raymii.org 2016-04-28 09:59:03 -07:00
Girish Ramakrishnan 5945bce00e add misssing arg 2016-04-26 16:21:26 -07:00
Girish Ramakrishnan d2a3925e04 add altDomain to install route 2016-04-26 14:45:58 -07:00
Girish Ramakrishnan 9c9f82e2c5 fix usage of waitForDns 2016-04-26 11:09:14 -07:00
Girish Ramakrishnan 8fd3ff0ccc 0.12.6 changes 2016-04-26 09:53:33 -07:00
Girish Ramakrishnan dec2fdb6bb merge altDomain into location field 2016-04-25 21:22:09 -07:00
Girish Ramakrishnan c581d2a52c stash altDomain in oldConfig 2016-04-25 21:22:08 -07:00
Girish Ramakrishnan e6e748e30d fix cname answer 2016-04-25 16:16:14 -07:00
Girish Ramakrishnan 36fddacf5c add more debugs 2016-04-25 16:06:50 -07:00
Girish Ramakrishnan 183c1608a6 it is fine for one or more ns to be dead 2016-04-25 15:58:42 -07:00
Girish Ramakrishnan 51c8f65e8d wait for altDomain on install as well
restore calls install when there is no lastBackupId
2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 0da6e9a5b9 fix casing 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 31c7a17684 Infinity does not work 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan a1e2cd438e return altDomain in response 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 6b0e00e28b use app.fqdn 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 19948851e0 return altDomain as the app.fqdn if present 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan bfe4f75881 display altDomain as the title of the app 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 7f13594f01 fix two typos 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 4fafac035e remove options in waitForDns 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan ca41e6acfd remove attempt module 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 9893dd6640 make waitfordns get the zone itself 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 8f7e4c2053 Make waitForDns wait for cname 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan d037b13401 wait for alt domain dns 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 83c955d25b add a label for alt domain 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 0bb6d969a4 inject altDomain in the APP_ORIGIN and APP_DOMAIN env vars 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 6062a5bdd2 move exports to top because sysinfo backend source the error class 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 70ab492efa remove jslint header 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan aab035f7b9 use the acme backend when using altDomain 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 0e825272ae ensureCertificate now takes app object 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 46fee9e431 use config.adminFqdn instead 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 0789c96992 altDomain ui 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan a4adc581fa make nginx.configure/unconfigure use altDomain 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 500fb452e7 use altDomain when present to configure certs and nginx 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan e11b762ea1 use vhost instead 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan f5d1726352 remove jslint header 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 3d5aa9fd23 pass altDomain in configure route 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan ef12740060 add altDomain to appdb fields 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 415902d68e add altDomain to apps table 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 0ef0e010a3 use defines for role names 2016-04-25 10:30:56 -07:00
Girish Ramakrishnan 2d27da89d2 validate individual scopes 2016-04-25 10:26:47 -07:00
Girish Ramakrishnan 9d8def8349 update bytes ejs-cli dockerode morgan superagent ursa validator x509 2016-04-22 22:26:32 -07:00
Girish Ramakrishnan 2533111bfa more 0.12.5 changes 2016-04-20 19:41:45 -07:00
Girish Ramakrishnan 20d6da8230 add debugs 2016-04-20 19:40:58 -07:00
Girish Ramakrishnan f159cacfbb Use same timestamp for archive and config
This fixes a very curious case:
1. App has backup.
2. App dies.
3. Box backs up. This make it reuse the backup. But it generates wrong config file timestamp.
4. Box cannot update anymore. This is because the backup of app fails - it tries to reuse
   the backup and that fails with AccessDenied because the timestamp above is wrong!
2016-04-20 19:37:00 -07:00
Girish Ramakrishnan 5e9ea98b66 ignore apps in errored state 2016-04-20 19:05:49 -07:00
Girish Ramakrishnan d87b7dcb75 fix typo 2016-04-20 12:56:35 -07:00
Girish Ramakrishnan 6eea2fef9a retry fetching icon
e2e randomly fails with EAI_AGAIN
2016-04-20 00:40:22 -07:00
Girish Ramakrishnan 34fd5f14a5 0.12.5 changes 2016-04-19 21:52:24 -07:00
Girish Ramakrishnan a4e73e747c fix crash mail subject 2016-04-19 19:12:47 -07:00
Girish Ramakrishnan eadff099eb send logs when apptask fails 2016-04-19 18:40:46 -07:00
Girish Ramakrishnan 15653cb3f8 rename to logcollector 2016-04-19 18:13:05 -07:00
Girish Ramakrishnan 2f8dc35c5d rename to sendFailureLogs 2016-04-19 18:06:11 -07:00
Girish Ramakrishnan a97720d204 rename to failure_notification 2016-04-19 18:04:45 -07:00
Girish Ramakrishnan 73898505b0 remove jslint header 2016-04-19 16:59:12 -07:00
Girish Ramakrishnan 88b4b6a38b use the crashnotifier module 2016-04-19 16:47:21 -07:00
Girish Ramakrishnan 3da82e3a63 rename to crashnotifierservice 2016-04-19 16:45:05 -07:00
Girish Ramakrishnan dad1585704 send crash notification on apptask crash 2016-04-19 16:43:58 -07:00
Girish Ramakrishnan e81dbdb36c add crashnotifier module 2016-04-19 16:42:05 -07:00
Girish Ramakrishnan ee2478e500 collect last 300 lines 2016-04-19 16:39:28 -07:00
Girish Ramakrishnan 0f7a6964a4 0.12.4 changes 2016-04-19 16:24:37 -07:00
Girish Ramakrishnan 5fa974ffe6 wait for 30 seconds in taskmanager instead
problem: because the apps are not inserted into appdb, the cloudron starts out
with an empty view. apps appear suddenly after 30 seconds.

besides, it makes more sense because 30 secs is not really tied to first run
2016-04-19 13:56:41 -07:00
Girish Ramakrishnan e1b7198a29 reverse setTimeout args 2016-04-19 12:28:36 -07:00
Girish Ramakrishnan 37d6354627 wait for 30 seconds for the addons to start up
The platform sometimes takes time to start up (especially in 1GB droplet).
This means that apps like wordpress begin auto installing and they fail
since mysql has not started yet.
2016-04-19 12:15:25 -07:00
Girish Ramakrishnan 6ab3e04fc1 0.12.3 changes 2016-04-19 12:11:59 -07:00
Girish Ramakrishnan b1987868be Set sn attribute only if non-empty
sn and givenName have as their superior the name attribute, which is of DirectoryString syntax,
that is, the syntax is 1.3.6.1.4.1.1466.115.121.1.15. Attributes which are of syntax
DirectoryString are not allowed to be null, that is, a DirectoryString is required to have
at least one character.

http://stackoverflow.com/questions/15027094/how-to-filter-null-or-empty-attributes-from-an-active-directory-query

This fixes a crash in paperwork which relies on this.
2016-04-19 12:03:03 -07:00
Girish Ramakrishnan 72eb3007c4 tmp -> obj 2016-04-19 12:00:34 -07:00
Girish Ramakrishnan 6c1da45ad1 skip the not automated part 2016-04-19 10:49:46 -07:00
Girish Ramakrishnan b9857cdb65 use async.retry 2016-04-18 22:06:49 -07:00
Girish Ramakrishnan d5c251115c typo 2016-04-18 18:57:29 -07:00
Girish Ramakrishnan 64c66e248b add mongodb test 2016-04-18 18:12:56 -07:00
Girish Ramakrishnan bb53c4f331 fix setup of mongodb 2016-04-18 18:05:23 -07:00
Girish Ramakrishnan 3215d4a3c9 move the exports to the top 2016-04-18 16:30:58 -07:00
Girish Ramakrishnan 68c4d77494 0.12.2 changes 2016-04-18 15:24:33 -07:00
Girish Ramakrishnan 44bf299e10 Merge remote-tracking branch 'origin/users' 2016-04-18 15:19:38 -07:00
Girish Ramakrishnan 6b1e14b464 add option to buffer stdout 2016-04-18 15:02:31 -07:00
Girish Ramakrishnan 8dcde84c3c remove memorystream 2016-04-18 14:56:47 -07:00
Girish Ramakrishnan a0deedb958 fixup backup and restore to use docker.execContainer 2016-04-18 14:56:01 -07:00
Girish Ramakrishnan a2096bec18 use options.stdout to pass back result 2016-04-18 12:22:42 -07:00
Girish Ramakrishnan 4f82bcec43 make execContainer take options arg 2016-04-18 11:42:34 -07:00
Girish Ramakrishnan 491356ce8d fix teardownMongoDb 2016-04-18 11:29:11 -07:00
Girish Ramakrishnan 6c99105a7e Make teardown commands use docker.execContainer 2016-04-18 11:25:16 -07:00
Girish Ramakrishnan 71f847776b fix up all the seutp code to use docker.execContainer 2016-04-18 11:15:21 -07:00
Girish Ramakrishnan 87c5371603 use docker exec instead of dockerode exec in mysql
this way we can check the exit code of the exec process.
preivously, we were only wait for the stream to end.
2016-04-18 11:06:09 -07:00
Girish Ramakrishnan 01d676628d rename docker variable 2016-04-18 10:37:33 -07:00
Girish Ramakrishnan 60badce935 add docker.execContainer 2016-04-18 10:32:22 -07:00
Johannes Zellner 182ae6bf1f Add some description to the upgrade dialog 2016-04-18 17:21:26 +02:00
Johannes Zellner c62ef9e156 Implement upgrade request dialog
This is currently merely a placeholder for some real upgrade ui
2016-04-18 17:21:26 +02:00
Johannes Zellner 96383a1fae Support upgrade_request feedback type 2016-04-18 17:11:36 +02:00
Johannes Zellner 5e9542ee76 Offer upgrade on install if resources are low 2016-04-18 16:16:44 +02:00
Johannes Zellner cc28d49df4 That api already returns the user array 2016-04-18 15:13:22 +02:00
Johannes Zellner 18f3733d6e Simplify the password change logic
We now can use verifyPassword and this makes
user.changePassword() route obsolete
2016-04-17 19:17:03 +02:00
Johannes Zellner 87dcf42c7e Remove redundant client api listUsers() 2016-04-17 18:42:56 +02:00
Johannes Zellner 32d8627045 Password change api is now in /profile 2016-04-17 18:41:13 +02:00
Johannes Zellner 6a607f9565 Adjust user route tests 2016-04-17 18:39:00 +02:00
Johannes Zellner c623770b44 Group /users routes better 2016-04-17 18:38:49 +02:00
Johannes Zellner 69f3620b22 remove unused user route functions 2016-04-17 18:27:11 +02:00
Johannes Zellner 21110bb2e0 Enable profile password change tests 2016-04-17 17:51:37 +02:00
Johannes Zellner fabe55622e Fix the first bunch of profile tests 2016-04-17 16:49:09 +02:00
Johannes Zellner 73e079cc6c Add initial profile route tests 2016-04-17 16:42:45 +02:00
Johannes Zellner a7d22a1972 Add specific user profile routes 2016-04-17 16:22:39 +02:00
Girish Ramakrishnan 5c1970b37f Fix crash where portBindings is set to undefined 2016-04-15 21:27:42 -07:00
Girish Ramakrishnan db065bd0fc 0.12.1 changes 2016-04-15 18:28:02 -07:00
Girish Ramakrishnan db6d8deec4 fix another typo 2016-04-15 18:25:46 -07:00
Girish Ramakrishnan 414b21f29a add sysadmin route test 2016-04-15 12:33:54 -07:00
100 changed files with 3532 additions and 1765 deletions
+42
View File
@@ -468,3 +468,45 @@
- Fix crash when user attempts to set a duplicate email
- Improved mongodb crash recovery
[0.12.1]
- Fix crash when backing up apps
[0.12.2]
- Improved error handling for addons
[0.12.3]
- LDAP: Do not set sn attribute when user has no surname
[0.12.4]
- Install app only after platform is ready
[0.12.5]
- Get alerts for app task failures
- Fix update issue when one or more apps are in failed state
[0.12.6]
- Allow setting an alternate external domain for apps
[0.12.7]
- Fix changing password
[0.13.0]
- Upgrade to ubuntu 16.04
- Add event log
[0.13.1]
- Make activity log viewable to admins
- Fix geoip lookup
[0.13.2]
- Fix crash in app auto updater
- Fix crash with empty timezone
[0.13.3]
- Enable auth in email addon
- Add search for activity log
- Add tutorial for first time users
[0.13.4]
- Fix mail addon restart issue
+1 -1
View File
@@ -138,7 +138,7 @@ while true; do
done
echo "Copying INFRA_VERSION"
$scp22 "${SCRIPT_DIR}/../setup/INFRA_VERSION" root@${server_ip}:.
$scp22 "${SCRIPT_DIR}/../src/INFRA_VERSION" root@${server_ip}:.
echo "Copying box source"
cd "${SOURCE_DIR}"
+1 -1
View File
@@ -30,7 +30,7 @@ function create_droplet() {
local box_name="$2"
local image_region="sfo1"
local ubuntu_image_slug="ubuntu-15-10-x64"
local ubuntu_image_slug="ubuntu-16-04-x64"
local box_size="512mb"
local data="{\"name\":\"${box_name}\",\"size\":\"${box_size}\",\"region\":\"${image_region}\",\"image\":\"${ubuntu_image_slug}\",\"ssh_keys\":[ \"${ssh_key_id}\" ],\"backups\":false}"
+4 -4
View File
@@ -17,7 +17,7 @@ function die {
exit 1
}
[[ "$(systemd --version 2>&1)" == *"systemd 225"* ]] || die "Expecting systemd to be 225"
[[ "$(systemd --version 2>&1)" == *"systemd 229"* ]] || die "Expecting systemd to be 229"
if [ -f "${SOURCE_DIR}/INFRA_VERSION" ]; then
source "${SOURCE_DIR}/INFRA_VERSION"
@@ -183,7 +183,7 @@ fi
echo "==== Install nginx ===="
apt-get -y install nginx-full
[[ "$(nginx -v 2>&1)" == *"nginx/1.9."* ]] || die "Expecting nginx version to be 1.9.x"
[[ "$(nginx -v 2>&1)" == *"nginx/1.10."* ]] || die "Expecting nginx version to be 1.10.x"
echo "==== Install build-essential ===="
apt-get -y install build-essential rcconf
@@ -191,8 +191,8 @@ apt-get -y install build-essential rcconf
echo "==== Install mysql ===="
debconf-set-selections <<< 'mysql-server mysql-server/root_password password password'
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
apt-get -y install mysql-server
[[ "$(mysqld --version 2>&1)" == *"5.6."* ]] || die "Expecting nginx version to be 5.6.x"
apt-get -y install mysql-server-5.7
[[ "$(mysqld --version 2>&1)" == *"5.7."* ]] || die "Expecting mysql version to be 5.7.x"
echo "==== Install pwgen and swaks awscli ===="
apt-get -y install pwgen swaks awscli
-42
View File
@@ -1,42 +0,0 @@
#!/usr/bin/env node
'use strict';
var assert = require('assert'),
mailer = require('./src/mailer.js'),
safe = require('safetydance'),
path = require('path'),
util = require('util');
var COLLECT_LOGS_CMD = path.join(__dirname, 'src/scripts/collectlogs.sh');
function collectLogs(program, callback) {
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof callback, 'function');
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + program, { encoding: 'utf8' });
callback(null, logs);
}
function sendCrashNotification(processName) {
collectLogs(processName, function (error, result) {
if (error) {
console.error('Failed to collect logs.', error);
result = util.format('Failed to collect logs.', error);
}
console.log('Sending crash notification email for', processName);
mailer.sendCrashNotification(processName, result);
});
}
function main() {
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
sendCrashNotification(processName);
}
main();
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env node
'use strict';
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
function main() {
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
sendFailureLogs(processName, { unit: processName });
}
main();
@@ -0,0 +1,17 @@
'use strict';
var dbm = dbm || require('db-migrate');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN altDomain', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,24 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = "CREATE TABLE eventlog(" +
"id VARCHAR(128) NOT NULL," +
"source JSON," +
"creationTime TIMESTAMP," +
"action VARCHAR(128) NOT NULL," +
"data JSON," +
"PRIMARY KEY (id))";
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE eventlog', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
dbm = dbm || require('db-migrate');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN showTutorial BOOLEAN DEFAULT 0', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN showTutorial', function (error) {
if (error) console.error(error);
callback(error);
});
};
+12 -1
View File
@@ -19,6 +19,7 @@ CREATE TABLE IF NOT EXISTS users(
modifiedAt VARCHAR(512) NOT NULL,
admin INTEGER NOT NULL,
displayName VARCHAR(512) DEFAULT '',
showTutorial BOOLEAN DEFAULT 0,
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groups(
@@ -65,6 +66,7 @@ CREATE TABLE IF NOT EXISTS apps(
oauthProxy BOOLEAN DEFAULT 0,
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
memoryLimit BIGINT DEFAULT 0,
altDomain VARCHAR(256),
lastBackupId VARCHAR(128),
lastBackupConfigJson TEXT, // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
@@ -99,7 +101,7 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL,
filename VARCHAR(128) NOT NULL,
creationTime TIMESTAMP,
version VARCHAR(128) NOT NULL, /* app version or box version */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
@@ -107,3 +109,12 @@ CREATE TABLE IF NOT EXISTS backups(
state VARCHAR(16) NOT NULL,
PRIMARY KEY (filename));
CREATE TABLE IF NOT EXISTS eventlog(
id VARCHAR(128) NOT NULL,
action VARCHAR(128) NOT NULL,
source JSON, /* { userId, username, ip }. userId can be null for cron,sysadmin */
data JSON, /* free flowing json based on action */
creationTime TIMESTAMP,
PRIMARY KEY (id));
+969 -790
View File
File diff suppressed because it is too large Load Diff
+8 -10
View File
@@ -14,10 +14,9 @@
],
"dependencies": {
"async": "^1.2.1",
"attempt": "^1.0.1",
"aws-sdk": "^2.1.46",
"body-parser": "^1.13.1",
"bytes": "^2.1.0",
"bytes": "^2.3.0",
"cloudron-manifestformat": "^2.3.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "0.0.13",
@@ -28,17 +27,16 @@
"csurf": "^1.6.6",
"db-migrate": "^0.9.2",
"debug": "^2.2.0",
"dockerode": "^2.2.2",
"dockerode": "^2.2.10",
"ejs": "^2.2.4",
"ejs-cli": "^1.0.1",
"ejs-cli": "^1.2.0",
"express": "^4.12.4",
"express-session": "^1.11.3",
"hat": "0.0.3",
"json": "^9.0.3",
"ldapjs": "^0.7.1",
"memorystream": "^0.3.0",
"mime": "^1.3.4",
"morgan": "^1.6.0",
"morgan": "^1.7.0",
"multiparty": "^4.1.2",
"mysql": "^2.7.0",
"native-dns": "^0.7.0",
@@ -60,15 +58,15 @@
"semver": "^4.3.6",
"serve-favicon": "^2.2.0",
"split": "^1.0.0",
"superagent": "^1.5.0",
"superagent": "^1.8.3",
"supererror": "^0.7.1",
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
"tldjs": "^1.6.2",
"underscore": "^1.7.0",
"ursa": "^0.9.1",
"ursa": "^0.9.3",
"valid-url": "^1.0.9",
"validator": "^4.4.0",
"x509": "^0.2.2"
"validator": "^4.9.0",
"x509": "^0.2.4"
},
"devDependencies": {
"apidoc": "*",
+1 -1
View File
@@ -4,4 +4,4 @@
# http://bugs.mysql.com/bug.php?id=68514
[mysqld]
performance_schema=OFF
max_connection=50
max_connections=50
+3
View File
@@ -31,3 +31,6 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
Defaults!/home/yellowtent/box/src/scripts/retire.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/retire.sh
Defaults!/home/yellowtent/box/src/scripts/setup_infra.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/setup_infra.sh
@@ -7,7 +7,7 @@ StopWhenUnneeded=false
[Service]
Type=idle
WorkingDirectory=/home/yellowtent/box
ExecStart="/home/yellowtent/box/crashnotifier.js" %I
ExecStart="/home/yellowtent/box/crashnotifierservice.js" %I
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
KillMode=process
User=yellowtent
+1 -1
View File
@@ -9,7 +9,7 @@ readonly BOX_SRC_DIR="/home/yellowtent/box"
readonly DATA_DIR="/home/yellowtent/data"
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
source "${script_dir}/INFRA_VERSION" # this injects INFRA_VERSION
source "${script_dir}/../src/INFRA_VERSION" # this injects INFRA_VERSION
echo "Setting up nginx update page"
+1 -4
View File
@@ -39,7 +39,7 @@ set_progress "10" "Ensuring directories"
[[ "${is_update}" == "false" ]] && btrfs subvolume create "${DATA_DIR}/box"
mkdir -p "${DATA_DIR}/box/appicons"
mkdir -p "${DATA_DIR}/box/certs"
mkdir -p "${DATA_DIR}/box/mail"
mkdir -p "${DATA_DIR}/box/mail/dkim/${arg_fqdn}"
mkdir -p "${DATA_DIR}/box/acme" # acme keys
mkdir -p "${DATA_DIR}/graphite"
@@ -122,9 +122,6 @@ set_progress "33" "Changing ownership"
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
chown "${USER}:${USER}" "${DATA_DIR}"
set_progress "40" "Setting up infra"
${script_dir}/start/setup_infra.sh "${arg_fqdn}"
set_progress "65" "Creating cloudron.conf"
sudo -u yellowtent -H bash <<EOF
set -eu
+2 -1
View File
@@ -18,9 +18,10 @@ server {
# https://bettercrypto.org/static/applied-crypto-hardening.pdf
# https://mozilla.github.io/server-side-tls/ssl-config-generator/
# https://cipherli.st/
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
ssl_ciphers 'AES128+EECDH:AES128+EDH';
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
proxy_http_version 1.1;
+2 -2
View File
@@ -3,7 +3,7 @@
# If you change the infra version, be sure to put a warning
# in the change log
INFRA_VERSION=27
INFRA_VERSION=30
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well
@@ -12,7 +12,7 @@ MYSQL_IMAGE=cloudron/mysql:0.11.0
POSTGRESQL_IMAGE=cloudron/postgresql:0.9.0
MONGODB_IMAGE=cloudron/mongodb:0.9.0
REDIS_IMAGE=cloudron/redis:0.8.0 # if you change this, fix src/addons.js as well
MAIL_IMAGE=cloudron/mail:0.10.0
MAIL_IMAGE=cloudron/mail:0.12.0
GRAPHITE_IMAGE=cloudron/graphite:0.8.0
MYSQL_REPO=cloudron/mysql
+60 -180
View File
@@ -1,6 +1,8 @@
'use strict';
exports = module.exports = {
initialize: initialize,
setupAddons: setupAddons,
teardownAddons: teardownAddons,
backupAddons: backupAddons,
@@ -19,22 +21,21 @@ exports = module.exports = {
var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
child_process = require('child_process'),
certificates = require('./certificates.js'),
clientdb = require('./clientdb.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:addons'),
docker = require('./docker.js').connection,
docker = require('./docker.js'),
dockerConnection = docker.connection,
fs = require('fs'),
generatePassword = require('password-generator'),
hat = require('hat'),
MemoryStream = require('memorystream'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
spawn = child_process.spawn,
util = require('util'),
uuid = require('node-uuid');
@@ -111,7 +112,8 @@ var KNOWN_ADDONS = {
}
};
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh');
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
SETUP_INFRA_CMD = path.join(__dirname, 'scripts/setup_infra.sh');;
function debugApp(app, args) {
assert(!app || typeof app === 'object');
@@ -120,6 +122,17 @@ function debugApp(app, args) {
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function initialize(callback) {
if (process.env.BOX_ENV === 'test') return callback();
debug('initializing addon infrastructure');
certificates.getAdminCertificatePath(function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
shell.sudo('seutp_infra', [ SETUP_INFRA_CMD, config.fqdn(), config.adminFqdn(), certFilePath, keyFilePath ], callback);
});
}
function setupAddons(app, addons, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addons || typeof addons === 'object');
@@ -388,13 +401,14 @@ function setupSendMail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var username = app.location ? app.location + '-app' : 'no-reply'; // use no-reply for bare domains
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '-app';
var env = [
'MAIL_SMTP_SERVER=mail',
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
'MAIL_SMTP_USERNAME=' + username,
'MAIL_SMTP_PASSWORD=' + hat(256), // this is ignored
'MAIL_SMTP_USERNAME=' + from, // change this to app.id after apps have moved
'MAIL_SMTP_PASSWORD=' + 'app-' + app.id, // this is ignored
'MAIL_FROM=' + from + '@' + config.fqdn(),
'MAIL_DOMAIN=' + config.fqdn()
];
@@ -420,31 +434,14 @@ function setupMySql(app, options, callback) {
debugApp(app, 'Setting up mysql');
var container = docker.getContainer('mysql');
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', app.id ];
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
execContainer.start(function (error, stream) {
if (error) return callback(error);
var stdout = new MemoryStream();
var stderr = new MemoryStream();
execContainer.modem.demuxStream(stream, stdout, stderr);
stderr.on('data', function (data) { debugApp(app, data.toString('utf8')); }); // set -e output
var chunks = [ ];
stdout.on('data', function (chunk) { chunks.push(chunk); });
stream.on('error', callback);
stream.on('end', function () {
var env = Buffer.concat(chunks).toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting mysql addon config to %j', env);
appdb.setAddonConfig(app.id, 'mysql', env, callback);
});
});
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting mysql addon config to %j', env);
appdb.setAddonConfig(app.id, 'mysql', env, callback);
});
}
@@ -453,24 +450,14 @@ function teardownMySql(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('mysql');
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', app.id ];
debugApp(app, 'Tearing down mysql');
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
docker.execContainer('mysql', cmd, { }, function (error) {
if (error) return callback(error);
execContainer.start(function (error, stream) {
if (error) return callback(error);
var data = '';
stream.on('error', callback);
stream.on('data', function (d) { data += d.toString('utf8'); });
stream.on('end', function () {
appdb.unsetAddonConfig(app.id, 'mysql', callback);
});
});
appdb.unsetAddonConfig(app.id, 'mysql', callback);
});
}
@@ -482,15 +469,9 @@ function backupMySql(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'mysqldump'));
output.on('error', callback);
var cp = spawn('/usr/bin/docker', [ 'exec', 'mysql', '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'backupMySql: done. code:%s signal:%s', code, signal);
if (!callback.called) callback(code ? 'backupMySql failed with status ' + code : null);
});
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ];
cp.stdout.pipe(output);
cp.stderr.pipe(process.stderr);
docker.execContainer('mysql', cmd, { stdout: output }, callback);
}
function restoreMySql(app, options, callback) {
@@ -504,17 +485,8 @@ function restoreMySql(app, options, callback) {
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'mysqldump'));
input.on('error', callback);
// cannot get this to work through docker.exec
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mysql', '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'restoreMySql: done %s %s', code, signal);
if (!callback.called) callback(code ? 'restoreMySql failed with status ' + code : null);
});
cp.stdout.pipe(process.stdout);
cp.stderr.pipe(process.stderr);
input.pipe(cp.stdin).on('error', callback);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ];
docker.execContainer('mysql', cmd, { stdin: input }, callback);
});
}
@@ -525,31 +497,14 @@ function setupPostgreSql(app, options, callback) {
debugApp(app, 'Setting up postgresql');
var container = docker.getContainer('postgresql');
var cmd = [ '/addons/postgresql/service.sh', 'add', app.id ];
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
execContainer.start(function (error, stream) {
if (error) return callback(error);
var stdout = new MemoryStream();
var stderr = new MemoryStream();
execContainer.modem.demuxStream(stream, stdout, stderr);
stderr.on('data', function (data) { debugApp(app, data.toString('utf8')); }); // set -e output
var chunks = [ ];
stdout.on('data', function (chunk) { chunks.push(chunk); });
stream.on('error', callback);
stream.on('end', function () {
var env = Buffer.concat(chunks).toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting postgresql addon config to %j', env);
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
});
});
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting postgresql addon config to %j', env);
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
});
}
@@ -558,24 +513,14 @@ function teardownPostgreSql(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('postgresql');
var cmd = [ '/addons/postgresql/service.sh', 'remove', app.id ];
debugApp(app, 'Tearing down postgresql');
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
docker.execContainer('postgresql', cmd, { }, function (error) {
if (error) return callback(error);
execContainer.start(function (error, stream) {
if (error) return callback(error);
var data = '';
stream.on('error', callback);
stream.on('data', function (d) { data += d.toString('utf8'); });
stream.on('end', function () {
appdb.unsetAddonConfig(app.id, 'postgresql', callback);
});
});
appdb.unsetAddonConfig(app.id, 'postgresql', callback);
});
}
@@ -587,19 +532,13 @@ function backupPostgreSql(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'postgresqldump'));
output.on('error', callback);
var cp = spawn('/usr/bin/docker', [ 'exec', 'postgresql', '/addons/postgresql/service.sh', 'backup', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'backupPostgreSql: done %s %s', code, signal);
if (!callback.called) callback(code ? 'backupPostgreSql failed with status ' + code : null);
});
var cmd = [ '/addons/postgresql/service.sh', 'backup', app.id ];
cp.stdout.pipe(output);
cp.stderr.pipe(process.stderr);
docker.execContainer('postgresql', cmd, { stdout: output }, callback);
}
function restorePostgreSql(app, options, callback) {
callback = once(callback); // ChildProcess exit may or may not be called after error
callback = once(callback);
setupPostgreSql(app, options, function (error) {
if (error) return callback(error);
@@ -609,17 +548,9 @@ function restorePostgreSql(app, options, callback) {
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'postgresqldump'));
input.on('error', callback);
// cannot get this to work through docker.exec
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'postgresql', '/addons/postgresql/service.sh', 'restore', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'restorePostgreSql: done %s %s', code, signal);
if (!callback.called) callback(code ? 'restorePostgreSql failed with status ' + code : null);
});
var cmd = [ '/addons/postgresql/service.sh', 'restore', app.id ];
cp.stdout.pipe(process.stdout);
cp.stderr.pipe(process.stderr);
input.pipe(cp.stdin).on('error', callback);
docker.execContainer('postgresql', cmd, { stdin: input }, callback);
});
}
@@ -630,31 +561,14 @@ function setupMongoDb(app, options, callback) {
debugApp(app, 'Setting up mongodb');
var container = docker.getContainer('mongodb');
var cmd = [ '/addons/mongodb/service.sh', 'add', app.id ];
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
execContainer.start(function (error, stream) {
if (error) return callback(error);
var stdout = new MemoryStream();
var stderr = new MemoryStream();
execContainer.modem.demuxStream(stream, stdout, stderr);
stderr.on('data', function (data) { debugApp(app, data.toString('utf8')); }); // set -e output
var chunks = [ ];
stdout.on('data', function (chunk) { chunks.push(chunk); });
stream.on('error', callback);
stream.on('end', function () {
var env = Buffer.concat(chunks).toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
});
});
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
});
}
@@ -663,24 +577,14 @@ function teardownMongoDb(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('mongodb');
var cmd = [ '/addons/mongodb/service.sh', 'remove', app.id ];
debugApp(app, 'Tearing down mongodb');
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
docker.execContainer('mongodb', cmd, { }, function (error) {
if (error) return callback(error);
execContainer.start(function (error, stream) {
if (error) return callback(error);
var data = '';
stream.on('error', callback);
stream.on('data', function (d) { data += d.toString('utf8'); });
stream.on('end', function () {
appdb.unsetAddonConfig(app.id, 'mongodb', callback);
});
});
appdb.unsetAddonConfig(app.id, 'mongodb', callback);
});
}
@@ -692,15 +596,9 @@ function backupMongoDb(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'mongodbdump'));
output.on('error', callback);
var cp = spawn('/usr/bin/docker', [ 'exec', 'mongodb', '/addons/mongodb/service.sh', 'backup', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'backupMongoDb: done %s %s', code, signal);
if (!callback.called) callback(code ? 'backupMongoDb failed with status ' + code : null);
});
var cmd = [ '/addons/mongodb/service.sh', 'backup', app.id ];
cp.stdout.pipe(output);
cp.stderr.pipe(process.stderr);
docker.execContainer('mongodb', cmd, { stdout: output }, callback);
}
function restoreMongoDb(app, options, callback) {
@@ -714,26 +612,16 @@ function restoreMongoDb(app, options, callback) {
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'mongodbdump'));
input.on('error', callback);
// cannot get this to work through docker.exec
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mongodb', '/addons/mongodb/service.sh', 'restore', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'restoreMongoDb: done %s %s', code, signal);
if (!callback.called) callback(code ? 'restoreMongoDb failed with status ' + code : null);
});
cp.stdout.pipe(process.stdout);
cp.stderr.pipe(process.stderr);
input.pipe(cp.stdin).on('error', callback);
var cmd = [ '/addons/mongodb/service.sh', 'restore', app.id ];
docker.execContainer('mongodb', cmd, { stdin: input }, callback);
});
}
function forwardRedisPort(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
docker.getContainer('redis-' + appId).inspect(function (error, data) {
dockerConnection.getContainer('redis-' + appId).inspect(function (error, data) {
if (error) return callback(new Error('Unable to inspect container:' + error));
var redisPort = parseInt(safe.query(data, 'NetworkSettings.Ports.6379/tcp[0].HostPort'), 10);
@@ -781,7 +669,7 @@ function setupRedis(app, options, callback) {
name: 'redis-' + app.id,
Hostname: 'redis-' + app.location,
Tty: true,
Image: 'cloudron/redis:0.8.0', // if you change this, fix setup/INFRA_VERSION as well
Image: 'cloudron/redis:0.8.0', // if you change this, fix src/INFRA_VERSION as well
Cmd: null,
Volumes: {
'/tmp': {},
@@ -813,9 +701,9 @@ function setupRedis(app, options, callback) {
'REDIS_PORT=6379'
];
var redisContainer = docker.getContainer(createOptions.name);
var redisContainer = dockerConnection.getContainer(createOptions.name);
stopAndRemoveRedis(redisContainer, function () {
docker.createContainer(createOptions, function (error) {
dockerConnection.createContainer(createOptions, function (error) {
if (error && error.statusCode !== 409) return callback(error); // if not already created
redisContainer.start(function (error) {
@@ -836,7 +724,7 @@ function teardownRedis(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('redis-' + app.id);
var container = dockerConnection.getContainer('redis-' + app.id);
var removeOptions = {
force: true, // kill container if it's running
@@ -859,15 +747,7 @@ function teardownRedis(app, options, callback) {
function backupRedis(app, options, callback) {
debugApp(app, 'Backing up redis');
callback = once(callback); // ChildProcess exit may or may not be called after error
var cmd = [ '/addons/redis/service.sh', 'backup' ]; // the redis dir is volume mounted
var cp = spawn('/usr/bin/docker', [ 'exec', 'redis-' + app.id, '/addons/redis/service.sh', 'backup' ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'backupRedis: done. code:%s signal:%s', code, signal);
if (!callback.called) callback(code ? 'backupRedis failed with status ' + code : null);
});
cp.stdout.pipe(process.stdout);
cp.stderr.pipe(process.stderr);
docker.execContainer('redis-' + app.id, cmd, { }, callback);
}
+5 -21
View File
@@ -4,7 +4,6 @@
exports = module.exports = {
get: get,
getBySubdomain: getBySubdomain,
getByHttpPort: getByHttpPort,
getByContainerId: getByContainerId,
add: add,
@@ -59,7 +58,7 @@ var assert = require('assert'),
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.memoryLimit' ].join(',');
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -114,22 +113,6 @@ function get(id, callback) {
});
}
function getBySubdomain(subdomain, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE location = ? GROUP BY apps.id', [ subdomain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
callback(null, result[0]);
});
}
function getByHttpPort(httpPort, callback) {
assert.strictEqual(typeof httpPort, 'number');
assert.strictEqual(typeof callback, 'function');
@@ -177,7 +160,7 @@ function getAll(callback) {
});
}
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, memoryLimit, callback) {
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, memoryLimit, altDomain, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
@@ -186,6 +169,7 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof memoryLimit, 'number');
assert(altDomain === null || typeof altDomain === 'string');
assert.strictEqual(typeof callback, 'function');
portBindings = portBindings || { };
@@ -195,8 +179,8 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
var queries = [ ];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit) VALUES (?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, memoryLimit ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, memoryLimit, altDomain ]
});
Object.keys(portBindings).forEach(function (env) {
+1 -1
View File
@@ -162,7 +162,7 @@ function processDockerEvents() {
debug('OOM Context: %s', context);
// do not send mails for dev apps
if (error || app.appStoreId !== '') mailer.sendCrashNotification(program, context); // app can be null if it's an addon crash
if (error || app.appStoreId !== '') mailer.unexpectedExit(program, context); // app can be null if it's an addon crash
});
});
+47 -32
View File
@@ -6,7 +6,6 @@ exports = module.exports = {
hasAccessTo: hasAccessTo,
get: get,
getBySubdomain: getBySubdomain,
getByIpAddress: getByIpAddress,
getAll: getAll,
getAllByUser: getAllByUser,
@@ -50,6 +49,7 @@ var addons = require('./addons.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apps'),
docker = require('./docker.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
groups = require('./groups.js'),
manifestFormat = require('cloudron-manifestformat'),
@@ -103,7 +103,7 @@ AppsError.BAD_CERTIFICATE = 'Invalid certificate';
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name
function validateHostname(location, fqdn) {
var RESERVED_LOCATIONS = [ constants.ADMIN_LOCATION, constants.API_LOCATION ];
var RESERVED_LOCATIONS = [ constants.ADMIN_LOCATION, constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION ];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new Error(location + ' is reserved');
@@ -126,13 +126,17 @@ function validatePortBindings(portBindings, tcpPorts) {
25, /* smtp */
53, /* dns */
80, /* http */
143, /* imap */
443, /* https */
465, /* smtps */
587, /* submission */
919, /* ssh */
993, /* imaps */
2003, /* graphite (lo) */
2004, /* graphite (lo) */
2020, /* install server */
config.get('port'), /* app server (lo) */
config.get('internalPort'), /* internal app server (lo) */
config.get('sysadminPort'), /* sysadmin app server (lo) */
config.get('ldapPort'), /* ldap server (lo) */
config.get('oauthProxyPort'), /* oauth proxy server (lo) */
config.get('simpleAuthPort'), /* simple auth server (lo) */
@@ -261,22 +265,7 @@ function get(appId, callback) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.iconUrl = getIconUrlSync(app);
app.fqdn = config.appFqdn(app.location);
callback(null, app);
});
}
function getBySubdomain(subdomain, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.getBySubdomain(subdomain, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.iconUrl = getIconUrlSync(app);
app.fqdn = config.appFqdn(app.location);
app.fqdn = app.altDomain || config.appFqdn(app.location);
callback(null, app);
});
@@ -294,7 +283,7 @@ function getByIpAddress(ip, callback) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.iconUrl = getIconUrlSync(app);
app.fqdn = config.appFqdn(app.location);
app.fqdn = app.altDomain || config.appFqdn(app.location);
callback(null, app);
});
@@ -309,7 +298,7 @@ function getAll(callback) {
apps.forEach(function (app) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = config.appFqdn(app.location);
app.fqdn = app.altDomain || config.appFqdn(app.location);
});
callback(null, apps);
@@ -353,7 +342,7 @@ function purchase(appStoreId, callback) {
});
}
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, cert, key, memoryLimit, callback) {
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, cert, key, memoryLimit, altDomain, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
@@ -364,6 +353,8 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof memoryLimit, 'number');
assert(altDomain === null || typeof altDomain === 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var error = manifestFormat.parse(manifest);
@@ -387,6 +378,8 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
// memoryLimit might come in as 0 if not specified
memoryLimit = memoryLimit || manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
// singleUser mode requires accessRestriction to contain exactly one user
if (manifest.singleUser && accessRestriction === null) return callback(new AppsError(AppsError.USER_REQUIRED));
if (manifest.singleUser && accessRestriction.users.length !== 1) return callback(new AppsError(AppsError.USER_REQUIRED));
@@ -407,7 +400,7 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
purchase(appStoreId, function (error) {
if (error) return callback(error);
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, memoryLimit, function (error) {
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, memoryLimit, altDomain, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -419,12 +412,14 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest });
callback(null);
});
});
}
function configure(appId, location, portBindings, accessRestriction, cert, key, memoryLimit, callback) {
function configure(appId, location, portBindings, accessRestriction, cert, key, memoryLimit, altDomain, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
@@ -432,6 +427,8 @@ function configure(appId, location, portBindings, accessRestriction, cert, key,
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof memoryLimit, 'number');
assert(altDomain === null || typeof altDomain === 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var error = validateHostname(location, config.fqdn());
@@ -443,6 +440,8 @@ function configure(appId, location, portBindings, accessRestriction, cert, key,
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -467,12 +466,14 @@ function configure(appId, location, portBindings, accessRestriction, cert, key,
accessRestriction: accessRestriction,
portBindings: portBindings,
memoryLimit: memoryLimit,
altDomain: altDomain,
oldConfig: {
location: app.location,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit
memoryLimit: app.memoryLimit,
altDomain: altDomain
}
};
@@ -485,17 +486,20 @@ function configure(appId, location, portBindings, accessRestriction, cert, key,
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
callback(null);
});
});
}
function update(appId, force, manifest, portBindings, icon, callback) {
function update(appId, force, manifest, portBindings, icon, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof force, 'boolean');
assert(manifest && typeof manifest === 'object');
assert(!portBindings || typeof portBindings === 'object');
assert(typeof portBindings === 'object'); // can be null
assert(!icon || typeof icon === 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will update app with id:%s', appId);
@@ -544,7 +548,8 @@ function update(appId, force, manifest, portBindings, icon, callback) {
manifest: app.manifest,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction,
memoryLimit: app.memoryLimit
memoryLimit: app.memoryLimit,
altDomain: app.altDomain
}
};
@@ -555,6 +560,8 @@ function update(appId, force, manifest, portBindings, icon, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest });
callback(null);
});
});
@@ -605,8 +612,9 @@ function getLogs(appId, lines, follow, callback) {
});
}
function restore(appId, callback) {
function restore(appId, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will restore app with id:%s', appId);
@@ -637,7 +645,8 @@ function restore(appId, callback) {
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
manifest: app.manifest
manifest: app.manifest,
altDomain: app.altDomain
}
};
}
@@ -648,13 +657,16 @@ function restore(appId, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
callback(null);
});
});
}
function uninstall(appId, callback) {
function uninstall(appId, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will uninstall app with id:%s', appId);
@@ -664,6 +676,8 @@ function uninstall(appId, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
taskmanager.startAppTask(appId, callback);
});
});
@@ -796,7 +810,8 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
return iteratorDone();
}
update(appId, false /* force */, updateInfo[appId].manifest, app.portBindings, null /* icon */, function (error) {
update(appId, false /* force */, updateInfo[appId].manifest, app.portBindings,
null /* icon */, { userId: null, username: 'autoupdater' }, function (error) {
if (error) debug('Error initiating autoupdate of %s. %s', appId, error.message);
iteratorDone(null);
+40 -28
View File
@@ -1,7 +1,5 @@
#!/usr/bin/env node
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -19,7 +17,8 @@ exports = module.exports = {
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
_waitForDnsPropagation: waitForDnsPropagation
_waitForDnsPropagation: waitForDnsPropagation,
_waitForAltDomainDnsPropagation: waitForAltDomainDnsPropagation
};
require('supererror')({ splatchError: true });
@@ -59,6 +58,7 @@ var addons = require('./addons.js'),
sysinfo = require('./sysinfo.js'),
util = require('util'),
uuid = require('node-uuid'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
@@ -101,9 +101,7 @@ function configureNginx(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = config.appFqdn(app.location);
certificates.ensureCertificate(vhost, function (error, certFilePath, keyFilePath) {
certificates.ensureCertificate(app, function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
nginx.configureApp(app, certFilePath, keyFilePath, callback);
@@ -232,17 +230,19 @@ function downloadIcon(app, callback) {
var iconUrl = config.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
superagent
.get(iconUrl)
.buffer(true)
.end(function (error, res) {
if (error && !error.response) return callback(new Error('Network error downloading icon:' + error.message));
if (res.statusCode !== 200) return callback(null); // ignore error. this can also happen for apps installed with cloudron-cli
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
superagent
.get(iconUrl)
.buffer(true)
.end(function (error, res) {
if (error && !error.response) return retryCallback(new Error('Network error downloading icon:' + error.message));
if (res.statusCode !== 200) return retryCallback(null); // ignore error. this can also happen for apps installed with cloudron-cli
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, app.id + '.png'), res.body)) return callback(new Error('Error saving icon:' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, app.id + '.png'), res.body)) return retryCallback(new Error('Error saving icon:' + safe.error.message));
callback(null);
});
retryCallback(null);
});
}, callback);
}
function registerSubdomain(app, callback) {
@@ -319,20 +319,23 @@ function waitForDnsPropagation(app, callback) {
return callback(null);
}
function retry(error) {
debugApp(app, 'waitForDnsPropagation: ', error);
setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000);
}
async.retry({ interval: 5000, times: 120 }, function checkStatus(retryCallback) {
subdomains.status(app.dnsRecordId, function (error, result) {
if (error) return retryCallback(new Error('Failed to get dns record status : ' + error.message));
subdomains.status(app.dnsRecordId, function (error, result) {
if (error) return retry(new Error('Failed to get dns record status : ' + error.message));
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, result);
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, result);
if (result !== 'done') return retryCallback(new Error(util.format('app:%s not ready yet: %s', app.id, result)));
if (result !== 'done') return retry(new Error(util.format('app:%s not ready yet: %s', app.id, result)));
retryCallback(null, result);
});
}, callback);
}
callback(null);
});
function waitForAltDomainDnsPropagation(app, callback) {
if (!app.altDomain) return callback(null);
waitForDns(app.altDomain, config.appFqdn(app.location), 'CNAME', callback); // waits forever
}
// updates the app object and the database
@@ -411,9 +414,12 @@ function install(app, callback) {
runApp.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for Alt Domain DNS propagation' }),
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !lastBackupId
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
configureNginx.bind(null, app),
@@ -512,9 +518,12 @@ function restore(app, callback) {
runApp.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for Alt Domain DNS propagation' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Configuring Nginx' }),
configureNginx.bind(null, app),
@@ -574,6 +583,9 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '85, Waiting for Alt Domain DNS propagation' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
configureNginx.bind(null, app),
@@ -630,7 +642,7 @@ function update(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '30, Backup app' }),
apps.backupApp.bind(null, app, app.oldConfig.manifest.addons)
backups.backupApp.bind(null, app, app.oldConfig.manifest.addons)
], next);
},
+23 -11
View File
@@ -27,6 +27,7 @@ var addons = require('./addons.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:backups'),
eventlog = require('./eventlog.js'),
locker = require('./locker.js'),
path = require('path'),
paths = require('./paths.js'),
@@ -189,18 +190,23 @@ function copyLastBackup(app, callback) {
assert.strictEqual(typeof app.lastBackupId, 'string');
assert.strictEqual(typeof callback, 'function');
var toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', app.id, (new Date()).toISOString(), app.manifest.version);
var now = new Date();
var toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, now.toISOString(), app.manifest.version);
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', app.id, now.toISOString(), app.manifest.version);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug('copyLastBackup: copying archive %s to %s', app.lastBackupId, toFilenameArchive);
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilenameArchive, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
// TODO change that logic by adjusting app.lastBackupId to not contain the file type
var configFileId = app.lastBackupId.slice(0, -'.tar.gz'.length) + '.json';
debug('copyLastBackup: copying config %s to %s', configFileId, toFilenameConfig);
api(backupConfig.provider).copyObject(backupConfig, configFileId, toFilenameConfig, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
@@ -242,10 +248,10 @@ function backupBoxWithAppBackupIds(appBackupIds, callback) {
// function backupBox(callback) {
// apps.getAll(function (error, allApps) {
// if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
//
//
// var appBackupIds = allApps.map(function (app) { return app.lastBackupId; });
// appBackupIds = appBackupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
//
//
// backupBoxWithAppBackupIds(appBackupIds, callback);
// });
// }
@@ -362,9 +368,13 @@ function backupApp(app, addonsToBackup, callback) {
}
// this function expects you to have a lock
function backupBoxAndApps(callback) {
function backupBoxAndApps(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
callback = callback || NOOP_CALLBACK;
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { });
apps.getAll(function (error, allApps) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
@@ -397,23 +407,24 @@ function backupBoxAndApps(callback) {
backupBoxWithAppBackupIds(backupIds, function (error, filename) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, filename: filename });
callback(error, filename);
});
});
});
}
function backup(callback) {
function backup(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, error.message));
// ensure tools can 'wait' on progress
progress.set(progress.BACKUP, 0, 'Starting');
progress.set(progress.BACKUP, 0, 'Starting'); // ensure tools can 'wait' on progress
// start the backup operation in the background
backupBoxAndApps(function (error) {
backupBoxAndApps(auditSource, function (error) { // start the backup operation in the background
if (error) console.error('backup failed.', error);
locker.unlock(locker.OP_FULL_BACKUP);
@@ -436,7 +447,8 @@ function ensureBackup(callback) {
return callback(null);
}
backup(callback);
var eventSource = { userId: null, username: 'cron' };
backup(eventSource, callback);
});
}
+57 -28
View File
@@ -1,5 +1,16 @@
'use strict';
exports = module.exports = {
installAdminCertificate: installAdminCertificate,
autoRenew: autoRenew,
setFallbackCertificate: setFallbackCertificate,
setAdminCertificate: setAdminCertificate,
CertificatesError: CertificatesError,
validateCertificate: validateCertificate,
ensureCertificate: ensureCertificate,
getAdminCertificatePath: getAdminCertificatePath
};
var acme = require('./cert/acme.js'),
apps = require('./apps.js'),
assert = require('assert'),
@@ -9,6 +20,7 @@ var acme = require('./cert/acme.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:src/certificates'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
@@ -17,22 +29,11 @@ var acme = require('./cert/acme.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
user = require('./user.js'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
x509 = require('x509');
exports = module.exports = {
installAdminCertificate: installAdminCertificate,
autoRenew: autoRenew,
setFallbackCertificate: setFallbackCertificate,
setAdminCertificate: setAdminCertificate,
CertificatesError: CertificatesError,
validateCertificate: validateCertificate,
ensureCertificate: ensureCertificate
};
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function CertificatesError(reason, errorOrMessage) {
@@ -56,12 +57,16 @@ function CertificatesError(reason, errorOrMessage) {
util.inherits(CertificatesError, Error);
CertificatesError.INTERNAL_ERROR = 'Internal Error';
CertificatesError.INVALID_CERT = 'Invalid certificate';
CertificatesError.NOT_FOUND = 'Not Found';
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
function getApi(callback) {
settings.getTlsConfig(function (error, tlsConfig) {
if (error) return callback(error);
var api = tlsConfig.provider === 'caas' ? caas : acme;
var api = !app.altDomain && tlsConfig.provider === 'caas' ? caas : acme;
var options = { };
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
@@ -89,11 +94,10 @@ function installAdminCertificate(callback) {
sysinfo.getIp(function (error, ip) {
if (error) return callback(error);
var zoneName = tld.getDomain(config.fqdn());
waitForDns(config.adminFqdn(), ip, zoneName, function (error) {
waitForDns(config.adminFqdn(), ip, 'A', function (error) {
if (error) return callback(error); // this cannot happen because we retry forever
ensureCertificate(config.adminFqdn(), function (error, certFilePath, keyFilePath) {
ensureCertificate({ location: constants.ADMIN_LOCATION }, function (error, certFilePath, keyFilePath) {
if (error) { // currently, this can never happen
debug('Error obtaining certificate. Proceed anyway', error);
return callback();
@@ -126,11 +130,11 @@ function autoRenew(callback) {
apps.getAll(function (error, allApps) {
if (error) return callback(error);
allApps.push({ location: 'my' }); // inject fake webadmin app
allApps.push({ location: constants.ADMIN_LOCATION }); // inject fake webadmin app
var expiringApps = [ ];
for (var i = 0; i < allApps.length; i++) {
var appDomain = config.appFqdn(allApps[i].location);
var appDomain = allApps[i].altDomain || config.appFqdn(allApps[i].location);
var certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.key');
@@ -144,20 +148,23 @@ function autoRenew(callback) {
}
}
debug('autoRenew: %j needs to be renewed', expiringApps.map(function (a) { return config.appFqdn(a.location); }));
debug('autoRenew: %j needs to be renewed', expiringApps.map(function (a) { return a.altDomain || config.appFqdn(a.location); }));
getApi(function (error, api, apiOptions) {
if (error) return callback(error);
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
var domain = app.altDomain || config.appFqdn(app.location);
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
var domain = config.appFqdn(app.location);
debug('autoRenew: renewing cert for %s with options %j', domain, apiOptions);
api.getCertificate(domain, apiOptions, function (error) {
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
mailer.certificateRenewed(domain, error ? error.message : '');
var errorMessage = error ? error.message : '';
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, { userId: null, username: 'cron' }, { domain: domain, errorMessage: errorMessage });
mailer.certificateRenewed(domain, errorMessage);
if (error) {
debug('autoRenew: could not renew cert for %s because %s', domain, error);
@@ -252,12 +259,20 @@ function setFallbackCertificate(cert, key, callback) {
});
}
function getFallbackCertificatePath(callback) {
assert.strictEqual(typeof callback, 'function');
// any user fallback cert is always copied over to nginx cert dir
callback(null, path.join(paths.NGINX_CERT_DIR, 'host.cert'), path.join(paths.NGINX_CERT_DIR, 'host.key'));
}
// FIXME: setting admin cert needs to restart the mail container because it uses admin cert
function setAdminCertificate(cert, key, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
var vhost = config.appFqdn(constants.ADMIN_LOCATION);
var vhost = config.adminFqdn();
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
@@ -271,10 +286,24 @@ function setAdminCertificate(cert, key, callback) {
nginx.configureAdmin(certFilePath, keyFilePath, callback);
}
function ensureCertificate(domain, callback) {
assert.strictEqual(typeof domain, 'string');
function getAdminCertificatePath(callback) {
assert.strictEqual(typeof callback, 'function');
var vhost = config.adminFqdn();
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
getFallbackCertificatePath(callback);
}
function ensureCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var domain = app.altDomain || config.appFqdn(app.location);
// check if user uploaded a specific cert. ideally, we should not mix user certs and automatic certs as we do here...
var userCertFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
var userKeyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
@@ -287,7 +316,7 @@ function ensureCertificate(domain, callback) {
debug('ensureCertificate: %s cert require renewal', domain);
getApi(function (error, api, apiOptions) {
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('ensureCertificate: getting certificate for %s with options %j', domain, apiOptions);
+19 -2
View File
@@ -8,7 +8,14 @@ exports = module.exports = {
del: del,
getAllWithDetailsByUserId: getAllWithDetailsByUserId,
getClientTokensByUserId: getClientTokensByUserId,
delClientTokensByUserId: delClientTokensByUserId
delClientTokensByUserId: delClientTokensByUserId,
SCOPE_APPS: 'apps',
SCOPE_DEVELOPER: 'developer',
SCOPE_PROFILE: 'profile',
SCOPE_ROOT: 'root',
SCOPE_SETTINGS: 'settings',
SCOPE_USERS: 'users'
};
var assert = require('assert'),
@@ -47,10 +54,20 @@ ClientsError.INVALID_CLIENT = 'Invalid client';
function validateScope(scope) {
assert.strictEqual(typeof scope, 'string');
var VALID_SCOPES = [
exports.SCOPE_APPS,
exports.SCOPE_DEVELOPER,
exports.SCOPE_PROFILE,
exports.SCOPE_ROOT,
exports.SCOPE_SETTINGS,
exports.SCOPE_USERS
];
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE);
if (scope === '*') return null;
// TODO maybe validate all individual scopes if they exist
var allValid = scope.split(',').every(function (s) { return VALID_SCOPES.indexOf(s) !== -1; });
if (!allValid) return new ClientsError(ClientsError.INVALID_SCOPE);
return null;
}
+41 -11
View File
@@ -35,6 +35,7 @@ var apps = require('./apps.js'),
config = require('./config.js'),
debug = require('debug')('box:cloudron'),
df = require('node-df'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
locker = require('./locker.js'),
mailer = require('./mailer.js'),
@@ -101,6 +102,8 @@ CloudronError.NOT_FOUND = 'Not found';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
ensureDkimKeySync();
exports.events.on(exports.EVENT_CONFIGURED, addDnsRecords);
exports.events.on(exports.EVENT_FIRST_RUN, installAppBundle);
@@ -190,36 +193,43 @@ function setTimeZone(ip, callback) {
debug('setTimeZone ip:%s', ip);
superagent.get('http://www.telize.com/geoip/' + ip).end(function (error, result) {
// https://github.com/bluesmoon/node-geoip
// https://github.com/runk/node-maxmind
// { url: 'http://freegeoip.net/json/%s', jpath: 'time_zone' },
// { url: 'http://ip-api.com/json/%s', jpath: 'timezone' },
// { url: 'http://geoip.nekudo.com/api/%s', jpath: 'time_zone }
superagent.get('http://freegeoip.net/json/' + ip).end(function (error, result) {
if ((error && !error.response) || result.statusCode !== 200) {
debug('Failed to get geo location: %s', error.message);
return callback(null);
}
if (!result.body.timezone) {
if (!result.body.time_zone || typeof result.body.time_zone !== 'string') {
debug('No timezone in geoip response : %j', result.body);
return callback(null);
}
debug('Setting timezone to ', result.body.timezone);
debug('Setting timezone to ', result.body.time_zone);
settings.setTimeZone(result.body.timezone, callback);
settings.setTimeZone(result.body.time_zone, callback);
});
}
function activate(username, password, email, displayName, ip, callback) {
function activate(username, password, email, displayName, ip, auditSource, callback) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('activating user:%s email:%s', username, email);
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
user.createOwner(username, password, email, displayName, function (error, userObject) {
user.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
if (error && error.reason === UserError.BAD_USERNAME) return callback(new CloudronError(CloudronError.BAD_USERNAME));
if (error && error.reason === UserError.BAD_PASSWORD) return callback(new CloudronError(CloudronError.BAD_PASSWORD));
@@ -239,6 +249,8 @@ function activate(username, password, email, displayName, ip, callback) {
// EE API is sync. do not keep the REST API reponse waiting
process.nextTick(function () { exports.events.emit(exports.EVENT_ACTIVATED); });
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
callback(null, { token: token, expires: expires });
});
});
@@ -352,6 +364,21 @@ function sendHeartbeat() {
});
}
function ensureDkimKeySync() {
var dkimPrivateKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/private');
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
if (fs.existsSync(dkimPrivateKeyFile) && fs.existsSync(dkimPublicKeyFile)) {
debug('DKIM keys already present');
return;
}
debug('Generating new DKIM keys');
safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024');
safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM');
}
function readDkimPublicKeySync() {
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
@@ -412,7 +439,7 @@ function addDnsRecords() {
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
var dkimKey = readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('internal error failed to read dkim public key')));
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
sysinfo.getIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
@@ -510,12 +537,15 @@ function update(boxUpdateInfo, callback) {
}
function updateToLatest(callback) {
function updateToLatest(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
update(boxUpdateInfo, callback);
}
@@ -539,7 +569,7 @@ function doUpgrade(boxUpdateInfo, callback) {
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
backups.backupBoxAndApps(function (error) {
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
@@ -568,7 +598,7 @@ function doUpdate(boxUpdateInfo, callback) {
progress.set(progress.UPDATE, 5, 'Backing up for update');
backups.backupBoxAndApps(function (error) {
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
if (error) return updateError(error);
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
@@ -641,7 +671,7 @@ function installAppBundle(callback) {
apps.install(uuid.v4(), appstoreId, result.body.manifest, appInfo.location,
appInfo.portBindings || null, appInfo.accessRestriction || null,
null /* icon */, null /* cert */, null /* key */, 0 /* default mem limit */,
iteratorCallback);
null /* altDomain */, { userId: null, username: 'autoinstaller' }, iteratorCallback);
});
}, function (error) {
if (error) debug('autoInstallApps: ', error);
+6 -1
View File
@@ -26,6 +26,7 @@ exports = module.exports = {
// these values are derived
adminOrigin: adminOrigin,
internalAdminOrigin: internalAdminOrigin,
sysadminOrigin: sysadminOrigin, // caas routes
adminFqdn: adminFqdn,
appFqdn: appFqdn,
zoneName: zoneName,
@@ -77,7 +78,7 @@ function initConfig() {
data.version = null;
data.isCustomDomain = false;
data.webServerOrigin = null;
data.internalPort = 3001;
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.oauthProxyPort = 3003;
data.simpleAuthPort = 3004;
@@ -174,6 +175,10 @@ function internalAdminOrigin() {
return 'http://127.0.0.1:' + get('port');
}
function sysadminOrigin() {
return 'http://127.0.0.1:' + get('sysadminPort');
}
function token() {
return get('token');
}
+3
View File
@@ -4,6 +4,9 @@
exports = module.exports = {
ADMIN_LOCATION: 'my',
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
ADMIN_NAME: 'Settings',
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
+2 -1
View File
@@ -121,7 +121,8 @@ function clear(callback) {
require('./tokendb.js')._clear,
require('./groupdb.js')._clear,
require('./userdb.js')._clear,
require('./settingsdb.js')._clear
require('./settingsdb.js')._clear,
require('./eventlogdb.js')._clear
], callback);
}
+10 -2
View File
@@ -14,6 +14,7 @@ exports = module.exports = {
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:developer'),
eventlog = require('./eventlog.js'),
tokendb = require('./tokendb.js'),
settings = require('./settings.js'),
superagent = require('superagent'),
@@ -50,18 +51,23 @@ function enabled(callback) {
});
}
function setEnabled(enabled, callback) {
function setEnabled(enabled, auditSource, callback) {
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
settings.setDeveloperMode(enabled, function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_CLI_MODE, auditSource, { enabled: enabled });
callback(null);
});
}
function issueDeveloperToken(user, callback) {
function issueDeveloperToken(user, auditSource, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var token = tokendb.generateToken();
@@ -70,6 +76,8 @@ function issueDeveloperToken(user, callback) {
tokendb.add(token, tokendb.PREFIX_DEV + user.id, '', expiresAt, 'developer,apps,settings,users,profile', function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { authType: 'cli', userId: user.id, username: user.username });
callback(null, { token: token, expiresAt: expiresAt });
});
}
+51 -15
View File
@@ -1,17 +1,5 @@
'use strict';
var addons = require('./addons.js'),
async = require('async'),
assert = require('assert'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:src/docker.js'),
Docker = require('dockerode'),
safe = require('safetydance'),
semver = require('semver'),
util = require('util'),
_ = require('underscore');
exports = module.exports = {
connection: connectionInstance(),
downloadImage: downloadImage,
@@ -25,10 +13,12 @@ exports = module.exports = {
deleteImage: deleteImage,
deleteContainers: deleteContainers,
createSubcontainer: createSubcontainer,
getContainerIdByIp: getContainerIdByIp
getContainerIdByIp: getContainerIdByIp,
execContainer: execContainer
};
function connectionInstance() {
var Docker = require('dockerode');
var docker;
if (process.env.BOX_ENV === 'test') {
@@ -44,6 +34,20 @@ function connectionInstance() {
return docker;
}
var addons = require('./addons.js'),
async = require('async'),
assert = require('assert'),
child_process = require('child_process'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:src/docker.js'),
once = require('once'),
safe = require('safetydance'),
semver = require('semver'),
spawn = child_process.spawn,
util = require('util'),
_ = require('underscore');
function debugApp(app, args) {
assert(!app || typeof app === 'object');
@@ -134,12 +138,13 @@ function createSubcontainer(app, name, cmd, options, callback) {
var manifest = app.manifest;
var developmentMode = !!manifest.developmentMode;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.altDomain || config.appFqdn(app.location);
var stdEnv = [
'CLOUDRON=1',
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
'API_ORIGIN=' + config.adminOrigin(),
'APP_ORIGIN=https://' + config.appFqdn(app.location),
'APP_DOMAIN=' + config.appFqdn(app.location)
'APP_ORIGIN=https://' + domain,
'APP_DOMAIN=' + domain
];
// docker portBindings requires ports to be exposed
@@ -389,3 +394,34 @@ function getContainerIdByIp(ip, callback) {
callback(null, containerId);
});
}
function execContainer(containerId, cmd, options, callback) {
assert.strictEqual(typeof containerId, 'string');
assert(util.isArray(cmd));
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback); // ChildProcess exit may or may not be called after error
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', containerId ].concat(cmd));
var chunks = [ ];
if (options.stdout) {
cp.stdout.pipe(options.stdout);
} else if (options.bufferStdout) {
cp.stdout.on('data', function (chunk) { chunks.push(chunk); });
} else {
cp.stdout.pipe(process.stdout);
}
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debug('execContainer code: %s signal: %s', code, signal);
if (!callback.called) callback(code ? 'Failed with status ' + code : null, Buffer.concat(chunks));
});
cp.stderr.pipe(options.stderr || process.stderr);
if (options.stdin) options.stdin.pipe(cp.stdin).on('error', callback);
}
+101
View File
@@ -0,0 +1,101 @@
'use strict';
exports = module.exports = {
EventLogError: EventLogError,
add: add,
get: get,
getAllPaged: getAllPaged,
// keep in sync with webadmin index.js filter
ACTION_ACTIVATE: 'cloudron.activate',
ACTION_APP_CONFIGURE: 'app.configure',
ACTION_APP_INSTALL: 'app.install',
ACTION_APP_RESTORE: 'app.restore',
ACTION_APP_UNINSTALL: 'app.uninstall',
ACTION_APP_UPDATE: 'app.update',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
ACTION_CLI_MODE: 'settings.climode',
ACTION_START: 'cloudron.start',
ACTION_UPDATE: 'cloudron.update',
ACTION_USER_ADD: 'user.add',
ACTION_USER_LOGIN: 'user.login',
ACTION_USER_REMOVE: 'user.remove',
ACTION_USER_UPDATE: 'user.update'
};
var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:eventlog'),
eventlogdb = require('./eventlogdb.js'),
util = require('util'),
uuid = require('node-uuid');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function EventLogError(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(EventLogError, Error);
EventLogError.INTERNAL_ERROR = 'Internal error';
EventLogError.NOT_FOUND = 'Not Found';
function add(action, source, data, callback) {
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof source, 'object');
assert.strictEqual(typeof data, 'object');
assert(!callback || typeof callback === 'function');
callback = callback || NOOP_CALLBACK;
var id = uuid.v4();
eventlogdb.add(id, action, source, data, function (error) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, { id: id });
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
eventlogdb.get(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new EventLogError(EventLogError.NOT_FOUND, 'No such event'));
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
eventlogdb.getAllPaged(action, search, page, perPage, function (error, boxes) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, boxes);
});
}
+104
View File
@@ -0,0 +1,104 @@
'use strict';
exports = module.exports = {
get: get,
getAllPaged: getAllPaged,
add: add,
count: count,
_clear: clear
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mysql = require('mysql'),
safe = require('safetydance');
var EVENTLOGS_FIELDS = [ 'id', 'action', 'source', 'data', 'creationTime' ].join(',');
// until mysql module supports automatic type coercion
function postProcess(eventLog) {
eventLog.source = safe.JSON.parse(eventLog.source);
eventLog.data = safe.JSON.parse(eventLog.data);
return eventLog;
}
function get(eventId, callback) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE id = ?', [ eventId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, postProcess(result[0]));
});
}
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
var data = [];
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog';
if (action || search) query += ' WHERE';
if (search) query += ' data LIKE ' + mysql.escape('%' + search + '%');
if (action && search) query += ' AND ';
if (action) {
query += ' action=?';
data.push(action);
}
query += ' ORDER BY creationTime DESC LIMIT ?,?';
data.push((page-1)*perPage);
data.push(perPage);
database.query(query, data, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
callback(null, results);
});
}
function add(id, action, source, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof source, 'object');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO eventlog (id, action, source, data) VALUES (?, ?, ?, ?)', [ id, action, JSON.stringify(source), JSON.stringify(data) ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function count(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM eventlog', function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
return callback(null, result[0].total);
});
}
function clear(callback) {
database.query('DELETE FROM eventlog', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(error);
});
}
+20 -7
View File
@@ -9,6 +9,7 @@ var assert = require('assert'),
apps = require('./apps.js'),
config = require('./config.js'),
debug = require('debug')('box:ldap'),
eventlog = require('./eventlog.js'),
user = require('./user.js'),
UserError = user.UserError,
ldap = require('ldapjs');
@@ -62,7 +63,7 @@ function start(callback) {
var firstName = nameParts[0];
var lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; // choose last part, if it exists
var tmp = {
var obj = {
dn: dn.toString(),
attributes: {
objectclass: ['user'],
@@ -72,18 +73,21 @@ function start(callback) {
mail: entry.email,
displayname: displayName,
givenName: firstName,
sn: lastName,
username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
memberof: groups
}
};
// http://www.zytrax.com/books/ldap/ape/core-schema.html#sn has 'name' as SUP which is a DirectoryString
// which is required to have atleast one character if present
if (lastName.length !== 0) obj.attributes.sn = lastName;
// ensure all filter values are also lowercase
var lowerCaseFilter = ldap.parseFilter(req.filter.toString().toLowerCase());
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(tmp.attributes)) {
res.send(tmp);
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
res.send(obj);
}
});
@@ -109,7 +113,7 @@ function start(callback) {
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
var tmp = {
var obj = {
dn: dn.toString(),
attributes: {
objectclass: ['group'],
@@ -121,8 +125,8 @@ function start(callback) {
// ensure all filter values are also lowercase
var lowerCaseFilter = ldap.parseFilter(req.filter.toString().toLowerCase());
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(tmp.attributes)) {
res.send(tmp);
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
res.send(obj);
}
});
@@ -130,6 +134,13 @@ function start(callback) {
});
});
// this is the bind for the mail addon to authorize apps
gServer.bind('ou=sendmail,dc=cloudron', function(req, res, next) {
// TODO: validate password
debug('sendmail bind: %s', req.dn.toString()); // note: cn can be email or id
res.end();
});
gServer.bind('ou=apps,dc=cloudron', function(req, res, next) {
// TODO: validate password
debug('application bind: %s', req.dn.toString());
@@ -174,6 +185,8 @@ function start(callback) {
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: userObject.id });
res.end();
});
});
+37
View File
@@ -0,0 +1,37 @@
'use strict';
exports = module.exports = {
sendFailureLogs: sendFailureLogs
};
var assert = require('assert'),
mailer = require('./mailer.js'),
safe = require('safetydance'),
path = require('path'),
util = require('util');
var COLLECT_LOGS_CMD = path.join(__dirname, 'scripts/collectlogs.sh');
function collectLogs(unitName, callback) {
assert.strictEqual(typeof unitName, 'string');
assert.strictEqual(typeof callback, 'function');
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + unitName, { encoding: 'utf8' });
callback(null, logs);
}
function sendFailureLogs(processName, options) {
assert.strictEqual(typeof processName, 'string');
assert.strictEqual(typeof options, 'object');
collectLogs(options.unit || processName, function (error, result) {
if (error) {
console.error('Failed to collect logs.', error);
result = util.format('Failed to collect logs.', error);
}
console.log('Sending failure logs for', processName);
mailer.unexpectedExit(processName, result);
});
}
@@ -2,7 +2,7 @@
Dear Cloudron Team,
Unfortunately <%= program %> on <%= fqdn %> crashed unexpectedly!
Unfortunately <%= program %> on <%= fqdn %> exited unexpectedly!
Please see some excerpt of the logs below.
+10 -4
View File
@@ -12,7 +12,7 @@ exports = module.exports = {
appUpdateAvailable: appUpdateAvailable,
sendInvite: sendInvite,
sendCrashNotification: sendCrashNotification,
unexpectedExit: unexpectedExit,
appDied: appDied,
@@ -24,6 +24,7 @@ exports = module.exports = {
FEEDBACK_TYPE_TICKET: 'ticket',
FEEDBACK_TYPE_APP_MISSING: 'app_missing',
FEEDBACK_TYPE_APP_ERROR: 'app_error',
FEEDBACK_TYPE_UPGRADE_REQUEST: 'upgrade_request',
sendFeedback: sendFeedback,
_getMailQueue: _getMailQueue,
@@ -158,7 +159,11 @@ function sendMails(queue) {
var transport = nodemailer.createTransport(smtpTransport({
host: mailServerIp,
port: 2500 // this value comes from mail container
port: 2500, // this value comes from mail container
auth: {
user: 'no-reply', // derive from adminEmail
pass: 'supersecret'
}
}));
debug('Processing mail queue of size %d (through %s:2500)', queue.length, mailServerIp);
@@ -394,7 +399,7 @@ function certificateRenewed(domain, message) {
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
// crashnotifier should be able to send mail when there is no db
function sendCrashNotification(program, context) {
function unexpectedExit(program, context) {
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'string');
@@ -402,7 +407,7 @@ function sendCrashNotification(program, context) {
from: config.adminEmail(),
to: 'admin@cloudron.io',
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
text: render('crash_notification.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
text: render('unexpected_exit.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
};
sendMails([ mailOptions ]);
@@ -417,6 +422,7 @@ function sendFeedback(user, type, subject, description) {
assert(type === exports.FEEDBACK_TYPE_TICKET ||
type === exports.FEEDBACK_TYPE_FEEDBACK ||
type === exports.FEEDBACK_TYPE_APP_MISSING ||
type === exports.FEEDBACK_TYPE_UPGRADE_REQUEST ||
type === exports.FEEDBACK_TYPE_APP_ERROR);
var mailOptions = {
+6 -6
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
@@ -66,7 +64,7 @@ function configureApp(app, certFilePath, keyFilePath, callback) {
var sourceDir = path.resolve(__dirname, '..');
var oauthProxy = requiresOAuthProxy(app);
var endpoint = oauthProxy ? 'oauthproxy' : 'app';
var vhost = config.appFqdn(app.location);
var vhost = app.altDomain || config.appFqdn(app.location);
var data = {
sourceDir: sourceDir,
@@ -80,10 +78,10 @@ function configureApp(app, certFilePath, keyFilePath, callback) {
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debug('writing config for "%s" to %s', app.location, nginxConfigFilename);
debug('writing config for "%s" to %s', vhost, nginxConfigFilename);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx config for "%s" : %s', app.location, safe.error.message);
debug('Error creating nginx config for "%s" : %s', vhost, safe.error.message);
return callback(safe.error);
}
@@ -94,9 +92,11 @@ function unconfigureApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = app.altDomain || config.appFqdn(app.location);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
debug('Error removing nginx configuration of "%s": %s', app.location, safe.error.message);
debug('Error removing nginx configuration of "%s": %s', vhost, safe.error.message);
return callback(null);
}
+14 -20
View File
@@ -1,10 +1,7 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
getApp: getApp,
getAppBySubdomain: getAppBySubdomain,
getApps: getApps,
getAppIcon: getAppIcon,
installApp: installApp,
@@ -34,6 +31,11 @@ var apps = require('../apps.js'),
util = require('util'),
uuid = require('node-uuid');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function removeInternalAppFields(app) {
return {
id: app.id,
@@ -49,7 +51,8 @@ function removeInternalAppFields(app) {
portBindings: app.portBindings,
iconUrl: app.iconUrl,
fqdn: app.fqdn,
memoryLimit: app.memoryLimit
memoryLimit: app.memoryLimit,
altDomain: app.altDomain
};
}
@@ -64,17 +67,6 @@ function getApp(req, res, next) {
});
}
function getAppBySubdomain(req, res, next) {
assert.strictEqual(typeof req.params.subdomain, 'string');
apps.getBySubdomain(req.params.subdomain, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such subdomain'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, removeInternalAppFields(app)));
});
}
function getApps(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
@@ -125,13 +117,14 @@ function installApp(req, res, next) {
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
// allow tests to provide an appId for testing
var appId = (process.env.BOX_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4();
debug('Installing app id:%s storeid:%s loc:%s port:%j accessRestriction:%j memoryLimit:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.memoryLimit, data.manifest);
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.icon || null, data.cert || null, data.key || null, data.memoryLimit || 0, function (error) {
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.icon || null, data.cert || null, data.key || null, data.memoryLimit || 0, data.altDomain || null, auditSource(req), function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
@@ -168,10 +161,11 @@ function configureApp(req, res, next) {
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
debug('Configuring app id:%s location:%s bindings:%j accessRestriction:%j memoryLimit:%s', req.params.id, data.location, data.portBindings, data.accessRestriction, data.memoryLimit);
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.cert || null, data.key || null, data.memoryLimit || 0, function (error) {
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.cert || null, data.key || null, data.memoryLimit || 0, data.altDomain || null, auditSource(req), function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
@@ -190,7 +184,7 @@ function restoreApp(req, res, next) {
debug('Restore app id:%s', req.params.id);
apps.restore(req.params.id, function (error) {
apps.restore(req.params.id, auditSource(req), function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
@@ -224,7 +218,7 @@ function uninstallApp(req, res, next) {
debug('Uninstalling app id:%s', req.params.id);
apps.uninstall(req.params.id, function (error) {
apps.uninstall(req.params.id, auditSource(req), function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
@@ -274,7 +268,7 @@ function updateApp(req, res, next) {
debug('Update app id:%s to manifest:%j with portBindings:%j', req.params.id, data.manifest, data.portBindings);
apps.update(req.params.id, data.force || false, data.manifest, data.portBindings, data.icon, function (error) {
apps.update(req.params.id, data.force || false, data.manifest, data.portBindings || null, data.icon, auditSource(req), function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
+6 -1
View File
@@ -12,6 +12,11 @@ var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function get(req, res, next) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
@@ -30,7 +35,7 @@ function get(req, res, next) {
function create(req, res, next) {
// note that cloudron.backup only waits for backup initiation and not for backup to complete
// backup progress can be checked up ny polling the progress api call
backups.backup(function (error) {
backups.backup(auditSource(req), function (error) {
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
-2
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
+12 -8
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -15,15 +13,20 @@ exports = module.exports = {
var assert = require('assert'),
cloudron = require('../cloudron.js'),
config = require('../config.js'),
progress = require('../progress.js'),
mailer = require('../mailer.js'),
CloudronError = cloudron.CloudronError,
config = require('../config.js'),
debug = require('debug')('box:routes/cloudron'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
progress = require('../progress.js'),
mailer = require('../mailer.js'),
superagent = require('superagent');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
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.
*
@@ -50,7 +53,7 @@ function activate(req, res, next) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('activate: username:%s ip:%s', username, ip);
cloudron.activate(username, password, email, displayName, ip, function (error, info) {
cloudron.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
if (error && error.reason === CloudronError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CloudronError.BAD_USERNAME) return next(new HttpError(400, 'Bad username'));
if (error && error.reason === CloudronError.BAD_PASSWORD) return next(new HttpError(400, 'Bad password'));
@@ -119,7 +122,7 @@ function getConfig(req, res, next) {
function update(req, res, next) {
// this only initiates the update, progress can be checked via the progress route
cloudron.updateToLatest(function (error) {
cloudron.updateToLatest(auditSource(req), function (error) {
if (error && error.reason === CloudronError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
@@ -134,7 +137,8 @@ function feedback(req, res, next) {
if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK &&
req.body.type !== mailer.FEEDBACK_TYPE_TICKET &&
req.body.type !== mailer.FEEDBACK_TYPE_APP_MISSING &&
req.body.type !== mailer.FEEDBACK_TYPE_APP_ERROR) return next(new HttpError(400, 'type must be either "ticket", "feedback" or "app_missing" or "app_error"'));
req.body.type !== mailer.FEEDBACK_TYPE_UPGRADE_REQUEST &&
req.body.type !== mailer.FEEDBACK_TYPE_APP_ERROR) return next(new HttpError(400, 'type must be either "ticket", "feedback", "app_missing", "app_error" or "upgrade_request"'));
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
+8 -4
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -15,6 +13,11 @@ var developer = require('../developer.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function enabled(req, res, next) {
developer.enabled(function (error, enabled) {
if (enabled) return next();
@@ -25,8 +28,9 @@ function enabled(req, res, next) {
function setEnabled(req, res, next) {
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled must be boolean'));
developer.setEnabled(req.body.enabled, function (error) {
developer.setEnabled(req.body.enabled, auditSource(req), function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
@@ -40,7 +44,7 @@ function login(req, res, next) {
if (error) return next(new HttpError(500, error));
if (!user) return next(new HttpError(401, 'Invalid credentials'));
developer.issueDeveloperToken(user, function (error, result) {
developer.issueDeveloperToken(user, auditSource(req), function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { token: result.token, expiresAt: result.expiresAt }));
+26
View File
@@ -0,0 +1,26 @@
'use strict';
exports = module.exports = {
get: get
};
var eventlog = require('../eventlog.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function get(req, res, next) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
if (req.query.action && typeof req.query.action !== 'string') return next(new HttpError(400, 'action must be a string'));
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
eventlog.getAllPaged(req.query.action || null, req.query.search || null, page, perPage, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { eventlogs: result }));
});
}
-2
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
+3 -1
View File
@@ -6,10 +6,12 @@ exports = module.exports = {
clients: require('./clients.js'),
cloudron: require('./cloudron.js'),
developer: require('./developer.js'),
eventlog: require('./eventlog.js'),
graphs: require('./graphs.js'),
groups: require('./groups.js'),
internal: require('./internal.js'),
oauth2: require('./oauth2.js'),
profile: require('./profile.js'),
settings: require('./settings.js'),
sysadmin: require('./sysadmin.js'),
user: require('./user.js')
};
+17 -9
View File
@@ -1,33 +1,36 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
var appdb = require('../appdb'),
apps = require('../apps'),
assert = require('assert'),
authcodedb = require('../authcodedb'),
clientdb = require('../clientdb'),
config = require('../config.js'),
constants = require('../constants.js'),
DatabaseError = require('../databaseerror'),
debug = require('debug')('box:routes/oauth2'),
eventlog = require('../eventlog.js'),
hat = require('hat'),
HttpError = require('connect-lastmile').HttpError,
middleware = require('../middleware/index.js'),
oauth2orize = require('oauth2orize'),
passport = require('passport'),
querystring = require('querystring'),
util = require('util'),
session = require('connect-ensure-login'),
tokendb = require('../tokendb'),
appdb = require('../appdb'),
url = require('url'),
user = require('../user.js'),
UserError = user.UserError,
hat = require('hat');
util = require('util');
function auditSource(req, appId) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { authType: 'oauth', ip: ip, appId: appId };
}
// create OAuth 2.0 server
var gServer = oauth2orize.createServer();
// Register serialialization and deserialization functions.
//
// The client id is stored in the session and can thus be retrieved for each
@@ -306,7 +309,7 @@ function accountSetup(req, res, next) {
userObject.username = req.body.username;
userObject.displayName = req.body.displayName;
user.update(userObject.id, userObject.username, userObject.email, userObject.displayName, function (error) {
user.update(userObject.id, userObject.username, userObject.email, userObject.displayName, auditSource(req), function (error) {
if (error && error.reason === UserError.ALREADY_EXISTS) return renderAccountSetupSite(res, req, userObject, 'Username already exists');
if (error) return next(new HttpError(500, error));
@@ -411,7 +414,10 @@ var authorization = [
// Handle our different types of oauth clients
var type = req.oauth2.client.type;
if (type === clientdb.TYPE_ADMIN) return next();
if (type === clientdb.TYPE_ADMIN) {
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, 'admin'), { userId: req.oauth2.user.id });
return next();
}
if (type === clientdb.TYPE_EXTERNAL) return next();
if (type === clientdb.TYPE_SIMPLE_AUTH) return sendError(req, res, 'Unknown OAuth client.');
@@ -422,6 +428,8 @@ var authorization = [
if (error) return sendError(req, res, 'Internal error');
if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id), { userId: req.oauth2.user.id });
next();
});
});
+98
View File
@@ -0,0 +1,98 @@
'use strict';
exports = module.exports = {
get: get,
update: update,
changePassword: changePassword,
setShowTutorial: setShowTutorial
};
var assert = require('assert'),
groups = require('../groups.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
user = require('../user.js'),
tokendb = require('../tokendb.js'),
UserError = user.UserError;
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function get(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
var result = {};
result.id = req.user.id;
result.tokenType = req.user.tokenType;
if (req.user.tokenType === tokendb.TYPE_USER || req.user.tokenType === tokendb.TYPE_DEV) {
result.username = req.user.username;
result.email = req.user.email;
result.displayName = req.user.displayName;
result.showTutorial = req.user.showTutorial;
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
if (error) return next(new HttpError(500, error));
result.admin = isAdmin;
next(new HttpSuccess(200, result));
});
} else {
next(new HttpSuccess(200, result));
}
}
function update(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.body, 'object');
if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
user.update(req.user.id, req.user.username, req.body.email || req.user.email, req.body.displayName || req.user.displayName, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
function changePassword(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.user, 'object');
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires the users old password.'));
if (typeof req.body.newPassword !== 'string') return next(new HttpError(400, 'API call requires the users new password.'));
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
user.setPassword(req.user.id, req.body.newPassword, function (error) {
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Wrong password'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
function setShowTutorial(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.showTutorial !== 'boolean') return next(new HttpError(400, 'showTutorial must be a boolean.'));
user.setShowTutorial(req.user.id, req.body.showTutorial, function (error) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Wrong password'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
+9 -2
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -18,6 +16,8 @@ exports = module.exports = {
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
getTimeZone: getTimeZone,
setCertificate: setCertificate,
setAdminCertificate: setAdminCertificate
};
@@ -71,6 +71,13 @@ function getCloudronName(req, res, next) {
});
}
function getTimeZone(req, res, next) {
settings.getTimeZone(function (error, tz) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { timeZone: tz }));
});
}
function setCloudronAvatar(req, res, next) {
assert.strictEqual(typeof req.files, 'object');
@@ -10,7 +10,7 @@ var backups = require('../backups.js'),
BackupsError = require('../backups.js').BackupsError,
cloudron = require('../cloudron.js'),
CloudronError = require('../cloudron.js').CloudronError,
debug = require('debug')('box:routes/internal'),
debug = require('debug')('box:routes/sysadmin'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
@@ -19,7 +19,8 @@ function backup(req, res, next) {
// note that cloudron.backup only waits for backup initiation and not for backup to complete
// backup progress can be checked up ny polling the progress api call
backups.backup(function (error) {
var auditSource = { userId: null, username: 'sysadmin' };
backups.backup(auditSource, function (error) {
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
@@ -31,7 +32,8 @@ function update(req, res, next) {
debug('triggering update');
// this only initiates the update, progress can be checked via the progress route
cloudron.updateToLatest(function (error) {
var auditSource = { userId: null, username: 'sysadmin' };
cloudron.updateToLatest(auditSource, function (error) {
if (error && error.reason === CloudronError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
+43 -37
View File
@@ -42,7 +42,7 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '10.0.0';
var TEST_IMAGE_TAG = '11.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();
@@ -114,24 +114,20 @@ describe('Apps', function () {
before(function (done) {
console.log('Starting addons, this can take 10 seconds');
child_process.exec(__dirname + '/start_addons.sh', function (error) {
if (error) return done(error);
dockerProxy = startDockerProxy(function interceptor(req, res) {
if (req.method === 'POST' && req.url === '/images/create?fromImage=' + encodeURIComponent(TEST_IMAGE_REPO) + '&tag=' + TEST_IMAGE_TAG) {
imageCreated = true;
res.writeHead(200);
res.end();
return true;
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE + '?force=false&noprune=false') {
imageDeleted = true;
res.writeHead(200);
res.end();
return true;
}
return false;
}, done);
});
dockerProxy = startDockerProxy(function interceptor(req, res) {
if (req.method === 'POST' && req.url === '/images/create?fromImage=' + encodeURIComponent(TEST_IMAGE_REPO) + '&tag=' + TEST_IMAGE_TAG) {
imageCreated = true;
res.writeHead(200);
res.end();
return true;
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE + '?force=false&noprune=false') {
imageDeleted = true;
res.writeHead(200);
res.end();
return true;
}
return false;
}, done);
});
after(function (done) {
@@ -446,24 +442,6 @@ describe('Apps', function () {
});
});
it('can get appBySubdomain', function (done) {
superagent.get(SERVER_URL + '/api/v1/subdomains/' + APP_LOCATION)
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.id).to.eql(APP_ID);
expect(res.body.installationState).to.be.ok();
done();
});
});
it('cannot get invalid app by Subdomain', function (done) {
superagent.get(SERVER_URL + '/api/v1/subdomains/tikaloma')
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('cannot uninstall invalid app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/whatever/uninstall')
.send({ password: PASSWORD })
@@ -830,6 +808,34 @@ describe('Apps', function () {
});
});
it('installation - mongodb addon config', function (done) {
var appContainer = docker.getContainer(appEntry.containerId);
appContainer.inspect(function (error, data) {
var mongodbUrl = null;
data.Config.Env.forEach(function (env) { if (env.indexOf('MONGODB_URL=') === 0) mongodbUrl = env.split('=')[1]; });
expect(mongodbUrl).to.be.ok();
var urlp = url.parse(mongodbUrl);
var username = urlp.auth.split(':')[0];
var password = urlp.auth.split(':')[1];
var dbname = urlp.path.substr(1);
expect(data.Config.Env).to.contain('MONGODB_PORT=27017');
expect(data.Config.Env).to.contain('MONGODB_HOST=mongodb');
expect(data.Config.Env).to.contain('MONGODB_USERNAME=' + username);
expect(data.Config.Env).to.contain('MONGODB_PASSWORD=' + password);
expect(data.Config.Env).to.contain('MONGODB_DATABASE=' + dbname);
var cmd = util.format('mongo --quiet -u %s -p %s %s:%s/%s --eval "db.collection.insert({ item: 34 })"',
username, password, 'mongodb', 27017, dbname);
child_process.exec('docker exec ' + appContainer.id + ' ' + cmd, { timeout: 5000 }, function (error) {
expect(!error).to.be.ok();
done();
});
});
});
it('installation - scheduler', function (done) {
async.retry({ times: 100, interval: 1000 }, function (retryCallback) {
if (fs.existsSync(paths.DATA_DIR + '/' + APP_ID + '/data/every_minute.env')) return retryCallback();
+1 -1
View File
@@ -53,7 +53,7 @@ function setup(done) {
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, null /* altDomain */, callback);
},
function createSettings(callback) {
+152
View File
@@ -0,0 +1,152 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
nock = require('nock'),
superagent = require('superagent'),
server = require('../../server.js'),
tokendb = require('../../tokendb.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var USER_1_ID = null, token_1;
function setup(done) {
config.setVersion('1.2.3');
async.series([
server.start.bind(server),
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();
});
},
function (callback) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: 'nonadmin', email: 'notadmin@server.test', invite: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
USER_1_ID = res.body.id;
callback(null);
});
},
function (callback) {
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, tokendb.PREFIX_USER + USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('Eventlog API', function () {
before(setup);
after(cleanup);
describe('get', function () {
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/eventlog')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails for non-admin', function (done) {
superagent.get(SERVER_URL + '/api/v1/eventlog')
.query({ access_token: token_1, page: 1, per_page: 10 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('succeeds for admin', function (done) {
superagent.get(SERVER_URL + '/api/v1/eventlog')
.query({ access_token: token, page: 1, per_page: 10 })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length >= 2).to.be.ok(); // activate, user.add
done();
});
});
it('succeeds with action', function (done) {
superagent.get(SERVER_URL + '/api/v1/eventlog')
.query({ access_token: token, page: 1, per_page: 10, action: 'cloudron.activate' })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(1);
done();
});
});
it('succeeds with search', function (done) {
superagent.get(SERVER_URL + '/api/v1/eventlog')
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(1);
done();
});
});
it('succeeds with search', function (done) {
superagent.get(SERVER_URL + '/api/v1/eventlog')
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL, action: 'cloudron.activate' })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(0);
done();
});
});
});
});
+17 -11
View File
@@ -145,7 +145,8 @@ describe('OAuth2', function () {
createdAt: (new Date()).toUTCString(),
modifiedAt: (new Date()).toUTCString(),
resetToken: hat(256),
displayName: ''
displayName: '',
showTutorial: false
};
var APP_0 = {
@@ -155,7 +156,8 @@ describe('OAuth2', function () {
location: 'test',
portBindings: {},
accessRestriction: null,
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
var APP_1 = {
@@ -165,7 +167,8 @@ describe('OAuth2', function () {
location: 'test1',
portBindings: {},
accessRestriction: { users: [ 'foobar' ] },
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
var APP_2 = {
@@ -175,7 +178,8 @@ describe('OAuth2', function () {
location: 'test2',
portBindings: {},
accessRestriction: { users: [ USER_0.id ] },
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
var APP_3 = {
@@ -185,7 +189,8 @@ describe('OAuth2', function () {
location: 'test3',
portBindings: {},
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
// unknown app
@@ -308,12 +313,12 @@ describe('OAuth2', function () {
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.accessRestriction, APP_0.memoryLimit),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, APP_0.altDomain),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit, APP_1.altDomain),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit, APP_2.altDomain),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit, APP_3.altDomain),
function (callback) {
user.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, function (error, userObject) {
user.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, null /* source */, function (error, userObject) {
expect(error).to.not.be.ok();
// update the global objects to reflect the new user id
@@ -1297,7 +1302,8 @@ describe('Password', function () {
createdAt: (new Date()).toUTCString(),
modifiedAt: (new Date()).toUTCString(),
resetToken: hat(256),
displayName: ''
displayName: '',
showTutorial: false
};
// make csrf always succeed for testing
+322
View File
@@ -0,0 +1,322 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var config = require('../../config.js'),
database = require('../../database.js'),
tokendb = require('../../tokendb.js'),
expect = require('expect.js'),
mailer = require('../../mailer.js'),
superagent = require('superagent'),
nock = require('nock'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME_0 = 'superaDmIn', PASSWORD = 'Foobar?1337', EMAIL_0 = 'silLY@me.com', EMAIL_0_NEW = 'stupID@me.com', DISPLAY_NAME_0_NEW = 'New Name';
describe('Profile API', function () {
this.timeout(5000);
var user_0 = null;
var token_0;
function setup(done) {
server.start(function (error) {
expect(!error).to.be.ok();
mailer._clearMailQueue();
database._clear(function (error) {
expect(error).to.eql(null);
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_0, password: PASSWORD, email: EMAIL_0 })
.end(function (err, res) {
expect(err).to.eql(null);
expect(res.statusCode).to.equal(201);
// stash for later use
token_0 = res.body.token;
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
done();
});
});
});
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
mailer._clearMailQueue();
server.stop(done);
});
}
describe('get profile', function () {
before(setup);
after(cleanup);
it('fails without token', function (done) {
superagent.get(SERVER_URL + '/api/v1/profile/').end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with empty token', function (done) {
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: '' }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with invalid token', function (done) {
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: 'some token' }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: token_0 }).end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.username).to.equal(USERNAME_0.toLowerCase());
expect(result.body.email).to.equal(EMAIL_0.toLowerCase());
expect(result.body.admin).to.be.ok();
expect(result.body.showTutorial).to.be.ok();
expect(result.body.displayName).to.be.a('string');
expect(result.body.password).to.not.be.ok();
expect(result.body.salt).to.not.be.ok();
user_0 = result.body;
done();
});
});
it('fails with expired token', function (done) {
var token = tokendb.generateToken();
var expires = Date.now() - 2000; // 1 sec
tokendb.add(token, tokendb.PREFIX_USER + user_0.id, null, expires, '*', function (error) {
expect(error).to.not.be.ok();
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
});
it('fails with invalid token in auth header', function (done) {
superagent.get(SERVER_URL + '/api/v1/profile').set('Authorization', 'Bearer ' + 'x' + token_0).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds with token in auth header', function (done) {
superagent.get(SERVER_URL + '/api/v1/profile').set('Authorization', 'Bearer ' + token_0).end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.username).to.equal(USERNAME_0.toLowerCase());
expect(result.body.email).to.equal(EMAIL_0.toLowerCase());
expect(result.body.admin).to.be.ok();
expect(result.body.showTutorial).to.be.ok();
expect(result.body.displayName).to.be.a('string');
expect(result.body.password).to.not.be.ok();
expect(result.body.salt).to.not.be.ok();
done();
});
});
});
describe('update', function () {
before(setup);
after(cleanup);
it('change email fails due to missing token', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('change email fails due to invalid email', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ email: 'foo@bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('change user succeeds without email nor displayName', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({})
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('change email succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0.toLowerCase());
expect(res.body.email).to.equal(EMAIL_0_NEW.toLowerCase());
expect(res.body.admin).to.equal(true);
expect(res.body.displayName).to.equal('');
done();
});
});
});
it('change displayName succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ displayName: DISPLAY_NAME_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0.toLowerCase());
expect(res.body.email).to.equal(EMAIL_0_NEW.toLowerCase());
expect(res.body.admin).to.be.ok();
expect(res.body.displayName).to.equal(DISPLAY_NAME_0_NEW);
done();
});
});
});
});
describe('password change', function () {
before(setup);
after(cleanup);
it('fails due to missing current password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ newPassword: 'some wrong password' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to missing new password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to wrong password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('fails due to invalid password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'five' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
});
describe('showTutorial change', function () {
before(setup);
after(cleanup);
it('fails due to missing showTutorial', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/tutorial')
.query({ access_token: token_0 })
.send({})
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to wrong showTutorial type', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/tutorial')
.query({ access_token: token_0 })
.send({ showTutorial: 'true' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/tutorial')
.query({ access_token: token_0 })
.send({ showTutorial: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: token_0 }).end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.showTutorial).to.not.be.ok();
done();
});
});
});
});
});
+13 -1
View File
@@ -56,7 +56,7 @@ function setup(done) {
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok' };
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, null /* altDomain */, callback);
}
], done);
}
@@ -360,5 +360,17 @@ describe('Settings API', function () {
done();
});
});
describe('time_zone', function () {
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/time_zone')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.timeZone).to.be('America/Los_Angeles');
done();
});
});
});
});
+12 -8
View File
@@ -30,7 +30,8 @@ describe('SimpleAuth API', function () {
location: 'test0',
portBindings: {},
accessRestriction: { users: [ 'foobar', 'someone'] },
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
var APP_1 = {
@@ -40,7 +41,8 @@ describe('SimpleAuth API', function () {
location: 'test1',
portBindings: {},
accessRestriction: { users: [ 'foobar', 'someone' ] },
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
var APP_2 = {
@@ -50,7 +52,8 @@ describe('SimpleAuth API', function () {
location: 'test2',
portBindings: {},
accessRestriction: null,
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
var APP_3 = {
@@ -60,7 +63,8 @@ describe('SimpleAuth API', function () {
location: 'test3',
portBindings: {},
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
var CLIENT_0 = {
@@ -155,10 +159,10 @@ describe('SimpleAuth API', function () {
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.accessRestriction, APP_0.memoryLimit),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit)
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, APP_0.altDomain),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit, APP_1.altDomain),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit, APP_2.altDomain),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit, APP_3.altDomain)
], done);
});
+7 -22
View File
@@ -4,7 +4,7 @@ set -eu -o pipefail
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
source ${SOURCE_DIR}/setup/INFRA_VERSION
source ${SOURCE_DIR}/src/INFRA_VERSION
readonly mysqldatadir="/tmp/mysqldata-$(date +%s)"
readonly postgresqldatadir="/tmp/postgresqldata-$(date +%s)"
@@ -14,13 +14,8 @@ root_password=secret
start_postgresql() {
postgresql_vars="POSTGRESQL_ROOT_PASSWORD=${root_password}; POSTGRESQL_ROOT_HOST=172.17.0.0/255.255.0.0"
if which boot2docker >/dev/null 2>&1; then
boot2docker ssh "sudo rm -rf /tmp/postgresql_vars.sh"
boot2docker ssh "echo \"${postgresql_vars}\" > /tmp/postgresql_vars.sh"
else
rm -rf /tmp/postgresql_vars.sh
echo "${postgresql_vars}" > /tmp/postgresql_vars.sh
fi
rm -rf /tmp/postgresql_vars.sh
echo "${postgresql_vars}" > /tmp/postgresql_vars.sh
docker rm -f postgresql 2>/dev/null 1>&2 || true
@@ -32,13 +27,8 @@ start_postgresql() {
start_mysql() {
local mysql_vars="MYSQL_ROOT_PASSWORD=${root_password}; MYSQL_ROOT_HOST=172.17.0.0/255.255.0.0"
if which boot2docker >/dev/null 2>&1; then
boot2docker ssh "sudo rm -rf /tmp/mysql_vars.sh"
boot2docker ssh "echo \"${mysql_vars}\" > /tmp/mysql_vars.sh"
else
rm -rf /tmp/mysql_vars.sh
echo "${mysql_vars}" > /tmp/mysql_vars.sh
fi
rm -rf /tmp/mysql_vars.sh
echo "${mysql_vars}" > /tmp/mysql_vars.sh
docker rm -f mysql 2>/dev/null 1>&2 || true
@@ -50,13 +40,8 @@ start_mysql() {
start_mongodb() {
local mongodb_vars="MONGODB_ROOT_PASSWORD=${root_password}"
if which boot2docker >/dev/null 2>&1; then
boot2docker ssh "sudo rm -rf /tmp/mongodb_vars.sh"
boot2docker ssh "echo \"${mongodb_vars}\" > /tmp/mongodb_vars.sh"
else
rm -rf /tmp/mongodb_vars.sh
echo "${mongodb_vars}" > /tmp/mongodb_vars.sh
fi
rm -rf /tmp/mongodb_vars.sh
echo "${mongodb_vars}" > /tmp/mongodb_vars.sh
docker rm -f mongodb 2>/dev/null 1>&2 || true
+97
View File
@@ -0,0 +1,97 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var appdb = require('../../appdb.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js'),
nock = require('nock');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var server;
function setup(done) {
config.setVersion('1.2.3');
async.series([
server.start.bind(server),
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();
});
},
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, null /* altDomain */, callback);
},
function createSettings(callback) {
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }, callback);
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('Internal API', function () {
before(setup);
after(cleanup);
describe('backup', function () {
it('succeeds', function (done) {
var scope = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } });
superagent.post(config.sysadminOrigin() + '/api/v1/backup')
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope.isDone()) {
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
});
});
+9 -85
View File
@@ -14,8 +14,7 @@ var config = require('../../config.js'),
mailer = require('../../mailer.js'),
superagent = require('superagent'),
nock = require('nock'),
server = require('../../server.js'),
userdb = require('../../userdb.js');
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -24,14 +23,13 @@ var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'taO@zen.mac';
var USERNAME_2 = 'userTheSecond', EMAIL_2 = 'USER@foo.bar', EMAIL_2_NEW = 'happy@ME.com';
var USERNAME_3 = 'userTheThird', EMAIL_3 = 'user3@FOO.bar';
var server;
function setup(done) {
server.start(function (error) {
expect(!error).to.be.ok();
mailer._clearMailQueue();
userdb._clear(function (error) {
database._clear(function (error) {
expect(error).to.eql(null);
groups.create('somegroupid', done);
@@ -64,7 +62,6 @@ describe('User API', function () {
var user_0, user_1, user_2, user_3 = null;
var token = null;
var token_1 = tokendb.generateToken();
var token_2 = tokendb.generateToken();
before(setup);
after(cleanup);
@@ -391,26 +388,14 @@ describe('User API', function () {
user_3 = result.body;
// one mail for first user creation, two mails for second user creation (see 'invite' flag)
checkMails(3, function () {
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_2, tokendb.PREFIX_USER + user_2.id, 'test-client-id', Date.now() + 10000, '*', done);
});
checkMails(3, done);
});
});
});
it('second user userInfo fails for first user', function (done) {
it('get userInfo succeeds for second user', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + user_2.id)
.query({ access_token: token_1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('second user userInfo succeeds for second user', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + user_2.id)
.query({ access_token: token_2 })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.username).to.equal(USERNAME_2.toLowerCase());
@@ -433,7 +418,7 @@ describe('User API', function () {
it('list users fails for normal user', function (done) {
superagent.get(SERVER_URL + '/api/v1/users')
.query({ access_token: token_2 })
.query({ access_token: token_1 })
.end(function (error, res) {
expect(res.statusCode).to.equal(403);
done();
@@ -541,16 +526,6 @@ describe('User API', function () {
});
});
it('change email for other user fails', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token_2 })
.send({ email: 'foobar@bar.baz' })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('change user succeeds without email nor displayName', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
@@ -561,15 +536,15 @@ describe('User API', function () {
});
});
it('change email for own user succeeds', function (done) {
it('change email succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_2.id)
.query({ access_token: token_2 })
.query({ access_token: token })
.send({ email: EMAIL_2_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/users/' + user_2.id)
.query({ access_token: token_2 })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_2.toLowerCase());
@@ -623,55 +598,4 @@ describe('User API', function () {
});
});
});
// Change password
it('change password fails due to missing current password', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
.query({ access_token: token })
.send({ newPassword: 'some wrong password' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('change password fails due to missing new password', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('change password fails due to wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
.query({ access_token: token })
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('change password fails due to invalid password', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
.query({ access_token: token })
.send({ password: PASSWORD, newPassword: 'five' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('change password succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
.query({ access_token: token })
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
});
+19 -68
View File
@@ -1,15 +1,11 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
profile: profile,
info: info,
get: get,
update: update,
list: listUser,
create: createUser,
changePassword: changePassword,
remove: removeUser,
list: list,
create: create,
remove: remove,
verifyPassword: verifyPassword,
requireAdmin: requireAdmin,
sendInvite: sendInvite,
@@ -25,31 +21,12 @@ var assert = require('assert'),
tokendb = require('../tokendb.js'),
UserError = user.UserError;
function profile(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
var result = {};
result.id = req.user.id;
result.tokenType = req.user.tokenType;
if (req.user.tokenType === tokendb.TYPE_USER || req.user.tokenType === tokendb.TYPE_DEV) {
result.username = req.user.username;
result.email = req.user.email;
result.displayName = req.user.displayName;
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
if (error) return next(new HttpError(500, error));
result.admin = isAdmin;
next(new HttpSuccess(200, result));
});
} else {
next(new HttpSuccess(200, result));
}
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function createUser(req, res, next) {
function create(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
@@ -63,7 +40,7 @@ function createUser(req, res, next) {
var username = req.body.username || '';
var displayName = req.body.displayName || '';
user.create(username, password, email, displayName, { invitor: req.user, sendInvite: sendInvite }, function (error, user) {
user.create(username, password, email, displayName, auditSource(req), { invitor: req.user, sendInvite: sendInvite }, function (error, user) {
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, 'Invalid username'));
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, 'Invalid email'));
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, 'Invalid password'));
@@ -99,7 +76,7 @@ function update(req, res, next) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
user.update(req.params.userId, result.username, req.body.email || result.email, req.body.displayName || result.displayName, function (error) {
user.update(req.params.userId, result.username, req.body.email || result.email, req.body.displayName || result.displayName, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
@@ -111,33 +88,14 @@ function update(req, res, next) {
});
}
function changePassword(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.user, 'object');
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires the users old password.'));
if (typeof req.body.newPassword !== 'string') return next(new HttpError(400, 'API call requires the users new password.'));
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
user.changePassword(req.user.username, req.body.password, req.body.newPassword, function (error) {
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(403, 'Wrong password'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Wrong password'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
function listUser(req, res, next) {
function list(req, res, next) {
user.list(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { users: result }));
});
}
function info(req, res, next) {
function get(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
assert.strictEqual(typeof req.user, 'object');
@@ -161,7 +119,7 @@ function info(req, res, next) {
});
}
function removeUser(req, res, next) {
function remove(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
// rules:
@@ -175,7 +133,7 @@ function removeUser(req, res, next) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
user.remove(userObject, function (error) {
user.remove(userObject, auditSource(req), function (error) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
@@ -187,24 +145,17 @@ function removeUser(req, res, next) {
function verifyPassword(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
// developers are allowed to through without password
// developers are allowed through without password
if (req.user.tokenType === tokendb.TYPE_DEV) return next();
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
user.verifyWithUsername(req.user.username, req.body.password, function (error) {
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(403, 'Password incorrect'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Password incorrect'));
if (error) return next(new HttpError(500, error));
// Only allow admins or users, operating on themselves
if (req.params.userId && !(req.user.id === req.params.userId || isAdmin)) return next(new HttpError(403, 'Not allowed'));
user.verifyWithUsername(req.user.username, req.body.password, function (error) {
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(403, 'Password incorrect'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Password incorrect'));
if (error) return next(new HttpError(500, error));
next();
});
next();
});
}
+1 -1
View File
@@ -21,7 +21,7 @@ readonly program_name=$1
echo "${program_name}.log"
echo "-------------------"
journalctl --all --no-pager -u ${program_name} -n 100
journalctl --all --no-pager -u ${program_name} -n 300
echo
echo
echo "dmesg"
+11 -1
View File
@@ -13,6 +13,16 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
fi
if [[ "${BOX_ENV}" == "cloudron" ]]; then
/etc/init.d/collectd restart
# when restoring the cloudron with many apps, the apptasks rush in to restart
# collectd which makes systemd/collectd very unhappy and puts the collectd in
# inactive state
for i in {1..10}; do
echo "Restarting collectd"
if systemctl restart collectd; then
exit 0
fi
echo "Failed to reload collectd. Maybe some other apptask is restarting it"
sleep $((RANDOM%30))
done
fi
@@ -2,12 +2,25 @@
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# == 1 && "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
readonly DATA_DIR="/home/yellowtent/data"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${script_dir}/../INFRA_VERSION" # this injects INFRA_VERSION
arg_fqdn="$1"
readonly fqdn="$1"
readonly mail_fqdn="$2"
readonly mail_tls_cert="$3"
readonly mail_tls_key="$4"
# removing containers ensures containers are launched with latest config updates
# restore code in appatask does not delete old containers
@@ -48,10 +61,12 @@ fi
mail_container_id=$(docker run --restart=always -d --name="mail" \
-m 75m \
--memory-swap 150m \
-h "${arg_fqdn}" \
-e "MAIL_SERVER_NAME=${arg_fqdn}" \
-e "MAIL_DOMAIN=${arg_fqdn}" \
-h "${fqdn}" \
-e "MAIL_DOMAIN=${fqdn}" \
-e "MAIL_SERVER_NAME=${mail_fqdn}" \
-v "${DATA_DIR}/box/mail:/app/data" \
-v "${mail_tls_key}:/etc/tls_key.pem:ro" \
-v "${mail_tls_cert}:/etc/tls_cert.pem:ro" \
--read-only -v /tmp -v /run \
"${MAIL_IMAGE}")
echo "Mail container id: ${mail_container_id}"
@@ -69,7 +84,7 @@ EOF
mysql_container_id=$(docker run --restart=always -d --name="mysql" \
-m 256m \
--memory-swap 512m \
-h "${arg_fqdn}" \
-h "${fqdn}" \
-v "${DATA_DIR}/mysql:/var/lib/mysql" \
-v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
--read-only -v /tmp -v /run \
@@ -87,7 +102,7 @@ EOF
postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \
-m 100m \
--memory-swap 200m \
-h "${arg_fqdn}" \
-h "${fqdn}" \
-v "${DATA_DIR}/postgresql:/var/lib/postgresql" \
-v "${DATA_DIR}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
--read-only -v /tmp -v /run \
@@ -105,7 +120,7 @@ EOF
mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \
-m 100m \
--memory-swap 200m \
-h "${arg_fqdn}" \
-h "${fqdn}" \
-v "${DATA_DIR}/mongodb:/var/lib/mongodb" \
-v "${DATA_DIR}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
--read-only -v /tmp -v /run \
+49 -42
View File
@@ -5,14 +5,17 @@ exports = module.exports = {
stop: stop
};
var assert = require('assert'),
var addons = require('./addons.js'),
assert = require('assert'),
async = require('async'),
auth = require('./auth.js'),
certificates = require('./certificates.js'),
clients = require('./clients.js'),
cloudron = require('./cloudron.js'),
cron = require('./cron.js'),
config = require('./config.js'),
database = require('./database.js'),
eventlog = require('./eventlog.js'),
express = require('express'),
http = require('http'),
mailer = require('./mailer.js'),
@@ -23,7 +26,7 @@ var assert = require('assert'),
taskmanager = require('./taskmanager.js');
var gHttpServer = null;
var gInternalHttpServer = null;
var gSysadminHttpServer = null;
function initializeExpressSync() {
var app = express();
@@ -65,12 +68,12 @@ function initializeExpressSync() {
var multipart = middleware.multipart({ maxFieldsSize: FIELD_LIMIT, limit: FILE_SIZE_LIMIT, timeout: FILE_TIMEOUT });
// scope middleware implicitly also adds bearer token verification
var rootScope = routes.oauth2.scope('root');
var profileScope = routes.oauth2.scope('profile');
var usersScope = routes.oauth2.scope('users');
var appsScope = routes.oauth2.scope('apps');
var developerScope = routes.oauth2.scope('developer');
var settingsScope = routes.oauth2.scope('settings');
var rootScope = routes.oauth2.scope(clients.SCOPE_ROOT);
var profileScope = routes.oauth2.scope(clients.SCOPE_PROFILE);
var usersScope = routes.oauth2.scope(clients.SCOPE_USERS);
var appsScope = routes.oauth2.scope(clients.SCOPE_APPS);
var developerScope = routes.oauth2.scope(clients.SCOPE_DEVELOPER);
var settingsScope = routes.oauth2.scope(clients.SCOPE_SETTINGS);
// csrf protection
var csrf = routes.oauth2.csrf;
@@ -96,20 +99,21 @@ function initializeExpressSync() {
// feedback
router.post('/api/v1/cloudron/feedback', usersScope, routes.cloudron.feedback);
router.get ('/api/v1/profile', profileScope, routes.user.profile);
// profile api, working off the user behind the provided token
router.get ('/api/v1/profile', profileScope, routes.profile.get);
router.put ('/api/v1/profile', profileScope, routes.profile.update);
router.put ('/api/v1/profile/password', profileScope, routes.user.verifyPassword, routes.profile.changePassword);
router.put ('/api/v1/profile/tutorial', profileScope, routes.profile.setShowTutorial);
// user routes only for admins
router.get ('/api/v1/users', usersScope, routes.user.requireAdmin, routes.user.list);
router.post('/api/v1/users', usersScope, routes.user.requireAdmin, routes.user.create);
router.get ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.get);
router.del ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.user.remove);
router.put ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.update);
router.put ('/api/v1/users/:userId/set_groups', usersScope, routes.user.requireAdmin, routes.user.setGroups);
router.post('/api/v1/users/:userId/invite', usersScope, routes.user.requireAdmin, routes.user.sendInvite);
// user routes for admins and users operating on their own account
router.get ('/api/v1/users/:userId', usersScope, routes.user.info);
router.put ('/api/v1/users/:userId', usersScope, routes.user.update);
router.post('/api/v1/users/:userId/password', usersScope, routes.user.changePassword); // changePassword verifies password
// Group management
router.get ('/api/v1/groups', usersScope, routes.user.requireAdmin, routes.groups.list);
router.post('/api/v1/groups', usersScope, routes.user.requireAdmin, routes.groups.create);
@@ -158,27 +162,28 @@ function initializeExpressSync() {
router.get ('/api/v1/apps/:id/logs', appsScope, routes.user.requireAdmin, routes.apps.getLogs);
router.get ('/api/v1/apps/:id/exec', routes.developer.enabled, appsScope, routes.user.requireAdmin, routes.apps.exec);
// subdomain routes
router.get ('/api/v1/subdomains/:subdomain', routes.apps.getAppBySubdomain);
// settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above)
router.get ('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.getAutoupdatePattern);
router.post('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.setAutoupdatePattern);
router.get ('/api/v1/settings/cloudron_name', settingsScope, routes.user.requireAdmin, routes.settings.getCloudronName);
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/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.setCertificate);
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);
// settings routes
router.get ('/api/v1/settings/autoupdate_pattern', settingsScope, routes.settings.getAutoupdatePattern);
router.post('/api/v1/settings/autoupdate_pattern', settingsScope, routes.settings.setAutoupdatePattern);
router.get ('/api/v1/settings/cloudron_name', settingsScope, routes.settings.getCloudronName);
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.settings.setCloudronName);
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.settings.getCloudronAvatar);
router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar);
router.get ('/api/v1/settings/dns_config', settingsScope, routes.settings.getDnsConfig);
router.post('/api/v1/settings/dns_config', settingsScope, routes.settings.setDnsConfig);
router.get ('/api/v1/settings/backup_config', settingsScope, routes.settings.getBackupConfig);
router.post('/api/v1/settings/backup_config', settingsScope, routes.settings.setBackupConfig);
router.post('/api/v1/settings/certificate', settingsScope, routes.settings.setCertificate);
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.settings.setAdminCertificate);
// eventlog route
router.get('/api/v1/eventlog', settingsScope, routes.user.requireAdmin, routes.eventlog.get);
// backup routes
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
router.post('/api/v1/backups', settingsScope, routes.backups.create);
router.get ('/api/v1/backups/:backupId', appsScope, routes.user.requireAdmin, routes.backups.download);
router.get ('/api/v1/backups', settingsScope, routes.user.requireAdmin, routes.backups.get);
router.post('/api/v1/backups', settingsScope, routes.user.requireAdmin, routes.backups.create);
router.get ('/api/v1/backups/:backupId', appsScope, routes.user.requireAdmin, routes.backups.download);
// disable server timeout. we use the timeout middleware to handle timeouts on a route level
httpServer.setTimeout(0);
@@ -210,7 +215,7 @@ function initializeExpressSync() {
}
// provides hooks for the 'installer'
function initializeInternalExpressSync() {
function initializeSysadminExpressSync() {
var app = express();
var httpServer = http.createServer(app);
@@ -220,7 +225,7 @@ function initializeInternalExpressSync() {
var json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json
urlencoded = middleware.urlencoded({ extended: false, limit: QUERY_LIMIT }); // application/x-www-form-urlencoded
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box Internal :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box Sysadmin :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
var router = new express.Router();
router.del = router.delete; // amend router.del for readability further on
@@ -232,10 +237,10 @@ function initializeInternalExpressSync() {
.use(router)
.use(middleware.lastMile());
// internal routes
router.post('/api/v1/backup', routes.internal.backup);
router.post('/api/v1/update', routes.internal.update);
router.post('/api/v1/retire', routes.internal.retire);
// Sysadmin routes
router.post('/api/v1/backup', routes.sysadmin.backup);
router.post('/api/v1/update', routes.sysadmin.update);
router.post('/api/v1/retire', routes.sysadmin.retire);
return httpServer;
}
@@ -245,18 +250,20 @@ function start(callback) {
assert.strictEqual(gHttpServer, null, 'Server is already up and running.');
gHttpServer = initializeExpressSync();
gInternalHttpServer = initializeInternalExpressSync();
gSysadminHttpServer = initializeSysadminExpressSync();
async.series([
auth.initialize,
database.initialize,
cloudron.initialize, // keep this here because it reads activation state that others depend on
certificates.installAdminCertificate, // keep this before cron to block heartbeats until cert is ready
addons.initialize, // starts the addons
taskmanager.initialize,
mailer.initialize,
cron.initialize,
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
gInternalHttpServer.listen.bind(gInternalHttpServer, config.get('internalPort'), '127.0.0.1')
gSysadminHttpServer.listen.bind(gSysadminHttpServer, config.get('sysadminPort'), '127.0.0.1'),
eventlog.add.bind(null, eventlog.ACTION_START, { userId: null, username: 'boot' }, { version: config.version() })
], callback);
}
@@ -273,12 +280,12 @@ function stop(callback) {
mailer.uninitialize,
database.uninitialize,
gHttpServer.close.bind(gHttpServer),
gInternalHttpServer.close.bind(gInternalHttpServer)
gSysadminHttpServer.close.bind(gSysadminHttpServer)
], function (error) {
if (error) console.error(error);
gHttpServer = null;
gInternalHttpServer = null;
gSysadminHttpServer = null;
callback(null);
});
+1 -1
View File
@@ -145,7 +145,7 @@ function getTimeZone(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.TIME_ZONE_KEY, function (error, tz) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.AUTOUPDATE_PATTERN_KEY]);
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.TIME_ZONE_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, tz);
+1 -1
View File
@@ -35,7 +35,7 @@ function exec(tag, file, args, callback) {
cp.on('exit', function (code, signal) {
if (code || signal) debug(tag + ' code: %s, signal: %s', code, signal);
callback(code === 0 ? null : new Error(util.format('Exited with error %s signal %s', code, signal)));
callback(code === 0 ? null : new Error(util.format(tag + ' exited with error %s signal %s', code, signal)));
});
cp.on('error', function (error) {
+3
View File
@@ -14,6 +14,7 @@ var apps = require('./apps.js'),
config = require('./config.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,
@@ -94,6 +95,8 @@ function login(req, res, next) {
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: {
+6 -8
View File
@@ -1,19 +1,17 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
caas = require('./sysinfo/caas.js'),
config = require('./config.js'),
ec2 = require('./sysinfo/ec2.js'),
util = require('util');
exports = module.exports = {
SysInfoError: SysInfoError,
getIp: getIp
};
var assert = require('assert'),
caas = require('./sysinfo/caas.js'),
config = require('./config.js'),
ec2 = require('./sysinfo/ec2.js'),
util = require('util');
var gCachedIp = null;
function SysInfoError(reason, errorOrMessage) {
+18
View File
@@ -19,11 +19,13 @@ var appdb = require('./appdb.js'),
cloudron = require('./cloudron.js'),
debug = require('debug')('box:taskmanager'),
locker = require('./locker.js'),
sendFailureLogs = require('./logcollector.js').sendFailureLogs,
util = require('util'),
_ = require('underscore');
var gActiveTasks = { };
var gPendingTasks = [ ];
var gPlatformReady = false; // PaaS (addons) up and running
var TASK_CONCURRENCY = 5;
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
@@ -39,6 +41,11 @@ function initialize(callback) {
cloudron.events.on(cloudron.EVENT_CONFIGURED, resumeTasks);
}
setTimeout(function () {
gPlatformReady = true;
resumeTasks();
}, 30000); // wait 30 seconds to signal platform ready
callback();
}
@@ -82,6 +89,8 @@ function resumeTasks(callback) {
apps.forEach(function (app) {
if (app.installationState === appdb.ISTATE_INSTALLED && app.runState === appdb.RSTATE_RUNNING) return;
if (app.installationState === appdb.ISTATE_ERROR) return;
debug('Creating process for %s (%s) with state %s', app.location, app.id, app.installationState);
startAppTask(app.id, NOOP_CALLBACK);
});
@@ -106,6 +115,12 @@ function startAppTask(appId, callback) {
return callback(new Error(util.format('Task for %s is already active', appId)));
}
if (!gPlatformReady) {
debug('Platform not ready yet, queueing task for %s', appId);
gPendingTasks.push(appId);
return callback();
}
if (Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
debug('Reached concurrency limit, queueing task for %s', appId);
gPendingTasks.push(appId);
@@ -130,7 +145,10 @@ function startAppTask(appId, callback) {
debug('Task for %s pid %s completed with status %s', appId, pid, code);
if (code === null /* signal */ || (code !== 0 && code !== 50)) { // apptask crashed
debug('Apptask crashed with code %s and signal %s', code, signal);
sendFailureLogs('apptask', { unit: 'box' });
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code + ' and signal ' + signal }, NOOP_CALLBACK);
} else if (code === 50) {
sendFailureLogs('apptask', { unit: 'box' });
}
delete gActiveTasks[appId];
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
+9 -24
View File
@@ -28,7 +28,8 @@ describe('Apps', function () {
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: ''
displayName: '',
showTutorial: false
};
var USER_0 = {
@@ -40,7 +41,8 @@ describe('Apps', function () {
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: ''
displayName: '',
showTutorial: false
};
var USER_1 = {
@@ -52,7 +54,8 @@ describe('Apps', function () {
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: ''
displayName: '',
showTutorial: false
};
var GROUP_0 = 'somegroup';
@@ -113,9 +116,9 @@ describe('Apps', function () {
groups.create.bind(null, GROUP_1),
groups.addMember.bind(null, groups.ADMIN_GROUP_ID, ADMIN_0.id),
groups.addMember.bind(null, GROUP_0, USER_1.id),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit)
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, null),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit, null),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit, null)
], done);
});
@@ -201,24 +204,6 @@ describe('Apps', function () {
});
});
it('cannot getBySubdomain', function (done) {
apps.getBySubdomain('moang', function (error, app) {
expect(error).to.be.ok();
expect(error.reason).to.be(AppsError.NOT_FOUND);
done();
});
});
it('can getBySubdomain', function (done) {
apps.getBySubdomain(APP_0.location, function (error, app) {
expect(error).to.be(null);
expect(app).to.be.ok();
expect(app.iconUrl).to.eql(null);
expect(app.fqdn).to.eql(APP_0.location + '-' + config.fqdn());
done();
});
});
it('can getAll', function (done) {
apps.getAll(function (error, apps) {
expect(error).to.be(null);
+1 -1
View File
@@ -84,7 +84,7 @@ describe('apptask', function () {
config.set('version', '0.5.0');
async.series([
database.initialize,
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.memoryLimit),
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.memoryLimit, null),
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
settings.setTlsConfig.bind(null, { provider: 'caas' })
], done);
+3 -2
View File
@@ -3,9 +3,9 @@
set -eu
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
readonly TEST_IMAGE="cloudron/test:10.0.0"
readonly TEST_IMAGE="cloudron/test:11.0.0"
source ${SOURCE_DIR}/setup/INFRA_VERSION
source ${SOURCE_DIR}/src/INFRA_VERSION
# reset sudo timestamp to avoid wrong success
sudo -k || sudo --reset-timestamp
@@ -13,6 +13,7 @@ sudo -k || sudo --reset-timestamp
# checks if all scripts are sudo access
scripts=("${SOURCE_DIR}/src/scripts/rmappdir.sh" \
"${SOURCE_DIR}/src/scripts/createappdir.sh" \
"${SOURCE_DIR}/src/scripts/setup_infra.sh" \
"${SOURCE_DIR}/src/scripts/reloadnginx.sh" \
"${SOURCE_DIR}/src/scripts/backupbox.sh" \
"${SOURCE_DIR}/src/scripts/backupapp.sh" \
+64 -10
View File
@@ -7,14 +7,15 @@
'use strict';
var appdb = require('../appdb.js'),
async = require('async'),
authcodedb = require('../authcodedb.js'),
backupdb = require('../backupdb.js'),
clientdb = require('../clientdb.js'),
hat = require('hat'),
database = require('../database'),
DatabaseError = require('../databaseerror.js'),
eventlogdb = require('../eventlogdb.js'),
expect = require('expect.js'),
async = require('async'),
hat = require('hat'),
settingsdb = require('../settingsdb.js'),
tokendb = require('../tokendb.js'),
userdb = require('../userdb.js'),
@@ -42,7 +43,8 @@ describe('database', function () {
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: ''
displayName: '',
showTutorial: false
};
var USER_1 = {
@@ -54,7 +56,8 @@ describe('database', function () {
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: '',
displayName: 'Herbert 1'
displayName: 'Herbert 1',
showTutorial: false
};
var USER_2 = {
@@ -66,7 +69,8 @@ describe('database', function () {
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: '',
displayName: 'Herbert 2'
displayName: 'Herbert 2',
showTutorial: false
};
it('can add user', function (done) {
@@ -498,7 +502,8 @@ describe('database', function () {
lastBackupId: null,
lastBackupConfig: null,
oldConfig: null,
memoryLimit: 4294967296
memoryLimit: 4294967296,
altDomain: null
};
var APP_1 = {
id: 'appid-1',
@@ -517,7 +522,8 @@ describe('database', function () {
lastBackupId: null,
lastBackupConfig: null,
oldConfig: null,
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
it('add fails due to missing arguments', function () {
@@ -534,7 +540,7 @@ describe('database', function () {
});
it('add succeeds', function (done) {
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, function (error) {
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, null, function (error) {
expect(error).to.be(null);
done();
});
@@ -558,7 +564,7 @@ describe('database', function () {
});
it('add of same app fails', function (done) {
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0.accessRestriction, APP_0.memoryLimit, function (error) {
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0.accessRestriction, APP_0.memoryLimit, null, function (error) {
expect(error).to.be.a(DatabaseError);
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
done();
@@ -630,7 +636,7 @@ describe('database', function () {
});
it('add second app succeeds', function (done) {
appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1.accessRestriction, APP_1.memoryLimit, function (error) {
appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1.accessRestriction, APP_1.memoryLimit, null, function (error) {
expect(error).to.be(null);
done();
});
@@ -1069,5 +1075,53 @@ describe('database', function () {
});
});
describe('eventlog', function () {
it('add succeeds', function (done) {
eventlogdb.add('someid', 'some.event', { ip: '1.2.3.4' }, { appId: 'thatapp' }, function (error) {
expect(error).to.be(null);
done();
});
});
it('get succeeds', function (done) {
eventlogdb.get('someid', function (error, result) {
expect(error).to.be(null);
expect(result.id).to.be('someid');
expect(result.action).to.be('some.event');
expect(result.creationTime).to.be.a(Date);
expect(result.source).to.be.eql({ ip: '1.2.3.4' });
expect(result.data).to.be.eql({ appId: 'thatapp' });
done();
});
});
it('get of unknown id fails', function (done) {
eventlogdb.get('notfoundid', function (error, result) {
expect(error).to.be.a(DatabaseError);
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
expect(result).to.not.be.ok();
done();
});
});
it('getAllPaged succeeds', function (done) {
eventlogdb.getAllPaged(null, null, 1, 1, function (error, results) {
expect(error).to.be(null);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
expect(results[0].id).to.be('someid');
expect(results[0].action).to.be('some.event');
expect(results[0].source).to.be.eql({ ip: '1.2.3.4' });
expect(results[0].data).to.be.eql({ appId: 'thatapp' });
done();
});
});
});
});
+81
View File
@@ -0,0 +1,81 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var database = require('../database.js'),
expect = require('expect.js'),
eventlog = require('../eventlog.js'),
EventLogError = eventlog.EventLogError;
function setup(done) {
// ensure data/config/mount paths
database.initialize(function (error) {
expect(error).to.be(null);
done();
});
}
function cleanup(done) {
database._clear(done);
}
describe('Eventlog', function () {
before(setup);
after(cleanup);
var eventId;
it('add succeeds', function (done) {
eventlog.add('some.event', { ip: '1.2.3.4' }, { appId: 'thatapp' }, function (error, result) {
expect(error).to.be(null);
expect(result.id).to.be.ok();
eventId = result.id;
done();
});
});
it('get succeeds', function (done) {
eventlog.get(eventId, function (error, result) {
expect(error).to.be(null);
expect(result.id).to.be(eventId);
expect(result.action).to.be('some.event');
expect(result.creationTime).to.be.a(Date);
expect(result.source).to.be.eql({ ip: '1.2.3.4' });
expect(result.data).to.be.eql({ appId: 'thatapp' });
done();
});
});
it('get of unknown id fails', function (done) {
eventlog.get('notfoundid', function (error, result) {
expect(error).to.be.a(EventLogError);
expect(error.reason).to.be(EventLogError.NOT_FOUND);
expect(result).to.not.be.ok();
done();
});
});
it('getAllPaged succeeds', function (done) {
eventlog.getAllPaged(null, null, 1, 1, function (error, results) {
expect(error).to.be(null);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
expect(results[0].id).to.be(eventId);
expect(results[0].action).to.be('some.event');
expect(results[0].source).to.be.eql({ ip: '1.2.3.4' });
expect(results[0].data).to.be.eql({ appId: 'thatapp' });
done();
});
});
});
+2 -1
View File
@@ -30,7 +30,8 @@ var USER_0 = {
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: ''
displayName: '',
showTutorial: false
};
function setup(done) {
+7 -3
View File
@@ -34,6 +34,10 @@ var USER_1 = {
displayName: 'User 1'
};
var AUDIT_SOURCE = {
ip: '1.2.3.4'
};
var APP_0 = {
id: 'appid-0',
appStoreId: 'appStoreId-0',
@@ -68,10 +72,10 @@ function setup(done) {
database.initialize.bind(null),
database._clear.bind(null),
ldapServer.start.bind(null),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, null),
appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }),
function (callback) {
user.createOwner(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, function (error, result) {
user.createOwner(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE, function (error, result) {
if (error) return callback(error);
USER_0.id = result.id;
@@ -80,7 +84,7 @@ function setup(done) {
});
},
function (callback) {
user.create(USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, { invitor: USER_0 }, function (error, result) {
user.create(USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, AUDIT_SOURCE, { invitor: USER_0 }, function (error, result) {
if (error) return callback(error);
USER_1.id = result.id;
+1 -1
View File
@@ -11,7 +11,7 @@ readonly source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)"
rm -rf $HOME/.cloudron_test
mkdir -p $HOME/.cloudron_test
cd $HOME/.cloudron_test
mkdir -p data/appdata data/box/appicons data/mail data/nginx/cert data/nginx/applications data/collectd/collectd.conf.d data/addons configs data/box/certs
mkdir -p data/appdata data/box/appicons data/mail data/nginx/cert data/nginx/applications data/collectd/collectd.conf.d data/addons configs data/box/certs data/box/mail/dkim/localhost
webadmin_scopes="root,profile,users,apps,settings"
webadmin_origin="https://${ADMIN_LOCATION}-localhost"
+7 -4
View File
@@ -12,7 +12,6 @@ var appdb = require('../appdb.js'),
database = require('../database.js'),
deepExtend = require('deep-extend'),
expect = require('expect.js'),
fs = require('fs'),
mailer = require('../mailer.js'),
nock = require('nock'),
settings = require('../settings.js'),
@@ -67,6 +66,10 @@ var USER_0 = {
displayName: 'User 0'
};
var AUDIT_SOURCE = {
ip: '1.2.3.4'
};
function checkMails(number, done) {
// mails are enqueued async
setTimeout(function () {
@@ -83,7 +86,7 @@ describe('updatechecker - checkBoxUpdates', function () {
async.series([
database.initialize,
mailer._clearMailQueue,
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName)
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE)
], done);
});
@@ -248,8 +251,8 @@ describe('updatechecker - checkAppUpdates', function () {
database.initialize,
database._clear,
mailer._clearMailQueue,
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName)
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, null),
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE)
], done);
});
+62 -61
View File
@@ -25,6 +25,8 @@ var NEW_PASSWORD = 'oTHER@#$235';
var DISPLAY_NAME = 'Nobody cares';
var DISPLAY_NAME_NEW = 'Somone cares';
var userObject = null;
var NON_ADMIN_GROUP = 'members';
var AUDIT_SOURCE = { ip: '1.2.3.4' };
function cleanupUsers(done) {
async.series([
@@ -36,13 +38,15 @@ function cleanupUsers(done) {
function createOwner(done) {
groups.create('admin', function () { // ignore error since it might already exist
user.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
groups.create(NON_ADMIN_GROUP, function () { // ignore error since it might already exist
user.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
userObject = result;
userObject = result;
done();
done();
});
});
});
}
@@ -79,7 +83,7 @@ describe('User', function () {
after(cleanupUsers);
it('fails due to short password', function (done) {
user.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, function (error, result) {
user.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
@@ -89,7 +93,7 @@ describe('User', function () {
});
it('fails due to missing upper case password', function (done) {
user.create(USERNAME, 'thisiseightch%$234arslong', EMAIL, DISPLAY_NAME, function (error, result) {
user.create(USERNAME, 'thisiseightch%$234arslong', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
@@ -99,7 +103,7 @@ describe('User', function () {
});
it('fails due to missing numerics in password', function (done) {
user.create(USERNAME, 'foobaRASDF%', EMAIL, DISPLAY_NAME, function (error, result) {
user.create(USERNAME, 'foobaRASDF%', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
@@ -109,7 +113,7 @@ describe('User', function () {
});
it('fails due to missing special chars in password', function (done) {
user.create(USERNAME, 'foobaRASDF23423', EMAIL, DISPLAY_NAME, function (error, result) {
user.create(USERNAME, 'foobaRASDF23423', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
@@ -119,7 +123,7 @@ describe('User', function () {
});
it('fails due to reserved username', function (done) {
user.create('admin', PASSWORD, EMAIL, DISPLAY_NAME, function (error, result) {
user.create('admin', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_USERNAME);
@@ -129,7 +133,7 @@ describe('User', function () {
});
it('fails due to reserved username', function (done) {
user.create('AdMiN', PASSWORD, EMAIL, DISPLAY_NAME, function (error, result) {
user.create('AdMiN', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_USERNAME);
@@ -139,7 +143,7 @@ describe('User', function () {
});
it('succeeds and attempts to send invite', function (done) {
user.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, function (error, result) {
user.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).not.to.be.ok();
expect(result).to.be.ok();
expect(result.username).to.equal(USERNAME.toLowerCase());
@@ -174,7 +178,7 @@ describe('User', function () {
});
it('fails because user exists', function (done) {
user.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, function (error, result) {
user.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).not.to.be.ok();
expect(error.reason).to.equal(UserError.ALREADY_EXISTS);
@@ -184,7 +188,7 @@ describe('User', function () {
});
it('fails because password is empty', function (done) {
user.create(USERNAME, '', EMAIL, DISPLAY_NAME, function (error, result) {
user.create(USERNAME, '', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).not.to.be.ok();
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
@@ -400,7 +404,7 @@ describe('User', function () {
after(cleanupUsers);
it('fails due to unknown userid', function (done) {
user.update(USERNAME, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, function (error) {
user.update(USERNAME, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, AUDIT_SOURCE, function (error) {
expect(error).to.be.a(UserError);
expect(error.reason).to.equal(UserError.NOT_FOUND);
@@ -409,7 +413,7 @@ describe('User', function () {
});
it('fails due to invalid email', function (done) {
user.update(userObject.id, USERNAME_NEW, 'brokenemailaddress', DISPLAY_NAME_NEW, function (error) {
user.update(userObject.id, USERNAME_NEW, 'brokenemailaddress', DISPLAY_NAME_NEW, AUDIT_SOURCE, function (error) {
expect(error).to.be.a(UserError);
expect(error.reason).to.equal(UserError.BAD_EMAIL);
@@ -418,7 +422,7 @@ describe('User', function () {
});
it('succeeds', function (done) {
user.update(userObject.id, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, function (error) {
user.update(userObject.id, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, AUDIT_SOURCE, function (error) {
expect(error).to.not.be.ok();
user.get(userObject.id, function (error, result) {
@@ -434,7 +438,7 @@ describe('User', function () {
});
it('succeeds with same data', function (done) {
user.update(userObject.id, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, function (error) {
user.update(userObject.id, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, AUDIT_SOURCE, function (error) {
expect(error).to.not.be.ok();
user.get(userObject.id, function (error, result) {
@@ -454,32 +458,42 @@ describe('User', function () {
before(createOwner);
after(cleanupUsers);
var user1 = {
username: 'seconduser',
password: 'ASDFkljsf#$^%2354',
email: 'some@thi.ng'
};
it('make second user admin succeeds', function (done) {
var user1 = {
username: 'seconduser',
password: 'ASDFkljsf#$^%2354',
email: 'some@thi.ng'
};
var invitor = { username: USERNAME, email: EMAIL };
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, { invitor: invitor }, function (error, result) {
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, AUDIT_SOURCE, { invitor: invitor }, function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
user1.id = result.id;
groups.setGroups(user1.id, [ groups.ADMIN_GROUP_ID ], function (error) {
user.setGroups(user1.id, [ groups.ADMIN_GROUP_ID ], function (error) {
expect(error).to.not.be.ok();
// one mail for user creation, one mail for admin change
checkMails(1, done);
checkMails(2, done);
});
});
});
xit('succeeds to remove admin flag of first user', function (done) {
groups.setGroups(USERNAME, [], function (error) {
it('add user to non admin group does not trigger admin mail', function (done) {
user.setGroups(user1.id, [ groups.ADMIN_GROUP_ID, NON_ADMIN_GROUP ], function (error) {
expect(error).to.equal(null);
checkMails(0, done);
});
});
it('succeeds to remove admin flag', function (done) {
user.setGroups(user1.id, [ NON_ADMIN_GROUP ], function (error) {
expect(error).to.eql(null);
checkMails(1, done);
});
});
@@ -506,7 +520,7 @@ describe('User', function () {
};
var invitor = { username: USERNAME, email: EMAIL };
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, { invitor: invitor }, function (error, result) {
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, AUDIT_SOURCE, { invitor: invitor }, function (error, result) {
expect(error).to.eql(null);
expect(result).to.be.ok();
@@ -529,53 +543,40 @@ describe('User', function () {
});
});
describe('password change', function () {
describe('set password', function () {
before(createOwner);
after(cleanupUsers);
it('fails due to wrong arumgent count', function () {
expect(function () { user.changePassword(); }).to.throwError();
expect(function () { user.changePassword(USERNAME); }).to.throwError();
expect(function () { user.changePassword(USERNAME, PASSWORD, NEW_PASSWORD); }).to.throwError();
});
it('fails due to wrong arumgents', function () {
expect(function () { user.changePassword(USERNAME, {}, NEW_PASSWORD, function () {}); }).to.throwError();
expect(function () { user.changePassword(1337, PASSWORD, NEW_PASSWORD, function () {}); }).to.throwError();
expect(function () { user.changePassword(USERNAME, PASSWORD, 1337, function () {}); }).to.throwError();
expect(function () { user.changePassword(USERNAME, PASSWORD, NEW_PASSWORD, 'some string'); }).to.throwError();
});
it('fails due to wrong password', function (done) {
user.changePassword(USERNAME, 'wrongpassword', NEW_PASSWORD, function (error) {
expect(error).to.be.ok();
done();
});
});
it('fails due to empty new password', function (done) {
user.changePassword(USERNAME, PASSWORD, '', function (error) {
expect(error).to.be.ok();
done();
});
});
it('fails due to unknown user', function (done) {
user.changePassword('somerandomuser', PASSWORD, NEW_PASSWORD, function (error) {
user.setPassword('doesnotexist', NEW_PASSWORD, function (error) {
expect(error).to.be.ok();
done();
});
});
it('fails due to empty password', function (done) {
user.setPassword(userObject.id, '', function (error) {
expect(error).to.be.ok();
done();
});
});
it('fails due to invalid password', function (done) {
user.setPassword(userObject.id, 'foobar', function (error) {
expect(error).to.be.ok();
done();
});
});
it('succeeds', function (done) {
user.changePassword(USERNAME, PASSWORD, NEW_PASSWORD, function (error) {
user.setPassword(userObject.id, NEW_PASSWORD, function (error) {
expect(error).to.not.be.ok();
done();
});
});
it('actually changed the password (unable to login with old pasword)', function (done) {
user.verifyWithUsername(USERNAME, PASSWORD, function (error, result) {
user.verify(userObject.id, PASSWORD, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.WRONG_PASSWORD);
@@ -584,7 +585,7 @@ describe('User', function () {
});
it('actually changed the password (login with new password)', function (done) {
user.verifyWithUsername(USERNAME, NEW_PASSWORD, function (error, result) {
user.verify(userObject.id, NEW_PASSWORD, function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
done();
+53 -34
View File
@@ -14,18 +14,19 @@ exports = module.exports = {
getAllAdmins: getAllAdmins,
resetPasswordByIdentifier: resetPasswordByIdentifier,
setPassword: setPassword,
changePassword: changePassword,
update: updateUser,
createOwner: createOwner,
getOwner: getOwner,
sendInvite: sendInvite,
setGroups: setGroups
setGroups: setGroups,
setShowTutorial: setShowTutorial
};
var assert = require('assert'),
clientdb = require('./clientdb.js'),
crypto = require('crypto'),
DatabaseError = require('./databaseerror.js'),
eventlog = require('./eventlog.js'),
groups = require('./groups.js'),
GroupError = groups.GroupError,
hat = require('hat'),
@@ -112,11 +113,12 @@ function validateDisplayName(name) {
return null;
}
function createUser(username, password, email, displayName, options, callback) {
function createUser(username, password, email, displayName, auditSource, options, callback) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert.strictEqual(typeof auditSource, 'object');
if (typeof options === 'function') {
callback = options;
@@ -159,13 +161,16 @@ function createUser(username, password, email, displayName, options, callback) {
createdAt: now,
modifiedAt: now,
resetToken: hat(256),
displayName: displayName
displayName: displayName,
showTutorial: true
};
userdb.add(user.id, user, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.ALREADY_EXISTS));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_ADD, auditSource, { userId: user.id, email: user.email });
callback(null, user);
if (!owner) mailer.userAdded(user, sendInvite);
@@ -238,14 +243,17 @@ function verifyWithEmail(email, password, callback) {
});
}
function removeUser(user, callback) {
function removeUser(user, auditSource, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
userdb.del(user.id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_REMOVE, auditSource, { userId: user.id });
callback(null);
mailer.userRemoved(user);
@@ -300,11 +308,12 @@ function getByResetToken(resetToken, callback) {
});
}
function updateUser(userId, username, email, displayName, callback) {
function updateUser(userId, username, email, displayName, auditSource, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
username = username.toLowerCase();
@@ -320,6 +329,9 @@ function updateUser(userId, username, email, displayName, callback) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.ALREADY_EXISTS, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, error));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_UPDATE, auditSource, { userId: userId });
callback(null);
});
}
@@ -329,19 +341,28 @@ function setGroups(userId, groupIds, callback) {
assert(Array.isArray(groupIds));
assert.strictEqual(typeof callback, 'function');
groups.setGroups(userId, groupIds, function (error) {
if (error && error.reason === GroupError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, 'One or more groups not found'));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
groups.getGroups(userId, function (error, oldGroupIds) {
if (error && error.reason !== GroupError.NOT_FOUND) return callback(new UserError(UserError.INTERNAL_ERROR, error));
if (groupIds.some(function (g) { return g === groups.ADMIN_GROUP_ID; })) {
getUser(userId, function (error, result) {
if (error) return console.error('Failed to send admin change mail.', error);
oldGroupIds = oldGroupIds || [];
mailer.adminChanged(result, true);
});
}
groups.setGroups(userId, groupIds, function (error) {
if (error && error.reason === GroupError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, 'One or more groups not found'));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
callback();
var isAdmin = groupIds.some(function (g) { return g === groups.ADMIN_GROUP_ID; });
var wasAdmin = oldGroupIds.some(function (g) { return g === groups.ADMIN_GROUP_ID; });
if ((isAdmin && !wasAdmin) || (!isAdmin && wasAdmin)) {
getUser(userId, function (error, result) {
if (error) return console.error('Failed to send admin change mail.', error);
mailer.adminChanged(result, isAdmin);
});
}
callback(null);
});
});
}
@@ -421,27 +442,12 @@ function setPassword(userId, newPassword, callback) {
});
}
function changePassword(username, oldPassword, newPassword, callback) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof oldPassword, 'string');
assert.strictEqual(typeof newPassword, 'string');
assert.strictEqual(typeof callback, 'function');
var error = validatePassword(newPassword);
if (error) return callback(new UserError(UserError.BAD_PASSWORD, error.message));
verifyWithUsername(username.toLowerCase(), oldPassword, function (error, user) {
if (error) return callback(error);
setPassword(user.id, newPassword, callback);
});
}
function createOwner(username, password, email, displayName, callback) {
function createOwner(username, password, email, displayName, auditSource, callback) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
// This is only not allowed for the owner
@@ -451,7 +457,7 @@ function createOwner(username, password, email, displayName, callback) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
if (count !== 0) return callback(new UserError(UserError.ALREADY_EXISTS));
createUser(username, password, email, displayName, { owner: true }, function (error, user) {
createUser(username, password, email, displayName, auditSource, { owner: true }, function (error, user) {
if (error) return callback(error);
groups.addMember(groups.ADMIN_GROUP_ID, user.id, function (error) {
@@ -492,3 +498,16 @@ function sendInvite(userId, callback) {
});
});
}
function setShowTutorial(userId, showTutorial, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof showTutorial, 'boolean');
assert.strictEqual(typeof callback, 'function');
userdb.update(userId, { showTutorial: showTutorial }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, error));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
callback(null);
});
}
+8 -3
View File
@@ -23,13 +23,14 @@ var assert = require('assert'),
DatabaseError = require('./databaseerror'),
groups = require('./groups.js');
var USERS_FIELDS = [ 'id', 'username', 'email', 'password', 'salt', 'createdAt', 'modifiedAt', 'resetToken', 'displayName' ].join(',');
var USERS_FIELDS = [ 'id', 'username', 'email', 'password', 'salt', 'createdAt', 'modifiedAt', 'resetToken', 'displayName', 'showTutorial' ].join(',');
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
// The username may be null or undefined in the db, let's ensure it is a string
result.username = result.username || '';
result.showTutorial = !!result.showTutorial;
return result;
}
@@ -137,10 +138,11 @@ function add(userId, user, callback) {
assert.strictEqual(typeof user.modifiedAt, 'string');
assert.strictEqual(typeof user.resetToken, 'string');
assert.strictEqual(typeof user.displayName, 'string');
assert.strictEqual(typeof user.showTutorial, 'boolean');
assert.strictEqual(typeof callback, 'function');
var data = [ userId, user.username || null, user.password, user.email, user.salt, user.createdAt, user.modifiedAt, user.resetToken, user.displayName ];
database.query('INSERT INTO users (id, username, password, email, salt, createdAt, modifiedAt, resetToken, displayName) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', data, function (error, result) {
var data = [ userId, user.username || null, user.password, user.email, user.salt, user.createdAt, user.modifiedAt, user.resetToken, user.displayName, user.showTutorial ];
database.query('INSERT INTO users (id, username, password, email, salt, createdAt, modifiedAt, resetToken, displayName, showTutorial) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', data, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -204,6 +206,9 @@ function update(userId, user, callback) {
} else if (k === 'email') {
assert.strictEqual(typeof user.email, 'string');
args.push(user.email);
} else if (k === 'showTutorial') {
assert.strictEqual(typeof user.showTutorial, 'boolean');
args.push(user.showTutorial);
} else {
args.push(user[k]);
}
+45 -38
View File
@@ -1,43 +1,61 @@
/* jslint node:true */
'use strict';
exports = module.exports = waitForDns;
var assert = require('assert'),
async = require('async'),
attempt = require('attempt'),
debug = require('debug')('box:src/waitfordns'),
dns = require('native-dns');
dns = require('native-dns'),
tld = require('tldjs');
// the first arg to callback is not an error argument; this is required for async.every
function isChangeSynced(domain, ip, nameserver, callback) {
function isChangeSynced(domain, value, type, nameserver, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof nameserver, 'string');
assert.strictEqual(typeof callback, 'function');
// ns records cannot have cname
dns.resolve4(nameserver, function (error, nsIps) {
if (error || !nsIps || nsIps.length === 0) return callback(false);
if (error || !nsIps || nsIps.length === 0) {
debug('nameserver %s does not resolve. assuming it stays bad.', nameserver); // it's fine if one or more ns are dead
return callback(true);
}
async.every(nsIps, function (nsIp, iteratorCallback) {
var req = dns.Request({
question: dns.Question({ name: domain, type: 'A' }),
question: dns.Question({ name: domain, type: type }),
server: { address: nsIp },
timeout: 5000
});
req.on('timeout', function () { return iteratorCallback(false); });
req.on('timeout', function () {
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, domain);
return iteratorCallback(false);
});
req.on('message', function (error, message) {
if (error || !message.answer || message.answer.length === 0) return iteratorCallback(false);
if (error) {
debug('nameserver %s (%s) returned error trying to resolve %s: %s', nameserver, nsIp, domain, error);
return iteratorCallback(false);
}
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, message.answer[0], ip);
var answer = message.answer;
if (message.answer[0].address !== ip) return iteratorCallback(false);
if (!answer || answer.length === 0) {
debug('bad answer from nameserver %s (%s) resolving %s (%s): %j', nameserver, nsIp, domain, type, message);
return iteratorCallback(false);
}
iteratorCallback(true); // done
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, answer, value);
if ((type === 'A' && answer[0].address === value) ||
(type === 'CNAME' && answer[0].data === value)) {
return iteratorCallback(true); // done!
}
iteratorCallback(false);
});
req.send();
@@ -46,40 +64,29 @@ function isChangeSynced(domain, ip, nameserver, callback) {
}
// check if IP change has propagated to every nameserver
function waitForDns(domain, ip, zoneName, options, callback) {
function waitForDns(domain, value, type, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof value, 'string');
assert(type === 'A' || type === 'CNAME');
assert.strictEqual(typeof callback, 'function');
var defaultOptions = {
retryInterval: 5000,
retries: Infinity
};
var zoneName = tld.getDomain(domain);
debug('waitForIp: domain %s to be %s in zone %s.', domain, value, zoneName);
if (typeof options === 'function') {
callback = options;
options = defaultOptions;
} else {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
}
debug('waitForDNS: domain %s to be %s in zone %s.', domain, ip, zoneName);
attempt(function (attempts) {
var callback = this; // gross
debug('waitForDNS: %s attempt %s.', domain, attempts);
var attempt = 1;
async.retry({ interval: 5000, times: 50000 }, function (retryCallback) {
debug('waitForDNS: %s attempt %s.', domain, attempt++);
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || !nameservers) return callback(error || new Error('Unable to get nameservers'));
if (error || !nameservers) return retryCallback(error || new Error('Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, domain, ip), function (synced) {
debug('waitForDNS: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
async.every(nameservers, isChangeSynced.bind(null, domain, value, type), function (synced) {
debug('waitForIp: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
callback(synced ? null : new Error('ETRYAGAIN'));
retryCallback(synced ? null : new Error('ETRYAGAIN'));
});
});
}, { interval: options.retryInterval, retries: options.retries }, function (error) {
}, function retryDone(error) {
if (error) return callback(error);
debug('waitForDNS: %s done.', domain);
+50
View File
@@ -58,6 +58,35 @@
<body>
<!-- Welcome logic -->
<div class="welcome" ng-show="tutorialSteps[tutorialStep]">
<div class="dialog" ng-show="tutorialSteps[tutorialStep].title === 'intro'">
<h2>Welcome {{ user.username }}</h2>
<p>This is the administration page of your Cloudron. It shows your installed apps.</p>
<br/>
<button class="btn btn-default pull-left" ng-click="endTutorial();">Skip</button>
<button class="btn btn-success pull-right" ng-click="nextTutorialStep();">Next: <b>Cloudron Store</b></button>
</div>
<div class="dialog" ng-show="tutorialSteps[tutorialStep].title === 'appstore'">
<h2>Cloudron Store</h2>
<p>Install apps easily into any subdomain and control who can access it.</p>
<p>Your apps stay updated, backed up and secure with no effort on your part.</p>
<br/>
<button class="btn btn-default pull-left" ng-click="prevTutorialStep();">Back</button>
<button class="btn btn-success pull-right" ng-click="nextTutorialStep();">Last: <b>Users and Groups</b></button>
</div>
<div class="dialog" ng-show="tutorialSteps[tutorialStep].title === 'users'">
<h2>Users and Groups</h2>
<p>The Cloudron comes with built-in user and group managment</p>
<p>Remember, you can use the same credentials to login to all your apps.</p>
<br/>
<button class="btn btn-default pull-left" ng-click="prevTutorialStep();">Back</button>
<button class="btn btn-success pull-right" ng-click="nextTutorialStep();">Start using apps and invite users</button>
</div>
</div>
<!-- Modal update -->
<div class="modal fade" id="updateModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel" aria-hidden="true">
<div class="modal-dialog">
@@ -111,6 +140,26 @@
</div>
</div>
<!-- Modal plan upgrade -->
<div class="modal fade" id="upgradeModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Cloudron Upgrade</h4>
</div>
<div class="modal-body">
<p>See available plans for upgrade at <a href="{{ config.webServerOrigin + '/pricing.html' }}" target="_blank">cloudron.io</a>.</p>
<p>If you request an upgrade, we will get back to you as soon as possible.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" ng-click="requestUpgrade()" ng-disabled="upgradeRequest.busy"><i class="fa fa-spinner fa-pulse" ng-show="upgradeRequest.busy"></i> Request Upgrade</button>
</div>
</div>
</div>
</div>
<div class="animateMe ng-hide" ng-show="initialized">
<!-- Navigation -->
@@ -149,6 +198,7 @@
<ul class="dropdown-menu" role="menu">
<li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li>
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li>
<li ng-show="user.admin"><a href="#/activity"><i class="fa fa-list-alt fa-fw"></i> Activity</a></li>
<li><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
<li ng-show="user.admin && config.isCustomDomain"><a href="#/certs"><i class="fa fa-certificate fa-fw"></i> DNS & Certs</a></li>
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
+29 -9
View File
@@ -164,6 +164,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
this._userInfo.email = userInfo.email;
this._userInfo.displayName = userInfo.displayName;
this._userInfo.admin = !!userInfo.admin;
this._userInfo.showTutorial = !!userInfo.showTutorial;
this._userInfo.gravatar = 'https://www.gravatar.com/avatar/' + md5.createHash(userInfo.email.toLowerCase()) + '.jpg?s=24&d=mm';
this._userInfo.gravatarHuge = 'https://www.gravatar.com/avatar/' + md5.createHash(userInfo.email.toLowerCase()) + '.jpg?s=128&d=mm';
};
@@ -294,7 +295,8 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
accessRestriction: config.accessRestriction,
cert: config.cert,
key: config.key,
memoryLimit: config.memoryLimit
memoryLimit: config.memoryLimit,
altDomain: config.altDomain || null
};
$http.post(client.apiOrigin + '/api/v1/apps/' + id + '/configure', data).success(function (data, status) {
@@ -378,6 +380,22 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.getEventLogs = function (action, search, page, perPage, callback) {
var config = {
params: {
action: action,
search: search,
page: page,
per_page: perPage
}
};
$http.get(client.apiOrigin + '/api/v1/eventlog', config).success(function (data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data.eventlogs);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getApps = function (callback) {
$http.get(client.apiOrigin + '/api/v1/apps').success(function (data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
@@ -498,13 +516,6 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.listUsers = function (callback) {
$http.get(client.apiOrigin + '/api/v1/users').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getOAuthClients = function (callback) {
$http.get(client.apiOrigin + '/api/v1/oauth/clients').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
@@ -625,7 +636,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
newPassword: newPassword
};
$http.post(client.apiOrigin + '/api/v1/users/' + this._userInfo.id + '/password', data).success(function(data, status) {
$http.put(client.apiOrigin + '/api/v1/profile/password', data).success(function(data, status) {
if (status !== 204) return callback(new ClientError(status, data));
callback(null, data);
}).error(defaultErrorHandler(callback));
@@ -768,6 +779,15 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
return (available - needed) >= 0;
};
Client.prototype.setShowTutorial = function (show, callback) {
var data = { showTutorial: show };
$http.put(client.apiOrigin + '/api/v1/profile/tutorial', data).success(function (data, status) {
if (status !== 204) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
}
client = new Client();
return client;
}]);
+50
View File
@@ -48,6 +48,9 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/settings', {
controller: 'SettingsController',
templateUrl: 'views/settings.html'
}).when('/activity', {
controller: 'ActivityController',
templateUrl: 'views/activity.html'
}).when('/support', {
controller: 'SupportController',
templateUrl: 'views/support.html'
@@ -207,6 +210,53 @@ app.filter('markdown2html', function () {
};
});
// keep this in sync with eventlog.js
var ACTION_ACTIVATE = 'cloudron.activate';
var ACTION_APP_CONFIGURE = 'app.configure';
var ACTION_APP_INSTALL = 'app.install';
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_BACKUP_FINISH = 'backup.finish';
var ACTION_BACKUP_START = 'backup.start';
var ACTION_CERTIFICATE_RENEWAL = 'certificate.renew';
var ACTION_CLI_MODE = 'settings.climode';
var ACTION_START = 'cloudron.start';
var ACTION_UPDATE = 'cloudron.update';
var ACTION_USER_ADD = 'user.add';
var ACTION_USER_LOGIN = 'user.login';
var ACTION_USER_REMOVE = 'user.remove';
var ACTION_USER_UPDATE = 'user.update';
app.filter('eventLogDetails', function() {
return function(eventLog) {
var source = eventLog.source;
var data = eventLog.data;
var errorMessage = data.errorMessage;
switch (eventLog.action) {
case ACTION_ACTIVATE: return 'Cloudron activated';
case ACTION_APP_CONFIGURE: return 'App ' + data.appId + ' was configured';
case ACTION_APP_INSTALL: return 'App ' + data.manifest.id + '@' + data.manifest.version + ' installed at ' + data.location + ' with id ' + data.appId;
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_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');
case ACTION_CLI_MODE: return 'CLI mode was ' + (data.enabled ? 'enabled' : 'disabled');
case ACTION_START: return 'Cloudron started with version ' + data.version;
case ACTION_UPDATE: return 'Updating to version ' + data.boxUpdateInfo.version;
case ACTION_USER_ADD: return 'User ' + data.email + ' added with id ' + data.userId;
case ACTION_USER_LOGIN: return 'User ' + data.userId + ' logged in';
case ACTION_USER_REMOVE: return 'User ' + data.userId + ' removed';
case ACTION_USER_UPDATE: return 'User ' + data.userId + ' updated';
default: return eventLog.action;
}
};
});
// custom directive for dynamic names in forms
// See http://stackoverflow.com/questions/23616578/issue-registering-form-control-with-interpolated-name#answer-23617401
app.directive('laterName', function () { // (2)
+61 -1
View File
@@ -7,12 +7,52 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
$scope.config = {};
$scope.client = Client;
$scope.tutorialStep = -1;
$scope.tutorialSteps = [
{ title: 'intro', page: '#/apps' },
{ title: 'appstore', page: '#/appstore' },
{ title: 'users', page: '#/users' }
];
$scope.startTutorial = function () {
$scope.tutorialStep = 0;
if ($scope.tutorialSteps[$scope.tutorialStep]) window.location.href = $scope.tutorialSteps[$scope.tutorialStep].page;
};
$scope.endTutorial = function () {
$scope.tutorialStep = -1;
Client.setShowTutorial(false, function (error) {
if (error) console.error(error);
window.location.href = '#/apps';
});
};
$scope.nextTutorialStep = function () {
$scope.tutorialStep += 1;
if ($scope.tutorialSteps[$scope.tutorialStep]) window.location.href = $scope.tutorialSteps[$scope.tutorialStep].page;
if ($scope.tutorialStep >= $scope.tutorialSteps.length) $scope.endTutorial();
};
$scope.prevTutorialStep = function () {
$scope.tutorialStep -= 1;
if ($scope.tutorialSteps[$scope.tutorialStep]) window.location.href = $scope.tutorialSteps[$scope.tutorialStep].page;
};
$scope.update = {
busy: false,
error: {},
password: ''
};
$scope.upgradeRequest = {
busy: false
};
$scope.isActive = function (url) {
if (!$route.current) return false;
return $route.current.$$route.originalPath.indexOf(url) === 0;
@@ -37,6 +77,23 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
window.location.href = '/error.html';
};
$scope.requestUpgrade = function () {
$scope.upgradeRequest.busy = true;
var subject = 'User requested upgrade for ' + $scope.config.fqdn;
var description = 'User ' + $scope.user.email + ' requested an upgrade for ' + $scope.config.fqdn + '. Get back to him!!';
Client.feedback('upgrade_request', subject, description, function (error) {
$scope.upgradeRequest.busy = false;
if (error) return Client.notify('Error', error.message, false, 'error');
Client.notify('Success', 'We will get back to you as soon as possible for the upgrade.', true, 'success');
$('#upgradeModal').modal('hide');
});
};
$scope.showUpdateModal = function (form) {
$scope.update.error.password = null;
$scope.update.password = '';
@@ -85,7 +142,7 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
}
Client.refreshUserInfo(function (error, result) {
Client.refreshUserInfo(function (error) {
if (error) return $scope.error(error);
Client.refreshInstalledApps(function (error) {
@@ -121,6 +178,9 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
}
});
}
// welcome screen
if ($scope.user.showTutorial && $scope.user.admin) $scope.startTutorial();
});
});
});
+47
View File
@@ -997,3 +997,50 @@ $graphs-success-alt: lighten(#27CE65, 20%);
}
}
}
// ----------------------------
// Welcome tutorial
// ----------------------------
.welcome {
display: block;
position: absolute;
left: 0;
top: 0;
background-color: rgba(0,0,0,0.3);
z-index: 3000;
width: 100%;
height: 100%;
.dialog {
display: block;
position: absolute;
width: 600px;
background-color: white;
padding: 15px;
box-shadow: 0 3px 5px rgba(0,0,0,.5);
right: 20px;
bottom: 80px;
h2 {
margin-bottom: 20px;
margin-top: 0;
}
p {
font-size: 18px;
}
}
}
.filter {
display: inline-block;
padding-left: 0;
margin: 20px 0;
border-radius: 2px;
.form-control {
display: inline-block;
width: 200px;
}
}
+1
View File
@@ -130,6 +130,7 @@
<tr>
<td class="text-right" colspan="2" style="vertical-align: top;">
<br/>
<button class="btn btn-outline btn-xs btn-default" ng-click="showTutorial()">Show Tutorial</button>
<button class="btn btn-outline btn-xs btn-danger" ng-click="passwordchange.show()">Change Password</button>
</td>
</tr>
+7
View File
@@ -173,6 +173,13 @@ angular.module('Application').controller('AccountController', ['$scope', '$locat
});
};
$scope.showTutorial = function () {
Client.setShowTutorial(true, function (error) {
if (error) return console.error(error);
$scope.$parent.startTutorial();
});
};
Client.onReady(function () {
$scope.tokenInUse = Client._token;
+47
View File
@@ -0,0 +1,47 @@
<br/>
<div class="row">
<div class="col-md-10 col-md-offset-1">
<h1>Activity Log</h1>
</div>
</div>
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="filter">
<input type="text" class="form-control" ng-model="search" ng-model-options="{ debounce: 1000 }" ng-change="updateFilter()" placeholder="Search"/>
<select class="form-control" ng-model="action" ng-options="a.name for a in actions" ng-change="updateFilter()">
<option value="">-- all actions --</option>
</select>
</div>
<div class="pagination pull-right">
<button class="btn btn-default btn-outline" ng-click="showPrevPage()" ng-disabled="busy || currentPage <= 1"><i class="fa fa-angle-double-left"></i> prev</button>
<button class="btn btn-default btn-outline" ng-click="showNextPage()" ng-disabled="busy || pageItems > eventLogs.length">next <i class="fa fa-angle-double-right"></i></button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="card card-block" style="max-width: 100%">
<center ng-show="busy"><h2><i class="fa fa-spinner fa-pulse"></i></h2></center>
<table ng-hide="busy" class="table table-striped table-condensed table-hover">
<thead>
<tr>
<th class="col-md-2">Time</th>
<th class="col-md-2">Source</th>
<th class="col-md-6">Action</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="eventLog in eventLogs">
<th scope="row">{{ eventLog.creationTime | prettyDate }}</td>
<td>{{ eventLog.source.username || eventLog.source.userId || eventLog.source.authType }} <span ng-show="eventLog.source.ip || eventLog.source.appId"> ({{ eventLog.source.ip || eventLog.source.appId }}) </span> </td>
<td>{{ eventLog | eventLogDetails }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
+65
View File
@@ -0,0 +1,65 @@
'use strict';
angular.module('Application').controller('ActivityController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
$scope.config = Client.getConfig();
$scope.busy = false;
$scope.eventLogs = [ ];
// TODO sync this with the eventlog filter
$scope.actions = [
{ name: 'cloudron.activate', value: 'cloudron.activate' },
{ name: 'app.configure', value: 'app.configure' },
{ name: 'app.install', value: 'app.install' },
{ name: 'app.restore', value: 'app.restore' },
{ name: 'app.uninstall', value: 'app.uninstall' },
{ name: 'app.update', value: 'app.update' },
{ name: 'backup.finish', value: 'backup.finish' },
{ name: 'backup.start', value: 'backup.start' },
{ name: 'certificate.renew', value: 'certificate.renew' },
{ name: 'settings.climode', value: 'settings.climode' },
{ name: 'cloudron.start', value: 'cloudron.start' },
{ name: 'cloudron.update', value: 'cloudron.update' },
{ name: 'user.add', value: 'user.add' },
{ name: 'user.login', value: 'user.login' },
{ name: 'user.remove', value: 'user.remove' },
{ name: 'user.update', value: 'user.update' }
];
$scope.currentPage = 1;
$scope.pageItems = 20;
$scope.action = '';
$scope.search = '';
function fetchEventLogs() {
$scope.busy = true;
Client.getEventLogs($scope.action ? $scope.action.value : null, $scope.search || null, $scope.currentPage, $scope.pageItems, function (error, eventLogs) {
$scope.busy = false;
if (error) return console.error(error);
$scope.eventLogs = eventLogs;
});
}
Client.onReady(function () {
fetchEventLogs();
});
$scope.showNextPage = function () {
$scope.currentPage++;
fetchEventLogs();
};
$scope.showPrevPage = function () {
if ($scope.currentPage > 1) $scope.currentPage--;
else $scope.currentPage = 1;
fetchEventLogs();
};
$scope.updateFilter = function () {
fetchEventLogs();
};
}]);
+28 -10
View File
@@ -30,12 +30,30 @@
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.location.$dirty && appConfigureForm.location.$invalid) || (!appConfigureForm.location.$dirty && appConfigure.error.location) }">
<label class="control-label" for="appConfigureLocationInput">Location {{ appConfigure.error.location }} </label>
<div class="input-group form-inline">
<input type="text" class="form-control" ng-model="appConfigure.location" id="appConfigureLocationInput" name="location" placeholder="Leave empty to use bare domain" autofocus>
<div class="input-group-addon">
{{ !appConfigure.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }}
<input type="text" class="form-control" ng-model="appConfigure.location" id="appConfigureLocationInput" name="location" placeholder="{{ appConfigure.usingAltDomain ? 'app.example.com' : 'Leave empty to use bare domain' }}" autofocus>
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
{{ appConfigure.usingAltDomain ? 'External Domain' : ((!appConfigure.location ? '' : (config.isCustomDomain ? '.' : '-')) + config.fqdn) }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li>
<a href="" ng-click="useAltDomain(false)">{{ config.fqdn }}</a>
</li>
<li>
<a href="" ng-click="useAltDomain(true)"><i class="fa fa-star"></i> External Domain</a>
</li>
</ul>
</div>
</div>
</div>
<p class="text-center" ng-show="appConfigure.usingAltDomain && appConfigure.location && appConfigure.isAltDomainValid()">
Add a CNAME record for {{ appConfigure.location }} to {{ appConfigure.app.fqdn }}
<br>
</p>
<div class="has-error text-center" ng-show="appConfigure.error.port">{{ appConfigure.error.port }}</div>
<div ng-repeat="(env, info) in appConfigure.portBindingsInfo">
<ng-form name="portInfo_form">
@@ -106,7 +124,7 @@
</div>
</div>
<a ng-show="!!appConfigure.app.manifest.configurePath" ng-href="https://{{ appConfigure.app.location }}{{ !appConfigure.app.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }}/{{ appConfigure.app.manifest.configurePath }}" target="_blank">Application Specific Settings</a>
<a ng-show="!!appConfigure.app.manifest.configurePath" ng-href="https://{{ appConfigure.app.fqdn }}/{{ appConfigure.app.manifest.configurePath }}" target="_blank">Application Specific Settings</a>
<br/>
<br/>
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.password.$dirty && appConfigureForm.password.$invalid) || (!appConfigureForm.password.$dirty && appConfigure.error.password) }">
@@ -123,7 +141,7 @@
</div>
<div class="modal-footer ">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption !== '' && !appConfigure.isAccessRestrictionValid())"><i class="fa fa-spinner fa-pulse" ng-show="appConfigure.busy"></i> Save</button>
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption !== '' && !appConfigure.isAccessRestrictionValid()) || !appConfigure.isAltDomainValid()"><i class="fa fa-spinner fa-pulse" ng-show="appConfigure.busy"></i> Save</button>
</div>
</div>
</div>
@@ -134,7 +152,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Really Restore {{ appRestore.app.location }}</h4>
<h4 class="modal-title">Really restore {{ appRestore.app.fqdn }} ?</h4>
</div>
<div class="modal-body">
<p ng-show="appRestore.app.lastBackupId !== null">Restoring the app will lose all content generated since last backup of this app!</p>
@@ -167,7 +185,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ appError.app.location }}</h4>
<h4 class="modal-title">{{ appError.app.fqdn }}</h4>
</div>
<div class="modal-body">
<p><b>There was an error:</b></p>
@@ -185,7 +203,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Really uninstall {{ appUninstall.app.location }}</h4>
<h4 class="modal-title">Really uninstall {{ appUninstall.app.fqdn }} ?</h4>
</div>
<div class="modal-body">
<p>Deleting the app will also remove all content generated within this app!</p>
@@ -217,7 +235,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Update {{ appUpdate.app.location }}</h4>
<h4 class="modal-title">Update {{ appUpdate.app.fqdn }}</h4>
</div>
<div class="modal-body">
<p>Recent Changes for new version <b>{{ appUpdate.manifest.version}}</b>:</p>
@@ -297,7 +315,7 @@
<br/>
<div class="row">
<div class="col-xs-12 text-center">
<div class="grid-item-top-title">{{ app.location || app.fqdn }}</div>
<div class="grid-item-top-title">{{ app.altDomain || app.location || app.fqdn }}</div>
<div class="text-muted" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
{{ app | installationStateLabel }}
</div>
+21 -2
View File
@@ -23,6 +23,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
error: {},
app: {},
location: '',
usingAltDomain: false,
password: '',
portBindings: {},
portBindingsEnabled: {},
@@ -38,6 +39,11 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
isAccessRestrictionValid: function () {
var tmp = $scope.appConfigure.accessRestriction;
return !!(tmp.users.length || tmp.groups.length);
},
isAltDomainValid: function () {
if (!$scope.appConfigure.usingAltDomain) return true;
return /.+\..+\..+/.test($scope.appConfigure.location); // 2 dots
}
};
@@ -73,6 +79,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.error = {};
$scope.appConfigure.app = {};
$scope.appConfigure.location = '';
$scope.appConfigure.usingAltDomain = false;
$scope.appConfigure.password = '';
$scope.appConfigure.portBindings = {}; // This is the actual model holding the env:port pair
$scope.appConfigure.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
@@ -150,12 +157,23 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
else groups.splice(pos, 1);
};
$scope.useAltDomain = function (use) {
$scope.appConfigure.usingAltDomain = use;
if (use) {
$scope.appConfigure.location = '';
} else {
$scope.appConfigure.location = $scope.appConfigure.app.location;
}
};
$scope.showConfigure = function (app) {
$scope.reset();
// fill relevant info from the app
$scope.appConfigure.app = app;
$scope.appConfigure.location = app.location;
$scope.appConfigure.location = app.altDomain || app.location;
$scope.appConfigure.usingAltDomain = !!app.altDomain;
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information
$scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'restricted' : '';
$scope.appConfigure.accessRestriction = app.accessRestriction || { users: [], groups: [] };
@@ -190,7 +208,8 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
}
var data = {
location: $scope.appConfigure.location || '',
location: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.app.location : $scope.appConfigure.location,
altDomain: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.location : null,
portBindings: finalPortBindings,
accessRestriction: !$scope.appConfigure.accessRestrictionOption ? null : $scope.appConfigure.accessRestriction,
cert: $scope.appConfigure.certificateFile,
+2 -1
View File
@@ -103,10 +103,11 @@
</div>
<div class="collapse" id="collapseResourceConstraint" data-toggle="false">
<h4 class="text-danger">This Cloudron is running low on resources.</h4>
<p>Installing this app might decrease the performance of other apps. The Cloudron's resources can be extended with a model upgrade or available resources may be freed up by uninstalling unused applications.</p>
<p>Installing this app might decrease the performance of other apps. The Cloudron's resources can be extended with a plan upgrade or available resources may be freed up by uninstalling unused applications.</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success pull-left" ng-show="!appInstall.installFormVisible && user.admin && appInstall.resourceConstraintVisible" ng-click="showRequestUpgrade()">Upgrade Cloudron</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" ng-show="!appInstall.installFormVisible && user.admin && appInstall.resourceConstraintVisible" ng-click="appInstall.showForm(true)">Install anyway</button>
<button type="button" class="btn btn-success" ng-show="!appInstall.installFormVisible && user.admin && !appInstall.resourceConstraintVisible" ng-click="appInstall.showForm()">Install</button>
+9 -2
View File
@@ -60,8 +60,10 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$('#collapseResourceConstraint').collapse('hide');
$('#collapseMediaLinksCarousel').collapse('show');
$scope.appInstallForm.$setPristine();
$scope.appInstallForm.$setUntouched();
if ($scope.appInstallForm) {
$scope.appInstallForm.$setPristine();
$scope.appInstallForm.$setUntouched();
}
},
showForm: function (force) {
@@ -314,6 +316,11 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$('#appNotFoundModal').modal('show');
};
$scope.showRequestUpgrade = function () {
$('#appInstallModal').modal('hide');
$('#upgradeModal').modal('show');
};
$scope.gotoApp = function (app) {
$location.path('/appstore/' + app.manifest.id, false).search({ version : app.manifest.version });
};
+4
View File
@@ -107,6 +107,10 @@
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.version }}</td>
</tr>
</table>
<br/>
<br/>
<br/>
<button class="btn btn-primary pull-right" data-toggle="modal" data-target="#upgradeModal">Upgrade</button>
</div>
</div>
</div>
+2 -2
View File
@@ -331,10 +331,10 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
$scope.groups = result;
Client.listUsers(function (error, result) {
Client.getUsers(function (error, result) {
if (error) return console.error('Unable to get user listing.', error);
$scope.users = result.users;
$scope.users = result;
$scope.ready = true;
});
});