Compare commits

..

619 Commits

Author SHA1 Message Date
Girish Ramakrishnan c4c7668b5a Use new env vars 2016-04-15 11:57:51 -07:00
Girish Ramakrishnan b9fa87cca2 cloudron.backup -> backups.backup 2016-04-15 11:57:51 -07:00
Girish Ramakrishnan 218c9099fd more CHANGES 2016-04-15 11:09:25 -07:00
Girish Ramakrishnan 916d97f7bd reserve the no-reply mailbox 2016-04-15 11:09:25 -07:00
Johannes Zellner 109f777c00 Fix focus in setup wizard 2016-04-15 14:49:10 +02:00
Johannes Zellner 4bf3a78227 Add display name input in setup wizard 2016-04-15 12:18:41 +02:00
Girish Ramakrishnan c03e69232e Remove admin name (already set in cloudron.conf now) 2016-04-14 20:39:05 -07:00
Girish Ramakrishnan 91a016ee91 Change the admin email to no-reply 2016-04-14 19:56:54 -07:00
Girish Ramakrishnan 8256f97e9d use latest mail image 2016-04-14 19:37:34 -07:00
Girish Ramakrishnan d095899aef add note that admin@fqdn is reserved as well 2016-04-14 13:34:41 -07:00
Johannes Zellner 6293c0aede Add test for reserved username 'admin' 2016-04-14 16:30:31 +02:00
Johannes Zellner 101ce62ef3 Move username and email lowercasing to where it belongs
Fixes #592
2016-04-14 16:25:48 +02:00
Girish Ramakrishnan 9f443e2d07 should ideally use shutdown commands at some point (for mongodb) 2016-04-13 20:53:07 -07:00
Girish Ramakrishnan 0a30585a05 bump mongodb (handles mongod crash recovery) 2016-04-13 20:35:20 -07:00
Girish Ramakrishnan ed78bd05c8 reserve the "admin" username 2016-04-13 16:50:20 -07:00
Girish Ramakrishnan c24d7e7b3c do not crash on duplicate email 2016-04-13 14:47:35 -07:00
Girish Ramakrishnan 389d2be82d CLI is not a mode 2016-04-13 11:11:04 -07:00
Girish Ramakrishnan 38b85e6006 set givenName and sn in ldap response 2016-04-13 10:52:25 -07:00
Johannes Zellner de2cde7333 Test oauth with mixed case username and email 2016-04-13 12:48:02 +02:00
Johannes Zellner 08410569c0 Actually fix the correct thing in the janitor tests 2016-04-13 12:43:18 +02:00
Johannes Zellner be3b08a7b4 Test case-insensitive developer login 2016-04-13 12:39:50 +02:00
Johannes Zellner 2724cfd0ad Test simpleauth with uppercase username input 2016-04-13 12:30:55 +02:00
Johannes Zellner d7c8cf5e0e Ensure ldap filter values are treated lowercase only 2016-04-13 12:28:44 +02:00
Johannes Zellner 11f89da3a0 Ensure username and email are treated lower case in the database layer 2016-04-13 12:15:49 +02:00
Johannes Zellner a803af2300 Revert "add get route for user"
This route is already there, the reason is, that the users api works off
the :userId but the profile api works off the req.user coming from the used
access token.

This reverts commit dbef4d71be5a68239133ab9b6e0fc1fd88ee27cd.
2016-04-13 11:36:49 +02:00
Johannes Zellner 6991402a8c Fix typo 2016-04-13 11:33:01 +02:00
Johannes Zellner 259798a8f2 Ensure auth code expiration is calculated at the right time 2016-04-13 11:32:30 +02:00
Johannes Zellner d83395ecfb Also test grant type token access tokens 2016-04-13 11:28:10 +02:00
Johannes Zellner 6d3dd452be Test that oauth tokens are actually usable after issuing 2016-04-13 11:03:35 +02:00
Johannes Zellner 40bee79e3d Fix oversight to store userId as user.username for auth codes 2016-04-13 10:45:11 +02:00
Girish Ramakrishnan 95de25560b add profile scope to developer tokens 2016-04-12 19:08:56 -07:00
Girish Ramakrishnan 79eee94a5e Fix setup link path 2016-04-12 18:40:00 -07:00
Girish Ramakrishnan 82651a33c7 typo 2016-04-12 18:16:52 -07:00
Girish Ramakrishnan 212a0ffcd9 add get route for user 2016-04-12 18:13:37 -07:00
Girish Ramakrishnan 115ed12c36 check that app patch releases does not send email 2016-04-12 13:49:49 -07:00
Girish Ramakrishnan 53268b67dc test: it does not send mail for box patch releases 2016-04-12 13:45:11 -07:00
Girish Ramakrishnan 40dd12ba68 verify emails are sent in updatechecker test 2016-04-12 13:24:23 -07:00
Girish Ramakrishnan 7a111e29ad test updatechecker emails 2016-04-12 13:15:40 -07:00
Girish Ramakrishnan 065c65317d create owner in app update checker test 2016-04-12 13:13:16 -07:00
Girish Ramakrishnan 91a5d711f4 test: create owner 2016-04-12 13:12:17 -07:00
Girish Ramakrishnan 9071ea6c5e test: fix prerelease version 2016-04-12 13:01:42 -07:00
Girish Ramakrishnan 34521735da skip email notification for patch releases 2016-04-12 12:30:13 -07:00
Girish Ramakrishnan b7f6dfb197 remove verbose from tar 2016-04-10 22:49:39 -07:00
Girish Ramakrishnan fa330b4652 remove redundant debug 2016-04-10 22:44:43 -07:00
Girish Ramakrishnan 3bdbcff811 add debug 2016-04-10 22:34:55 -07:00
Girish Ramakrishnan ea3bd6d71d remove trailing comma 2016-04-10 22:29:09 -07:00
Girish Ramakrishnan d5cc96b1ff clean up backups code 2016-04-10 22:24:01 -07:00
Girish Ramakrishnan 4ed368cdd8 remove getBackupUrl 2016-04-10 22:12:06 -07:00
Girish Ramakrishnan 5229222014 getBackupCredentials is never used 2016-04-10 22:09:29 -07:00
Girish Ramakrishnan 9b0aa331e1 remove unused function 2016-04-10 22:08:11 -07:00
Girish Ramakrishnan 70cc073b1c only add to backupdb when the backup succeeded 2016-04-10 21:55:08 -07:00
Girish Ramakrishnan 29502fd8af remove unused exports 2016-04-10 21:52:01 -07:00
Girish Ramakrishnan 8d75fcfe67 typo 2016-04-10 21:46:01 -07:00
Girish Ramakrishnan b2668579d6 pass appid to backup script 2016-04-10 21:41:53 -07:00
Girish Ramakrishnan ba663faa64 fix debug 2016-04-10 21:39:45 -07:00
Girish Ramakrishnan 8db76f6b70 backup swap is not required anymore 2016-04-10 20:55:59 -07:00
Girish Ramakrishnan 322e9faee7 rework backup code
move all the backup code into backups.js
2016-04-10 20:41:08 -07:00
Girish Ramakrishnan af9d489395 backup apps use aws-cli 2016-04-10 18:47:25 -07:00
Girish Ramakrishnan 4565291c1c use aws-cli to upload box backups 2016-04-10 18:22:05 -07:00
Girish Ramakrishnan be127ec313 fix failing test 2016-04-10 17:15:23 -07:00
Girish Ramakrishnan 8b3a44b33c Add getBackupCredentials to backups API 2016-04-10 11:01:59 -07:00
Girish Ramakrishnan 08b5d7003d expose getBackupCredentials from storage api 2016-04-10 10:55:59 -07:00
Girish Ramakrishnan 60cc4c988f bump mysql addon 2016-04-09 02:34:54 -07:00
Girish Ramakrishnan 68219748ec oops, bump postgresql 2016-04-09 01:07:46 -07:00
Girish Ramakrishnan cfb56d7eee install aws-cli tool (for backups) 2016-04-08 23:58:07 -07:00
Girish Ramakrishnan 4690616230 Add 0.12.0 changes proactively 2016-04-08 23:57:05 -07:00
Girish Ramakrishnan 96d625b866 bump the postgresql addon (required for gitlab) 2016-04-08 23:46:39 -07:00
Johannes Zellner 2e281f8554 Only directly callback if the config is not empty
apiServerOrigin is always set if the config was set
2016-04-08 17:29:14 +02:00
Johannes Zellner 5da5d86bc8 Pass billing through from the appstore to the cloudron config 2016-04-08 17:27:22 +02:00
Johannes Zellner 103c0bd688 Add initial upgrade button version
This is currently always hidden
2016-04-08 13:57:24 +02:00
Girish Ramakrishnan 275d8c2121 fix user create response 2016-04-06 10:20:32 -07:00
Girish Ramakrishnan 4c964bcaf8 set userid correctly in tokendb 2016-04-06 10:11:48 -07:00
Girish Ramakrishnan e6c2c77f03 set username for predictability 2016-04-06 09:18:00 -07:00
Girish Ramakrishnan 819095b465 order for predictable tests 2016-04-06 09:08:59 -07:00
Girish Ramakrishnan 1453fd3c54 order by username to make tests deterministic 2016-04-06 08:46:42 -07:00
Johannes Zellner 867278a0b6 Use verifyWithUsername() instead of verify() in simpleauth 2016-04-06 08:57:55 +02:00
Girish Ramakrishnan 382fca3cf2 minor rewording in password reset 2016-04-05 23:08:57 -07:00
Girish Ramakrishnan f210501e12 create -> set 2016-04-05 23:06:17 -07:00
Girish Ramakrishnan 499921e3af more changes 2016-04-05 18:45:34 -07:00
Girish Ramakrishnan db19df9395 Bump infra version to force app reconfigure
Required for collectd profiles to be regenerated
2016-04-05 17:36:30 -07:00
Girish Ramakrishnan 6e2067bfe7 fix memory.stat path 2016-04-05 17:24:54 -07:00
Girish Ramakrishnan 8eb1b374ef cgroups are not in system.slice anymore 2016-04-05 17:12:50 -07:00
Girish Ramakrishnan 1734555974 handle case where box is not activated 2016-04-05 12:23:27 -07:00
Girish Ramakrishnan 7136de4d08 add debugs 2016-04-05 12:07:37 -07:00
Girish Ramakrishnan 21e8bc1ce5 javascript much 2016-04-05 12:06:15 -07:00
Girish Ramakrishnan 13020be6e6 default app bundle to null, if absent 2016-04-05 12:00:33 -07:00
Girish Ramakrishnan 3b922ff8b2 Some 0.11.2 changes 2016-04-05 11:07:32 -07:00
Girish Ramakrishnan 69402d0079 check activation state for existing cloudrons without a first run file 2016-04-05 10:56:27 -07:00
Johannes Zellner 99850f1161 Support ldap DNs with userId, username and email 2016-04-05 16:32:12 +02:00
Johannes Zellner b205212bf2 Explicitly verifyWithUsername() and offer a verify() userId based 2016-04-05 16:27:04 +02:00
Johannes Zellner baf586b028 Add missing 'else' 2016-04-05 16:25:05 +02:00
Johannes Zellner 94faa3575c Ensure we lowercase all emails
This ensures the uniqueness of that field
2016-04-05 11:15:50 +02:00
Johannes Zellner 544c1474d1 Allow multiple empty usernames in the db 2016-04-05 10:54:09 +02:00
Johannes Zellner bb25279878 Fixup some bugs in the user handling ui 2016-04-05 10:11:04 +02:00
Johannes Zellner 4939f526d5 Fixup the user.js tests 2016-04-05 09:28:41 +02:00
Johannes Zellner 68af03f401 Fixup ldap tests 2016-04-05 09:28:41 +02:00
Johannes Zellner f744fee708 password change route also now takes the userId 2016-04-05 09:28:41 +02:00
Johannes Zellner c7ceb29845 Remove unused setAdmin() from webclient 2016-04-05 09:28:41 +02:00
Johannes Zellner 56d9d5913d Fixup the user route tests 2016-04-05 09:28:41 +02:00
Johannes Zellner f7887228d3 Fix oauth session view tests and simpleauth tests 2016-04-05 09:28:41 +02:00
Johannes Zellner 73ed0384ea Fixup group rest api tests 2016-04-05 09:28:41 +02:00
Johannes Zellner 3051d4c22a This is actually a callback, doh 2016-04-05 09:28:41 +02:00
Johannes Zellner b32a0bcfad Do not allow empty username on createOwner() 2016-04-05 09:28:41 +02:00
Johannes Zellner 61c79aab23 Add asserts for user.createOwner() 2016-04-05 09:28:41 +02:00
Johannes Zellner 9740ffd504 Remove displayName setting in user edit view 2016-04-05 09:28:41 +02:00
Johannes Zellner 435ec2365b fix sendError() args 2016-04-05 09:28:41 +02:00
Johannes Zellner ff3562b0e8 Show error page for invalid reset tokens 2016-04-05 09:28:41 +02:00
Johannes Zellner 3be5511e33 Ensure we pass the resetToken on error 2016-04-05 09:28:41 +02:00
Johannes Zellner c8604e95ab Prevent password reset for not activated user 2016-04-05 09:28:41 +02:00
Johannes Zellner bbaf4c77fd This is ejs not angular 2016-04-05 09:28:41 +02:00
Johannes Zellner 1c9fc3f3dc Make account setup view prettier 2016-04-05 09:28:41 +02:00
Johannes Zellner 577959f281 Ensure browser autofill is disabled 2016-04-05 09:28:41 +02:00
Johannes Zellner 8af01f2955 Give basic form feedback for account creation 2016-04-05 09:28:41 +02:00
Johannes Zellner c73213b2f2 Handle username conflict in account setup 2016-04-05 09:28:41 +02:00
Johannes Zellner 36f3f4b8f4 Remove superflous ' 2016-04-05 09:28:41 +02:00
Johannes Zellner 31bd5cdee3 Fix typo, userdb.del() wants an id 2016-04-05 09:28:41 +02:00
Johannes Zellner fd0326efb1 Allow to use username or password for user deletion form 2016-04-05 09:28:41 +02:00
Johannes Zellner 65c6806109 Send full user information on deletion, not just the uuid 2016-04-05 09:28:41 +02:00
Johannes Zellner 1b7406784e Fix the user invitation to use userId 2016-04-05 09:28:41 +02:00
Johannes Zellner 8cbf83058f Adjust the welcome mail 2016-04-05 09:27:32 +02:00
Johannes Zellner e058e22cae Fixup the client tests with userid change 2016-04-05 09:27:32 +02:00
Johannes Zellner c84674529b Calm down the app polling a bit 2016-04-05 09:27:32 +02:00
Johannes Zellner a0098a8883 Adjust email templates, as we do not have a username set now 2016-04-05 09:27:32 +02:00
Johannes Zellner f6547c9b71 Ensure we render a useful string for access restriction users 2016-04-05 09:27:32 +02:00
Johannes Zellner 6dc17183ee Do not use google font cdn 2016-04-05 09:27:32 +02:00
Johannes Zellner bba3dd5ec0 Fetch users and groups in apps view 2016-04-05 09:27:32 +02:00
Johannes Zellner 9eec6c2e9d Add an extra postprocess in client.js 2016-04-05 09:27:32 +02:00
Johannes Zellner c235b82660 Fallback to email if username is not set for single user apps 2016-04-05 09:27:32 +02:00
Johannes Zellner 67ac0fcd5a Add missing ajax-loader.gif for slick carousel 2016-04-05 09:27:32 +02:00
Johannes Zellner 87ca147e65 Show user and email in user selection for app permission 2016-04-05 09:27:32 +02:00
Johannes Zellner 0cf2bfb792 Use user.id for session serialization 2016-04-05 09:27:32 +02:00
Johannes Zellner a112e614e6 Actually query by username instead of just delegate to get() in getByUsername() 2016-04-05 09:27:32 +02:00
Johannes Zellner 0b1dcd2940 Use userdb.getByUsername() instead of get() 2016-04-05 09:27:32 +02:00
Johannes Zellner 951934f275 Remove unused require 2016-04-05 09:27:32 +02:00
Johannes Zellner 78518ff5f6 Hide username and displayName when adding an account 2016-04-05 09:27:32 +02:00
Johannes Zellner b8d0c01187 fix typo 2016-04-05 09:27:32 +02:00
Johannes Zellner 572e5c4938 Adjust the user add form to not require a username 2016-04-05 09:27:32 +02:00
Johannes Zellner e4fabd20c1 Do not require a username to be present when creating a user 2016-04-05 09:27:32 +02:00
Johannes Zellner 726d154890 Make user id a uuid.v4() and allow empty usernames 2016-04-05 09:27:32 +02:00
Johannes Zellner 7a5ac1a2f5 Add POST account/setup to distinguish between setup and password reset 2016-04-05 09:27:32 +02:00
Johannes Zellner c90a8041e2 Move password/setup.html -> account/setup.html 2016-04-05 09:27:32 +02:00
Johannes Zellner 18b91b5fa0 Rename password setup to account setup 2016-04-05 09:27:32 +02:00
Johannes Zellner f058c266d2 Add username and display name form fields on account setup 2016-04-05 09:27:32 +02:00
Johannes Zellner e0114c87ac Also update the user record when username and email is sent 2016-04-05 09:27:32 +02:00
Johannes Zellner c98275000b Optionally support username and email in password setter route 2016-04-05 09:27:32 +02:00
Girish Ramakrishnan 553509c462 implement installation of app bundle 2016-04-04 23:03:13 -07:00
Girish Ramakrishnan 306bef96b4 remove dead DNS_IN_SYNC 2016-04-04 22:14:05 -07:00
Girish Ramakrishnan 497eaea65e bump expiry to 60 mins 2016-04-04 16:02:13 -07:00
Girish Ramakrishnan 8aacc503a6 Revert "getRestoreUrl now uses caas restore api"
This reverts commit f9fc9325a8995dc0a9cb1dfcf22fb27eca697a89.

For now, we can simply assume that caas is s3 based.
2016-04-04 15:57:32 -07:00
Girish Ramakrishnan ec160fe45f make getBackupUrl return id as well 2016-04-04 12:45:09 -07:00
Girish Ramakrishnan 82c74e6787 add backupdb tests 2016-04-04 12:41:17 -07:00
Girish Ramakrishnan bbff195863 rename filename to id 2016-04-04 12:20:56 -07:00
Girish Ramakrishnan e528dbcfc0 creationTime is redundant 2016-04-04 12:13:54 -07:00
Girish Ramakrishnan 0467e80c71 remove unused require 2016-04-04 12:13:25 -07:00
Girish Ramakrishnan c9ef0056e0 rename getSignedUploadUrl to getBackupUrl 2016-04-04 12:01:47 -07:00
Girish Ramakrishnan efb228cf5e getRestoreUrl now uses caas restore api 2016-04-04 11:57:29 -07:00
Girish Ramakrishnan af700827c5 info is not passed anymore 2016-04-04 11:44:24 -07:00
Girish Ramakrishnan 3135783fe3 rename getSignedDownloadUrl to getRestoreUrl 2016-04-04 11:43:56 -07:00
Girish Ramakrishnan 496f530b9f sessionToken is required in credentials (when signing) 2016-04-04 11:23:38 -07:00
Girish Ramakrishnan f44c2707f0 install swaks in base image 2016-04-04 09:50:19 -07:00
Johannes Zellner 9fbbddc3eb Show setupLink properly 2016-04-04 18:41:51 +02:00
Johannes Zellner 5afb16aa98 Show dialog with setupLink on invite 2016-04-04 18:41:51 +02:00
Johannes Zellner 8f2b0bae5e Receive the resetToken in the webadmin 2016-04-04 18:41:51 +02:00
Johannes Zellner fcfd1dceac Deliver the resetToken when an invite was sent 2016-04-04 18:41:51 +02:00
Girish Ramakrishnan d839f0b762 remove redundant session token 2016-04-03 23:23:23 -07:00
Girish Ramakrishnan 16a65fb185 drop configJson
The initial idea was to store exactly where the backups are stored.
But this only causes problems for migrations where the bucket might
change and clones where the prefix (box.id) changes.

Thus, it's best to leave the url creation to the caas side. (That
has to be done in another change)
2016-04-03 22:55:08 -07:00
Girish Ramakrishnan aaeb355183 add version 0.11.1 changes 2016-04-03 11:34:47 -07:00
Girish Ramakrishnan c236072c4c add comment 2016-04-02 18:04:58 -07:00
Girish Ramakrishnan 5d92cff638 backup config.json first because tarball takes lot of time and leads to token expiration 2016-04-02 18:01:49 -07:00
Girish Ramakrishnan 1b539b8d22 upload as binary 2016-04-02 17:58:10 -07:00
Girish Ramakrishnan a21a913f34 delete snapshot on failure path 2016-04-02 17:57:15 -07:00
Girish Ramakrishnan 357f6f0552 use same region as what we uploaded to 2016-04-02 13:32:14 -07:00
Girish Ramakrishnan b16aa4c007 check for region as well 2016-04-02 13:31:12 -07:00
Girish Ramakrishnan 1fed5ee353 0.11.0 changes 2016-04-01 23:38:35 -07:00
Girish Ramakrishnan 29077abf7c pass back the changeId 2016-04-01 23:21:10 -07:00
Girish Ramakrishnan f5c7116573 Add 0.10.4 changelog 2016-04-01 13:59:07 -07:00
Girish Ramakrishnan 42fc2d446c do not set the session_token header
this seems to be part of url now in signature v4
2016-04-01 13:26:25 -07:00
Girish Ramakrishnan 9ef04dc67f more 0.10.3 changes 2016-03-31 10:48:08 -07:00
Girish Ramakrishnan 3ea2070cdb pass filename (it is not part of the config!) 2016-03-31 09:53:56 -07:00
Girish Ramakrishnan fc11484b51 pick region from apiConfig if present 2016-03-31 09:48:38 -07:00
Girish Ramakrishnan b4ddfa94cc rename to apiConfig 2016-03-31 09:48:13 -07:00
Girish Ramakrishnan 9e7ae1a4f7 we really need a better linter 2016-03-31 09:38:40 -07:00
Girish Ramakrishnan d27159275b pick region as well 2016-03-31 09:34:57 -07:00
Girish Ramakrishnan 6c2ae756f1 fix usage of backupInfo 2016-03-31 09:23:41 -07:00
Girish Ramakrishnan 92e4433dff make backupdb.get return app backups 2016-03-31 09:12:12 -07:00
Girish Ramakrishnan c4cbd9f4e4 mailer: check for the correct SPF record 2016-03-31 08:44:31 -07:00
Johannes Zellner f413afb835 Support email login in simple auth 2016-03-31 16:59:44 +02:00
Johannes Zellner 915c37a72f Add tests for displayName change 2016-03-31 16:24:28 +02:00
Johannes Zellner 1ddb3a58da Also send displayName in simpleAuth 2016-03-31 16:24:28 +02:00
Girish Ramakrishnan a4aa5bbc59 fix linting 2016-03-31 00:51:38 -07:00
Girish Ramakrishnan 39cc5d07d1 use the backupdb config to determine bucket and prefix 2016-03-31 00:50:56 -07:00
Girish Ramakrishnan f3a05931df fix typo 2016-03-30 23:43:04 -07:00
Girish Ramakrishnan df39384056 do not save backup secrets in database 2016-03-30 23:39:48 -07:00
Girish Ramakrishnan 47c5cad239 fix typo 2016-03-30 17:06:50 -07:00
Girish Ramakrishnan ec380aa41e each change really needs to be in separate line 2016-03-30 15:34:09 -07:00
Girish Ramakrishnan 7d1a663a87 0.10.3 changes 2016-03-30 15:26:52 -07:00
Girish Ramakrishnan ba69316c14 add note that filename is reused as id 2016-03-30 15:17:04 -07:00
Girish Ramakrishnan c097651a88 store backup configuration as part of backups table 2016-03-30 15:04:39 -07:00
Girish Ramakrishnan 22b8154a39 0.11.0 changes 2016-03-30 11:39:50 -07:00
Girish Ramakrishnan 9e8179a235 up link is relative 2016-03-29 14:02:53 -07:00
Girish Ramakrishnan 3fbeb2a1c1 more 0.10.2 changes 2016-03-29 13:24:26 -07:00
Girish Ramakrishnan 2c4cf0a505 Download intermediate cert following the 'up' Link 2016-03-29 12:51:05 -07:00
Girish Ramakrishnan adab544e99 Version 0.10.2 changes 2016-03-28 10:55:20 -07:00
Girish Ramakrishnan ae8a371597 add adminFqdn in the spf record
For custom domains, we do not set the A record for the naked domain
(because the user might be using it for his own). This means that
a:domain.com will not work.

The solution is to simply use the admin domain.
2016-03-27 23:05:29 -07:00
Girish Ramakrishnan ead076bd9f add MAIL_SMTP_PASSWORD 2016-03-25 23:14:09 -07:00
Girish Ramakrishnan f8c683f451 Disallow updating an app with mismatching manifest id
Story so far:
1. App installed from store. appStoreId is set to manifest.id.
2. User installed a custom built app with a custom manifest.id using cloudron install --app <id>. The appStoreId is still set.
3. When we make a new release, it overrides the users install.

The fix (for now) is:
1. Do not allow mismatching ids to start with.
2. When forced, it is allowed but appStoreId is cleared so as to not get any auto updates.

This leaves the user vulnerable to 'cloudron uninstall' simply autoselecting this new app.
For this, they have to simply disable CLI mode for now.

There is also a corner case where:
1. Dev installs from app store
2. Dev compiles from source and updates on top of app store install with --app <id>
3. Dev find out that his installation has auto-updated the next day.
2016-03-25 11:46:25 -07:00
Johannes Zellner b56bc08e9a Allow to use email and username for ldap bind 2016-03-24 21:03:04 +01:00
Girish Ramakrishnan daadbfa23f fix wording 2016-03-23 12:00:30 -07:00
Girish Ramakrishnan a215443c56 do not renew apps without any cert
autoRenew was mistakenly reconfiguring app without a cert (this
is the common case for apps in non-custom domain)
2016-03-23 08:49:08 -07:00
girish@cloudron.io 4e22c6d5ac minor nakedomain fixes 2016-03-21 15:07:10 -07:00
girish@cloudron.io d43810fea9 add comment on why we add naked domain for custom domains 2016-03-21 13:50:26 -07:00
girish@cloudron.io f5ab63e8ec naked domain page styling 2016-03-21 13:49:11 -07:00
girish@cloudron.io b1f172ed17 trim the output string 2016-03-21 08:25:10 -07:00
Girish Ramakrishnan 413f9231b3 fix formatting 2016-03-20 12:12:22 -07:00
Girish Ramakrishnan 11513f9428 send a message for cert renewal status 2016-03-19 20:40:03 -07:00
Girish Ramakrishnan 5042741435 renew cert every 12 hours 2016-03-19 20:30:01 -07:00
Girish Ramakrishnan 75ed9c4a63 Check for key file instead of csr file
1) csr file in older backups got corrupt
2) new key results in a new cert request in LE (for rate limits)
2016-03-19 18:49:55 -07:00
Girish Ramakrishnan 8c36f3aab4 add debug for fallback case 2016-03-19 18:37:05 -07:00
Girish Ramakrishnan 7aa5e8720a 0.10.1 changes 2016-03-19 14:17:28 -07:00
Girish Ramakrishnan 14ef71002f write the DER cert properly into the csr file 2016-03-19 14:07:58 -07:00
Girish Ramakrishnan ea87841e77 merge fallback cert job into renewal
this is becase we need to reconfigure for the case where we got a
renewed cert (but the app was switched to fallback cert at some point)
2016-03-19 13:54:52 -07:00
Girish Ramakrishnan 091e424c0e Fix description 2016-03-19 13:37:58 -07:00
Girish Ramakrishnan 20629ea078 fix linter errors 2016-03-19 13:22:38 -07:00
Girish Ramakrishnan b1b6a9ae65 reconfigure admin using configureAdmin 2016-03-19 12:54:11 -07:00
Girish Ramakrishnan 7ddbf7b652 refactor expiry check 2016-03-19 12:50:31 -07:00
Girish Ramakrishnan 3d088aa9c4 fix debug message 2016-03-19 12:31:48 -07:00
Girish Ramakrishnan f329e0da92 fix typo 2016-03-19 12:14:23 -07:00
Girish Ramakrishnan a18737882b run more aggressively in test mode 2016-03-19 12:12:39 -07:00
Girish Ramakrishnan a58a458950 do not abbrev 2016-03-19 12:11:28 -07:00
Girish Ramakrishnan 44c5f84c56 Fix usage of isExpiringSync 2016-03-19 12:06:13 -07:00
Girish Ramakrishnan d6b92ee301 remove Job suffix 2016-03-19 10:25:19 -07:00
Girish Ramakrishnan c769a12c45 set the box version for test for pass 2016-03-19 10:23:12 -07:00
Girish Ramakrishnan 017c32c3dd fix certificate renewal
Do the whole acme flow for certificate renewal. the idea here is
simply reuse the key and the csr. In this case, it does not count
as a new certificate issuance.

https://github.com/diafygi/letsencrypt-nosudo/issues/55
2016-03-19 02:44:05 -07:00
Girish Ramakrishnan 5d54c9e668 check my domain for expiry and falling back 2016-03-18 23:43:56 -07:00
Girish Ramakrishnan adaaca5ceb switch expired certs of domains to use fallback cert
1) nginx won't reload when using expired certs
2) this is the only way the user can use the app now
2016-03-18 23:26:57 -07:00
Girish Ramakrishnan 4a73e1490e Refactor code to take hours 2016-03-18 23:00:02 -07:00
Girish Ramakrishnan f31a7a5061 use fallback certs if renewal fails 2016-03-17 12:20:02 -07:00
Girish Ramakrishnan 3499a4cc6c move requiresOAuthProxy to nginx
we have 3 levels
    * routes, cron, apptask
    * everything else where everyone calls everyone :-)
    * the db layer
2016-03-17 11:38:29 -07:00
girish@cloudron.io 42796b12dc update safetydance to 0.1.1 2016-03-14 22:50:48 -07:00
girish@cloudron.io 20ac040dde cert: check expiry correctly 2016-03-14 22:50:06 -07:00
girish@cloudron.io 7f2b3eb835 acme: disable renewal via url fetch for now
this does not seem to work.

From https://github.com/ericchiang/letsencrypt/commit/cf85854177b22540ca1aeba770c2b86534c6c5ef:

// RenewCertificate attempts to renew an existing certificate.
// Let's Encrypt may return the same certificate. You should load your
// current x509.Certificate and use the Equal method to compare to the "new"
// certificate. If it's identical, you'll need to run NewCertificate and/or
// start a new certificate flow.
2016-03-14 22:22:57 -07:00
girish@cloudron.io 2b562f76ea le: handle renewal upto 30 days in advance 2016-03-14 22:18:43 -07:00
Girish Ramakrishnan b942033512 acme: debug output the domain 2016-03-14 16:21:03 -07:00
Girish Ramakrishnan fa4a8c2036 add debug for successful renewal 2016-03-14 15:55:51 -07:00
Johannes Zellner 27febbf1e9 The blue cloud is gone 2016-03-11 16:48:56 -08:00
girish@cloudron.io 8da2eb36cc fix email wording 2016-03-09 18:37:02 -08:00
girish@cloudron.io cbb34005c6 restoreKey -> filename 2016-03-09 14:23:42 -08:00
girish@cloudron.io efc1627648 more changes 2016-03-09 09:34:57 -08:00
girish@cloudron.io f513dcdf3b Add 0.10.0 changelog 2016-03-09 09:29:17 -08:00
girish@cloudron.io 61a52d8888 dist-upgrade to update more aggressively 2016-03-09 09:29:07 -08:00
Johannes Zellner 4cfc187063 Add sender name to admin email 2016-03-09 07:41:50 +01:00
Johannes Zellner 065af03e5f Stop docker proxy in ldap tests 2016-03-09 07:34:44 +01:00
Johannes Zellner c4eeebdfbe Enable admin change test 2016-03-09 06:18:39 +01:00
Johannes Zellner b1004de358 Notify admins about newly added admin 2016-03-09 06:16:21 +01:00
Girish Ramakrishnan fbca0fef38 fix missing assert 2016-03-08 18:51:40 -08:00
Girish Ramakrishnan d658530e66 fix failing tests 2016-03-08 18:44:51 -08:00
Girish Ramakrishnan 21d4cc9cb2 getAllPaged -> getPaged 2016-03-08 18:10:39 -08:00
Girish Ramakrishnan e2b7ec3ffd store filename with tar.gz extension 2016-03-08 16:47:53 -08:00
Girish Ramakrishnan 8014e2eaf8 add route to download backup 2016-03-08 16:28:42 -08:00
girish@cloudron.io a10ed73af2 get zoneName using tldjs 2016-03-08 09:52:13 -08:00
girish@cloudron.io 8b2903015d list app backups from db 2016-03-08 08:57:28 -08:00
girish@cloudron.io d157bf30f3 remove box backups from the database 2016-03-08 08:52:20 -08:00
girish@cloudron.io 7996b32022 add backups to the database
ideally, these should be done _after_ the backup is successful and not when
the backup url is generated.

we had a discussion on why need backupdb to start with. Some rationale includes:
1. we can use it as a FK constraint (like make sure you delete backups when you delete app)
2. have labels for backups
3. dependancy relation from box backup to apps
4. s3 reverse sort is little painful and requires us to get all items in bulk and sort in JS
   (also requires us to change our backup filename format)
5. any metadata storage requires database

The cons include:
1. s3 and this db go out of sync
2. db entry is useless if s3 file is missing
2016-03-08 08:42:00 -08:00
girish@cloudron.io 4b77703902 export getByAppIdPaged 2016-03-07 17:52:13 -08:00
Girish Ramakrishnan 4dd82d10ad backup: ensure same timestamp for app data and config 2016-03-07 12:13:54 -08:00
Girish Ramakrishnan 83d05c99d3 mount manually instead of fstab because of race
I cannot figure how to make the box-setup.service run before the mounting
of a specific mount point. adding a dep on mount.target locks up the system.
2016-03-07 10:48:09 -08:00
Girish Ramakrishnan b0acdfb908 use truncate instead of fallocate 2016-03-07 10:44:35 -08:00
Girish Ramakrishnan b062dab65c mysql also uses the data partition 2016-03-07 10:38:59 -08:00
Girish Ramakrishnan eadcdeee1c not being mounted is the normal case 2016-03-07 10:37:26 -08:00
Girish Ramakrishnan 9de6f9c1c2 add backupdb
mostly same code as the appstore side
2016-03-07 09:30:44 -08:00
Girish Ramakrishnan 89f54245f7 Add backups table 2016-03-07 09:27:10 -08:00
Girish Ramakrishnan 5fbd1dae30 bump the mysql memory limit
we hit this memory limit often in phabricator backup. this is all
very crude but should suffice for now.
2016-03-05 18:35:28 -08:00
girish@cloudron.io 486ced0946 fix LDAP debug 2016-03-04 17:52:27 -08:00
girish@cloudron.io d1c1fb8786 fix ldap debug ("ldap" already appears as part of debug) 2016-03-04 17:51:18 -08:00
Johannes Zellner 57ff8b6770 fix feedback test 2016-03-04 22:27:18 +01:00
Johannes Zellner d12d8f5c0b Properly extract referrers, which contain queries on their own and are not properly encoded 2016-03-03 15:06:14 +01:00
Johannes Zellner 17deac756b Also log app manifest id for alive apps 2016-03-03 09:30:46 +01:00
Johannes Zellner f7bb3bac98 Log app manifest id in healthmonitor 2016-03-03 09:30:46 +01:00
girish@cloudron.io 744c721000 use docker 1.10.2 (untested) 2016-03-01 10:13:44 -08:00
girish@cloudron.io 0500bae221 install aufs tools
https://github.com/docker/docker/issues/915
2016-03-01 10:13:04 -08:00
girish@cloudron.io a7b5b49d96 fix language 2016-02-26 10:14:37 -08:00
Johannes Zellner 93ef1919c2 Hide superuser checkbox for the user himself 2016-02-26 18:08:56 +01:00
girish@cloudron.io 254d6ac92e handle singular case as well 2016-02-26 08:43:59 -08:00
girish@cloudron.io 3a12265f42 Do not preallocate data volume
This is not tested but will get tested in the next upgrade
2016-02-26 08:27:13 -08:00
Johannes Zellner 71eeb47f0f Hide groups in user listing if the screen is too tiny 2016-02-26 13:10:29 +01:00
Johannes Zellner 5ea5023d97 Better encapsulate the form related functions in install view 2016-02-26 12:50:05 +01:00
Johannes Zellner 1148e21cd4 Add more changes 2016-02-26 12:29:52 +01:00
Johannes Zellner e9a2b2a7cf Disable group access control and show info if there are no groups 2016-02-26 11:59:18 +01:00
Johannes Zellner 7a34f40611 Allow to specify accessRestrictions on app install 2016-02-26 11:47:20 +01:00
Johannes Zellner c630de1003 Fetch groups in appstore.js 2016-02-26 11:24:01 +01:00
Johannes Zellner 74da8f5af8 Add 0.9.3 changes 2016-02-26 11:19:45 +01:00
girish@cloudron.io b758be5ae2 0.9.2 changes 2016-02-26 11:17:36 +01:00
Johannes Zellner c585be4eec Adjust group length error message 2016-02-26 11:07:31 +01:00
Johannes Zellner 3ebc569438 Avoid using superuser in the ui, but describe what it is 2016-02-26 11:03:39 +01:00
Johannes Zellner 5a2cf3cbfe Move superuser checkbox at the bottom of the form 2016-02-26 11:02:43 +01:00
Johannes Zellner 715c5f9f61 Do not set admin group multiple times 2016-02-26 10:56:33 +01:00
Johannes Zellner 6843fda601 Show group names and make sure we don't break layout right away 2016-02-26 10:53:41 +01:00
Johannes Zellner a78f3b1db3 Give more space to some views 2016-02-26 10:52:47 +01:00
girish@cloudron.io 1419108a86 umount is for unmounting 2016-02-25 20:13:16 -08:00
girish@cloudron.io 7a8b457ce9 truncate to shrink the file if required 2016-02-25 19:26:46 -08:00
girish@cloudron.io 10967ff8ce allow 1.2 times RAM
This is basically to allow 2 phabricators and another small app with
no warning on a 4gb droplet :-)
2016-02-25 18:34:28 -08:00
girish@cloudron.io 1fdfd3681c Revert "Display group ids"
This reverts commit d80ce25363061f95cb14e223efd4ab9828739eea.

Didn't mean to commit this
2016-02-25 18:11:25 -08:00
girish@cloudron.io 187d4f9ca2 round memory to nearest GB
os.totalmem returns some close-to-GB number. Because of this two
apps with 2GB don't install on 4GB.
2016-02-25 17:19:39 -08:00
girish@cloudron.io 6b67e64bf1 Display group ids 2016-02-25 16:33:58 -08:00
girish@cloudron.io 7ae6061d72 Edit -> Save 2016-02-25 15:24:50 -08:00
Johannes Zellner e96b9c3e3f Give the user the perception he gets what he pays for 2016-02-26 00:18:47 +01:00
Johannes Zellner c9ca05a703 Do not offer admin group for access restriction
The same can be achieved with a new group and it just
keeps the superuser/admin out of the way here. In any case
admins can always access all apps.
2016-02-26 00:13:09 +01:00
Johannes Zellner 23e5bed247 Fix the group numbering to ignore admin group 2016-02-26 00:02:38 +01:00
Johannes Zellner bae0d728b3 Only show group count to not break layout and allow quickedit 2016-02-26 00:01:25 +01:00
Johannes Zellner 5cd1c7d714 add hand selector 2016-02-26 00:01:01 +01:00
Johannes Zellner d430e902bf Separate superuser checkbox from the other groups in user edit 2016-02-25 23:20:55 +01:00
Johannes Zellner 4fb89de34f Remove 'this is you' 2016-02-25 22:29:06 +01:00
Johannes Zellner 7cd3bb31e1 Add new support type for failing erroring apps 2016-02-25 21:38:37 +01:00
Girish Ramakrishnan 2857158543 do not set memoryLimit 2016-02-25 11:05:19 -08:00
Johannes Zellner 82a347ea4b Add tooltips for superusers 2016-02-25 16:07:31 +01:00
Johannes Zellner b5c7f978a2 Do not show admin group in group listing 2016-02-25 15:54:30 +01:00
Johannes Zellner 625da29fce Show admins with an icon instead of a group tag 2016-02-25 15:53:36 +01:00
Johannes Zellner b82b183df6 Show alternative text for non admins, when no apps are available for this user 2016-02-25 15:38:46 +01:00
Johannes Zellner ce36fadf2b Fix bug for non admins to view the appstore 2016-02-25 15:34:44 +01:00
Johannes Zellner 2429599733 Change text from installed to your applications 2016-02-25 15:34:33 +01:00
Johannes Zellner 261a0a1728 Add account ui to change displayName 2016-02-25 15:09:52 +01:00
Johannes Zellner d8def61f67 Encapsulate the business logic in the account controller 2016-02-25 14:58:26 +01:00
Johannes Zellner 2732af24c1 Some code cleanups and bugfixes for the accounts view 2016-02-25 14:46:53 +01:00
Johannes Zellner 3d48da0e8d Remove unused function in client to change email 2016-02-25 14:34:35 +01:00
Johannes Zellner d3b8bd1314 Remove password field for user email change 2016-02-25 14:34:16 +01:00
Johannes Zellner f600ebcf19 Remove password entry from user edit form 2016-02-25 14:15:48 +01:00
Johannes Zellner 160467e199 Do not require password for user profile changes 2016-02-25 14:03:42 +01:00
Johannes Zellner 384c410e7c Do not require a password for user profile changes 2016-02-25 13:54:44 +01:00
Johannes Zellner 84c4187fa9 Test normal users accessing the user api 2016-02-25 13:53:18 +01:00
Johannes Zellner 4f7fd9177c Allow user details only for the same user or admins 2016-02-25 13:44:53 +01:00
Johannes Zellner b5b0ab7475 Require admin rights for user listing 2016-02-25 13:43:15 +01:00
Johannes Zellner a0d7406b3c Do not allow normal users to get group listings or details 2016-02-25 13:34:01 +01:00
Johannes Zellner 7165be0513 Warn the user about installing too many apps, but give an override button 2016-02-25 12:41:15 +01:00
Johannes Zellner 9c995277f7 Fixup the apps unit tests 2016-02-25 12:20:18 +01:00
Johannes Zellner aa693e529b Only list apps where a user has access to 2016-02-25 12:20:11 +01:00
Johannes Zellner 63013c7297 Just check for .admin flag in the user object 2016-02-25 11:42:25 +01:00
Johannes Zellner c8db6419d8 Admins are not special cased in apps.js app listing
This is done in the route
2016-02-25 11:41:14 +01:00
Johannes Zellner 93c1ddd982 Always amend the admin flag for further use 2016-02-25 11:40:48 +01:00
Johannes Zellner df102ec374 Add basic getAllByUser() app tests 2016-02-25 11:28:45 +01:00
Johannes Zellner 9688e4c124 Add apps.getAllByUser() 2016-02-25 11:28:29 +01:00
Johannes Zellner 00d277b1c3 Make the error dialog generic not only for app install errors 2016-02-24 18:36:40 +01:00
Johannes Zellner 0fb44bfbc1 Forward the error.message instead of making a new Error object
That leads to only Internal Error
2016-02-24 18:12:31 +01:00
Johannes Zellner c167bd8996 Set error in installationProgress also on uninstallation errors 2016-02-24 17:53:21 +01:00
Johannes Zellner a3737c3797 Report access denied errors in route53 backend 2016-02-23 17:29:28 +01:00
Johannes Zellner 8fcb0b46a5 add 0.9.1 changes 2016-02-21 14:51:51 +01:00
Johannes Zellner f5189e0a56 Force the user to set at least one access restriction group 2016-02-19 18:02:51 +01:00
Johannes Zellner 86f14b0149 Thanks JS for being so value focused... 2016-02-19 17:23:09 +01:00
Johannes Zellner 30913006e3 Remove all occurances of oauthProxy in the webadmin 2016-02-19 16:50:25 +01:00
Johannes Zellner 81bd4f2ea5 Remove console.log() 2016-02-19 16:29:40 +01:00
Johannes Zellner 351ddcb218 Fixup the unit tests 2016-02-19 16:29:29 +01:00
Johannes Zellner dd18f9741a Dynamically detect oauth proxy needs in apptask 2016-02-19 16:18:47 +01:00
Johannes Zellner cdce6e605d Adjust to new apps api 2016-02-19 16:14:02 +01:00
Johannes Zellner d4480ec407 Remove oauthProxy usage in the database wrapper 2016-02-19 16:12:58 +01:00
Johannes Zellner 85c92ab0b4 Remove oauthProxy from the apps.js api 2016-02-19 16:07:49 +01:00
Johannes Zellner 230c24d6c6 Adjust the route unit tests, to remove oauthProxy 2016-02-19 16:01:08 +01:00
Johannes Zellner 07c935dfec Remove oauthProxy from client side api wrapper 2016-02-19 16:00:48 +01:00
Johannes Zellner eab3bda8e1 Remove oauthProxy from the apps rest routes 2016-02-19 15:54:01 +01:00
Johannes Zellner f731c1ed0b Dynamically detect if an oauth proxy should be used for an app 2016-02-19 15:44:15 +01:00
Johannes Zellner edec3601f4 Do not rely on oauthProxy property of app object in nginx configuration code 2016-02-19 15:43:39 +01:00
Johannes Zellner 9e87fd0440 Add apps.requiresOAuthProxy() 2016-02-19 15:03:36 +01:00
Johannes Zellner 8cb304e1c9 Ensure app ordering by location 2016-02-19 13:36:39 +01:00
Johannes Zellner a24335d68b Remove oauth proxy setting ui 2016-02-19 12:04:05 +01:00
Johannes Zellner 78d1ed7aa5 Special case the admin button in apps access control UI 2016-02-18 18:26:24 +01:00
Johannes Zellner deb30e440a Disable ejs debug flag 2016-02-18 18:00:23 +01:00
Johannes Zellner 86ef9074b1 Add access restriction tests for ldap auth 2016-02-18 17:40:53 +01:00
Johannes Zellner 1a13128ae1 Ensure accessRestriction is either an object or an empty string 2016-02-18 17:28:00 +01:00
Johannes Zellner b41642552d The ldap property is part of req.connection 2016-02-18 16:40:30 +01:00
Johannes Zellner f5570c2e63 Enable the apps group access control ui 2016-02-18 16:14:45 +01:00
Johannes Zellner b0d11ddcab Adhere to access control on ldap user bind 2016-02-18 16:04:53 +01:00
Johannes Zellner 804464c304 Add apps.getByIpAddress() 2016-02-18 15:43:46 +01:00
Johannes Zellner ecf7f442ba Add docker.getContainerIdByIp() 2016-02-18 15:39:27 +01:00
Johannes Zellner 9ddd3aeb07 Show app id and fix naked domain in debugApp() 2016-02-18 12:51:25 +01:00
Johannes Zellner 864e3ff217 Give form feedback if group name is invalid
Fixes #583
2016-02-15 13:09:58 +01:00
Johannes Zellner 9bf1fe3b7d Show naked_domain for healthtask summary 2016-02-14 17:42:52 +01:00
Johannes Zellner b32a48c212 Add changelog for 0.9.0 2016-02-14 13:19:12 +01:00
Johannes Zellner 22a3dd7653 Disable advanced accessControl ui
This is working, from a configuration standpoint.
However not all auth methods support this yet, so
we hide it until that is done, otherwise it is just
confusing
2016-02-14 13:15:09 +01:00
Johannes Zellner 132b463e0a Hide memoryLimit ui 2016-02-14 13:14:03 +01:00
Johannes Zellner 7aefe5226a Properly layout the group labels 2016-02-14 13:12:04 +01:00
Johannes Zellner 656c1bfd3a Make Admin group members visible 2016-02-14 13:11:49 +01:00
Johannes Zellner e237b609f5 Make admin group buttons red 2016-02-14 13:08:50 +01:00
Johannes Zellner 057b9e954e Support btn-admin 2016-02-14 13:08:40 +01:00
Johannes Zellner f79c00d9be Special case admin group button in user profile 2016-02-14 12:51:58 +01:00
Johannes Zellner 5f96d862ab Move default memory limit to constants.js 2016-02-14 12:13:49 +01:00
Johannes Zellner 79199bf023 Ensure we stash and restore the memoryLimit 2016-02-14 12:10:22 +01:00
girish@cloudron.io beec4dddca my -> our 2016-02-13 11:22:47 -08:00
Johannes Zellner 7c243cb219 Warn the user on group deletion if it still has members 2016-02-13 12:42:41 +01:00
Johannes Zellner 754e33af2a do not allow removing the admin group 2016-02-13 12:42:41 +01:00
Johannes Zellner 63cab7d751 Allow non-empty groups to be deleted 2016-02-13 12:42:41 +01:00
Johannes Zellner 503714a10b Special case admin group in group listing 2016-02-13 12:42:41 +01:00
girish@cloudron.io ada5be6ae0 Add 0.9.0 changes 2016-02-13 03:27:21 -08:00
girish@cloudron.io 2112494b43 bump mysql image version 2016-02-13 03:26:29 -08:00
girish@cloudron.io c0b45ad71e update manifestformat to 2.3.0 2016-02-12 17:40:00 -08:00
girish@cloudron.io 5669d387af check app state before exec 2016-02-12 12:32:58 -08:00
Johannes Zellner 957f20a9a8 Always store the memory limit in the app db record and adjust on update if needed 2016-02-11 18:14:16 +01:00
Johannes Zellner 71bfc1cbda Ensure we never go below minimum memoryLimit 2016-02-11 18:13:42 +01:00
Johannes Zellner 489ea3a980 Add memoryLimit validation 2016-02-11 17:39:15 +01:00
Johannes Zellner 8c6f655628 Ensure we deal with byte values for memoryLimit 2016-02-11 17:29:00 +01:00
Johannes Zellner 75d22d7988 Introduce memoryLimit to apps routes 2016-02-11 17:00:21 +01:00
Johannes Zellner a7bf043a9e Improve memory limit ui 2016-02-11 16:31:11 +01:00
Johannes Zellner 402385faca Remove unused linter options 2016-02-11 14:35:51 +01:00
Johannes Zellner cdd82fa456 Add access restriction by group ui to app configure dialog 2016-02-11 14:14:51 +01:00
Johannes Zellner 2f7d99f3f6 Do not allow to remove the user from the admin group 2016-02-11 13:43:02 +01:00
Johannes Zellner e4799991ec Remove reference to non-existing css selector form-signin 2016-02-11 12:53:35 +01:00
Johannes Zellner 66167e74dc Cleanup the user forms 2016-02-11 12:50:02 +01:00
Johannes Zellner 5643d49bef Check if user deletion actually affected a row 2016-02-11 12:07:43 +01:00
Johannes Zellner 81ec26e45c Ensure we can delete users which belong to a group 2016-02-11 12:02:35 +01:00
Johannes Zellner 72c5ebcc06 Add user api tests for adding/removing from admin group 2016-02-11 11:39:19 +01:00
Johannes Zellner ecf7575dd3 UserError.NOT_ALLOWED is not unused 2016-02-11 11:32:48 +01:00
Johannes Zellner 98a7f44dc1 Check for last admin not required anymore
This is now prevented by the fact that an admin
cannot remove itself from the admin group. There
remains a race, just like before, where two admins could
trigger an admin group removal of the other admin in parallel
and the calls are in a state after admin flag check of
the used tokens. This can only be prevented with a db constraint
in the end.
2016-02-11 11:30:21 +01:00
Johannes Zellner 5fce9c8d1f Do not allow an admin remove itself from admins group 2016-02-11 11:29:04 +01:00
Johannes Zellner 0ea89fccb8 Remove admin api route tests 2016-02-11 11:26:35 +01:00
Johannes Zellner 2c2922d725 Make tests succeed for now
We still have to bring back the sending of email when admins are changed
2016-02-11 11:26:35 +01:00
Johannes Zellner fbeefeca7d setGroups() has no result 2016-02-11 11:26:35 +01:00
Johannes Zellner 163ceef527 Remove the admin toggle route 2016-02-11 11:26:35 +01:00
Girish Ramakrishnan db5cc1f694 dropping groupMembers table makes not much sense 2016-02-10 08:57:06 -08:00
Johannes Zellner a3b9a7365c No need to check for admin, the whole view is admin only 2016-02-10 17:35:40 +01:00
Johannes Zellner 213b2a2802 show groups of each user 2016-02-10 17:34:29 +01:00
Johannes Zellner 229d09bb9e Give the group buttons some space 2016-02-10 17:26:51 +01:00
Johannes Zellner f127680c8c Fixup the group delete dialog title 2016-02-10 17:23:42 +01:00
Johannes Zellner f767f7f1b9 We do not actually allow group edit 2016-02-10 17:22:04 +01:00
Johannes Zellner acb1afa955 Make delete action the last one 2016-02-10 17:20:24 +01:00
Johannes Zellner d132109925 Fixup the modal dialog css selector 2016-02-10 17:16:50 +01:00
Johannes Zellner 820e417026 angular requires special treatment for DELETE 2016-02-10 17:12:58 +01:00
Johannes Zellner 94bd0c606b Add group removal ui 2016-02-10 16:59:24 +01:00
Johannes Zellner 9a8328e6db We should really use camelCase 2016-02-10 16:43:23 +01:00
Johannes Zellner 5c75d64a07 Do not forget to reset the busy state 2016-02-10 16:41:32 +01:00
Johannes Zellner a8001995c8 Add business logic for group adding 2016-02-10 16:37:58 +01:00
Johannes Zellner 9ba4d52fb7 Add Client.createGroup() 2016-02-10 16:37:46 +01:00
Johannes Zellner 0e613a1cab Ensure focus 2016-02-10 16:25:38 +01:00
Johannes Zellner cf3d503a74 Add group add form 2016-02-10 16:24:25 +01:00
Johannes Zellner 1ab46a96f9 Reset the user edit form password error 2016-02-10 15:18:36 +01:00
Johannes Zellner 1a3164ef32 Add ui components for group management 2016-02-10 15:15:09 +01:00
Johannes Zellner bd62efcff5 That api is of course a PUT api 2016-02-10 15:01:51 +01:00
Johannes Zellner 7fc37b7c70 Allow admins to edit other users 2016-02-10 14:48:54 +01:00
Johannes Zellner 8ddccae15a Save the groups on user edit 2016-02-10 14:47:49 +01:00
Johannes Zellner 675d7c8730 Add Client.setGroups() 2016-02-10 14:47:35 +01:00
Johannes Zellner ba35d4a313 remove unused class 2016-02-10 14:40:29 +01:00
Johannes Zellner c1280ddcc2 Make group buttons toggle able 2016-02-10 14:39:49 +01:00
Johannes Zellner 36ded4c06a Group -> Groups 2016-02-10 14:26:17 +01:00
Johannes Zellner 9fb276019e Add ui elements for group selection 2016-02-10 14:26:04 +01:00
Johannes Zellner 19982b1815 Add Client.getGroups() 2016-02-10 14:25:08 +01:00
Johannes Zellner 459d5b8f60 Adjust user action buttons 2016-02-10 14:07:37 +01:00
Johannes Zellner 8ba5dc2352 Fix indentation 2016-02-10 13:55:49 +01:00
Johannes Zellner 8c73a7c7c2 Send admin flag with user profile 2016-02-10 13:35:16 +01:00
Johannes Zellner e78dd41e88 Replace deprecated gulp-minify-css with gulp-cssnano 2016-02-10 13:13:08 +01:00
Johannes Zellner 59ecb056d0 Fixup the oauth tests to set memoryLimit 2016-02-10 12:49:02 +01:00
Johannes Zellner 11b17fec3a Ensure groupMembers table is created first 2016-02-10 12:38:00 +01:00
Johannes Zellner 5ea81d0fd3 Ensure default minimum memory limit 2016-02-10 12:30:19 +01:00
Johannes Zellner 19cbd1f394 Ensure we never go below 256mb memoryLimit 2016-02-10 12:30:19 +01:00
Johannes Zellner 1b7265f866 Fixup the merge 2016-02-10 12:28:57 +01:00
Johannes Zellner 1cdb64e78d Use memoryLimit from app object instead of manifest in docker.js 2016-02-10 12:25:26 +01:00
Johannes Zellner eec8708249 Set memory limit on app installation to the default or the one specified in the manifest 2016-02-10 12:25:26 +01:00
Johannes Zellner ab003bf81f adjust appdb.js and unit tests to support memoryLimit 2016-02-10 12:25:26 +01:00
Johannes Zellner 2d60901b6e memoryLimit has to be BIGINT 2016-02-10 12:25:26 +01:00
Johannes Zellner 3fc9bde4f4 Make memoryLimit step in 8s 2016-02-10 12:25:26 +01:00
Johannes Zellner 4fc0df31fe Add apps.memoryLimit 2016-02-10 12:25:26 +01:00
Girish Ramakrishnan 3ac326e766 try upto 5 minutes to download the tarball
DO/S3 can be really slow at times
2016-02-09 21:07:03 -08:00
Girish Ramakrishnan 4770f9ddf6 add hasAccessTo tests 2016-02-09 21:07:03 -08:00
girish@cloudron.io 7e60fd554a add some failing groups for good measure 2016-02-09 18:55:42 -08:00
girish@cloudron.io c1cd7ac129 fix typo 2016-02-09 18:53:14 -08:00
girish@cloudron.io aab62263a7 add accessRestriction group test in oauth2 2016-02-09 18:52:27 -08:00
girish@cloudron.io 79889a0aac test simple auth accessRestriction 2016-02-09 18:40:20 -08:00
Girish Ramakrishnan f413bfb3a0 Add route to set the users groups 2016-02-09 16:43:32 -08:00
Girish Ramakrishnan 2b0791f4a3 rename vars 2016-02-09 16:19:00 -08:00
Girish Ramakrishnan d95339534f rename test file 2016-02-09 16:17:01 -08:00
Girish Ramakrishnan 82cf667f3b Add groups route tests 2016-02-09 15:26:46 -08:00
girish@cloudron.io e20b3f75e4 Handle NOT_EMPTY group deletion 2016-02-09 13:45:28 -08:00
girish@cloudron.io 6cca7b3e0e initial rest API for groups 2016-02-09 13:34:36 -08:00
girish@cloudron.io 0b814af206 Add api to get groups 2016-02-09 13:33:30 -08:00
girish@cloudron.io bfdabf9272 check groups property in accessRestriction 2016-02-09 13:03:52 -08:00
girish@cloudron.io 60988ff7f3 make hasAccessTo take a callback 2016-02-09 12:48:21 -08:00
girish@cloudron.io 3649fd0c31 drop the gid: prefix for group id
like the username, id and name is same in groups.
2016-02-09 12:28:50 -08:00
girish@cloudron.io 00c5aa041f clear database in backups test 2016-02-09 12:21:36 -08:00
girish@cloudron.io 4569b67007 make users and admins groups as reserved 2016-02-09 12:16:30 -08:00
girish@cloudron.io 1fb26bc441 make startAppTask take a callback 2016-02-09 12:14:04 -08:00
girish@cloudron.io e6d23a9701 stop previous task explicitly
there is a race:
1. task is running
2. new task is created overwriting the installationState
3. new task kills the old task of step 1. this results in installationState getting overwritten by 'error' because of the sigkill
4. new task that is launched loses the installationState that was step in 2.
2016-02-09 12:09:20 -08:00
girish@cloudron.io 0785266741 kill immediately. by default, it sends SIGTERM 2016-02-09 12:03:21 -08:00
girish@cloudron.io e752949752 make all tests work after group changes 2016-02-09 11:29:32 -08:00
girish@cloudron.io 199eb2b3e1 set the admin flag in user object 2016-02-09 09:25:17 -08:00
Girish Ramakrishnan 49cbea93fb fix ldap test 2016-02-09 08:52:16 -08:00
girish@cloudron.io 451c410547 make user test pass 2016-02-08 21:17:21 -08:00
girish@cloudron.io f6541720c4 pass owner flag in createUser 2016-02-08 21:05:02 -08:00
girish@cloudron.io 5e5435e869 send email for userAded 2016-02-08 20:51:20 -08:00
girish@cloudron.io 0d4f113d7d add groupIds to user object 2016-02-08 20:38:50 -08:00
girish@cloudron.io 14fab0992f make user test mostly work 2016-02-08 16:53:20 -08:00
girish@cloudron.io d7eb004bc1 remove admin arg from user.create 2016-02-08 16:36:45 -08:00
girish@cloudron.io c34f3ee653 null invitor is ok 2016-02-08 16:36:26 -08:00
girish@cloudron.io 96d595de39 fix database test 2016-02-08 16:25:29 -08:00
girish@cloudron.io b1f4508313 remove admin references from userdb 2016-02-08 16:18:51 -08:00
girish@cloudron.io 52ce59faaf createUser does not take admin anymore 2016-02-08 16:14:43 -08:00
girish@cloudron.io 85085ae0b2 implement getAllAdmins based on groups 2016-02-08 16:10:44 -08:00
girish@cloudron.io c14cf9c260 migrate admin flag to group membership 2016-02-08 16:07:44 -08:00
girish@cloudron.io a47c6f0774 make requires alphabetical 2016-02-08 15:17:54 -08:00
girish@cloudron.io 888955bd9b add groups.isMember 2016-02-08 10:53:01 -08:00
girish@cloudron.io 6abf5e2c44 group members: add/remove/get 2016-02-08 10:48:21 -08:00
girish@cloudron.io b1935c3550 restrict group names 2016-02-08 09:41:25 -08:00
girish@cloudron.io e39d7750c5 add group membership table 2016-02-08 08:55:37 -08:00
girish@cloudron.io 1d83a48a1a make group id distinct from name 2016-02-08 08:44:18 -08:00
Girish Ramakrishnan 802ee6c456 more group tests 2016-02-07 20:49:55 -08:00
Girish Ramakrishnan 278085ba22 initial tests for adding group 2016-02-07 20:34:05 -08:00
Girish Ramakrishnan b945a8a04c add groups model and db code 2016-02-07 20:25:08 -08:00
Girish Ramakrishnan 7ef92071c5 add groups table 2016-02-07 20:11:37 -08:00
girish@cloudron.io c16ab95193 reword CLI message 2016-02-04 23:13:29 -08:00
Girish Ramakrishnan c5e2d9a9cc download new app image as the first thing in update
this will reduce downtime.
2016-02-04 22:49:22 -08:00
Girish Ramakrishnan 07df76b25e Change developer mode text to cli 2016-02-04 22:49:22 -08:00
girish@cloudron.io 5b264565db set the target for cloudron links 2016-02-04 18:22:53 -08:00
girish@cloudron.io a3561bd040 Make Cloudron a link and fix copyright 2016-02-04 17:47:31 -08:00
girish@cloudron.io 6e4f47e807 0.8.1 changes 2016-02-04 15:20:40 -08:00
Johannes Zellner 471965dc66 Add initial still disabled slider to app configure 2016-02-04 16:45:00 +01:00
Johannes Zellner 3b109ea2e7 Include bootstrap-slider in our angular app 2016-02-04 16:44:40 +01:00
Johannes Zellner 6011526d5e Add bootstrap-slider assets 2016-02-04 16:44:03 +01:00
Johannes Zellner 1395d2971b Send app update changelog with email
Fixes #579
2016-02-04 15:31:40 +01:00
Johannes Zellner e9d6badae7 Changelog is just a flat text, no array 2016-02-04 15:17:18 +01:00
Johannes Zellner 65ddc7f24c Show changelog in app update ui 2016-02-04 15:17:18 +01:00
girish@cloudron.io fa871c7ada some apache configs require Host header to be set 2016-02-03 20:18:59 -08:00
girish@cloudron.io 8652d6c136 Add 0.8.0 changes 2016-02-02 08:51:13 -08:00
girish@cloudron.io 16d976a145 use multidb version of mysql addon 2016-02-02 08:46:09 -08:00
girish@cloudron.io fa1f5cc454 call the multi methods if multipleDatabases is set 2016-02-02 08:41:41 -08:00
Johannes Zellner 84c3b367d5 Actually wait for apps to be stopped 2016-01-29 17:24:18 +01:00
Johannes Zellner 793aa6512d Rework parts of the apps tests to be more reliable
No need to create and tear down the addons everytime.
Docker proxy can also be just injected once.
Wait explicitly for apptasks to be terminated.
2016-01-29 16:27:37 +01:00
Johannes Zellner 98ab99ab34 Add callback to config._reset() for convenience 2016-01-29 16:27:04 +01:00
Johannes Zellner 24a826bdd1 Reduce from 20 to 10 sec wait for addons
Since that is reliable already with 10 on my laptop
I assume this is fine for any other more modern machine
2016-01-29 16:26:15 +01:00
Johannes Zellner 05245f5fc7 Add a way to wait and stop pending tasks 2016-01-29 16:25:31 +01:00
Johannes Zellner b718c8d044 Cleanup config before and after apps tests 2016-01-29 14:30:40 +01:00
Johannes Zellner 2888a85081 Remove cloudron side migrate api
This is some old api when we had a migrate view in the webadmin
This is entirely handled on the appstore for now.
2016-01-29 14:17:33 +01:00
Johannes Zellner 307262244a Also cleanup the config file on config._reset() 2016-01-29 14:17:10 +01:00
Johannes Zellner 9a875634f8 Improve error message in mailer 2016-01-29 12:40:34 +01:00
Johannes Zellner 4af33486ae Add missing fi in shell script 2016-01-29 12:31:59 +01:00
Johannes Zellner befa898f18 Do not show app version, but make it available as a tooltip on the title for us 2016-01-29 12:29:35 +01:00
Johannes Zellner 18525e1236 Show app website in appstore install dialog
Fixes #580
2016-01-29 12:26:42 +01:00
Johannes Zellner 28ffd01cf4 Invoke BACKUP_APP_CMD with the additional backupConfig.url 2016-01-29 11:55:52 +01:00
Johannes Zellner 09c7aa4440 Remove whitespace 2016-01-29 11:54:58 +01:00
Johannes Zellner ea4862d351 Strictly assert on app 2016-01-29 11:54:40 +01:00
Johannes Zellner 3e4d62329e Also copy the backup config json 2016-01-29 11:54:15 +01:00
Johannes Zellner d12366576b Add backup config url to backupapp.sh 2016-01-29 11:44:24 +01:00
Johannes Zellner 7b1d906494 Add backups.getAppBackupConfigUrl() 2016-01-29 11:44:24 +01:00
Johannes Zellner 0972c88b8b Set the correct environment for the installer.sh 2016-01-28 16:36:34 +01:00
girish@cloudron.io 9464a26a7e put version in the mail 2016-01-27 10:35:48 -08:00
Johannes Zellner 10f1ad5cfe Until we can distinguish by the return data, mark both fields red 2016-01-26 19:44:33 +01:00
Johannes Zellner fd11eb8da0 Ensure the user add form is reset on show 2016-01-26 19:40:57 +01:00
girish@cloudron.io 62d5e99802 Version 0.7.2 changes 2016-01-26 10:34:09 -08:00
girish@cloudron.io 48305f0e95 call retire after sending off the http response 2016-01-26 10:29:32 -08:00
girish@cloudron.io 8170b490f2 add retire.sh
this is a sudo script that retires the box
2016-01-26 09:37:25 -08:00
girish@cloudron.io 072962bbc3 add internal route to retire 2016-01-26 08:46:48 -08:00
girish@cloudron.io 33bc1cf7d9 Add cloudron.retire 2016-01-26 08:39:42 -08:00
Johannes Zellner 85df9d1472 We use 'wrong password' now everywhere 2016-01-26 16:09:14 +01:00
Johannes Zellner 109ba3bf56 Do not perform any client side password validation in user delete form 2016-01-26 16:06:42 +01:00
Johannes Zellner 8083362e71 Ensure we enforce the password pattern for setup view 2016-01-26 15:55:02 +01:00
Johannes Zellner 9b4c385a64 Ensure we send proper password requirements on password reset 2016-01-26 15:21:03 +01:00
girish@cloudron.io ee9c8ba4eb fix dead comment 2016-01-25 20:15:54 -08:00
girish@cloudron.io 000a64d54a remove installer.retire 2016-01-25 17:52:35 -08:00
girish@cloudron.io eba74d77a6 remove installer certs
the installer is no more a public server
2016-01-25 17:47:57 -08:00
girish@cloudron.io 714a1bcb1d installer: remove provisioning server
the box will retire itself
2016-01-25 17:46:24 -08:00
girish@cloudron.io 02d17dc2e4 fix typo 2016-01-25 16:51:14 -08:00
girish@cloudron.io 4b54e776cc reset update info after short-circuit 2016-01-25 16:46:54 -08:00
girish@cloudron.io ba6f05b119 Clear the progress for web ui to work 2016-01-25 16:35:03 -08:00
girish@cloudron.io 1d9ae120dc strip prerelease from box version and not the released version 2016-01-25 16:27:22 -08:00
girish@cloudron.io 3ce841e050 add test for checkDiskSpace 2016-01-25 16:03:12 -08:00
girish@cloudron.io 436fc2ba13 checking capacity does not work well for /
Just check if we have around 1.25GB left.

/root (data image file) is 26G
/home (our code) is 1G
/apps.swap is 2G
/backup.swap is 1G
/var is 6.2G (docker)

Rest of the system is 1.5G

Fixes #577
2016-01-25 15:56:46 -08:00
girish@cloudron.io 77d652fc2b add test for config.setVersion 2016-01-25 15:12:22 -08:00
girish@cloudron.io ac3681296e short-circuit updates from prerelease to release version
Fixes #575
2016-01-25 14:48:12 -08:00
girish@cloudron.io 5254d3325f add comment on fields on box update info object 2016-01-25 13:57:59 -08:00
girish@cloudron.io ce0a24a95d comment out public graphite paths 2016-01-25 12:51:37 -08:00
Johannes Zellner 1bb596bf58 Add changes for 0.7.1 2016-01-25 16:40:57 +01:00
Johannes Zellner c384ac6080 Use local cache when looking up apps 2016-01-25 16:25:25 +01:00
Johannes Zellner 61c2ce0f47 Keep appstore url bar up-to-date with the selected app
This allows sharing links directly

Fixes #526
2016-01-25 16:21:44 +01:00
Johannes Zellner 7a71315d33 Adjust angular's to allow non reload path changes 2016-01-25 16:21:20 +01:00
Johannes Zellner 0a658e5862 Ensure the focus is setup correctly 2016-01-25 15:30:29 +01:00
Johannes Zellner 5f8c99aa0e There is no body here 2016-01-25 15:29:52 +01:00
Johannes Zellner 4c6f1e4b4a Allow admins or users to operate on themselves 2016-01-25 15:29:52 +01:00
Johannes Zellner 226ae627f9 Move updateUser() to where it belongs 2016-01-25 15:29:52 +01:00
Johannes Zellner 27a02aa918 Make user edit form submit with enter 2016-01-25 15:29:52 +01:00
Johannes Zellner 3c43503df8 Add businesslogic for user edit form 2016-01-25 14:58:12 +01:00
Johannes Zellner 35c926d504 Ensure we actually update the correct user, not the user holding the token 2016-01-25 14:58:02 +01:00
Johannes Zellner ea18ca5c60 Fix copy and paste error in the user add form 2016-01-25 14:46:51 +01:00
Johannes Zellner 55a56355d5 Add email change fields to user edit form 2016-01-25 14:28:47 +01:00
Johannes Zellner dc83ba2686 Require displayName in updateUser() 2016-01-25 14:26:42 +01:00
Johannes Zellner 62615dfd0f Make email in user change optional 2016-01-25 14:12:09 +01:00
Johannes Zellner a6998550a7 Add displayName change unit tests 2016-01-25 14:08:35 +01:00
Johannes Zellner 3b199170be Support changing the displayName 2016-01-25 14:08:11 +01:00
Johannes Zellner 1f93787a63 Also send displayName for users 2016-01-25 13:36:51 +01:00
Johannes Zellner 199c5b926a Show tooltip for user action buttons 2016-01-25 13:31:49 +01:00
Johannes Zellner d9ad7085c3 Shorten the user action buttons 2016-01-25 13:28:48 +01:00
Johannes Zellner df12f31800 Add ui components for user edit 2016-01-25 13:28:30 +01:00
Johannes Zellner ad205da3db Match any loop back device
This is just a quick fix and will potentially match all
loop* devices.
2016-01-25 13:06:38 +01:00
Johannes Zellner 34aab65db3 Use the first part of the dn to get the common name in ldap
It is no must to have the first part named 'cn' but the first
part is always the id we want to verify
2016-01-25 11:31:57 +01:00
Johannes Zellner 63c06a508e Make /api available on just the IP
We might want to also show something else than
the naked domain placeholder page when just
accessing the ip
2016-01-24 12:08:10 +01:00
Girish Ramakrishnan a2899c9c65 add appupdate tests 2016-01-24 00:44:46 -08:00
Girish Ramakrishnan ff6d5e9efc complete the box update tests 2016-01-24 00:06:32 -08:00
Girish Ramakrishnan f48fe0a7c0 Get started with updatechecker tests 2016-01-23 22:38:46 -08:00
Girish Ramakrishnan 5f6c8ca520 fix crash 2016-01-23 13:37:21 -08:00
Girish Ramakrishnan 0eaa3a8d94 clear update info so that we use the latest settings 2016-01-23 11:08:01 -08:00
Girish Ramakrishnan 8ad190fa83 make updateConfig a provision argument 2016-01-23 10:15:09 -08:00
girish@cloudron.io 70f096c820 check settingsdb whether to notify for app prerelease
fixes #573
2016-01-23 05:56:08 -08:00
girish@cloudron.io 2840251862 check if updateInfo is null earlier 2016-01-23 05:37:22 -08:00
girish@cloudron.io b43966df22 code without callback is hard to read 2016-01-23 05:35:57 -08:00
girish@cloudron.io cc22285beb Check settingsdb whether to notify for prereleases
part of #573
2016-01-23 05:33:16 -08:00
girish@cloudron.io b72d48b49f set default update config 2016-01-23 05:07:12 -08:00
girish@cloudron.io 3a6b9c23c6 settings: add update config 2016-01-23 05:06:09 -08:00
girish@cloudron.io b2da364345 fix typo in comment 2016-01-22 17:58:38 -08:00
girish@cloudron.io de7a6abc50 Check for out of disk space
Fixes #567
2016-01-22 17:46:23 -08:00
girish@cloudron.io 10f74349ca collectd: disable vmem plugin 2016-01-22 15:44:46 -08:00
girish@cloudron.io 05a771c365 collectd: disable process plugin 2016-01-22 15:43:47 -08:00
girish@cloudron.io cfa2089d7b collectd: Remove ping metric 2016-01-22 15:36:13 -08:00
girish@cloudron.io d56abd94a9 collectd uses the data lo partition that is resized by box-setup.sh 2016-01-22 15:06:43 -08:00
girish@cloudron.io 2f20ff8def use loop1 instead of loop0
box-setup.sh always resizes the loopback partition on reboot. This
means that it always be loop1.
2016-01-22 15:03:23 -08:00
girish@cloudron.io 9706daf330 Just track ext4 and btrfs file systems 2016-01-22 14:33:02 -08:00
girish@cloudron.io a246b3e90c box-setup needs to run after mounting to prevent race in script 2016-01-22 14:21:36 -08:00
girish@cloudron.io e28e1b239f fix comment 2016-01-22 14:21:20 -08:00
girish@cloudron.io 4aead483de This hack is not needed in 15.10 anymore
collectd is still same version in 15.10 but it collects info correctly
as df-vda1 now.
2016-01-22 14:00:40 -08:00
girish@cloudron.io f8cc6e471e 0.7.0 changes 2016-01-22 11:02:04 -08:00
girish@cloudron.io 6b9ed9472d upgrade to 15.10 2016-01-22 10:46:13 -08:00
girish@cloudron.io a763b08c41 pin packages
fixes #558
2016-01-22 10:46:13 -08:00
Johannes Zellner 178f904143 Put installer.sh location in README 2016-01-22 13:03:31 +01:00
Girish Ramakrishnan bb88fa3620 Restart node processes if journald crashes
Note that we cannot simply ignore EPIPE in the node programs.
Doing so results in no logs anymore :-( This is supposedly
fixed in systemd 228.

Fixes #550
2016-01-21 22:13:19 -08:00
Girish Ramakrishnan 1e1249d8e0 Give journald more time to sync
Part of #550
2016-01-21 21:43:49 -08:00
girish@cloudron.io bcb0e61bfc Kill child processes
On Unix, child processes are not killed when parent dies.

Each process is part of a process group (pgid). When pgid == pid,
it is the process group leader.

node creates child processes with the parent as the group leader
(detached = false).

You can send a signal to entire group using kill(-pgid), as in,
negative value in argument. Systemd can be made to do this by
setting the KillMode=control-group.

Unrelated: Process groups reside inside session groups. Each session
group has a controlling terminal. Only one process in the session
group has access to the terminal. Process group is basically like
a bash pipeline. A session group is the entire login session with only
one process having terminal access at a time.

Fixes #543
2016-01-21 17:44:17 -08:00
girish@cloudron.io 022ff89836 Add 0.6.6 changes 2016-01-21 15:57:22 -08:00
girish@cloudron.io b9d4b8f6e8 Remove docker images by tag
docker pull previously used to pull down all tags.
    docker pull tag1 # might pull down tag2, tag3 if they are all same!
    docker rm tag1 # oops, tag2 and tag3 still hold on to the image

However, the above feature was not possible with registry v2 (some
technical stuff to do with each tag being separately signed). As a result,
this feature was removed from v1 as well - https://github.com/docker/docker/pull/10571

This means we can now do:
    docker pull tag1 # nice
    docker rm tag1 # image goes away if no one else is using it

references:
https://github.com/docker/docker/issues/8689
https://github.com/docker/docker/pull/8193 (added this feature to v1)
https://github.com/docker/docker/issues/8141 (the request)
https://github.com/docker/docker/pull/10571 (removes the v1 feature)

Fixes #563
2016-01-21 15:53:51 -08:00
158 changed files with 10052 additions and 4127 deletions
+86
View File
@@ -382,3 +382,89 @@
[0.6.5]
- Finalize stronger password requirement
[0.7.0]
- Upgrade to 15.10
- Do not remove docker images when in use by another container
- Fix sporadic error when reconfiguring apps
- Handle journald crashes gracefully
[0.7.1]
- Allow admins to edit users
- Fix graphs
- Support more LDAP cases
- Allow appstore deep linking
[0.7.2]
- Fix 5xx errors when password does not meet requirements
- Improved box update management using prereleases
- Less aggressive disk space checks
[0.8.0]
- MySQL addon : multiple database support
[0.8.1]
- Set Host HTTP header when querying healthCheckPath
- Show application Changelog in app update emails
[0.9.0]
- Fix bug in multdb mysql addon backup
- Add initial user group support
- Improved app memory limit handling
[0.9.1]
- Introduce per app group access control
[0.9.2]
- Fix bug where reconfiguring apps would trigger memory limit warning
- Allow more apps to be installed in bigger sized cloudrons
- Allow user to override memory limit warning and install anyway
[0.9.3]
- Admin flag is handled outside of groups
- User interface fixes for groups
- Allow to set access restrictions on app installation
[0.10.0]
- Upgrade to docker 1.10.2
- Fix MySQL addon to handle heavier loads
- Allow listing and download of backups (using the CLI tool)
- Ubuntu security updates till 8th March 2016 (http://www.ubuntu.com/usn)
[0.10.1]
- Fix Let's Encrypt certificate renewal
[0.10.2]
- Apps can now bind with username or email with LDAP
- Disallow updating an app with mismatching manifest id
- Use admin domain instead of naked domain in the SPF record
- Download Lets Encrypt intermediate cert
[0.10.3]
- Store the backup config for each backup. This will allow using multiple buckets/providers for backups simultaneously.
- Fix SPF record check
[0.10.4]
- Fix restore for droplets in EU region
[0.11.0]
- Store backups in the same region as the Cloudron
- Fix PCRE security issue (http://www.ubuntu.com/usn/usn-2943-1/)
[0.11.1]
- Improve the backup logic
[0.11.2]
- Allow users to choose a username on first sign up
- Fix app graphs
[0.12.0]
- Fix upload of large backups
- Postgres addon whitelists pg_trgm and hstore extensions
- Suppress boring update emails from patch releases
- Setup bounce alerts for emails
- Query admin's name in activation wizard
- Admin emails are now delivered as no-reply
- Fix crash when user attempts to set a duplicate email
- Improved mongodb crash recovery
+2 -1
View File
@@ -11,6 +11,7 @@ The smart server currently relies on an AWS account with access to Route53 and S
First create a virtual private server with Ubuntu 15.04 and run the following commands in an ssh session to initialize the base image:
```
TODO curl from a well known released version of installer.sh
curl https://s3.amazonaws.com/prod-cloudron-releases/installer.sh -o installer.sh
chmod +x installer.sh
./installer.sh <domain> <aws access key> <aws acccess secret> <backup bucket> <provider> <release sha1>
```
-6
View File
@@ -150,12 +150,6 @@ if ! $ssh22 "root@${server_ip}" "/bin/bash /root/initializeBaseUbuntuImage.sh ${
exit 1
fi
echo "Copy over certs"
cd "${SCRIPT_DIR}/../../secrets"
blackbox_cat installer/server.crt.gpg | $ssh202 "root@${server_ip}" "cat - > /home/yellowtent/installer/src/certs/server.crt"
blackbox_cat installer/server.key.gpg | $ssh202 "root@${server_ip}" "cat - > /home/yellowtent/installer/src/certs/server.key"
blackbox_cat installer_ca/ca.crt.gpg | $ssh202 "root@${server_ip}" "cat - > /home/yellowtent/installer/src/certs/ca.crt"
echo "Shutting down server with id : ${server_id}"
$ssh202 "root@${server_ip}" "shutdown -f now" || true # shutdown sometimes terminates ssh connection immediately making this command fail
+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-04-x64" # id=12658446
local ubuntu_image_slug="ubuntu-15-10-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}"
+29 -11
View File
@@ -12,6 +12,13 @@ readonly USER_DATA_DIR="/home/yellowtent/data"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
function die {
echo $1
exit 1
}
[[ "$(systemd --version 2>&1)" == *"systemd 225"* ]] || die "Expecting systemd to be 225"
if [ -f "${SOURCE_DIR}/INFRA_VERSION" ]; then
source "${SOURCE_DIR}/INFRA_VERSION"
else
@@ -42,7 +49,7 @@ export DEBIAN_FRONTEND=noninteractive
echo "=== Upgrade ==="
apt-get update
apt-get upgrade -y
apt-get dist-upgrade -y
apt-get install -y curl
# Setup firewall before everything. docker creates it's own chain and the -X below will remove it
@@ -87,7 +94,8 @@ apt-get -y install btrfs-tools
echo "==== Install docker ===="
# install docker from binary to pin it to a specific version. the current debian repo does not allow pinning
curl https://get.docker.com/builds/Linux/x86_64/docker-1.9.1 > /usr/bin/docker
curl https://get.docker.com/builds/Linux/x86_64/docker-1.10.2 > /usr/bin/docker
apt-get -y install aufs-tools
chmod +x /usr/bin/docker
groupadd docker
cat > /etc/systemd/system/docker.socket <<EOF
@@ -121,11 +129,11 @@ LimitCORE=infinity
WantedBy=multi-user.target
EOF
echo "=== Setup btrfs docker data ==="
fallocate -l "8192m" "${USER_DATA_FILE}" # 8gb start
echo "=== Setup btrfs data ==="
truncate -s "8192m" "${USER_DATA_FILE}" # 8gb start (this will get resized dynamically by box-setup.service)
mkfs.btrfs -L UserHome "${USER_DATA_FILE}"
echo "${USER_DATA_FILE} ${USER_DATA_DIR} btrfs loop,nosuid 0 0" >> /etc/fstab
mkdir -p "${USER_DATA_DIR}" && mount "${USER_DATA_FILE}"
mkdir -p "${USER_DATA_DIR}"
mount -t btrfs -o loop,nosuid "${USER_DATA_FILE}" ${USER_DATA_DIR}
systemctl daemon-reload
systemctl enable docker
@@ -175,18 +183,19 @@ 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"
echo "==== Install build-essential ===="
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"
echo "==== Install pwgen ===="
apt-get -y install pwgen
echo "==== Install pwgen and swaks awscli ===="
apt-get -y install pwgen swaks awscli
echo "==== Install collectd ==="
if ! apt-get install -y collectd collectd-utils; then
@@ -208,6 +217,7 @@ curl -sL https://nodejs.org/dist/v4.1.1/node-v4.1.1-linux-x64.tar.gz | tar zxvf
ln -s /usr/local/node-4.1.1/bin/node /usr/bin/node
ln -s /usr/local/node-4.1.1/bin/npm /usr/bin/npm
apt-get install -y python # Install python which is required for npm rebuild
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
echo "=== Rebuilding npm packages ==="
cd "${INSTALLER_SOURCE_DIR}" && npm install --production
@@ -222,12 +232,15 @@ fi
cat > /etc/systemd/system/cloudron-installer.service <<EOF
[Unit]
Description=Cloudron Installer
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
BindsTo=systemd-journald.service
[Service]
Type=idle
ExecStart="${INSTALLER_SOURCE_DIR}/src/server.js"
Environment="DEBUG=installer*,connect-lastmile" ${provisionEnv}
KillMode=process
; kill any child (installer.sh) as well
KillMode=control-group
Restart=on-failure
[Install]
@@ -256,7 +269,7 @@ echo "==== Install box-setup systemd script ===="
cat > /etc/systemd/system/box-setup.service <<EOF
[Unit]
Description=Box Setup
Before=docker.service
Before=docker.service collectd.service mysql.service
After=do-resize.service
[Service]
@@ -278,6 +291,11 @@ sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
-i /etc/systemd/journald.conf
# When rotating logs, systemd kills journald too soon sometimes
# See https://github.com/systemd/systemd/issues/1353 (this is upstream default)
sed -e "s/^WatchdogSec=.*$/WatchdogSec=3min/" \
-i /lib/systemd/system/systemd-journald.service
sync
# Configure time
+3 -2
View File
@@ -10,7 +10,7 @@ var ejs = require('gulp-ejs'),
serve = require('gulp-serve'),
sass = require('gulp-sass'),
sourcemaps = require('gulp-sourcemaps'),
minifyCSS = require('gulp-minify-css'),
cssnano = require('gulp-cssnano'),
autoprefixer = require('gulp-autoprefixer'),
argv = require('yargs').argv;
@@ -22,6 +22,7 @@ gulp.task('3rdparty', function () {
'webadmin/src/3rdparty/**/*.otf',
'webadmin/src/3rdparty/**/*.eot',
'webadmin/src/3rdparty/**/*.svg',
'webadmin/src/3rdparty/**/*.gif',
'webadmin/src/3rdparty/**/*.ttf',
'webadmin/src/3rdparty/**/*.woff',
'webadmin/src/3rdparty/**/*.woff2'
@@ -119,7 +120,7 @@ gulp.task('css', function () {
.pipe(sourcemaps.init())
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
.pipe(autoprefixer())
.pipe(minifyCSS())
.pipe(cssnano())
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist'))
.pipe(gulp.dest('setup/splash/website'));
+10 -5
View File
@@ -20,10 +20,15 @@ readonly provider="${5}"
readonly revision="${6}"
# environment specific urls
readonly api_server_origin="https://api.dev.cloudron.io"
readonly web_server_origin="https://dev.cloudron.io"
readonly release_bucket_url="https://s3.amazonaws.com/dev-cloudron-releases"
readonly versions_url="https://s3.amazonaws.com/dev-cloudron-releases/versions.json"
<% if (env === 'prod') { %>
readonly api_server_origin="https://api.cloudron.io"
readonly web_server_origin="https://cloudron.io"
<% } else { %>
readonly api_server_origin="https://api.<%= env %>.cloudron.io"
readonly web_server_origin="https://<%= env %>.cloudron.io"
<% } %>
readonly release_bucket_url="https://s3.amazonaws.com/<%= env %>-cloudron-releases"
readonly versions_url="https://s3.amazonaws.com/<%= env %>-cloudron-releases/versions.json"
readonly installer_code_url="${release_bucket_url}/box-${revision}.tar.gz"
# runtime consts
@@ -132,7 +137,7 @@ cat > /root/provision.json <<EOF
"secretAccessKey": "${aws_access_key_secret}"
},
"tlsConfig": {
"provider": "letsencrypt-dev"
"provider": "letsencrypt-<%= env %>"
}
}
}
+1 -17
View File
@@ -15,13 +15,11 @@ exports = module.exports = {
InstallerError: InstallerError,
provision: provision,
retire: retire,
_ensureVersion: ensureVersion
};
var INSTALLER_CMD = path.join(__dirname, 'scripts/installer.sh'),
RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh'),
SUDO = '/usr/bin/sudo';
function InstallerError(reason, info) {
@@ -36,6 +34,7 @@ util.inherits(InstallerError, Error);
InstallerError.INTERNAL_ERROR = 1;
InstallerError.ALREADY_PROVISIONED = 2;
// system until file has KillMode=control-group to bring down child processes
function spawn(tag, cmd, args, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof cmd, 'string');
@@ -62,21 +61,6 @@ function spawn(tag, cmd, args, callback) {
});
}
function retire(args, callback) {
assert.strictEqual(typeof args, 'object');
assert.strictEqual(typeof callback, 'function');
var pargs = [ RETIRE_CMD ];
pargs.push('--data', JSON.stringify(args.data));
debug('retire: calling with args %j', pargs);
if (process.env.NODE_ENV === 'test') return callback(null);
// sudo is required for retire()
spawn('retire', SUDO, pargs, callback);
}
function ensureVersion(args, callback) {
assert.strictEqual(typeof args, 'object');
assert.strictEqual(typeof callback, 'function');
+1 -1
View File
@@ -7,7 +7,7 @@ readonly DATA_DIR=/home/yellowtent/data
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly json="${script_dir}/../../node_modules/.bin/json"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 180"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 300"
readonly is_update=$([[ -d "${BOX_SRC_DIR}" ]] && echo "yes" || echo "no")
-30
View File
@@ -1,30 +0,0 @@
#!/bin/bash
# This script is called once at the end of a cloudrons lifetime
set -eu -o pipefail
readonly BOX_SRC_DIR=/home/yellowtent/box
arg_data=""
args=$(getopt -o "" -l "data:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--data) arg_data="$2";;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
shift 2
done
echo "Setting up splash screen"
"${BOX_SRC_DIR}/setup/splashpage.sh" --retire --data "${arg_data}" # show splash
"${BOX_SRC_DIR}/setup/stop.sh" # stop the cloudron code
systemctl stop docker # stop the apps
systemctl stop cloudron-installer # stop the installer
+2 -71
View File
@@ -11,13 +11,11 @@ var assert = require('assert'),
fs = require('fs'),
http = require('http'),
HttpError = require('connect-lastmile').HttpError,
https = require('https'),
HttpSuccess = require('connect-lastmile').HttpSuccess,
installer = require('./installer.js'),
json = require('body-parser').json,
lastMile = require('connect-lastmile'),
morgan = require('morgan'),
path = require('path'),
superagent = require('superagent');
exports = module.exports = {
@@ -28,8 +26,7 @@ exports = module.exports = {
var PROVISION_CONFIG_FILE = '/root/provision.json';
var CLOUDRON_CONFIG_FILE = '/home/yellowtent/configs/cloudron.conf';
var gHttpsServer = null, // provision server; used for install/restore
gHttpServer = null; // update server; used for updates
var gHttpServer = null; // update server; used for updates
function provisionDigitalOcean(callback) {
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) return callback(null); // already provisioned
@@ -74,23 +71,6 @@ function update(req, res, next) {
next(new HttpSuccess(202, { }));
}
function retire(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.data || typeof req.body.data !== 'object') return next(new HttpError(400, 'No data provided'));
if (typeof req.body.data.tlsCert !== 'string') console.error('No TLS cert provided');
if (typeof req.body.data.tlsKey !== 'string') console.error('No TLS key provided');
debug('retire: received from appstore %j', req.body);
installer.retire(req.body, function (error) {
if (error) console.error(error);
});
next(new HttpSuccess(202, {}));
}
function startUpdateServer(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -114,53 +94,6 @@ function startUpdateServer(callback) {
gHttpServer.listen(2020, '127.0.0.1', callback);
}
function startProvisionServer(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Starting provision server');
var app = express();
var router = new express.Router();
if (process.env.NODE_ENV !== 'test') app.use(morgan('dev', { immediate: false }));
app.use(json({ strict: true }))
.use(router)
.use(lastMile());
router.post('/api/v1/installer/retire', retire);
var caPath = path.join(__dirname, process.env.NODE_ENV === 'test' ? 'test/certs' : 'certs');
var certPath = path.join(__dirname, process.env.NODE_ENV === 'test' ? 'test/certs' : 'certs');
var options = {
key: fs.readFileSync(path.join(certPath, 'server.key')),
cert: fs.readFileSync(path.join(certPath, 'server.crt')),
ca: fs.readFileSync(path.join(caPath, 'ca.crt')),
// request cert from client and only allow from our CA
requestCert: true,
rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0' // this is set in the tests
};
gHttpsServer = https.createServer(options, app);
gHttpsServer.on('error', console.error);
gHttpsServer.listen(process.env.NODE_ENV === 'test' ? 4443 : 886, '0.0.0.0', callback);
}
function stopProvisionServer(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Stopping provision server');
if (!gHttpsServer) return callback(null);
gHttpsServer.close(callback);
gHttpsServer = null;
}
function stopUpdateServer(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -189,7 +122,6 @@ function start(callback) {
actions = [
startUpdateServer,
startProvisionServer,
provisionDigitalOcean
];
}
@@ -201,8 +133,7 @@ function stop(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
stopUpdateServer,
stopProvisionServer
stopUpdateServer
], callback);
}
-24
View File
@@ -1,24 +0,0 @@
-----BEGIN CERTIFICATE-----
MIID9zCCAt+gAwIBAgIJAMPL81PAySGAMA0GCSqGSIb3DQEBBQUAMFoxCzAJBgNV
BAYTAlVTMQswCQYDVQQIEwJDQTELMAkGA1UEBxMCU0MxFTATBgNVBAoTDENsb3Vk
cm9uIEluYzEaMBgGA1UEAxMRSW5zdGFsbCBTZXJ2ZXIgQ0EwHhcNMTUwMTE2MDEy
NDM2WhcNMTYwMTE2MDEyNDM2WjBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex
CzAJBgNVBAcTAlNDMRUwEwYDVQQKEwxDbG91ZHJvbiBJbmMxGjAYBgNVBAMTEUlu
c3RhbGwgU2VydmVyIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
31TkOEC3JXtieHiZgM5qWw771rV2JEDKs1C68+n/OmKrp3zAQV08A+w/KVurn1P9
gZlYF+CBRVZDV8lYbWzc6PgMPWEDHHV72FS5Kq6ZyikB+r5OQJ8qU61y840h6ZCD
MEYr6N9qXm9wSApJBQ/key/pg7+95B2CFYRrg5NVstIYqpJ1lyxCMFTrjYAmteOB
Bi/4GPApu9Tj0ifTMbZFGTPtWm/yhCZ6Anm6w+ok9tDMpPC6kRgUJ3B4HY75D9dV
aWSls9jdZw4JU1jIFlAdUjhGEEmHWOzAD8vBjvuBqcf9NQwvieWG5tDYfZ6DYRC2
/aG1C5UWhFLDv2/F+56k3wIDAQABo4G/MIG8MB0GA1UdDgQWBBQ088hd2sIIqVtw
xJeAkCORdclFRjCBjAYDVR0jBIGEMIGBgBQ088hd2sIIqVtwxJeAkCORdclFRqFe
pFwwWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMQswCQYDVQQHEwJTQzEVMBMG
A1UEChMMQ2xvdWRyb24gSW5jMRowGAYDVQQDExFJbnN0YWxsIFNlcnZlciBDQYIJ
AMPL81PAySGAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJcW+Wmz
/o0JBC2WsMjUjxVrzOiu9bdKQ1yn83Zcv74zEfmWfJotVOK1oKsTyOZfTvvWrpLc
GXXhh4oXWsNnFII3uJyZIY3v/DoE0pa7TCZhLYFbL2kEaC5rTwe/+VScHy5ROOiu
+gnzOU3MyrcMTT0v4qcT0NlkIptRdvIYNpqfXO6vG9sMp4C/NwWhl/IfHkIAv0eH
l3HTr8wxgldCjxbnJgYkyUcWAmLi2YEXKCEPWmsfqp3Z+Ng1M+A9OKjJLHWowl9X
4arvn6WaUbZjRxxjvK199If1R6KWwD6YQ9cKH4Ex4/hhIqg5I3MQFu+pOq/b0XH/
9I10o6FVU7vcFkQ=
-----END CERTIFICATE-----
-20
View File
@@ -1,20 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDMDCCAhgCCQCDr1HQJBr1izANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJV
UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAlNDMREwDwYDVQQKDAhDbG91ZHJvbjEe
MBwGA1UEAwwVaW5zdGFsbGVyLmNsb3Vkcm9uLmlvMB4XDTE1MTExNjIzMTcwMloX
DTE2MTExNTIzMTcwMlowWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYD
VQQHDAJTQzERMA8GA1UECgwIQ2xvdWRyb24xHjAcBgNVBAMMFWluc3RhbGxlci5j
bG91ZHJvbi5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK0suQX7
hKBhYsSH0msnEPVbRDIotYbtVDav/v7Sb/fRU7qVoL31tj2iZRDJRJ27uRM3J4ye
6hgJAAwQGtfXrcVZY3SOAlGXsFZF0wgBCw0pGtgF3HA1BcwbCwAd06J6w3lKActA
DMEUio/jRXpYELUU2Nzopq0MsMyyBSBkNC18i0HUB8vkF8yQvb1OpbcxERbpf3D5
zjeFf5kIE/k8lwBz1vMF0uAA2GfcXxs3dyDaxVteWeevVYZzAoY9EcUyBWX7OQnx
aUygl3OywN+xOJKXKCQpckzDvr9Vp1sKItoMMy5y81SyNhZIMBYGGG+oNp/wSgQf
Cht+LupI+bXoYrMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAgPHZx52qYuEUdzVO
t/+VXO7dxJkONYU8sjTYIfJme8ZZd7beZBMUni5s2gvv6i5HFyJ2Ol88sv8hAaI/
6Vmbszml+5tLyPK8Gygk62l6OcKDwU/yazTxxCApulNy1SV34kzruXUMZ28ybcqA
XJywMMx4RDmSIBXPdDCeaOgYwI7Wk56obJ8sa2+Z6100GNoX+qBSOsWMMJW+ohnp
eQWHkTOJzU4hIMfZCbW0cF5Xn/35xEh0xxaH7XWglJLM9neBPba+Ydz7567mN9co
vgv2dE5ZOKSjG63CtUvv819dvbWVKq8jiMCqPGRcr1iSeqbC02tnx0W762980uSx
QfOgAw==
-----END CERTIFICATE-----
-27
View File
@@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEArSy5BfuEoGFixIfSaycQ9VtEMii1hu1UNq/+/tJv99FTupWg
vfW2PaJlEMlEnbu5EzcnjJ7qGAkADBAa19etxVljdI4CUZewVkXTCAELDSka2AXc
cDUFzBsLAB3TonrDeUoBy0AMwRSKj+NFelgQtRTY3OimrQywzLIFIGQ0LXyLQdQH
y+QXzJC9vU6ltzERFul/cPnON4V/mQgT+TyXAHPW8wXS4ADYZ9xfGzd3INrFW15Z
569VhnMChj0RxTIFZfs5CfFpTKCXc7LA37E4kpcoJClyTMO+v1WnWwoi2gwzLnLz
VLI2FkgwFgYYb6g2n/BKBB8KG34u6kj5tehiswIDAQABAoIBAGNAQ5bbLYsh5ZKP
6ZhCHqUQtsgsrsVzFhX1zqbLgyK8VUmV4jedMOKoRVZWlD32zj7mGIOuvKoj1mQT
gt78HPsDnU266jdLQeRgRm/K8UOMsHbo/QtOSFFPmoFpltcDly7XrKmJvwWWOUf4
UOSqvoCaPyR1Lrn1kQrwaKHE7Ga4jfyOrIq9JI7y/ih+Y7D8xcMnyLAsjyVkSAtr
+XrGNHcx3yPuBmjaOglzeb6Ksdpt4ETElrvH3ByT5EV2zUVr9Txv+m8xSVBZfea9
aE7lWSQoOUz+e6RhIX3Df/QfR6KkDblAwEF9Se98DWcz46Y34oc2E0lSoJYpoPxP
vbRlfDkCgYEA3nAc8kDRkbQObSfnVjpijBSP5hfr3jX+XTbxK7Y3aTMViY+87iWK
bLNuX+2JRCmRjk0wy2YXnJQV3sU/EO5gLhOz9060MIHgFISq4KRgPorN/EFWryOe
mDzhPIuhZLMetv0ajS3Z5IxIAs+FLu7Yx9em80q540UA3kXsFWe2lpUCgYEAx03E
kk5zLirVFtoyP/yAES+KVppqBweCUA5vVxB8H26oIhi8G8kT4b77x6wXxQzdsA4H
a4ou3ZBZVK41PREgG1MWgzpbwk49T1FX6TLtvdhr/9QhYC+RIynynA/pA36LSKT5
pvWegYB4+9jaPrQ5L1zcrLF2XlTsgpuC43kXKicCgYA0dXxeJatHEY/VbnPAgkR7
hN3rBfk6jsFOeoamKHMo/EM4Dg4gm/npaOe+9+ZHjQYm6U14qrsm0kXWI+6br5w/
QaZPzN/yEK8oJ6GlGR8ZoOKzezVWWLAudy0neka12QiFX2vDn+yjWfIht49RYkL9
3n4hIp50WvG5egQTiEIngQKBgCn9yJzKypm/jIX0EwJIQPNeANeeURiKDHqxj+PY
JU66EdKdQ4TXKMk3Y/T93UQ3Ib4mNooB4z3rW+brjWwAX7NiHiwn741QzroXeV44
zL5jCt4r45xQaVPvUp5u+7kwwEfd+nui5HKEjvkBB3qOnj3MYvI/saDOY8Zg3YLv
0GGhAoGANBwFcDgwP9KDt0NxKXhe3rlSUyfGSSUF89hZPrLDCiaGFURD/w4j3EGr
Ui9Rcwm2ymqlFzTO4JYKy1/pRCWA7GDfslICJPOPG3Wytsjog0WymQuMjYC2tL/+
RwD0qG0/aBGE4PbigPRoJ/7BGZLKtdy99P0wyFC3o6OBoAl3Zqo=
-----END RSA PRIVATE KEY-----
-40
View File
@@ -109,46 +109,6 @@ describe('Server', function () {
});
});
describe('retire', function () {
var data = {
data: {
tlsKey: 'key',
tlsCert: 'cert'
}
};
before(function (done) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // TODO: use a installer ca signed cert instead
server.start(done);
});
after(function (done) {
server.stop(done);
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
});
Object.keys(data).forEach(function (key) {
it('fails due to missing ' + key, function (done) {
var dataCopy = _.merge({ }, data);
delete dataCopy[key];
request.post(EXTERNAL_SERVER_URL + '/api/v1/installer/retire').send(dataCopy).end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
});
it('succeeds', function (done) {
request.post(EXTERNAL_SERVER_URL + '/api/v1/installer/retire').send(data).end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(202);
done();
});
});
});
describe('ensureVersion', function () {
before(function () {
process.env.NODE_ENV = undefined;
+8 -19
View File
@@ -4,7 +4,6 @@ set -eu -o pipefail
readonly USER_HOME="/home/yellowtent"
readonly APPS_SWAP_FILE="/apps.swap"
readonly BACKUP_SWAP_FILE="/backup.swap" # used when doing app backups
readonly USER_DATA_FILE="/root/user_data.img"
readonly USER_DATA_DIR="/home/yellowtent/data"
@@ -19,12 +18,10 @@ fi
# all sizes are in mb
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
readonly swap_size="${physical_memory}"
readonly swap_size="${physical_memory}" # if you change this, fix enoughResourcesAvailable() in client.js
readonly app_count=$((${physical_memory} / 200)) # estimated app count
readonly disk_size_gb=$(fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ print $3 }')
readonly disk_size=$((disk_size_gb * 1024))
readonly backup_swap_size=1024
# readonly system_size=5120 # 5 gigs for system libs, installer, box code and tmp
readonly system_size=10240 # 10 gigs for system libs, apps images, installer, box code and tmp
readonly ext4_reserved=$((disk_size * 5 / 100)) # this can be changes using tune2fs -m percent /dev/vda1
@@ -33,8 +30,7 @@ echo "Physical memory: ${physical_memory}"
echo "Estimated app count: ${app_count}"
echo "Disk size: ${disk_size}"
# Allocate two sets of swap files - one for general app usage and another for backup
# The backup swap is setup for swap on the fly by the backup scripts
# Allocate swap for general app usage
if [[ ! -f "${APPS_SWAP_FILE}" ]]; then
echo "Creating Apps swap file of size ${swap_size}M"
fallocate -l "${swap_size}m" "${APPS_SWAP_FILE}"
@@ -46,20 +42,13 @@ else
echo "Apps Swap file already exists"
fi
if [[ ! -f "${BACKUP_SWAP_FILE}" ]]; then
echo "Creating Backup swap file of size ${backup_swap_size}M"
fallocate -l "${backup_swap_size}m" "${BACKUP_SWAP_FILE}"
chmod 600 "${BACKUP_SWAP_FILE}"
mkswap "${BACKUP_SWAP_FILE}"
else
echo "Backups Swap file already exists"
fi
echo "Resizing data volume"
home_data_size=$((disk_size - system_size - swap_size - backup_swap_size - ext4_reserved))
home_data_size=$((disk_size - system_size - swap_size - ext4_reserved))
echo "Resizing up btrfs user data to size ${home_data_size}M"
umount "${USER_DATA_DIR}"
fallocate -l "${home_data_size}m" "${USER_DATA_FILE}" # does not overwrite existing data
mount "${USER_DATA_FILE}"
umount "${USER_DATA_DIR}" || true
# Do not preallocate (non-sparse). Doing so overallocates for data too much in advance and causes problems when using many apps with smaller data
# fallocate -l "${home_data_size}m" "${USER_DATA_FILE}" # does not overwrite existing data
truncate -s "${home_data_size}m" "${USER_DATA_FILE}" # this will shrink it if the file had existed. this is useful when running this script on a live system
mount -t btrfs -o loop,nosuid "${USER_DATA_FILE}" ${USER_DATA_DIR}
btrfs filesystem resize max "${USER_DATA_DIR}"
@@ -0,0 +1,15 @@
dbm = dbm || require('db-migrate');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN memoryLimit BIGINT DEFAULT 0', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN memoryLimit', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,21 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = "CREATE TABLE groups(" +
"id VARCHAR(128) NOT NULL UNIQUE," +
"name VARCHAR(128) NOT NULL UNIQUE," +
"PRIMARY KEY(id))";
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE groups', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,22 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = "CREATE TABLE IF NOT EXISTS groupMembers(" +
"groupId VARCHAR(128) NOT NULL," +
"userId VARCHAR(128) NOT NULL," +
"FOREIGN KEY(groupId) REFERENCES groups(id)," +
"FOREIGN KEY(userId) REFERENCES users(id));";
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE groupMembers', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,30 @@
'use strict';
var dbm = global.dbm || require('db-migrate');
var async = require('async');
var ADMIN_GROUP_ID = 'admin'; // see groups.js
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'INSERT INTO groups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
function migrateAdminFlag(done) {
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
if (error) return done(error);
console.dir(results);
async.eachSeries(results, function (r, next) {
db.runSql('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ ADMIN_GROUP_ID, r.id ], next);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE users DROP COLUMN admin'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,25 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = "CREATE TABLE backups(" +
"filename VARCHAR(128) NOT NULL," +
"creationTime TIMESTAMP," +
"version VARCHAR(128) NOT NULL," +
"type VARCHAR(16) NOT NULL," +
"dependsOn VARCHAR(4096)," +
"state VARCHAR(16) NOT NULL," +
"PRIMARY KEY (filename))";
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE backups', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups ADD COLUMN configJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups DROP COLUMN configJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
var dbm = dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups DROP COLUMN configJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups ADD COLUMN configJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,16 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups CHANGE filename id VARCHAR(128)', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups CHANGE id filename VARCHAR(128)', [], 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 MODIFY username VARCHAR(254) UNIQUE', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users MODIFY username VARCHAR(254) NOT NULL UNIQUE', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
+22 -1
View File
@@ -11,7 +11,7 @@
CREATE TABLE IF NOT EXISTS users(
id VARCHAR(128) NOT NULL UNIQUE,
username VARCHAR(254) NOT NULL UNIQUE,
username VARCHAR(254) UNIQUE,
email VARCHAR(254) NOT NULL UNIQUE,
password VARCHAR(1024) NOT NULL,
salt VARCHAR(512) NOT NULL,
@@ -21,6 +21,17 @@ CREATE TABLE IF NOT EXISTS users(
displayName VARCHAR(512) DEFAULT '',
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groups(
id VARCHAR(128) NOT NULL UNIQUE,
username VARCHAR(254) NOT NULL UNIQUE,
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groupMembers(
groupId VARCHAR(128) NOT NULL,
userId VARCHAR(128) NOT NULL,
FOREIGN KEY(groupId) REFERENCES groups(id),
FOREIGN KEY(userId) REFERENCES users(id));
CREATE TABLE IF NOT EXISTS tokens(
accessToken VARCHAR(128) NOT NULL UNIQUE,
identifier VARCHAR(128) NOT NULL,
@@ -53,6 +64,7 @@ CREATE TABLE IF NOT EXISTS apps(
accessRestrictionJson TEXT,
oauthProxy BOOLEAN DEFAULT 0,
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
memoryLimit BIGINT DEFAULT 0,
lastBackupId VARCHAR(128),
lastBackupConfigJson TEXT, // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
@@ -86,3 +98,12 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
value VARCHAR(512) NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL,
creationTime TIMESTAMP,
version VARCHAR(128) NOT NULL, /* app version or box version */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
dependsOn VARCHAR(4096), /* comma separate list of objects this backup depends on */
state VARCHAR(16) NOT NULL,
PRIMARY KEY (filename));
+239 -218
View File
@@ -3,9 +3,9 @@
"version": "0.0.1",
"dependencies": {
"async": {
"version": "1.5.0",
"from": "https://registry.npmjs.org/async/-/async-1.5.0.tgz",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.0.tgz"
"version": "1.5.2",
"from": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
},
"attempt": {
"version": "1.0.1",
@@ -13,37 +13,39 @@
"resolved": "http://registry.npmjs.org/attempt/-/attempt-1.0.1.tgz"
},
"aws-sdk": {
"version": "2.2.22",
"from": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.2.22.tgz",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.2.22.tgz",
"version": "2.2.35",
"from": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.2.35.tgz",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.2.35.tgz",
"dependencies": {
"sax": {
"version": "0.5.3",
"from": "https://registry.npmjs.org/sax/-/sax-0.5.3.tgz",
"resolved": "https://registry.npmjs.org/sax/-/sax-0.5.3.tgz"
"version": "1.1.5",
"from": "https://registry.npmjs.org/sax/-/sax-1.1.5.tgz",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.1.5.tgz"
},
"xml2js": {
"version": "0.2.8",
"from": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz"
"version": "0.4.16",
"from": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.16.tgz",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.16.tgz"
},
"xmlbuilder": {
"version": "0.4.2",
"from": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz"
"version": "4.2.1",
"from": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz",
"dependencies": {
"lodash": {
"version": "4.3.0",
"from": "https://registry.npmjs.org/lodash/-/lodash-4.3.0.tgz",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.3.0.tgz"
}
}
}
}
},
"body-parser": {
"version": "1.14.1",
"from": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.1.tgz",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.1.tgz",
"version": "1.15.0",
"from": "https://registry.npmjs.org/body-parser/-/body-parser-1.15.0.tgz",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.15.0.tgz",
"dependencies": {
"bytes": {
"version": "2.1.0",
"from": "https://registry.npmjs.org/bytes/-/bytes-2.1.0.tgz",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-2.1.0.tgz"
},
"content-type": {
"version": "1.0.1",
"from": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz",
@@ -55,13 +57,13 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz"
},
"http-errors": {
"version": "1.3.1",
"from": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz",
"version": "1.4.0",
"from": "https://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz",
"dependencies": {
"inherits": {
"version": "2.0.1",
"from": "inherits@>=2.0.1 <2.1.0",
"from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"statuses": {
@@ -72,9 +74,9 @@
}
},
"iconv-lite": {
"version": "0.4.12",
"from": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.12.tgz",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.12.tgz"
"version": "0.4.13",
"from": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz"
},
"on-finished": {
"version": "2.3.0",
@@ -89,25 +91,15 @@
}
},
"qs": {
"version": "5.1.0",
"from": "https://registry.npmjs.org/qs/-/qs-5.1.0.tgz",
"resolved": "https://registry.npmjs.org/qs/-/qs-5.1.0.tgz"
"version": "6.1.0",
"from": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz"
},
"raw-body": {
"version": "2.1.5",
"from": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.5.tgz",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.5.tgz",
"dependencies": {
"bytes": {
"version": "2.2.0",
"from": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz"
},
"iconv-lite": {
"version": "0.4.13",
"from": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz"
},
"unpipe": {
"version": "1.0.0",
"from": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -116,24 +108,24 @@
}
},
"type-is": {
"version": "1.6.10",
"from": "https://registry.npmjs.org/type-is/-/type-is-1.6.10.tgz",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.10.tgz",
"version": "1.6.11",
"from": "https://registry.npmjs.org/type-is/-/type-is-1.6.11.tgz",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.11.tgz",
"dependencies": {
"media-typer": {
"version": "0.3.0",
"from": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
"from": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
},
"mime-types": {
"version": "2.1.8",
"from": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.8.tgz",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.8.tgz",
"version": "2.1.9",
"from": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.9.tgz",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.9.tgz",
"dependencies": {
"mime-db": {
"version": "1.20.0",
"from": "https://registry.npmjs.org/mime-db/-/mime-db-1.20.0.tgz",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.20.0.tgz"
"version": "1.21.0",
"from": "https://registry.npmjs.org/mime-db/-/mime-db-1.21.0.tgz",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.21.0.tgz"
}
}
}
@@ -147,28 +139,28 @@
"resolved": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz"
},
"cloudron-manifestformat": {
"version": "2.2.0",
"from": "cloudron-manifestformat@2.2.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.2.0.tgz",
"version": "2.3.0",
"from": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.3.0.tgz",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.3.0.tgz",
"dependencies": {
"java-packagename-regex": {
"version": "1.0.0",
"from": "java-packagename-regex@>=1.0.0 <2.0.0",
"from": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz"
},
"safetydance": {
"version": "0.0.15",
"from": "safetydance@0.0.15",
"from": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.15.tgz",
"resolved": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.15.tgz"
},
"tv4": {
"version": "1.2.7",
"from": "tv4@>=1.1.9 <2.0.0",
"from": "https://registry.npmjs.org/tv4/-/tv4-1.2.7.tgz",
"resolved": "https://registry.npmjs.org/tv4/-/tv4-1.2.7.tgz"
},
"validator": {
"version": "3.43.0",
"from": "validator@>=3.34.0 <4.0.0",
"from": "https://registry.npmjs.org/validator/-/validator-3.43.0.tgz",
"resolved": "https://registry.npmjs.org/validator/-/validator-3.43.0.tgz"
}
}
@@ -209,7 +201,7 @@
"dependencies": {
"inherits": {
"version": "2.0.1",
"from": "inherits@>=2.0.0 <3.0.0",
"from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"statuses": {
@@ -244,14 +236,14 @@
}
},
"cookie-parser": {
"version": "1.4.0",
"from": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.0.tgz",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.0.tgz",
"version": "1.4.1",
"from": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.1.tgz",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.1.tgz",
"dependencies": {
"cookie": {
"version": "0.2.2",
"from": "https://registry.npmjs.org/cookie/-/cookie-0.2.2.tgz",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.2.2.tgz"
"version": "0.2.3",
"from": "https://registry.npmjs.org/cookie/-/cookie-0.2.3.tgz",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.2.3.tgz"
},
"cookie-signature": {
"version": "1.0.6",
@@ -295,9 +287,9 @@
"resolved": "http://registry.npmjs.org/moment-timezone/-/moment-timezone-0.3.1.tgz",
"dependencies": {
"moment": {
"version": "2.10.6",
"from": "https://registry.npmjs.org/moment/-/moment-2.10.6.tgz",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.10.6.tgz"
"version": "2.11.2",
"from": "https://registry.npmjs.org/moment/-/moment-2.11.2.tgz",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.11.2.tgz"
}
}
}
@@ -319,9 +311,9 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
},
"csrf": {
"version": "3.0.0",
"from": "https://registry.npmjs.org/csrf/-/csrf-3.0.0.tgz",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.0.tgz",
"version": "3.0.1",
"from": "https://registry.npmjs.org/csrf/-/csrf-3.0.1.tgz",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.1.tgz",
"dependencies": {
"base64-url": {
"version": "1.2.1",
@@ -329,9 +321,9 @@
"resolved": "https://registry.npmjs.org/base64-url/-/base64-url-1.2.1.tgz"
},
"rndm": {
"version": "1.1.1",
"from": "https://registry.npmjs.org/rndm/-/rndm-1.1.1.tgz",
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.1.1.tgz"
"version": "1.2.0",
"from": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz"
},
"scmp": {
"version": "1.0.0",
@@ -339,9 +331,16 @@
"resolved": "https://registry.npmjs.org/scmp/-/scmp-1.0.0.tgz"
},
"uid-safe": {
"version": "2.0.0",
"from": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.0.0.tgz",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.0.0.tgz"
"version": "2.1.0",
"from": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.0.tgz",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.0.tgz",
"dependencies": {
"random-bytes": {
"version": "1.0.0",
"from": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz"
}
}
}
}
},
@@ -352,7 +351,7 @@
"dependencies": {
"inherits": {
"version": "2.0.1",
"from": "inherits@>=2.0.0 <3.0.0",
"from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"statuses": {
@@ -414,9 +413,9 @@
"resolved": "http://registry.npmjs.org/moment/-/moment-2.9.0.tgz"
},
"mongodb": {
"version": "1.4.39",
"from": "https://registry.npmjs.org/mongodb/-/mongodb-1.4.39.tgz",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-1.4.39.tgz",
"version": "1.4.40",
"from": "https://registry.npmjs.org/mongodb/-/mongodb-1.4.40.tgz",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-1.4.40.tgz",
"dependencies": {
"bson": {
"version": "0.2.22",
@@ -443,9 +442,9 @@
}
},
"readable-stream": {
"version": "2.0.4",
"from": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.4.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.4.tgz",
"version": "2.0.5",
"from": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.5.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.5.tgz",
"dependencies": {
"core-util-is": {
"version": "1.0.2",
@@ -1350,14 +1349,14 @@
}
},
"dockerode": {
"version": "2.2.7",
"from": "https://registry.npmjs.org/dockerode/-/dockerode-2.2.7.tgz",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.2.7.tgz",
"version": "2.2.9",
"from": "https://registry.npmjs.org/dockerode/-/dockerode-2.2.9.tgz",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.2.9.tgz",
"dependencies": {
"docker-modem": {
"version": "0.2.8",
"from": "https://registry.npmjs.org/docker-modem/-/docker-modem-0.2.8.tgz",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-0.2.8.tgz",
"version": "0.3.0",
"from": "https://registry.npmjs.org/docker-modem/-/docker-modem-0.3.0.tgz",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-0.3.0.tgz",
"dependencies": {
"JSONStream": {
"version": "0.10.0",
@@ -1386,11 +1385,6 @@
"from": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.3.tgz",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.3.tgz"
},
"querystring": {
"version": "0.2.0",
"from": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz"
},
"readable-stream": {
"version": "1.0.33",
"from": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz",
@@ -1428,9 +1422,9 @@
}
},
"ejs": {
"version": "2.3.4",
"from": "https://registry.npmjs.org/ejs/-/ejs-2.3.4.tgz",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.3.4.tgz"
"version": "2.4.1",
"from": "https://registry.npmjs.org/ejs/-/ejs-2.4.1.tgz",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.4.1.tgz"
},
"ejs-cli": {
"version": "1.0.1",
@@ -1496,9 +1490,9 @@
}
},
"express": {
"version": "4.13.3",
"from": "https://registry.npmjs.org/express/-/express-4.13.3.tgz",
"resolved": "https://registry.npmjs.org/express/-/express-4.13.3.tgz",
"version": "4.13.4",
"from": "https://registry.npmjs.org/express/-/express-4.13.4.tgz",
"resolved": "https://registry.npmjs.org/express/-/express-4.13.4.tgz",
"dependencies": {
"accepts": {
"version": "1.2.13",
@@ -1506,14 +1500,14 @@
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz",
"dependencies": {
"mime-types": {
"version": "2.1.8",
"from": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.8.tgz",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.8.tgz",
"version": "2.1.9",
"from": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.9.tgz",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.9.tgz",
"dependencies": {
"mime-db": {
"version": "1.20.0",
"from": "https://registry.npmjs.org/mime-db/-/mime-db-1.20.0.tgz",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.20.0.tgz"
"version": "1.21.0",
"from": "https://registry.npmjs.org/mime-db/-/mime-db-1.21.0.tgz",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.21.0.tgz"
}
}
},
@@ -1530,9 +1524,9 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz"
},
"content-disposition": {
"version": "0.5.0",
"from": "http://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz",
"resolved": "http://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz"
"version": "0.5.1",
"from": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.1.tgz",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.1.tgz"
},
"content-type": {
"version": "1.0.1",
@@ -1540,9 +1534,9 @@
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz"
},
"cookie": {
"version": "0.1.3",
"from": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz"
"version": "0.1.5",
"from": "https://registry.npmjs.org/cookie/-/cookie-0.1.5.tgz",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.5.tgz"
},
"cookie-signature": {
"version": "1.0.6",
@@ -1550,14 +1544,14 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
},
"depd": {
"version": "1.0.1",
"from": "http://registry.npmjs.org/depd/-/depd-1.0.1.tgz",
"resolved": "http://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
"version": "1.1.0",
"from": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz"
},
"escape-html": {
"version": "1.0.2",
"from": "http://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz",
"resolved": "http://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz"
"version": "1.0.3",
"from": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz"
},
"etag": {
"version": "1.7.0",
@@ -1565,9 +1559,9 @@
"resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz"
},
"finalhandler": {
"version": "0.4.0",
"from": "http://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz",
"resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz",
"version": "0.4.1",
"from": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.1.tgz",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.1.tgz",
"dependencies": {
"unpipe": {
"version": "1.0.0",
@@ -1582,14 +1576,14 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.3.0.tgz"
},
"merge-descriptors": {
"version": "1.0.0",
"from": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz"
"version": "1.0.1",
"from": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz"
},
"methods": {
"version": "1.1.1",
"from": "https://registry.npmjs.org/methods/-/methods-1.1.1.tgz",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.1.tgz"
"version": "1.1.2",
"from": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
},
"on-finished": {
"version": "2.3.0",
@@ -1604,9 +1598,9 @@
}
},
"parseurl": {
"version": "1.3.0",
"from": "http://registry.npmjs.org/parseurl/-/parseurl-1.3.0.tgz",
"resolved": "http://registry.npmjs.org/parseurl/-/parseurl-1.3.0.tgz"
"version": "1.3.1",
"from": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz"
},
"path-to-regexp": {
"version": "0.1.7",
@@ -1641,14 +1635,14 @@
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz"
},
"send": {
"version": "0.13.0",
"from": "http://registry.npmjs.org/send/-/send-0.13.0.tgz",
"resolved": "http://registry.npmjs.org/send/-/send-0.13.0.tgz",
"version": "0.13.1",
"from": "https://registry.npmjs.org/send/-/send-0.13.1.tgz",
"resolved": "https://registry.npmjs.org/send/-/send-0.13.1.tgz",
"dependencies": {
"destroy": {
"version": "1.0.3",
"from": "http://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz",
"resolved": "http://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz"
"version": "1.0.4",
"from": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz"
},
"http-errors": {
"version": "1.3.1",
@@ -1675,29 +1669,29 @@
}
},
"serve-static": {
"version": "1.10.0",
"from": "http://registry.npmjs.org/serve-static/-/serve-static-1.10.0.tgz",
"resolved": "http://registry.npmjs.org/serve-static/-/serve-static-1.10.0.tgz"
"version": "1.10.2",
"from": "https://registry.npmjs.org/serve-static/-/serve-static-1.10.2.tgz",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.10.2.tgz"
},
"type-is": {
"version": "1.6.10",
"from": "https://registry.npmjs.org/type-is/-/type-is-1.6.10.tgz",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.10.tgz",
"version": "1.6.11",
"from": "https://registry.npmjs.org/type-is/-/type-is-1.6.11.tgz",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.11.tgz",
"dependencies": {
"media-typer": {
"version": "0.3.0",
"from": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
"from": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
},
"mime-types": {
"version": "2.1.8",
"from": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.8.tgz",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.8.tgz",
"version": "2.1.9",
"from": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.9.tgz",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.9.tgz",
"dependencies": {
"mime-db": {
"version": "1.20.0",
"from": "https://registry.npmjs.org/mime-db/-/mime-db-1.20.0.tgz",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.20.0.tgz"
"version": "1.21.0",
"from": "https://registry.npmjs.org/mime-db/-/mime-db-1.21.0.tgz",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.21.0.tgz"
}
}
}
@@ -1716,9 +1710,9 @@
}
},
"express-session": {
"version": "1.12.1",
"from": "https://registry.npmjs.org/express-session/-/express-session-1.12.1.tgz",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.12.1.tgz",
"version": "1.13.0",
"from": "https://registry.npmjs.org/express-session/-/express-session-1.13.0.tgz",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.13.0.tgz",
"dependencies": {
"cookie": {
"version": "0.2.3",
@@ -1731,9 +1725,9 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
},
"crc": {
"version": "3.3.0",
"from": "https://registry.npmjs.org/crc/-/crc-3.3.0.tgz",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.3.0.tgz"
"version": "3.4.0",
"from": "https://registry.npmjs.org/crc/-/crc-3.4.0.tgz",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.4.0.tgz"
},
"depd": {
"version": "1.1.0",
@@ -1746,9 +1740,9 @@
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz"
},
"parseurl": {
"version": "1.3.0",
"from": "http://registry.npmjs.org/parseurl/-/parseurl-1.3.0.tgz",
"resolved": "http://registry.npmjs.org/parseurl/-/parseurl-1.3.0.tgz"
"version": "1.3.1",
"from": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz"
},
"uid-safe": {
"version": "2.0.0",
@@ -1942,14 +1936,14 @@
}
},
"mysql": {
"version": "2.9.0",
"from": "https://registry.npmjs.org/mysql/-/mysql-2.9.0.tgz",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.9.0.tgz",
"version": "2.10.2",
"from": "https://registry.npmjs.org/mysql/-/mysql-2.10.2.tgz",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.10.2.tgz",
"dependencies": {
"bignumber.js": {
"version": "2.0.7",
"from": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.0.7.tgz"
"version": "2.1.4",
"from": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.1.4.tgz",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.1.4.tgz"
},
"readable-stream": {
"version": "1.1.13",
@@ -2013,10 +2007,15 @@
"resolved": "http://registry.npmjs.org/buffercursor/-/buffercursor-0.0.12.tgz",
"dependencies": {
"verror": {
"version": "1.6.0",
"from": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
"version": "1.6.1",
"from": "https://registry.npmjs.org/verror/-/verror-1.6.1.tgz",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.1.tgz",
"dependencies": {
"core-util-is": {
"version": "1.0.2",
"from": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
},
"extsprintf": {
"version": "1.2.0",
"from": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz",
@@ -2030,15 +2029,20 @@
}
}
},
"node-df": {
"version": "0.1.1",
"from": "https://registry.npmjs.org/node-df/-/node-df-0.1.1.tgz",
"resolved": "https://registry.npmjs.org/node-df/-/node-df-0.1.1.tgz"
},
"node-uuid": {
"version": "1.4.7",
"from": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz",
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz"
},
"nodemailer": {
"version": "1.10.0",
"from": "https://registry.npmjs.org/nodemailer/-/nodemailer-1.10.0.tgz",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-1.10.0.tgz",
"version": "1.11.0",
"from": "https://registry.npmjs.org/nodemailer/-/nodemailer-1.11.0.tgz",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-1.11.0.tgz",
"dependencies": {
"libmime": {
"version": "1.2.0",
@@ -2121,18 +2125,18 @@
"resolved": "https://registry.npmjs.org/nodemailer-direct-transport/-/nodemailer-direct-transport-1.1.0.tgz",
"dependencies": {
"smtp-connection": {
"version": "1.3.1",
"from": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-1.3.1.tgz",
"resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-1.3.1.tgz"
"version": "1.3.8",
"from": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-1.3.8.tgz",
"resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-1.3.8.tgz"
}
}
}
}
},
"nodemailer-smtp-transport": {
"version": "1.0.4",
"from": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-1.0.4.tgz",
"resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-1.0.4.tgz",
"version": "1.1.0",
"from": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-1.1.0.tgz",
"dependencies": {
"clone": {
"version": "1.0.2",
@@ -2145,16 +2149,16 @@
"resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.7.tgz"
},
"smtp-connection": {
"version": "1.3.1",
"from": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-1.3.1.tgz",
"resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-1.3.1.tgz"
"version": "1.3.8",
"from": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-1.3.8.tgz",
"resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-1.3.8.tgz"
}
}
},
"oauth2orize": {
"version": "1.2.0",
"from": "https://registry.npmjs.org/oauth2orize/-/oauth2orize-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/oauth2orize/-/oauth2orize-1.2.0.tgz",
"version": "1.2.2",
"from": "https://registry.npmjs.org/oauth2orize/-/oauth2orize-1.2.2.tgz",
"resolved": "https://registry.npmjs.org/oauth2orize/-/oauth2orize-1.2.2.tgz",
"dependencies": {
"uid2": {
"version": "0.0.3",
@@ -2180,6 +2184,11 @@
}
}
},
"parse-links": {
"version": "0.1.0",
"from": "parse-links@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/parse-links/-/parse-links-0.1.0.tgz"
},
"passport": {
"version": "0.2.2",
"from": "https://registry.npmjs.org/passport/-/passport-0.2.2.tgz",
@@ -2287,9 +2296,9 @@
"resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.13.1.tgz"
},
"safetydance": {
"version": "0.1.0",
"from": "https://registry.npmjs.org/safetydance/-/safetydance-0.1.0.tgz",
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.1.0.tgz"
"version": "0.1.1",
"from": "https://registry.npmjs.org/safetydance/-/safetydance-0.1.1.tgz",
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.1.1.tgz"
},
"semver": {
"version": "4.3.6",
@@ -2317,9 +2326,9 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
},
"parseurl": {
"version": "1.3.0",
"from": "http://registry.npmjs.org/parseurl/-/parseurl-1.3.0.tgz",
"resolved": "http://registry.npmjs.org/parseurl/-/parseurl-1.3.0.tgz"
"version": "1.3.1",
"from": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz"
}
}
},
@@ -2336,9 +2345,9 @@
}
},
"superagent": {
"version": "1.5.0",
"from": "https://registry.npmjs.org/superagent/-/superagent-1.5.0.tgz",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-1.5.0.tgz",
"version": "1.7.2",
"from": "https://registry.npmjs.org/superagent/-/superagent-1.7.2.tgz",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-1.7.2.tgz",
"dependencies": {
"qs": {
"version": "2.3.3",
@@ -2346,24 +2355,24 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz"
},
"formidable": {
"version": "1.0.14",
"from": "https://registry.npmjs.org/formidable/-/formidable-1.0.14.tgz",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.14.tgz"
"version": "1.0.17",
"from": "https://registry.npmjs.org/formidable/-/formidable-1.0.17.tgz",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.17.tgz"
},
"component-emitter": {
"version": "1.1.2",
"from": "http://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz",
"resolved": "http://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz"
"version": "1.2.0",
"from": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz"
},
"methods": {
"version": "1.0.1",
"from": "https://registry.npmjs.org/methods/-/methods-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.0.1.tgz"
"version": "1.1.2",
"from": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
},
"cookiejar": {
"version": "2.0.1",
"from": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.0.1.tgz",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.0.1.tgz"
"version": "2.0.6",
"from": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.0.6.tgz",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.0.6.tgz"
},
"reduce-component": {
"version": "1.0.1",
@@ -2371,9 +2380,9 @@
"resolved": "http://registry.npmjs.org/reduce-component/-/reduce-component-1.0.1.tgz"
},
"extend": {
"version": "1.2.1",
"from": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz",
"resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz"
"version": "3.0.0",
"from": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz"
},
"form-data": {
"version": "0.2.0",
@@ -2418,7 +2427,7 @@
"dependencies": {
"core-util-is": {
"version": "1.0.2",
"from": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"from": "core-util-is@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
},
"isarray": {
@@ -2467,6 +2476,11 @@
"from": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
"resolved": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz"
},
"tldjs": {
"version": "1.6.2",
"from": "https://registry.npmjs.org/tldjs/-/tldjs-1.6.2.tgz",
"resolved": "https://registry.npmjs.org/tldjs/-/tldjs-1.6.2.tgz"
},
"underscore": {
"version": "1.8.3",
"from": "http://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
@@ -2483,9 +2497,9 @@
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz"
},
"nan": {
"version": "2.1.0",
"from": "https://registry.npmjs.org/nan/-/nan-2.1.0.tgz",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.1.0.tgz"
"version": "2.2.0",
"from": "https://registry.npmjs.org/nan/-/nan-2.2.0.tgz",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.2.0.tgz"
}
}
},
@@ -2495,9 +2509,16 @@
"resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz"
},
"validator": {
"version": "4.4.0",
"from": "https://registry.npmjs.org/validator/-/validator-4.4.0.tgz",
"resolved": "https://registry.npmjs.org/validator/-/validator-4.4.0.tgz"
"version": "4.8.0",
"from": "https://registry.npmjs.org/validator/-/validator-4.8.0.tgz",
"resolved": "https://registry.npmjs.org/validator/-/validator-4.8.0.tgz",
"dependencies": {
"depd": {
"version": "1.1.0",
"from": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz"
}
}
},
"x509": {
"version": "0.2.3",
+7 -3
View File
@@ -18,7 +18,7 @@
"aws-sdk": "^2.1.46",
"body-parser": "^1.13.1",
"bytes": "^2.1.0",
"cloudron-manifestformat": "^2.2.0",
"cloudron-manifestformat": "^2.3.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "0.0.13",
"connect-timeout": "^1.5.0",
@@ -42,11 +42,13 @@
"multiparty": "^4.1.2",
"mysql": "^2.7.0",
"native-dns": "^0.7.0",
"node-df": "^0.1.1",
"node-uuid": "^1.4.3",
"nodemailer": "^1.3.0",
"nodemailer-smtp-transport": "^1.0.3",
"oauth2orize": "^1.0.1",
"once": "^1.3.2",
"parse-links": "^0.1.0",
"passport": "^0.2.2",
"passport-http": "^0.2.2",
"passport-http-bearer": "^1.0.1",
@@ -54,13 +56,14 @@
"passport-oauth2-client-password": "^0.1.2",
"password-generator": "^2.0.2",
"proxy-middleware": "^0.13.0",
"safetydance": "^0.1.0",
"safetydance": "^0.1.1",
"semver": "^4.3.6",
"serve-favicon": "^2.2.0",
"split": "^1.0.0",
"superagent": "^1.5.0",
"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",
"valid-url": "^1.0.9",
@@ -70,13 +73,14 @@
"devDependencies": {
"apidoc": "*",
"bootstrap-sass": "^3.3.3",
"deep-extend": "^0.4.1",
"del": "^1.1.1",
"expect.js": "*",
"gulp": "^3.8.11",
"gulp-autoprefixer": "^2.3.0",
"gulp-concat": "^2.4.3",
"gulp-cssnano": "^2.1.0",
"gulp-ejs": "^1.0.0",
"gulp-minify-css": "^1.1.3",
"gulp-sass": "^2.0.1",
"gulp-serve": "^1.0.0",
"gulp-sourcemaps": "^1.5.2",
+5 -5
View File
@@ -3,16 +3,16 @@
# If you change the infra version, be sure to put a warning
# in the change log
INFRA_VERSION=21
INFRA_VERSION=27
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well
BASE_IMAGE=cloudron/base:0.8.0
MYSQL_IMAGE=cloudron/mysql:0.8.0
POSTGRESQL_IMAGE=cloudron/postgresql:0.8.0
MONGODB_IMAGE=cloudron/mongodb:0.8.0
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.9.0
MAIL_IMAGE=cloudron/mail:0.10.0
GRAPHITE_IMAGE=cloudron/graphite:0.8.0
MYSQL_REPO=cloudron/mysql
+8
View File
@@ -19,7 +19,9 @@ arg_version=""
arg_web_server_origin=""
arg_backup_config=""
arg_dns_config=""
arg_update_config=""
arg_provider=""
arg_app_bundle=""
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
eval set -- "${args}"
@@ -36,6 +38,9 @@ while true; do
$(echo "$2" | $json apiServerOrigin webServerOrigin fqdn isCustomDomain boxVersionsUrl version | tr '\n' ' ')
EOF
# read possibly empty parameters here
arg_app_bundle=$(echo "$2" | $json appBundle)
[[ "${arg_app_bundle}" == "" ]] && arg_app_bundle="[]"
arg_tls_cert=$(echo "$2" | $json tlsCert)
arg_tls_key=$(echo "$2" | $json tlsKey)
arg_token=$(echo "$2" | $json token)
@@ -56,6 +61,9 @@ EOF
arg_dns_config=$(echo "$2" | $json dnsConfig)
[[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""
arg_update_config=$(echo "$2" | $json updateConfig)
[[ "${arg_update_config}" == "null" ]] && arg_update_config=""
shift 2
;;
--) break;;
+4 -3
View File
@@ -25,8 +25,9 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reboot.sh
Defaults!/home/yellowtent/box/src/scripts/reloadcollectd.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadcollectd.sh
Defaults!/home/yellowtent/box/src/scripts/backupswap.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupswap.sh
Defaults!/home/yellowtent/box/src/scripts/collectlogs.sh env_keep="HOME BOX_ENV"
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
+4 -1
View File
@@ -2,6 +2,8 @@
Description=Cloudron Admin
OnFailure=crashnotifier@%n.service
StopWhenUnneeded=true
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
BindsTo=systemd-journald.service
[Service]
Type=idle
@@ -9,7 +11,8 @@ WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart=/usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
KillMode=process
; kill apptask processes as well
KillMode=control-group
User=yellowtent
Group=yellowtent
MemoryLimit=200M
+1 -1
View File
@@ -1,5 +1,5 @@
[Unit]
Description=Cloudron Smart Cloud
Description=Cloudron Smartserver
Documentation=https://cloudron.io/documentation.html
StopWhenUnneeded=true
Requires=box.service
+11 -9
View File
@@ -92,13 +92,6 @@ EOF
set_progress "28" "Setup collectd"
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
## collectd 5.4.1 has some bug where we simply cannot get it to create df-vda1
#mkdir -p "${DATA_DIR}/graphite/whisper/collectd/localhost/"
## detect device, let it fail if non exists
#[[ -b "/dev/vda1" ]] && disk_device="/dev/vda1"
#[[ -b "/dev/xvda1" ]] && disk_device="/dev/xvda1"
#vda1_id=$(blkid -s UUID -o value ${disk_device})
#ln -sfF "df-disk_by-uuid_${vda1_id}" "${DATA_DIR}/graphite/whisper/collectd/localhost/df-vda1"
service collectd restart
set_progress "30" "Setup nginx"
@@ -145,7 +138,7 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"fqdn": "${arg_fqdn}",
"isCustomDomain": ${arg_is_custom_domain},
"boxVersionsUrl": "${arg_box_versions_url}",
"adminEmail": "admin@${arg_fqdn}",
"adminEmail": "\"Cloudron\" <no-reply@${arg_fqdn}>",
"provider": "${arg_provider}",
"database": {
"hostname": "localhost",
@@ -153,7 +146,8 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"password": "${mysql_root_password}",
"port": 3306,
"name": "box"
}
},
"appBundle": ${arg_app_bundle}
}
CONF_END
@@ -181,6 +175,14 @@ if [[ ! -z "${arg_dns_config}" ]]; then
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
fi
# Add Update Configuration
if [[ ! -z "${arg_update_config}" ]]; then
echo "Add Update Config"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"update_config\", '$arg_update_config')" box
fi
# Add TLS Configuration
if [[ ! -z "${arg_tls_config}" ]]; then
echo "Add TLS Config"
+6 -21
View File
@@ -133,10 +133,10 @@ LoadPlugin nginx
# Globals true
#</LoadPlugin>
#LoadPlugin pinba
LoadPlugin ping
#LoadPlugin ping
#LoadPlugin postgresql
#LoadPlugin powerdns
LoadPlugin processes
#LoadPlugin processes
#LoadPlugin protocols
#<LoadPlugin python>
# Globals true
@@ -161,7 +161,7 @@ LoadPlugin tail
#LoadPlugin users
#LoadPlugin uuid
#LoadPlugin varnish
LoadPlugin vmem
#LoadPlugin vmem
#LoadPlugin vserver
#LoadPlugin wireless
LoadPlugin write_graphite
@@ -193,11 +193,11 @@ LoadPlugin write_graphite
</Plugin>
<Plugin df>
FSType "tmpfs"
MountPoint "/dev"
FSType "ext4"
FSType "btrfs"
ReportByDevice true
IgnoreSelected true
IgnoreSelected false
ValuesAbsolute true
ValuesPercentage true
@@ -212,17 +212,6 @@ LoadPlugin write_graphite
URL "http://127.0.0.1/nginx_status"
</Plugin>
<Plugin ping>
Host "google.com"
Interval 1.0
Timeout 0.9
TTL 255
</Plugin>
<Plugin processes>
ProcessMatch "app" "node box.js"
</Plugin>
<Plugin swap>
ReportByDevice false
ReportBytes true
@@ -255,10 +244,6 @@ LoadPlugin write_graphite
</File>
</Plugin>
<Plugin vmem>
Verbose false
</Plugin>
<Plugin write_graphite>
<Node "graphing">
Host "localhost"
+4 -4
View File
@@ -64,10 +64,10 @@ server {
}
# graphite paths
location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
proxy_pass http://127.0.0.1:8000;
client_max_body_size 1m;
}
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
# proxy_pass http://127.0.0.1:8000;
# client_max_body_size 1m;
# }
location / {
root <%= sourceDir %>/webadmin/dist;
+18 -1
View File
@@ -58,6 +58,14 @@ http {
ssl_certificate cert/host.cert;
ssl_certificate_key cert/host.key;
# increase the proxy buffer sizes to not run into buffer issues (http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffers)
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# Disable check to allow unlimited body sizes
client_max_body_size 0;
error_page 404 = @fallback;
location @fallback {
internal;
@@ -65,7 +73,16 @@ http {
rewrite ^/$ /nakeddomain.html break;
}
return 404;
location / {
internal;
root /home/yellowtent/box/webadmin/dist;
rewrite ^/$ /nakeddomain.html break;
}
location /api/ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 1m;
}
}
include applications/*.conf;
+7 -3
View File
@@ -20,6 +20,7 @@ fi
echo "Upgrading infrastructure from ${infra_version} to ${INFRA_VERSION}"
# TODO: be nice and stop addons cleanly (example, shutdown commands)
existing_containers=$(docker ps -qa)
echo "Remove containers: ${existing_containers}"
if [[ -n "${existing_containers}" ]]; then
@@ -42,11 +43,14 @@ if docker images "${GRAPHITE_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | g
fi
# mail (MAIL_SMTP_PORT is 2500 in addons.js. used in mailer.js as well)
# MAIL_SERVER_NAME is the hostname of the mailserver i.e server uses these certs
# MAIL_DOMAIN is the domain for which this server is relaying mails
mail_container_id=$(docker run --restart=always -d --name="mail" \
-m 75m \
--memory-swap 150m \
-h "${arg_fqdn}" \
-e "DOMAIN_NAME=${arg_fqdn}" \
-e "MAIL_SERVER_NAME=${arg_fqdn}" \
-e "MAIL_DOMAIN=${arg_fqdn}" \
-v "${DATA_DIR}/box/mail:/app/data" \
--read-only -v /tmp -v /run \
"${MAIL_IMAGE}")
@@ -63,8 +67,8 @@ readonly MYSQL_ROOT_PASSWORD='${mysql_addon_root_password}'
readonly MYSQL_ROOT_HOST='${docker0_ip}'
EOF
mysql_container_id=$(docker run --restart=always -d --name="mysql" \
-m 100m \
--memory-swap 200m \
-m 256m \
--memory-swap 512m \
-h "${arg_fqdn}" \
-v "${DATA_DIR}/mysql:/var/lib/mysql" \
-v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
+5 -4
View File
@@ -394,6 +394,7 @@ function setupSendMail(app, options, callback) {
'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_DOMAIN=' + config.fqdn()
];
@@ -420,7 +421,7 @@ function setupMySql(app, options, callback) {
debugApp(app, 'Setting up mysql');
var container = docker.getContainer('mysql');
var cmd = [ '/addons/mysql/service.sh', 'add', app.id ];
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', app.id ];
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
if (error) return callback(error);
@@ -453,7 +454,7 @@ function teardownMySql(app, options, callback) {
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('mysql');
var cmd = [ '/addons/mysql/service.sh', 'remove', app.id ];
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', app.id ];
debugApp(app, 'Tearing down mysql');
@@ -481,7 +482,7 @@ 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', 'backup', app.id ]);
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);
@@ -504,7 +505,7 @@ function restoreMySql(app, options, callback) {
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', 'restore', app.id ]);
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);
+6 -7
View File
@@ -59,7 +59,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.oauthProxy' ].join(',');
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.memoryLimit' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -92,8 +92,6 @@ function postProcess(result) {
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
}
result.oauthProxy = !!result.oauthProxy;
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
result.accessRestriction = safe.JSON.parse(result.accessRestrictionJson);
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
@@ -179,7 +177,7 @@ function getAll(callback) {
});
}
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, callback) {
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, memoryLimit, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
@@ -187,7 +185,7 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert.strictEqual(typeof memoryLimit, 'number');
assert.strictEqual(typeof callback, 'function');
portBindings = portBindings || { };
@@ -197,8 +195,8 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
var queries = [ ];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, oauthProxy) VALUES (?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, oauthProxy ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit) VALUES (?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, memoryLimit ]
});
Object.keys(portBindings).forEach(function (env) {
@@ -283,6 +281,7 @@ function updateWithConstraints(id, app, constraints, callback) {
assert.strictEqual(typeof constraints, 'string');
assert.strictEqual(typeof callback, 'function');
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
var queries = [ ];
+8 -3
View File
@@ -3,6 +3,7 @@
var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
@@ -24,8 +25,11 @@ var gDockerEventStream = null;
function debugApp(app) {
assert(!app || typeof app === 'object');
var prefix = app ? app.location : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? (app.location || 'naked_domain') : '(no app)';
var manifestAppId = app ? app.manifest.id : '';
var id = app ? app.id : '';
debug(prefix + ' ' + manifestAppId + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + id);
}
function setHealth(app, health, callback) {
@@ -89,6 +93,7 @@ function checkAppHealth(app, callback) {
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
superagent
.get(healthCheckUrl)
.set('Host', config.appFqdn(app.location)) // required for some apache configs with rewrite rules
.redirects(0)
.timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) {
@@ -114,7 +119,7 @@ function processApps(callback) {
var alive = apps
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
.map(function (a) { return a.location; }).join(', ');
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
debug('apps alive: [%s]', alive);
+139 -188
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -9,19 +7,19 @@ exports = module.exports = {
get: get,
getBySubdomain: getBySubdomain,
getByIpAddress: getByIpAddress,
getAll: getAll,
getAllByUser: getAllByUser,
purchase: purchase,
install: install,
configure: configure,
uninstall: uninstall,
restore: restore,
restoreApp: restoreApp,
update: update,
backup: backup,
backupApp: backupApp,
listBackups: listBackups,
getLogs: getLogs,
@@ -33,8 +31,6 @@ exports = module.exports = {
checkManifestConstraints: checkManifestConstraints,
setRestorePoint: setRestorePoint,
autoupdateApps: autoupdateApps,
// exported for testing
@@ -48,7 +44,6 @@ var addons = require('./addons.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
certificates = require('./certificates.js'),
config = require('./config.js'),
constants = require('./constants.js'),
@@ -56,12 +51,12 @@ var addons = require('./addons.js'),
debug = require('debug')('box:apps'),
docker = require('./docker.js'),
fs = require('fs'),
groups = require('./groups.js'),
manifestFormat = require('cloudron-manifestformat'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
superagent = require('superagent'),
@@ -69,26 +64,6 @@ var addons = require('./addons.js'),
util = require('util'),
validator = require('validator');
var BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'),
RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh'),
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh');
function debugApp(app, args) {
assert(!app || typeof app === 'object');
var prefix = app ? app.location : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function ignoreError(func) {
return function (callback) {
func(function (error) {
if (error) console.error('Ignored error:', error);
callback();
});
};
}
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function AppsError(reason, errorOrMessage) {
@@ -192,9 +167,38 @@ function validateAccessRestriction(accessRestriction) {
if (accessRestriction === null) return null;
if (!accessRestriction.users || !Array.isArray(accessRestriction.users)) return new Error('users array property required');
if (accessRestriction.users.length === 0) return new Error('users array cannot be empty');
if (!accessRestriction.users.every(function (e) { return typeof e === 'string'; })) return new Error('All users have to be strings');
var noUsers = true, noGroups = true;
if (accessRestriction.users) {
if (!Array.isArray(accessRestriction.users)) return new Error('users array property required');
if (!accessRestriction.users.every(function (e) { return typeof e === 'string'; })) return new Error('All users have to be strings');
noUsers = accessRestriction.users.length === 0;
}
if (accessRestriction.groups) {
if (!Array.isArray(accessRestriction.groups)) return new Error('groups array property required');
if (!accessRestriction.groups.every(function (e) { return typeof e === 'string'; })) return new Error('All groups have to be strings');
noGroups = accessRestriction.groups.length === 0;
}
if (noUsers && noGroups) return new Error('users and groups array cannot both be empty');
return null;
}
function validateMemoryLimit(manifest, memoryLimit) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof memoryLimit, 'number');
var min = manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
var max = (4096 * 1024 * 1024);
// allow 0, which indicates that it is not set, the one from the manifest will be choosen but we don't commit any user value
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
if (memoryLimit === 0) return null;
if (memoryLimit < min) return new Error('memoryLimit too small');
if (memoryLimit > max) return new Error('memoryLimit too large');
return null;
}
@@ -226,12 +230,26 @@ function getIconUrlSync(app) {
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
}
function hasAccessTo(app, user) {
function hasAccessTo(app, user, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.accessRestriction === null) return true;
return app.accessRestriction.users.some(function (e) { return e === user.id; });
if (app.accessRestriction === null) return callback(null, true);
// check user access
if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return callback(null, true);
// check group access
if (!app.accessRestriction.groups) return callback(null, false);
async.some(app.accessRestriction.groups, function (groupId, iteratorDone) {
groups.isMember(groupId, user.id, function (error, member) {
iteratorDone(!error && member); // async.some does not take error argument in callback
});
}, function (result) {
callback(null, result);
});
}
function get(appId, callback) {
@@ -264,6 +282,25 @@ function getBySubdomain(subdomain, callback) {
});
}
function getByIpAddress(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
docker.getContainerIdByIp(ip, function (error, containerId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.getByContainerId(containerId, 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);
callback(null, app);
});
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -279,6 +316,21 @@ function getAll(callback) {
});
}
function getAllByUser(user, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof callback, 'function');
getAll(function (error, result) {
if (error) return callback(error);
async.filter(result, function (app, callback) {
hasAccessTo(app, user, function (error, hasAccess) {
callback(hasAccess);
});
}, callback.bind(null, null)); // never error
});
}
function purchase(appStoreId, callback) {
assert.strictEqual(typeof appStoreId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -301,17 +353,17 @@ function purchase(appStoreId, callback) {
});
}
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, icon, cert, key, callback) {
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, cert, key, memoryLimit, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert(!icon || typeof icon === 'string');
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof memoryLimit, 'number');
assert.strictEqual(typeof callback, 'function');
var error = manifestFormat.parse(manifest);
@@ -329,6 +381,12 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
error = validateAccessRestriction(accessRestriction);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = validateMemoryLimit(manifest, memoryLimit);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
// memoryLimit might come in as 0 if not specified
memoryLimit = memoryLimit || manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
// 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));
@@ -349,7 +407,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, oauthProxy, function (error) {
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, memoryLimit, 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));
@@ -366,14 +424,14 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
});
}
function configure(appId, location, portBindings, accessRestriction, oauthProxy, cert, key, callback) {
function configure(appId, location, portBindings, accessRestriction, cert, key, memoryLimit, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof memoryLimit, 'number');
assert.strictEqual(typeof callback, 'function');
var error = validateHostname(location, config.fqdn());
@@ -392,6 +450,12 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
error = validatePortBindings(portBindings, app.manifest.tcpPorts);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = validateMemoryLimit(app.manifest, memoryLimit);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
// memoryLimit might come in as 0 if not specified
memoryLimit = memoryLimit || app.memoryLimit || app.manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
// save cert to data/box/certs
if (cert && key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
@@ -401,14 +465,14 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
var values = {
location: location.toLowerCase(),
accessRestriction: accessRestriction,
oauthProxy: oauthProxy,
portBindings: portBindings,
memoryLimit: memoryLimit,
oldConfig: {
location: app.location,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
oauthProxy: app.oauthProxy
memoryLimit: app.memoryLimit
}
};
@@ -457,14 +521,30 @@ function update(appId, force, manifest, portBindings, icon, 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));
var appStoreId = app.appStoreId;
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
if (app.manifest.id !== manifest.id) {
if (!force) return callback(new AppsError(AppsError.BAD_FIELD, 'manifest id does not match. force to override'));
// clear appStoreId so that this app does not get updates anymore. this will mark is a dev app
appStoreId = '';
}
// Ensure we update the memory limit in case the new app requires more memory as a minimum
var memoryLimit = manifest.memoryLimit ? (app.memoryLimit < manifest.memoryLimit ? manifest.memoryLimit : app.memoryLimit) : app.memoryLimit;
var values = {
appStoreId: appStoreId,
manifest: manifest,
portBindings: portBindings,
memoryLimit: memoryLimit,
oldConfig: {
manifest: app.manifest,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy
memoryLimit: app.memoryLimit
}
};
@@ -550,12 +630,13 @@ function restore(appId, callback) {
values = {
manifest: restoreConfig.manifest,
portBindings: restoreConfig.portBindings,
memoryLimit: restoreConfig.memoryLimit,
oldConfig: {
location: app.location,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
manifest: app.manifest
}
};
@@ -578,13 +659,13 @@ function uninstall(appId, callback) {
debug('Will uninstall app with id:%s', appId);
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, function (error) {
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));
taskmanager.stopAppTask(appId, function () {
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, function (error) {
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));
taskmanager.restartAppTask(appId); // since uninstall is allowed from any state, kill current task
callback(null);
taskmanager.startAppTask(appId, callback);
});
});
}
@@ -646,6 +727,10 @@ function exec(appId, options, 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));
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
return callback(new AppsError(AppsError.BAD_STATE, 'App not installed or running'));
}
var container = docker.connection.getContainer(app.containerId);
var execOptions = {
@@ -676,20 +761,6 @@ function exec(appId, options, callback) {
});
}
function setRestorePoint(appId, lastBackupId, lastBackupConfig, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof lastBackupId, 'string');
assert.strictEqual(typeof lastBackupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
appdb.update(appId, { lastBackupId: lastBackupId, lastBackupConfig: lastBackupConfig }, function (error) {
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));
return callback(null);
});
}
function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { manifest } }
assert.strictEqual(typeof updateInfo, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -734,97 +805,6 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
}, callback);
}
function canBackupApp(app) {
// only backup apps that are installed or pending configure or called from apptask. Rest of them are in some
// state not good for consistent backup (i.e addons may not have been setup completely)
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
app.installationState === appdb.ISTATE_PENDING_BACKUP || // called from apptask
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
}
// set the 'creation' date of lastBackup so that the backup persists across time based archival rules
// s3 does not allow changing creation time, so copying the last backup is easy way out for now
function reuseOldBackup(app, callback) {
assert.strictEqual(typeof app.lastBackupId, 'string');
assert.strictEqual(typeof callback, 'function');
backups.copyLastBackup(app, function (error, newBackupId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
debugApp(app, 'reuseOldBackup: reused old backup %s as %s', app.lastBackupId, newBackupId);
callback(null, newBackupId);
});
}
function createNewBackup(app, addonsToBackup, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert.strictEqual(typeof callback, 'function');
backups.getBackupUrl(app, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
debugApp(app, 'backupApp: backup url:%s backup id:%s', result.url, result.id);
async.series([
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
addons.backupAddons.bind(null, app, addonsToBackup),
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ]),
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
], function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
callback(null, result.id);
});
});
}
function backupApp(app, addonsToBackup, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert.strictEqual(typeof callback, 'function');
var appConfig = null, backupFunction;
if (!canBackupApp(app)) {
if (!app.lastBackupId) {
debugApp(app, 'backupApp: cannot backup app');
return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy and never backed up previously'));
}
appConfig = app.lastBackupConfig;
backupFunction = reuseOldBackup.bind(null, app);
} else {
appConfig = {
manifest: app.manifest,
location: app.location,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy
};
backupFunction = createNewBackup.bind(null, app, addonsToBackup);
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
return callback(safe.error);
}
}
backupFunction(function (error, backupId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
debugApp(app, 'backupApp: successful id:%s', backupId);
setRestorePoint(app.id, backupId, appConfig, function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
return callback(null, backupId);
});
});
}
function backup(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -844,28 +824,11 @@ function backup(appId, callback) {
});
}
function restoreApp(app, addonsToRestore, backupId, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof addonsToRestore, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function');
assert(app.lastBackupId);
backups.getRestoreUrl(backupId, function (error, result) {
if (error && error.reason == BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
function listBackups(page, perPage, appId, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
debugApp(app, 'restoreApp: restoreUrl:%s', result.url);
shell.sudo('restoreApp', [ RESTORE_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ], function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
addons.restoreAddons(app, addonsToRestore, callback);
});
});
}
function listBackups(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -873,22 +836,10 @@ function listBackups(appId, callback) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!exists) return callback(new AppsError(AppsError.NOT_FOUND));
// TODO pagination is not implemented in the backend yet
backups.getAllPaged(0, 1000, function (error, result) {
backups.getByAppIdPaged(page, perPage, appId, function (error, results) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var appBackups = [];
result.forEach(function (backup) {
appBackups = appBackups.concat(backup.dependsOn.filter(function (d) {
return d.indexOf('appbackup_' + appId) === 0;
}));
});
// alphabetic should be sufficient
appBackups.sort();
callback(null, appBackups);
callback(null, results);
});
});
}
+21 -11
View File
@@ -35,6 +35,7 @@ var addons = require('./addons.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
certificates = require('./certificates.js'),
clientdb = require('./clientdb.js'),
config = require('./config.js'),
@@ -162,7 +163,7 @@ function allocateOAuthProxyCredentials(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!app.oauthProxy) return callback(null);
if (!nginx.requiresOAuthProxy(app)) return callback(null);
var id = 'cid-' + uuid.v4();
var clientSecret = hat(256);
@@ -436,7 +437,7 @@ function backup(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
apps.backupApp.bind(null, app, app.manifest.addons),
backups.backupApp.bind(null, app, app.manifest.addons),
// done!
function (callback) {
@@ -501,7 +502,7 @@ function restore(app, callback) {
createVolume.bind(null, app),
updateApp.bind(null, app, { installationProgress: '70, Download backup and restore addons' }),
apps.restoreApp.bind(null, app, app.manifest.addons, backupId),
backups.restoreApp.bind(null, app, app.manifest.addons, backupId),
updateApp.bind(null, app, { installationProgress: '75, Creating container' }),
createContainer.bind(null, app),
@@ -598,15 +599,21 @@ function update(app, callback) {
debugApp(app, 'Updating to %s', safe.query(app, 'manifest.version'));
// app does not want these addons anymore
// FIXME: this does not handle option changes (like multipleDatabases)
var unusedAddons = _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
async.series([
updateApp.bind(null, app, { installationProgress: '0, Verify manifest' }),
verifyManifest.bind(null, app),
// download new image before app is stopped. this is so we can reduce downtime
// and also not remove the 'common' layers when the old image is deleted
updateApp.bind(null, app, { installationProgress: '15, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
// note: we cleanup first and then backup. this is done so that the app is not running should backup fail
// we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
updateApp.bind(null, app, { installationProgress: '25, Cleaning up old install' }),
removeCollectdProfile.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
@@ -622,17 +629,14 @@ function update(app, callback) {
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
async.series([
updateApp.bind(null, app, { installationProgress: '20, Backup app' }),
updateApp.bind(null, app, { installationProgress: '30, Backup app' }),
apps.backupApp.bind(null, app, app.oldConfig.manifest.addons)
], next);
},
updateApp.bind(null, app, { installationProgress: '35, Downloading icon' }),
updateApp.bind(null, app, { installationProgress: '45, Downloading icon' }),
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '45, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '70, Updating addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
@@ -697,7 +701,13 @@ function uninstall(app, callback) {
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
appdb.del.bind(null, app.id)
], callback);
], function seriesDone(error) {
if (error) {
debugApp(app, 'error uninstalling app: %s', error);
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
}
callback(null);
});
}
function runApp(app, callback) {
@@ -760,7 +770,7 @@ function startTask(appId, callback) {
case appdb.ISTATE_PENDING_INSTALL: return install(app, callback);
case appdb.ISTATE_PENDING_FORCE_UPDATE: return update(app, callback);
case appdb.ISTATE_ERROR:
debugApp(app, 'Apptask launched with error states.');
debugApp(app, 'Internal error. apptask launched with error status.');
return callback(null);
default:
debugApp(app, 'apptask launched with invalid command');
+14 -6
View File
@@ -16,6 +16,7 @@ var assert = require('assert'),
debug = require('debug')('box:auth'),
LocalStrategy = require('passport-local').Strategy,
crypto = require('crypto'),
groups = require('./groups'),
passport = require('passport'),
tokendb = require('./tokendb'),
user = require('./user'),
@@ -27,11 +28,11 @@ function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
passport.serializeUser(function (user, callback) {
callback(null, user.username);
callback(null, user.id);
});
passport.deserializeUser(function(username, callback) {
userdb.get(username, function (error, result) {
passport.deserializeUser(function(userId, callback) {
userdb.get(userId, function (error, result) {
if (error) return callback(error);
var md5 = crypto.createHash('md5').update(result.email.toLowerCase()).digest('hex');
@@ -43,7 +44,7 @@ function initialize(callback) {
passport.use(new LocalStrategy(function (username, password, callback) {
if (username.indexOf('@') === -1) {
user.verify(username, password, function (error, result) {
user.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
@@ -73,7 +74,7 @@ function initialize(callback) {
return callback(null, client);
});
} else {
user.verify(username, password, function (error, result) {
user.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
@@ -123,7 +124,14 @@ function initialize(callback) {
// amend the tokenType of the token owner
user.tokenType = tokenType;
callback(null, user, info);
// amend the admin flag
groups.isMember(groups.ADMIN_GROUP_ID, user.id, function (error, isAdmin) {
if (error) return callback(error);
user.admin = isAdmin;
callback(null, user, info);
});
});
});
}));
+114
View File
@@ -0,0 +1,114 @@
'use strict';
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
util = require('util');
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', ];
exports = module.exports = {
add: add,
getPaged: getPaged,
get: get,
del: del,
getByAppIdPaged: getByAppIdPaged,
_clear: clear,
BACKUP_TYPE_APP: 'app',
BACKUP_TYPE_BOX: 'box',
BACKUP_STATE_NORMAL: 'normal', // should rename to created to avoid listing in UI?
};
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
result.dependsOn = result.dependsOn ? result.dependsOn.split(',') : [ ];
}
function getPaged(page, perPage, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
[ exports.BACKUP_TYPE_BOX, exports.BACKUP_STATE_NORMAL, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function getByAppIdPaged(page, perPage, appId, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? AND id LIKE ? ORDER BY creationTime DESC LIMIT ?,?',
[ exports.BACKUP_TYPE_APP, exports.BACKUP_STATE_NORMAL, 'appbackup\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE id = ? ORDER BY creationTime DESC',
[ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
callback(null, result[0]);
});
}
function add(backup, callback) {
assert(backup && typeof backup === 'object');
assert.strictEqual(typeof backup.id, 'string');
assert.strictEqual(typeof backup.version, 'string');
assert(backup.type === exports.BACKUP_TYPE_APP || backup.type === exports.BACKUP_TYPE_BOX);
assert(util.isArray(backup.dependsOn));
assert.strictEqual(typeof callback, 'function');
var creationTime = backup.creationTime || new Date(); // allow tests to set the time
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn) VALUES (?, ?, ?, ?, ?, ?)',
[ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(',') ],
function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('TRUNCATE TABLE backups', [], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM backups WHERE id=?', [ id ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
+354 -35
View File
@@ -3,21 +3,53 @@
exports = module.exports = {
BackupsError: BackupsError,
getAllPaged: getAllPaged,
getPaged: getPaged,
getByAppIdPaged: getByAppIdPaged,
getBackupUrl: getBackupUrl,
getRestoreUrl: getRestoreUrl,
copyLastBackup: copyLastBackup
ensureBackup: ensureBackup,
backup: backup,
backupApp: backupApp,
restoreApp: restoreApp,
backupBoxAndApps: backupBoxAndApps
};
var assert = require('assert'),
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
async = require('async'),
assert = require('assert'),
backupdb = require('./backupdb.js'),
caas = require('./storage/caas.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:backups'),
locker = require('./locker.js'),
path = require('path'),
paths = require('./paths.js'),
progress = require('./progress.js'),
s3 = require('./storage/s3.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
settings = require('./settings.js'),
util = require('util');
util = require('util'),
webhooks = require('./webhooks.js');
var BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'),
BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'),
RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function debugApp(app, args) {
assert(!app || typeof app === 'object');
var prefix = app ? app.location : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function BackupsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -40,6 +72,7 @@ function BackupsError(reason, errorOrMessage) {
util.inherits(BackupsError, Error);
BackupsError.EXTERNAL_ERROR = 'external error';
BackupsError.INTERNAL_ERROR = 'internal error';
BackupsError.BAD_STATE = 'bad state';
BackupsError.MISSING_CREDENTIALS = 'missing credentials';
// choose which storage backend we use for test purpose we use s3
@@ -51,49 +84,78 @@ function api(provider) {
}
}
function getAllPaged(page, perPage, callback) {
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
function getPaged(page, perPage, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
backupdb.getPaged(page, perPage, function (error, results) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, results);
});
}
function getByAppIdPaged(page, perPage, appId, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
backupdb.getByAppIdPaged(page, perPage, appId, function (error, results) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, results);
});
}
function getBoxBackupCredentials(appBackupIds, callback) {
assert(util.isArray(appBackupIds));
assert.strictEqual(typeof callback, 'function');
var now = new Date();
var filebase = util.format('backup_%s-v%s', now.toISOString(), config.version());
var filename = filebase + '.tar.gz';
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getAllPaged(backupConfig, page, perPage, function (error, backups) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
api(backupConfig.provider).getBackupCredentials(backupConfig, function (error, result) {
if (error) return callback(error);
return callback(null, backups); // [ { creationTime, restoreKey } ] sorted by time (latest first
result.id = filename;
result.s3Url = 's3://' + backupConfig.bucket + '/' + backupConfig.prefix + '/' + filename;
result.backupKey = backupConfig.key;
debug('getBoxBackupCredentials: %j', result);
callback(null, result);
});
});
}
function getBackupUrl(app, callback) {
assert(!app || typeof app === 'object');
function getAppBackupCredentials(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var filename = '';
if (app) {
filename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
} else {
filename = util.format('backup_%s-v%s.tar.gz', (new Date()).toISOString(), config.version());
}
var now = new Date();
var filebase = util.format('appbackup_%s_%s-v%s', app.id, now.toISOString(), app.manifest.version);
var configFilename = filebase + '.json', dataFilename = filebase + '.tar.gz';
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getSignedUploadUrl(backupConfig, filename, function (error, result) {
api(backupConfig.provider).getBackupCredentials(backupConfig, function (error, result) {
if (error) return callback(error);
var obj = {
id: filename,
url: result.url,
sessionToken: result.sessionToken,
backupKey: backupConfig.key
};
result.id = dataFilename;
result.s3ConfigUrl = 's3://' + backupConfig.bucket + '/' + backupConfig.prefix + '/' + configFilename;
result.s3DataUrl = 's3://' + backupConfig.bucket + '/' + backupConfig.prefix + '/' + dataFilename;
result.backupKey = backupConfig.key;
debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
debug('getAppBackupCredentials: %j', result);
callback(null, obj);
callback(null, result);
});
});
}
@@ -106,17 +168,16 @@ function getRestoreUrl(backupId, callback) {
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getSignedDownloadUrl(backupConfig, backupId, function (error, result) {
api(backupConfig.provider).getRestoreUrl(backupConfig, backupId, function (error, result) {
if (error) return callback(error);
var obj = {
id: backupId,
url: result.url,
sessionToken: result.sessionToken,
backupKey: backupConfig.key
};
debug('getRestoreUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
debug('getRestoreUrl: id:%s url:%s backupKey:%s', obj.id, obj.url, obj.backupKey);
callback(null, obj);
});
@@ -124,19 +185,277 @@ function getRestoreUrl(backupId, callback) {
}
function copyLastBackup(app, callback) {
assert(app && typeof app === 'object');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof app.lastBackupId, 'string');
assert.strictEqual(typeof callback, 'function');
var toFilename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
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);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilename, function (error) {
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilenameArchive, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilename);
// TODO change that logic by adjusting app.lastBackupId to not contain the file type
var configFileId = app.lastBackupId.slice(0, -'.tar.gz'.length) + '.json';
api(backupConfig.provider).copyObject(backupConfig, configFileId, toFilenameConfig, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilenameArchive);
});
});
});
}
function backupBoxWithAppBackupIds(appBackupIds, callback) {
assert(util.isArray(appBackupIds));
getBoxBackupCredentials(appBackupIds, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug('backupBoxWithAppBackupIds: %j', result);
var args = [ result.s3Url, result.accessKeyId, result.secretAccessKey, result.sessionToken, result.region, result.backupKey ];
shell.sudo('backupBox', [ BACKUP_BOX_CMD ].concat(args), function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug('backupBoxWithAppBackupIds: success');
backupdb.add({ id: result.id, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
webhooks.backupDone(result.id, null /* app */, appBackupIds, function (error) {
if (error) return callback(error);
callback(null, result.id);
});
});
});
});
}
// this function expects you to have a lock
// 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);
// });
// }
function canBackupApp(app) {
// only backup apps that are installed or pending configure or called from apptask. Rest of them are in some
// state not good for consistent backup (i.e addons may not have been setup completely)
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
app.installationState === appdb.ISTATE_PENDING_BACKUP || // called from apptask
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
}
// set the 'creation' date of lastBackup so that the backup persists across time based archival rules
// s3 does not allow changing creation time, so copying the last backup is easy way out for now
function reuseOldAppBackup(app, callback) {
assert.strictEqual(typeof app.lastBackupId, 'string');
assert.strictEqual(typeof callback, 'function');
copyLastBackup(app, function (error, newBackupId) {
if (error) return callback(error);
debugApp(app, 'reuseOldAppBackup: reused old backup %s as %s', app.lastBackupId, newBackupId);
callback(null, newBackupId);
});
}
function createNewAppBackup(app, addonsToBackup, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert.strictEqual(typeof callback, 'function');
getAppBackupCredentials(app, function (error, result) {
if (error) return callback(error);
debugApp(app, 'createNewAppBackup: backup url:%s backup config url:%s', result.s3DataUrl, result.s3ConfigUrl);
var args = [ app.id, result.s3ConfigUrl, result.s3DataUrl, result.accessKeyId, result.secretAccessKey,
result.sessionToken, result.region, result.backupKey ];
async.series([
addons.backupAddons.bind(null, app, addonsToBackup),
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD ].concat(args))
], function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debugApp(app, 'createNewAppBackup: %s done', result.id);
backupdb.add({ id: result.id, version: app.manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ] }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, result.id);
});
});
});
}
function setRestorePoint(appId, lastBackupId, lastBackupConfig, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof lastBackupId, 'string');
assert.strictEqual(typeof lastBackupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
appdb.update(appId, { lastBackupId: lastBackupId, lastBackupConfig: lastBackupConfig }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, 'No such app'));
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
return callback(null);
});
}
function backupApp(app, addonsToBackup, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert.strictEqual(typeof callback, 'function');
var appConfig = null, backupFunction;
if (!canBackupApp(app)) {
if (!app.lastBackupId) {
debugApp(app, 'backupApp: cannot backup app');
return callback(new BackupsError(BackupsError.BAD_STATE, 'App not healthy and never backed up previously'));
}
appConfig = app.lastBackupConfig;
backupFunction = reuseOldAppBackup.bind(null, app);
} else {
appConfig = {
manifest: app.manifest,
location: app.location,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction,
memoryLimit: app.memoryLimit
};
backupFunction = createNewAppBackup.bind(null, app, addonsToBackup);
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
return callback(safe.error);
}
}
backupFunction(function (error, backupId) {
if (error) return callback(error);
debugApp(app, 'backupApp: successful id:%s', backupId);
setRestorePoint(app.id, backupId, appConfig, function (error) {
if (error) return callback(error);
return callback(null, backupId);
});
});
}
// this function expects you to have a lock
function backupBoxAndApps(callback) {
callback = callback || NOOP_CALLBACK;
apps.getAll(function (error, allApps) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var processed = 0;
var step = 100/(allApps.length+1);
progress.set(progress.BACKUP, processed, '');
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
++processed;
backupApp(app, app.manifest.addons, function (error, backupId) {
if (error && error.reason !== BackupsError.BAD_STATE) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
}
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
}, function appsBackedUp(error, backupIds) {
if (error) {
progress.set(progress.BACKUP, 100, error.message);
return callback(error);
}
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
backupBoxWithAppBackupIds(backupIds, function (error, filename) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
callback(error, filename);
});
});
});
}
function backup(callback) {
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');
// start the backup operation in the background
backupBoxAndApps(function (error) {
if (error) console.error('backup failed.', error);
locker.unlock(locker.OP_FULL_BACKUP);
});
callback(null);
}
function ensureBackup(callback) {
callback = callback || NOOP_CALLBACK;
getPaged(1, 1, function (error, backups) {
if (error) {
debug('Unable to list backups', error);
return callback(error); // no point trying to backup if appstore is down
}
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
debug('Previous backup was %j, no need to backup now', backups[0]);
return callback(null);
}
backup(callback);
});
}
function restoreApp(app, addonsToRestore, backupId, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof addonsToRestore, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function');
assert(app.lastBackupId);
getRestoreUrl(backupId, function (error, result) {
if (error) return callback(error);
debugApp(app, 'restoreApp: restoreUrl:%s', result.url);
shell.sudo('restoreApp', [ RESTORE_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ], function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
addons.restoreAddons(app, addonsToRestore, callback);
});
});
}
+54 -30
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
@@ -7,6 +5,7 @@ var assert = require('assert'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme'),
fs = require('fs'),
parseLinks = require('parse-links'),
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
@@ -58,7 +57,6 @@ function Acme(options) {
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
this.accountKeyPem = null; // Buffer
this.email = options.email;
this.chainPem = options.prod ? safe.fs.readFileSync(__dirname + '/lets-encrypt-x1-cross-signed.pem.txt') : new Buffer('');
}
Acme.prototype.getNonce = function (callback) {
@@ -304,7 +302,7 @@ Acme.prototype.signCertificate = function (domain, csrDer, callback) {
if (!certUrl) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // for renewal
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // maybe use for renewal
return callback(null, result.headers.location);
});
@@ -315,25 +313,57 @@ Acme.prototype.createKeyAndCsr = function (domain, callback) {
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var csrFile = path.join(outdir, domain + '.csr');
var privateKeyFile = path.join(outdir, domain + '.key');
var execSync = safe.child_process.execSync;
var privateKeyFile = path.join(outdir, domain + '.key');
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (safe.fs.existsSync(privateKeyFile)) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
if (!csrDer) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
var csrFile = path.join(outdir, domain + '.csr');
if (!safe.fs.writeFileSync(csrFile, csrFile)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
};
// TODO: download the chain in a loop following 'up' header
Acme.prototype.downloadChain = function (linkHeader, callback) {
if (!linkHeader) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
var linkInfo = parseLinks(linkHeader);
if (!linkInfo || !linkInfo.up) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
debug('downloadChain: downloading from %s', this.caOrigin + linkInfo.up);
superagent.get(this.caOrigin + linkInfo.up).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).end(function (error, result) {
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var chainDer = result.text;
var execSync = safe.child_process.execSync;
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
callback(null, chainPem);
});
};
Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof certUrl, 'string');
@@ -355,18 +385,22 @@ Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
var execSync = safe.child_process.execSync;
safe.fs.writeFileSync(path.join(outdir, domain + '.der'), certificateDer);
debug('downloadCertificate: cert der file saved');
debug('downloadCertificate: cert der file for %s saved', domain);
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
if (!certificatePem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, that.chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
that.downloadChain(result.header['link'], function (error, chainPem) {
if (error) return callback(error);
debug('downloadCertificate: cert file saved at %s', certificateFile);
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
callback();
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
callback();
});
});
};
@@ -414,21 +448,11 @@ Acme.prototype.getCertificate = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var certUrl = safe.fs.readFileSync(path.join(outdir, domain + '.url'), 'utf8');
var certificateGetter;
if (certUrl) {
debug('getCertificate: renewing existing cert for %s from %s', domain, certUrl);
certificateGetter = this.downloadCertificate.bind(this, domain, certUrl);
} else {
debug('getCertificate: start acme flow for %s from %s', domain, this.caOrigin);
certificateGetter = this.acmeFlow.bind(this, domain);
}
certificateGetter(function (error) {
debug('getCertificate: start acme flow for %s from %s', domain, this.caOrigin);
this.acmeFlow(domain, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
callback(null, path.join(outdir, domain + '.cert'), path.join(outdir, domain + '.key'));
});
};
@@ -1,27 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEqDCCA5CgAwIBAgIRAJgT9HUT5XULQ+dDHpceRL0wDQYJKoZIhvcNAQELBQAw
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
Ew5EU1QgUm9vdCBDQSBYMzAeFw0xNTEwMTkyMjMzMzZaFw0yMDEwMTkyMjMzMzZa
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAJzTDPBa5S5Ht3JdN4OzaGMw6tc1Jhkl4b2+NfFwki+3uEtB
BaupnjUIWOyxKsRohwuj43Xk5vOnYnG6eYFgH9eRmp/z0HhncchpDpWRz/7mmelg
PEjMfspNdxIknUcbWuu57B43ABycrHunBerOSuu9QeU2mLnL/W08lmjfIypCkAyG
dGfIf6WauFJhFBM/ZemCh8vb+g5W9oaJ84U/l4avsNwa72sNlRZ9xCugZbKZBDZ1
gGusSvMbkEl4L6KWTyogJSkExnTA0DHNjzE4lRa6qDO4Q/GxH8Mwf6J5MRM9LTb4
4/zyM2q5OTHFr8SNDR1kFjOq+oQpttQLwNh9w5MCAwEAAaOCAZIwggGOMBIGA1Ud
EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMH8GCCsGAQUFBwEBBHMwcTAy
BggrBgEFBQcwAYYmaHR0cDovL2lzcmcudHJ1c3RpZC5vY3NwLmlkZW50cnVzdC5j
b20wOwYIKwYBBQUHMAKGL2h0dHA6Ly9hcHBzLmlkZW50cnVzdC5jb20vcm9vdHMv
ZHN0cm9vdGNheDMucDdjMB8GA1UdIwQYMBaAFMSnsaR7LHH62+FLkHX/xBVghYkQ
MFQGA1UdIARNMEswCAYGZ4EMAQIBMD8GCysGAQQBgt8TAQEBMDAwLgYIKwYBBQUH
AgEWImh0dHA6Ly9jcHMucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcwPAYDVR0fBDUw
MzAxoC+gLYYraHR0cDovL2NybC5pZGVudHJ1c3QuY29tL0RTVFJPT1RDQVgzQ1JM
LmNybDATBgNVHR4EDDAKoQgwBoIELm1pbDAdBgNVHQ4EFgQUqEpqYwR93brm0Tm3
pkVl7/Oo7KEwDQYJKoZIhvcNAQELBQADggEBANHIIkus7+MJiZZQsY14cCoBG1hd
v0J20/FyWo5ppnfjL78S2k4s2GLRJ7iD9ZDKErndvbNFGcsW+9kKK/TnY21hp4Dd
ITv8S9ZYQ7oaoqs7HwhEMY9sibED4aXw09xrJZTC9zK1uIfW6t5dHQjuOWv+HHoW
ZnupyxpsEUlEaFb+/SCI4KCSBdAsYxAcsHYI5xxEI4LutHp6s3OT2FuO90WfdsIk
6q78OMSdn875bNjdBYAqxUp2/LEIHfDBkLoQz0hFJmwAbYahqKaLn73PAAm1X2kj
f1w8DdnkabOLGeOVcj9LQ+s67vBykx4anTjURkbqZslUEUsn2k5xeua2zUk=
-----END CERTIFICATE-----
+74 -29
View File
@@ -1,8 +1,7 @@
/* jslint node:true */
'use strict';
var acme = require('./cert/acme.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
caas = require('./cert/caas.js'),
@@ -11,12 +10,14 @@ var acme = require('./cert/acme.js'),
constants = require('./constants.js'),
debug = require('debug')('box:src/certificates'),
fs = require('fs'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
path = require('path'),
paths = require('./paths.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'),
@@ -88,7 +89,8 @@ function installAdminCertificate(callback) {
sysinfo.getIp(function (error, ip) {
if (error) return callback(error);
waitForDns(config.adminFqdn(), ip, config.fqdn(), function (error) {
var zoneName = tld.getDomain(config.fqdn());
waitForDns(config.adminFqdn(), ip, zoneName, function (error) {
if (error) return callback(error); // this cannot happen because we retry forever
ensureCertificate(config.adminFqdn(), function (error, certFilePath, keyFilePath) {
@@ -104,41 +106,84 @@ function installAdminCertificate(callback) {
});
}
function needsRenewalSync(certFilePath) {
var result = safe.child_process.execSync('openssl x509 -checkend %s -in %s', 60 * 60 * 24 * 5, certFilePath);
function isExpiringSync(certFilePath, hours) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof hours, 'number');
return result === null; // command errored
if (!fs.existsSync(certFilePath)) return 2; // not found
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
return result.status === 1; // 1 - expired 0 - not expired
}
function autoRenew(callback) {
debug('autoRenew: Checking certificates for renewal');
callback = callback || NOOP_CALLBACK;
var filenames = safe.fs.readdirSync(paths.APP_CERTS_DIR);
if (!filenames) {
debug('autoRenew: Error getting filenames: %s', safe.error.message);
return;
}
var certs = filenames.filter(function (f) {
return f.match(/\.cert$/) !== null && needsRenewalSync(path.join(paths.APP_CERTS_DIR, f));
});
debug('autoRenew: %j needs to be renewed', certs);
getApi(function (error, api, apiOptions) {
apps.getAll(function (error, allApps) {
if (error) return callback(error);
async.eachSeries(certs, function iterator(cert, iteratorCallback) {
var domain = cert.match(/^(.*)\.cert$/)[1];
if (domain === 'host') return iteratorCallback(); // cannot renew fallback cert
allApps.push({ location: 'my' }); // inject fake webadmin app
debug('autoRenew: renewing cert for %s with options %j', domain, apiOptions);
var expiringApps = [ ];
for (var i = 0; i < allApps.length; i++) {
var appDomain = 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');
api.getCertificate(domain, apiOptions, function (error) {
if (error) debug('autoRenew: could not renew cert for %s', domain, error);
if (!safe.fs.existsSync(keyFilePath)) {
debug('autoRenew: no existing key file for %s. skipping', appDomain);
continue;
}
iteratorCallback(); // move on to next cert
if (isExpiringSync(certFilePath, 24 * 30)) { // expired or not found
expiringApps.push(allApps[i]);
}
}
debug('autoRenew: %j needs to be renewed', expiringApps.map(function (a) { return config.appFqdn(a.location); }));
getApi(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 : '');
if (error) {
debug('autoRenew: could not renew cert for %s because %s', domain, error);
// check if we should fallback if we expire in the coming day
if (!isExpiringSync(certFilePath, 24 * 1)) return iteratorCallback();
debug('autoRenew: using fallback certs for %s since it expires soon', domain, error);
certFilePath = 'cert/host.cert';
keyFilePath = 'cert/host.key';
} else {
debug('autoRenew: certificate for %s renewed', domain);
}
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
var configureFunc = app.location === constants.ADMIN_LOCATION ?
nginx.configureAdmin.bind(null, certFilePath, keyFilePath)
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
iteratorCallback(); // move to next app
});
});
});
});
});
@@ -237,11 +282,11 @@ function ensureCertificate(domain, callback) {
if (fs.existsSync(userCertFilePath) && fs.existsSync(userKeyFilePath)) {
debug('ensureCertificate: %s. certificate already exists at %s', domain, userKeyFilePath);
if (!needsRenewalSync(userCertFilePath)) return callback(null, userCertFilePath, userKeyFilePath);
debug('ensureCertificate: %s cert require renewal', domain);
if (!isExpiringSync(userCertFilePath, 24 * 1)) return callback(null, userCertFilePath, userKeyFilePath);
}
debug('ensureCertificate: %s cert require renewal', domain);
getApi(function (error, api, apiOptions) {
if (error) return callback(error);
+121 -178
View File
@@ -1,5 +1,3 @@
/* jslint node: true */
'use strict';
exports = module.exports = {
@@ -16,29 +14,30 @@ exports = module.exports = {
updateToLatest: updateToLatest,
update: update,
reboot: reboot,
migrate: migrate,
backup: backup,
ensureBackup: ensureBackup,
retire: retire,
isConfiguredSync: isConfiguredSync,
checkDiskSpace: checkDiskSpace,
events: new (require('events').EventEmitter)(),
EVENT_ACTIVATED: 'activated',
EVENT_CONFIGURED: 'configured'
EVENT_CONFIGURED: 'configured',
EVENT_FIRST_RUN: 'firstrun'
};
var apps = require('./apps.js'),
AppsError = require('./apps.js').AppsError,
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
clientdb = require('./clientdb.js'),
config = require('./config.js'),
debug = require('debug')('box:cloudron'),
df = require('node-df'),
fs = require('fs'),
locker = require('./locker.js'),
mailer = require('./mailer.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
@@ -55,35 +54,19 @@ var apps = require('./apps.js'),
UserError = user.UserError,
userdb = require('./userdb.js'),
util = require('util'),
webhooks = require('./webhooks.js');
uuid = require('node-uuid');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'),
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'),
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update';
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update',
RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var gUpdatingDns = false, // flag for dns update reentrancy
gCloudronDetails = null, // cached cloudron details like region,size...
gAppstoreUserDetails = {},
gIsConfigured = null; // cached configured state so that return value is synchronous. null means we are not initialized yet
function debugApp(app, args) {
assert(!app || typeof app === 'object');
var prefix = app ? app.location : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function ignoreError(func) {
return function (callback) {
func(function (error) {
if (error) console.error('Ignored error:', error);
callback();
});
};
}
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -119,14 +102,33 @@ function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events.on(exports.EVENT_CONFIGURED, addDnsRecords);
exports.events.on(exports.EVENT_FIRST_RUN, installAppBundle);
syncConfigState(callback);
// check activation state for existing cloudrons that do not have first run file
// can be removed once cloudrons have been updated
isActivated(function (error, activated) {
if (error) return callback(error);
debug('initialize: cloudron %s activated', activated ? '' : 'not');
if (activated) fs.writeFileSync(paths.FIRST_RUN_FILE, 'been there, done that', 'utf8');
if (!fs.existsSync(paths.FIRST_RUN_FILE)) {
// EE API is sync. do not keep the server waiting
debug('initialize: emitting first run event');
process.nextTick(function () { exports.events.emit(exports.EVENT_FIRST_RUN); });
fs.writeFileSync(paths.FIRST_RUN_FILE, 'been there, done that', 'utf8');
}
syncConfigState(callback);
});
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events.removeListener(exports.EVENT_CONFIGURED, addDnsRecords);
exports.events.removeListener(exports.EVENT_FIRST_RUN, installAppBundle);
callback(null);
}
@@ -135,6 +137,15 @@ function isConfiguredSync() {
return gIsConfigured === true;
}
function isActivated(callback) {
user.getOwner(function (error) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
callback(null, true);
});
}
function isConfigured(callback) {
// set of rules to see if we have the configs required for cloudron to function
// note this checks for missing configs and not invalid configs
@@ -277,6 +288,7 @@ function getCloudronDetails(callback) {
if (result.statusCode !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
gCloudronDetails = result.body.box;
gAppstoreUserDetails = result.body.user;
return callback(null, gCloudronDetails);
});
@@ -318,6 +330,7 @@ function getConfig(callback) {
developerMode: developerMode,
region: result.region,
size: result.size,
billing: !!gAppstoreUserDetails.billing,
memory: os.totalmem(),
provider: config.provider(),
cloudronName: cloudronName
@@ -354,6 +367,7 @@ function readDkimPublicKeySync() {
return publicKey;
}
// NOTE: if you change the SPF record here, be sure the wait check in mailer.js
function txtRecordsWithSpf(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -367,16 +381,16 @@ function txtRecordsWithSpf(callback) {
for (i = 0; i < txtRecords.length; i++) {
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
validSpf = txtRecords[i].indexOf(' a:' + config.fqdn() + ' ') !== -1;
validSpf = txtRecords[i].indexOf(' a:' + config.adminFqdn() + ' ') !== -1;
break;
}
if (validSpf) return callback(null, null);
if (i == txtRecords.length) {
txtRecords[i] = '"v=spf1 a:' + config.fqdn() + ' ~all"';
txtRecords[i] = '"v=spf1 a:' + config.adminFqdn() + ' ~all"';
} else {
txtRecords[i] = '"v=spf1 a:' + config.fqdn() + ' ' + txtRecords[i].slice('"v=spf1 '.length);
txtRecords[i] = '"v=spf1 a:' + config.adminFqdn() + ' ' + txtRecords[i].slice('"v=spf1 '.length);
}
return callback(null, txtRecords);
@@ -403,7 +417,6 @@ function addDnsRecords() {
sysinfo.getIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
var webadminRecord = { subdomain: 'my', type: 'A', values: [ ip ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
@@ -415,6 +428,9 @@ function addDnsRecords() {
records.push(webadminRecord);
records.push(dkimRecord);
} else {
// for custom domains, we show a nakeddomain.html page
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
records.push(nakedDomainRecord);
records.push(webadminRecord);
records.push(dkimRecord);
@@ -454,49 +470,6 @@ function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
function migrate(size, region, callback) {
assert.strictEqual(typeof size, 'string');
assert.strictEqual(typeof region, 'string');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
function unlock(error) {
if (error) {
debug('Failed to migrate', error);
locker.unlock(locker.OP_MIGRATE);
} else {
debug('Migration initiated successfully');
// do not unlock; cloudron is migrating
}
return;
}
// initiate the migration in the background
backupBoxAndApps(function (error, restoreKey) {
if (error) return unlock(error);
debug('migrate: size %s region %s restoreKey %s', size, region, restoreKey);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
.query({ token: config.token() })
.send({ size: size, region: region, restoreKey: restoreKey })
.end(function (error, result) {
if (error && !error.response) return unlock(error);
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return unlock(null);
});
});
callback(null);
}
function update(boxUpdateInfo, callback) {
assert.strictEqual(typeof boxUpdateInfo, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -510,7 +483,12 @@ function update(boxUpdateInfo, callback) {
progress.set(progress.UPDATE, 0, 'Starting');
// initiate the update/upgrade but do not wait for it
if (boxUpdateInfo.upgrade) {
if (config.version().match(/[-+]/) !== null && config.version().replace(/[-+].*/, '') === boxUpdateInfo.version) {
doShortCircuitUpdate(boxUpdateInfo, function (error) {
if (error) debug('Short-circuit update failed', error);
locker.unlock(locker.OP_BOX_UPDATE);
});
} else if (boxUpdateInfo.upgrade) {
debug('Starting upgrade');
doUpgrade(boxUpdateInfo, function (error) {
if (error) {
@@ -541,6 +519,16 @@ function updateToLatest(callback) {
update(boxUpdateInfo, callback);
}
function doShortCircuitUpdate(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
debug('Starting short-circuit from prerelease version %s to release version %s', config.version(), boxUpdateInfo.version);
config.setVersion(boxUpdateInfo.version);
progress.clear(progress.UPDATE);
updateChecker.resetUpdateInfo();
callback();
}
function doUpgrade(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
@@ -551,7 +539,7 @@ function doUpgrade(boxUpdateInfo, callback) {
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
backupBoxAndApps(function (error) {
backups.backupBoxAndApps(function (error) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
@@ -564,8 +552,8 @@ function doUpgrade(boxUpdateInfo, callback) {
progress.set(progress.UPDATE, 10, 'Updating base system');
// no need to unlock since this is the last thing we ever do on this box
callback(null);
callback();
retire();
});
});
}
@@ -580,7 +568,7 @@ function doUpdate(boxUpdateInfo, callback) {
progress.set(progress.UPDATE, 5, 'Backing up for update');
backupBoxAndApps(function (error) {
backups.backupBoxAndApps(function (error) {
if (error) return updateError(error);
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
@@ -627,118 +615,73 @@ function doUpdate(boxUpdateInfo, callback) {
});
}
function backup(callback) {
assert.strictEqual(typeof callback, 'function');
function installAppBundle(callback) {
callback = callback || NOOP_CALLBACK;
var error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
var bundle = config.get('appBundle');
// ensure tools can 'wait' on progress
progress.set(progress.BACKUP, 0, 'Starting');
if (!bundle || bundle.length === 0) {
debug('installAppBundle: no bundle set');
return callback();
}
// start the backup operation in the background
backupBoxAndApps(function (error) {
if (error) console.error('backup failed.', error);
async.eachSeries(bundle, function (appInfo, iteratorCallback) {
var appstoreId = appInfo.appstoreId;
var parts = appstoreId.split('@');
locker.unlock(locker.OP_FULL_BACKUP);
var url = config.apiServerOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : '');
superagent.get(url).end(function (error, result) {
if (error && !error.response) return iteratorCallback(new Error('Network error: ' + error.message));
if (result.statusCode !== 200) return iteratorCallback(util.format('Failed to get app info from store.', result.statusCode, result.text));
debug('autoInstall: installing %s at %s', appstoreId, appInfo.location);
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);
});
}, function (error) {
if (error) debug('autoInstallApps: ', error);
callback();
});
callback(null);
}
function ensureBackup(callback) {
callback = callback || function () { };
function checkDiskSpace(callback) {
callback = callback || NOOP_CALLBACK;
backups.getAllPaged(1, 1, function (error, backups) {
debug('Checking disk space');
df(function (error, entries) {
if (error) {
debug('Unable to list backups', error);
return callback(error); // no point trying to backup if appstore is down
debug('df error %s', error.message);
mailer.outOfDiskSpace(error.message);
return callback();
}
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
debug('Previous backup was %j, no need to backup now', backups[0]);
return callback(null);
}
backup(callback);
});
}
function backupBoxWithAppBackupIds(appBackupIds, callback) {
assert(util.isArray(appBackupIds));
backups.getBackupUrl(null /* app */, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('backup: url %s', result.url);
async.series([
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
shell.sudo.bind(null, 'backupBox', [ BACKUP_BOX_CMD, result.url, result.backupKey, result.sessionToken ]),
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
], function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('backup: successful');
webhooks.backupDone(result.id, null /* app */, appBackupIds, function (error) {
if (error) return callback(error);
callback(null, result.id);
});
var oos = entries.some(function (entry) {
return (entry.mount === paths.DATA_DIR && entry.capacity >= 0.90) ||
(entry.mount === '/' && entry.used <= (1.25 * 1024 * 1024)); // 1.5G
});
debug('Disk space checked. ok: %s', !oos);
if (oos) mailer.outOfDiskSpace(JSON.stringify(entries, null, 4));
callback();
});
}
// this function expects you to have a lock
function backupBox(callback) {
apps.getAll(function (error, allApps) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
function retire(callback) {
callback = callback || NOOP_CALLBACK;
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);
});
var data = {
isCustomDomain: config.isCustomDomain(),
fqdn: config.fqdn()
};
shell.sudo('retire', [ RETIRE_CMD, JSON.stringify(data) ], callback);
}
// this function expects you to have a lock
function backupBoxAndApps(callback) {
callback = callback || function () { }; // callback can be empty for timer triggered backup
apps.getAll(function (error, allApps) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
var processed = 0;
var step = 100/(allApps.length+1);
progress.set(progress.BACKUP, processed, '');
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
++processed;
apps.backupApp(app, app.manifest.addons, function (error, backupId) {
if (error && error.reason !== AppsError.BAD_STATE) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
}
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
}, function appsBackedUp(error, backupIds) {
if (error) {
progress.set(progress.BACKUP, 100, error.message);
return callback(error);
}
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
backupBoxWithAppBackupIds(backupIds, function (error, restoreKey) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
callback(error, restoreKey);
});
});
});
}
+3 -3
View File
@@ -1,6 +1,6 @@
LoadPlugin "table"
<Plugin table>
<Table "/sys/fs/cgroup/memory/system.slice/docker.service/docker/<%= containerId %>/memory.stat">
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.stat">
Instance "<%= appId %>-memory"
Separator " \\n"
<Result>
@@ -10,7 +10,7 @@ LoadPlugin "table"
</Result>
</Table>
<Table "/sys/fs/cgroup/memory/system.slice/docker.service/docker/<%= containerId %>/memory.max_usage_in_bytes">
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.max_usage_in_bytes">
Instance "<%= appId %>-memory"
Separator "\\n"
<Result>
@@ -20,7 +20,7 @@ LoadPlugin "table"
</Result>
</Table>
<Table "/sys/fs/cgroup/cpuacct/system.slice/docker/<%= containerId %>/cpuacct.stat">
<Table "/sys/fs/cgroup/cpuacct/docker/<%= containerId %>/cpuacct.stat">
Instance "<%= appId %>-cpu"
Separator " \\n"
<Result>
+20 -13
View File
@@ -1,11 +1,7 @@
/* jslint node: true */
'use strict';
exports = module.exports = {
baseDir: baseDir,
dnsInSync: dnsInSync,
setDnsInSync: setDnsInSync,
// values set here will be lost after a upgrade/update. use the sqlite database
// for persistent values that need to be backed up
@@ -23,6 +19,7 @@ exports = module.exports = {
fqdn: fqdn,
token: token,
version: version,
setVersion: setVersion,
isCustomDomain: isCustomDomain,
database: database,
@@ -32,11 +29,12 @@ exports = module.exports = {
adminFqdn: adminFqdn,
appFqdn: appFqdn,
zoneName: zoneName,
adminEmail: adminEmail,
isDev: isDev,
// for testing resets to defaults
_reset: initConfig
_reset: _reset
};
var assert = require('assert'),
@@ -57,18 +55,18 @@ function baseDir() {
var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf');
function dnsInSync() {
return !!safe.fs.statSync(require('./paths.js').DNS_IN_SYNC_FILE);
}
function setDnsInSync(content) {
safe.fs.writeFileSync(require('./paths.js').DNS_IN_SYNC_FILE, content || 'if this file exists, dns is in sync');
}
function saveSync() {
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
}
function _reset (callback) {
safe.fs.unlinkSync(cloudronConfigFileName);
initConfig();
if (callback) callback();
}
function initConfig() {
// setup defaults
data.fqdn = 'localhost';
@@ -84,6 +82,7 @@ function initConfig() {
data.oauthProxyPort = 3003;
data.simpleAuthPort = 3004;
data.provider = 'caas';
data.appBundle = [ ];
if (exports.CLOUDRON) {
data.port = 3000;
@@ -139,6 +138,10 @@ function get(key) {
return safe.query(data, key);
}
function adminEmail() {
return get('adminEmail');
}
function apiServerOrigin() {
return get('apiServerOrigin');
}
@@ -179,6 +182,10 @@ function version() {
return get('version');
}
function setVersion(version) {
set('version', version);
}
function isCustomDomain() {
return get('isCustomDomain');
}
+3 -1
View File
@@ -7,6 +7,8 @@ exports = module.exports = {
ADMIN_NAME: 'Settings',
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
ADMIN_APPID: 'admin' // admin appid (settingsdb)
ADMIN_APPID: 'admin', // admin appid (settingsdb)
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024) // see also client.js
};
+12 -2
View File
@@ -7,6 +7,7 @@ exports = module.exports = {
var apps = require('./apps.js'),
assert = require('assert'),
backups = require('./backups.js'),
certificates = require('./certificates.js'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
@@ -25,7 +26,8 @@ var gAutoupdaterJob = null,
gCleanupTokensJob = null,
gDockerVolumeCleanerJob = null,
gSchedulerSyncJob = null,
gCertificateRenewJob = null;
gCertificateRenewJob = null,
gCheckDiskSpaceJob = null;
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
@@ -64,7 +66,15 @@ function recreateJobs(unusedTimeZone, callback) {
if (gBackupJob) gBackupJob.stop();
gBackupJob = new CronJob({
cronTime: '00 00 */4 * * *', // every 4 hours
onTick: cloudron.ensureBackup,
onTick: backups.ensureBackup,
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
if (gCheckDiskSpaceJob) gCheckDiskSpaceJob.stop();
gCheckDiskSpaceJob = new CronJob({
cronTime: '00 30 */4 * * *', // every 4 hours
onTick: cloudron.checkDiskSpace,
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
+2
View File
@@ -116,8 +116,10 @@ function clear(callback) {
async.series([
require('./appdb.js')._clear,
require('./authcodedb.js')._clear,
require('./backupdb.js')._clear,
require('./clientdb.js')._clear,
require('./tokendb.js')._clear,
require('./groupdb.js')._clear,
require('./userdb.js')._clear,
require('./settingsdb.js')._clear
], callback);
+1
View File
@@ -30,3 +30,4 @@ DatabaseError.INTERNAL_ERROR = 'Internal error';
DatabaseError.ALREADY_EXISTS = 'Entry already exist';
DatabaseError.NOT_FOUND = 'Record not found';
DatabaseError.BAD_FIELD = 'Invalid field';
DatabaseError.IN_USE = 'In Use';
+1 -1
View File
@@ -67,7 +67,7 @@ function issueDeveloperToken(user, callback) {
var token = tokendb.generateToken();
var expiresAt = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(token, tokendb.PREFIX_DEV + user.id, '', expiresAt, 'developer,apps,settings,users', function (error) {
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));
callback(null, { token: token, expiresAt: expiresAt });
+14 -12
View File
@@ -39,7 +39,8 @@ function getZoneByName(dnsConfig, zoneName, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.listHostedZones({}, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
var zone = result.HostedZones.filter(function (zone) {
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
@@ -84,11 +85,9 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'PriorRequestNotComplete') {
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
} else if (error) {
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
}
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error && error.code === 'PriorRequestNotComplete') return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
callback(null, result.ChangeInfo.Id);
});
@@ -131,7 +130,8 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.listResourceRecordSets(params, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
if (result.ResourceRecordSets.length === 0) return callback(null, [ ]);
if (result.ResourceRecordSets[0].Name !== params.StartRecordName && result.ResourceRecordSets[0].Type !== params.StartRecordType) return callback(null, [ ]);
@@ -175,21 +175,22 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
debug('del: resource record set not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
} else if (error && error.code === 'NoSuchHostedZone') {
debug('del: hosted zone not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
} else if (error && error.code === 'PriorRequestNotComplete') {
debug('del: resource is still busy', error);
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
} else if (error && error.code === 'InvalidChangeBatch') {
debug('del: invalid change batch. No such record to be deleted.');
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
} else if (error) {
debug('del: error', error);
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
}
callback(null);
@@ -206,6 +207,7 @@ function getChangeStatus(dnsConfig, changeId, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.getChange({ Id: changeId }, function (error, result) {
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error) return callback(error);
callback(null, result.ChangeInfo.Status);
+57 -18
View File
@@ -4,6 +4,7 @@ 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'),
@@ -23,7 +24,8 @@ exports = module.exports = {
deleteContainerByName: deleteContainer,
deleteImage: deleteImage,
deleteContainers: deleteContainers,
createSubcontainer: createSubcontainer
createSubcontainer: createSubcontainer,
getContainerIdByIp: getContainerIdByIp
};
function connectionInstance() {
@@ -156,7 +158,15 @@ function createSubcontainer(app, name, cmd, options, callback) {
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
}
var memoryLimit = manifest.memoryLimit || (developmentMode ? 0 : 1024 * 1024 * 200); // 200mb by default
// first check db record, then manifest
var memoryLimit = app.memoryLimit || manifest.memoryLimit;
// ensure we never go below minimum
memoryLimit = memoryLimit < constants.DEFAULT_MEMORY_LIMIT ? constants.DEFAULT_MEMORY_LIMIT : memoryLimit; // 256mb by default
// developerMode does not restrict memory usage
memoryLimit = developmentMode ? 0 : memoryLimit;
// for subcontainers, this should ideally be false. but docker does not allow network sharing if the app container is not running
// this means cloudron exec does not work
var isolatedNetworkNs = true;
@@ -329,24 +339,53 @@ function deleteImage(manifest, callback) {
var docker = exports.connection;
docker.getImage(dockerImage).inspect(function (error, result) {
var removeOptions = {
force: false, // might be shared with another instance of this app
noprune: false // delete untagged parents
};
// registry v1 used to pull down all *tags*. this meant that deleting image by tag was not enough (since that
// just removes the tag). we used to remove the image by id. this is not required anymore because aliases are
// not created anymore after https://github.com/docker/docker/pull/10571
docker.getImage(dockerImage).remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null);
if (error && error.statusCode === 409) return callback(null); // another container using the image
if (error) return callback(error);
if (error) debug('Error removing image %s : %j', dockerImage, error);
var removeOptions = {
force: true,
noprune: false
};
// delete image by id because 'docker pull' pulls down all the tags and this is the only way to delete all tags
docker.getImage(result.Id).remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null);
if (error && error.statusCode === 409) return callback(null); // another container using the image
if (error) debug('Error removing image %s : %j', dockerImage, error);
callback(error);
});
callback(error);
});
}
function getContainerIdByIp(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
debug('get container by ip %s', ip);
var docker = exports.connection;
docker.listNetworks({}, function (error, result) {
if (error) return callback(error);
var bridge;
result.forEach(function (n) {
if (n.Name === 'bridge') bridge = n;
});
if (!bridge) return callback(new Error('Unable to find the bridge network'));
var containerId;
for (var id in bridge.Containers) {
if (bridge.Containers[id].IPv4Address.indexOf(ip) === 0) {
containerId = id;
break;
}
}
if (!containerId) return callback(new Error('No container with that ip'));
debug('found container %s with ip %s', containerId, ip);
callback(null, containerId);
});
}
+202
View File
@@ -0,0 +1,202 @@
'use strict';
exports = module.exports = {
get: get,
getWithMembers: getWithMembers,
getAll: getAll,
add: add,
del: del,
count: count,
getMembers: getMembers,
addMember: addMember,
removeMember: removeMember,
isMember: isMember,
getGroups: getGroups,
setGroups: setGroups,
_clear: clear
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror');
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
function get(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups WHERE id = ?', [ groupId ], 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, result[0]);
});
}
function getWithMembers(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' WHERE groups.id = ? ' +
' GROUP BY groups.id', [ groupId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
var result = results[0];
result.userIds = result.userIds ? result.userIds.split(',') : [ ];
callback(null, result);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function add(id, name, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
var data = [ id, name ];
database.query('INSERT INTO groups (id, name) 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));
callback(null);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
// also cleanup the groupMembers table
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(error);
});
}
function count(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM groups', 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 groupMembers', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('DELETE FROM groups WHERE id != ?', [ 'admin' ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(error);
});
});
}
function getMembers(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT userId FROM groupMembers WHERE groupId=?', [ groupId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
// if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); // need to differentiate group with no members and invalid groupId
callback(error, result.map(function (r) { return r.userId; }));
});
}
function getGroups(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT groupId FROM groupMembers WHERE userId=? ORDER BY groupId', [ userId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
// if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); // need to differentiate group with no members and invalid groupId
callback(error, result.map(function (r) { return r.groupId; }));
});
}
function setGroups(userId, groupIds, callback) {
assert.strictEqual(typeof userId, 'string');
assert(Array.isArray(groupIds));
assert.strictEqual(typeof callback, 'function');
var queries = [ ];
queries.push({ query: 'DELETE from groupMembers WHERE userId = ?', args: [ userId ] });
groupIds.forEach(function (gid) {
queries.push({ query: 'INSERT INTO groupMembers (groupId, userId) VALUES (? , ?)', args: [ gid, userId ] });
});
database.transaction(queries, function (error) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, error.message));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function addMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ groupId, userId ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function removeMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM groupMembers WHERE groupId = ? AND userId = ?', [ groupId, userId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
function isMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT 1 FROM groupMembers WHERE groupId=? AND userId=?', [ groupId, userId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, result.length !== 0);
});
}
+210
View File
@@ -0,0 +1,210 @@
/* jshint node:true */
'use strict';
exports = module.exports = {
GroupError: GroupError,
create: create,
remove: remove,
get: get,
getWithMembers: getWithMembers,
getAll: getAll,
getMembers: getMembers,
addMember: addMember,
removeMember: removeMember,
isMember: isMember,
getGroups: getGroups,
setGroups: setGroups,
ADMIN_GROUP_ID: 'admin' // see db migration code and groupdb._clear
};
var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
groupdb = require('./groupdb.js'),
util = require('util');
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function GroupError(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(GroupError, Error);
GroupError.INTERNAL_ERROR = 'Internal Error';
GroupError.ALREADY_EXISTS = 'Already Exists';
GroupError.NOT_FOUND = 'Not Found';
GroupError.BAD_NAME = 'Bad name';
GroupError.NOT_EMPTY = 'Not Empty';
GroupError.NOT_ALLOWED = 'Not Allowed';
function validateGroupname(name) {
assert.strictEqual(typeof name, 'string');
var RESERVED = [ 'admins', 'users' ]; // ldap code uses 'users' pseudo group
if (name.length <= 2) return new GroupError(GroupError.BAD_NAME, 'name must be atleast 2 chars');
if (name.length >= 200) return new GroupError(GroupError.BAD_NAME, 'name too long');
if (!/^[A-Za-z0-9_-]*$/.test(name)) return new GroupError(GroupError.BAD_NAME, 'name can only have A-Za-z0-9_-');
if (RESERVED.indexOf(name) !== -1) return new GroupError(GroupError.BAD_NAME, 'name is reserved');
return null;
}
function create(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
var error = validateGroupname(name);
if (error) return callback(error);
groupdb.add(name /* id */, name, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new GroupError(GroupError.ALREADY_EXISTS));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
callback(null, { id: name, name: name });
});
}
function remove(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
// never allow admin group to be deleted
if (id === exports.ADMIN_GROUP_ID) return callback(new GroupError(GroupError.NOT_ALLOWED));
groupdb.del(id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
callback(null);
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.get(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
return callback(null, result);
});
}
function getWithMembers(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.getWithMembers(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
return callback(null, result);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getAll(function (error, result) {
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
return callback(null, result);
});
}
function getMembers(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.getMembers(groupId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
return callback(null, result);
});
}
function getGroups(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.getGroups(userId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
return callback(null, result);
});
}
function setGroups(userId, groupIds, callback) {
assert.strictEqual(typeof userId, 'string');
assert(Array.isArray(groupIds));
assert.strictEqual(typeof callback, 'function');
groupdb.setGroups(userId, groupIds, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
return callback(null);
});
}
function addMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.addMember(groupId, userId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
return callback(null);
});
}
function removeMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.removeMember(groupId, userId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
return callback(null);
});
}
function isMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.isMember(groupId, userId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
return callback(null, result);
});
}
+65 -11
View File
@@ -6,6 +6,7 @@ exports = module.exports = {
};
var assert = require('assert'),
apps = require('./apps.js'),
config = require('./config.js'),
debug = require('debug')('box:ldap'),
user = require('./user.js'),
@@ -28,15 +29,25 @@ var gLogger = {
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
function getAppByRequest(req, callback) {
var sourceIp = req.connection.ldap.id.split(':')[0];
if (sourceIp.split('.').length !== 4) return callback(new ldap.InsufficientAccessRightsError('Missing source identifier'));
apps.getByIpAddress(sourceIp, function (error, app) {
// we currently allow access in case we can't find the source app
callback(null, app || null);
});
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
gServer = ldap.createServer({ log: gLogger });
gServer.search('ou=users,dc=cloudron', function (req, res, next) {
debug('ldap user search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString());
debug('user search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString());
user.list(function (error, result){
user.list(function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
// send user objects
@@ -46,6 +57,11 @@ function start(callback) {
var groups = [ GROUP_USERS_DN ];
if (entry.admin) groups.push(GROUP_ADMINS_DN);
var displayName = entry.displayName || entry.username;
var nameParts = displayName.split(' ');
var firstName = nameParts[0];
var lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; // choose last part, if it exists
var tmp = {
dn: dn.toString(),
attributes: {
@@ -54,14 +70,19 @@ function start(callback) {
cn: entry.id,
uid: entry.id,
mail: entry.email,
displayname: entry.displayName || entry.username,
displayname: displayName,
givenName: firstName,
sn: lastName,
username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
memberof: groups
}
};
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
// 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);
}
});
@@ -71,7 +92,7 @@ function start(callback) {
});
gServer.search('ou=groups,dc=cloudron', function (req, res, next) {
debug('ldap group search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString());
debug('group search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString());
user.list(function (error, result){
if (error) return next(new ldap.OperationsError(error.toString()));
@@ -97,7 +118,10 @@ function start(callback) {
}
};
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
// 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);
}
});
@@ -108,21 +132,51 @@ function start(callback) {
gServer.bind('ou=apps,dc=cloudron', function(req, res, next) {
// TODO: validate password
debug('ldap application bind: %s', req.dn.toString());
debug('application bind: %s', req.dn.toString());
res.end();
});
gServer.bind('ou=users,dc=cloudron', function(req, res, next) {
debug('ldap user bind: %s', req.dn.toString());
debug('user bind: %s', req.dn.toString());
if (!req.dn.rdns[0].cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
// extract the common name which might have different attribute names
var attributeName = Object.keys(req.dn.rdns[0])[0];
var commonName = req.dn.rdns[0][attributeName];
if (!commonName) return next(new ldap.NoSuchObjectError(req.dn.toString()));
user.verify(req.dn.rdns[0].cn, req.credentials || '', function (error, result) {
var api;
// if mail is specified, enforce mail check
if (commonName.indexOf('@') !== -1 || attributeName === 'mail') {
api = user.verifyWithEmail;
} else if (commonName.indexOf('uid-') === 0) {
api = user.verify;
} else {
api = user.verifyWithUsername;
}
// TODO this should be done after we verified the app has access to avoid leakage of user existence
api(commonName, req.credentials || '', function (error, userObject) {
if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error));
res.end();
getAppByRequest(req, function (error, app) {
if (error) return next(error);
if (!app) {
debug('no app found for this container, allow access');
return res.end();
}
apps.hasAccessTo(app, userObject, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
res.end();
});
});
});
});
+4 -1
View File
@@ -2,10 +2,13 @@
Dear Admin,
A new version of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
The app will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
Changes:
<%= updateInfo.manifest.changelog %>
Thank you,
your Cloudron
+1 -1
View File
@@ -2,7 +2,7 @@
Dear Admin,
A new version of Cloudron <%= fqdn %> is available!
Version <%= newBoxVersion %> of Cloudron <%= fqdn %> is now available!
Your Cloudron will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
@@ -0,0 +1,16 @@
<%if (format === 'text') { %>
Dear Cloudron Team,
<% if (message) { %>
<%= domain %> was not renewed.
<%- message %>
<% } else { %>
<%= domain %> was renewed.
<% } %>
Thank you,
Your Cloudron
<% } else { %>
<% } %>
+19
View File
@@ -0,0 +1,19 @@
<%if (format === 'text') { %>
Dear Cloudron Team,
<%= fqdn %> is running out of disk space.
Please see some excerpts of the logs below.
Thank you,
Your Cloudron
-------------------------------------
<%- message %>
<% } else { %>
<% } %>
+1 -1
View File
@@ -1,6 +1,6 @@
<%if (format === 'text') { %>
Dear <%= username %>,
Dear <%= user.username || user.email %>,
Someone, hopefully you, has requested your <%= fqdn %>'s account password
be reset. If you did not request this reset, please ignore this message.
+5 -5
View File
@@ -2,16 +2,16 @@
Dear Admin,
User with name '<%= username %>' (<%= email %>) was added in the Cloudron at <%= fqdn %>.
User with name <%= user.email %> was added in the Cloudron at <%= fqdn %>.
You are receiving this email because you are an Admin of the Cloudron at <%= fqdn %>.
<% if (inviteLink) { %>
This user was not invited immediately, he has to get invited manually later, using the "send invite" button in the admin panel.
To perform any configuration on behalf of the user, please use this link
As requested, this user has not been sent an invitation email.
To set a password and perform any configuration on behalf of the user, please use this link:
<%= inviteLink %>
It allows to setup a temporary password, which the user will be able to override, once he gets invited.
This link will become invalid as soon as the user was invited.
<% } %>
Thank you,
+1 -1
View File
@@ -2,7 +2,7 @@
Dear Admin,
User with name '<%= username %>' (<%= email %>) <%= event %> in the Cloudron at <%= fqdn %>.
User <%= user.username %> <%= user.email %> <%= event %> in the Cloudron at <%= fqdn %>.
You are receiving this email because you are an Admin of the Cloudron at <%= fqdn %>.
+2 -7
View File
@@ -1,20 +1,15 @@
<%if (format === 'text') { %>
Dear <%= user.username %>,
Dear <%= user.email %>,
Welcome to my Cloudron <%= fqdn %>!
Welcome to our Cloudron <%= fqdn %>!
The Cloudron is our own Smart Server. You can read more about it
at https://www.cloudron.io.
You username is '<%= user.username %>'
To get started, create your account by visiting the following page:
<%= setupLink %>
When you visit the above page, you will be prompted to enter a new password.
After you have submitted the form, you can login using the new password.
<% if (invitor && invitor.email) { %>
Thank you,
<%= invitor.email %>
+72 -33
View File
@@ -1,5 +1,3 @@
/* jslint node: true */
'use strict';
exports = module.exports = {
@@ -18,9 +16,14 @@ exports = module.exports = {
appDied: appDied,
outOfDiskSpace: outOfDiskSpace,
certificateRenewed: certificateRenewed,
FEEDBACK_TYPE_FEEDBACK: 'feedback',
FEEDBACK_TYPE_TICKET: 'ticket',
FEEDBACK_TYPE_APP: 'app',
FEEDBACK_TYPE_APP_MISSING: 'app_missing',
FEEDBACK_TYPE_APP_ERROR: 'app_error',
sendFeedback: sendFeedback,
_getMailQueue: _getMailQueue,
@@ -105,10 +108,11 @@ function getTxtRecords(callback) {
});
}
// keep this in sync with the cloudron.js dns changes
function checkDns() {
getTxtRecords(function (error, records) {
if (error || !records) {
debug('checkDns: DNS error or no records looking up TXT records for %s %s', config.fqdn(), error, records);
debug('checkDns: DNS error or no records looking up TXT records for %s %s', config.adminFqdn(), error, records);
gCheckDnsTimerId = setTimeout(checkDns, 60000);
return;
}
@@ -118,7 +122,7 @@ function checkDns() {
for (var i = 0; i < records.length; i++) {
if (records[i].indexOf('v=spf1 ') !== 0) continue; // not SPF
allowedToSendMail = records[i].indexOf('a:' + config.fqdn()) !== -1;
allowedToSendMail = records[i].indexOf('a:' + config.adminFqdn()) !== -1;
break; // only one SPF record can exist (https://support.google.com/a/answer/4568483?hl=en)
}
@@ -174,8 +178,8 @@ function sendMails(queue) {
function enqueue(mailOptions) {
assert.strictEqual(typeof mailOptions, 'object');
if (!mailOptions.from) console.error('from is missing');
if (!mailOptions.to) console.error('to is missing');
if (!mailOptions.from) console.error('sender address is missing');
if (!mailOptions.to) console.error('recipient address is missing');
debug('Queued mail for ' + mailOptions.from + ' to ' + mailOptions.to);
gMailQueue.push(mailOptions);
@@ -194,6 +198,8 @@ function getAdminEmails(callback) {
users.getAllAdmins(function (error, admins) {
if (error) return callback(error);
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
var adminEmails = [ ];
admins.forEach(function (admin) { adminEmails.push(admin.email); });
@@ -211,10 +217,10 @@ function mailUserEventToAdmins(user, event) {
adminEmails = _.difference(adminEmails, [ user.email ]);
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: adminEmails.join(', '),
subject: util.format('%s %s in Cloudron %s', user.username, event, config.fqdn()),
text: render('user_event.ejs', { fqdn: config.fqdn(), username: user.username, email: user.email, event: event, format: 'text' }),
subject: util.format('%s %s in Cloudron %s', user.username || user.email, event, config.fqdn()),
text: render('user_event.ejs', { fqdn: config.fqdn(), user: user, event: event, format: 'text' }),
};
enqueue(mailOptions);
@@ -230,14 +236,14 @@ function sendInvite(user, invitor) {
var templateData = {
user: user,
webadminUrl: config.adminOrigin(),
setupLink: config.adminOrigin() + '/api/v1/session/password/setup.html?reset_token=' + user.resetToken,
setupLink: config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
format: 'text',
fqdn: config.fqdn(),
invitor: invitor
};
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: user.email,
subject: util.format('Welcome to Cloudron %s', config.fqdn()),
text: render('welcome_user.ejs', templateData)
@@ -257,47 +263,48 @@ function userAdded(user, inviteSent) {
adminEmails = _.difference(adminEmails, [ user.email ]);
var inviteLink = inviteSent ? null : config.adminOrigin() + '/api/v1/session/password/setup.html?reset_token=' + user.resetToken;
var inviteLink = inviteSent ? null : config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken;
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: adminEmails.join(', '),
subject: util.format('%s added in Cloudron %s', user.username, config.fqdn()),
text: render('user_added.ejs', { fqdn: config.fqdn(), username: user.username, email: user.email, inviteLink: inviteLink, format: 'text' }),
subject: util.format('%s added in Cloudron %s', user.email, config.fqdn()),
text: render('user_added.ejs', { fqdn: config.fqdn(), user: user, inviteLink: inviteLink, format: 'text' }),
};
enqueue(mailOptions);
});
}
function userRemoved(username) {
assert.strictEqual(typeof username, 'string');
function userRemoved(user) {
assert.strictEqual(typeof user, 'object');
debug('Sending mail for userRemoved');
debug('Sending mail for userRemoved.', user.id, user.email);
mailUserEventToAdmins({ username: username }, 'was removed');
mailUserEventToAdmins(user, 'was removed');
}
function adminChanged(user) {
function adminChanged(user, admin) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof admin, 'boolean');
debug('Sending mail for adminChanged');
mailUserEventToAdmins(user, user.admin ? 'is now an admin' : 'is no more an admin');
mailUserEventToAdmins(user, admin ? 'is now an admin' : 'is no more an admin');
}
function passwordReset(user) {
assert.strictEqual(typeof user, 'object');
debug('Sending mail for password reset for user %s.', user.username);
debug('Sending mail for password reset for user %s.', user.email, user.id);
var resetLink = config.adminOrigin() + '/api/v1/session/password/reset.html?reset_token=' + user.resetToken;
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: user.email,
subject: 'Password Reset Request',
text: render('password_reset.ejs', { fqdn: config.fqdn(), username: user.username, resetLink: resetLink, format: 'text' })
text: render('password_reset.ejs', { fqdn: config.fqdn(), user: user, resetLink: resetLink, format: 'text' })
};
enqueue(mailOptions);
@@ -312,7 +319,7 @@ function appDied(app) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: adminEmails.concat('support@cloudron.io').join(', '),
subject: util.format('App %s is down', app.location),
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: config.appFqdn(app.location), format: 'text' })
@@ -330,7 +337,7 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: adminEmails.join(', '),
subject: util.format('%s has a new update available', config.fqdn()),
text: render('box_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), newBoxVersion: newBoxVersion, changelog: changelog, format: 'text' })
@@ -348,16 +355,43 @@ function appUpdateAvailable(app, updateInfo) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: adminEmails.join(', '),
subject: util.format('%s has a new update available', app.fqdn),
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, format: 'text' })
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, updateInfo: updateInfo, format: 'text' })
};
enqueue(mailOptions);
});
}
function outOfDiskSpace(message) {
assert.strictEqual(typeof message, 'string');
var mailOptions = {
from: config.adminEmail(),
to: 'admin@cloudron.io',
subject: util.format('[%s] Out of disk space alert', config.fqdn()),
text: render('out_of_disk_space.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
};
sendMails([ mailOptions ]);
}
function certificateRenewed(domain, message) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof message, 'string');
var mailOptions = {
from: config.adminEmail(),
to: 'admin@cloudron.io',
subject: util.format('[%s] Certificate was %s renewed', domain, message ? 'not' : ''),
text: render('certificate_renewed.ejs', { domain: domain, message: message, format: 'text' })
};
sendMails([ mailOptions ]);
}
// 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) {
@@ -365,7 +399,7 @@ function sendCrashNotification(program, context) {
assert.strictEqual(typeof context, 'string');
var mailOptions = {
from: config.get('adminEmail'),
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' })
@@ -380,10 +414,13 @@ function sendFeedback(user, type, subject, description) {
assert.strictEqual(typeof subject, 'string');
assert.strictEqual(typeof description, 'string');
assert(type === exports.FEEDBACK_TYPE_TICKET || type === exports.FEEDBACK_TYPE_FEEDBACK || type === exports.FEEDBACK_TYPE_APP);
assert(type === exports.FEEDBACK_TYPE_TICKET ||
type === exports.FEEDBACK_TYPE_FEEDBACK ||
type === exports.FEEDBACK_TYPE_APP_MISSING ||
type === exports.FEEDBACK_TYPE_APP_ERROR);
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: 'support@cloudron.io',
subject: util.format('[%s] %s - %s', type, config.fqdn(), subject),
text: render('feedback.ejs', { fqdn: config.fqdn(), type: type, user: user, subject: subject, description: description, format: 'text'})
@@ -396,6 +433,8 @@ function _getMailQueue() {
return gMailQueue;
}
function _clearMailQueue() {
function _clearMailQueue(callback) {
gMailQueue = [];
if (callback) callback();
}
+16 -1
View File
@@ -13,6 +13,7 @@ var assert = require('assert'),
shell = require('./shell.js');
exports = module.exports = {
requiresOAuthProxy: requiresOAuthProxy,
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
@@ -22,6 +23,19 @@ exports = module.exports = {
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
function requiresOAuthProxy(app) {
assert.strictEqual(typeof app, 'object');
var tmp = app.accessRestriction;
// if no accessRestriction set, or the app uses one of the auth modules, we do not need the oauth proxy
if (tmp === null) return false;
if (app.manifest.addons['ldap'] || app.manifest.addons['oauth'] || app.manifest.addons['simpleauth']) return false;
// check if any restrictions are set
return !!((tmp.users && tmp.users.length) || (tmp.groups && tmp.groups.length));
}
function configureAdmin(certFilePath, keyFilePath, callback) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
@@ -50,7 +64,8 @@ function configureApp(app, certFilePath, keyFilePath, callback) {
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = app.oauthProxy ? 'oauthproxy' : 'app';
var oauthProxy = requiresOAuthProxy(app);
var endpoint = oauthProxy ? 'oauthproxy' : 'app';
var vhost = config.appFqdn(app.location);
var data = {
+68
View File
@@ -0,0 +1,68 @@
<% include header %>
<!-- tester -->
<script>
'use strict';
// very basic angular app
var app = angular.module('Application', []);
app.controller('Controller', [function () {}]);
</script>
<center>
<br/>
<h4>Hello <%= (user && user.email) ? user.email : '' %>, welcome to your Cloudron.</h4>
<h2>Setup your account and password.</h2>
</center>
<div class="container" ng-app="Application" ng-controller="Controller">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form action="/api/v1/session/account/setup" method="post" name="setupForm" autocomplete="off" role="form" novalidate>
<input type="password" style="display: none;">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
<center><p class="has-error"><%= error %></p></center>
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) }">
<label class="control-label">Username</label>
<div class="control-label" ng-show="setupForm.username.$dirty && setupForm.username.$invalid">
<small ng-show="setupForm.username.$error.minlength">The username is too short</small>
<small ng-show="setupForm.username.$error.maxlength">The username is too long</small>
<small ng-show="setupForm.username.$dirty && setupForm.username.$invalid">Not a valid username</small>
</div>
<input type="text" class="form-control" ng-model="username" name="username" ng-maxlength="512" ng-minlength="3" required autofocus>
</div>
<div class="form-group">
<label class="control-label">Display Name</label>
<input type="displayName" class="form-control" ng-model="displayName" name="displayName" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.password.$dirty && setupForm.password.$invalid) }">
<label class="control-label">New Password</label>
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
</div>
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
<label class="control-label">Repeat Password</label>
<div class="control-label" ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
<small ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
</div>
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
</form>
</div>
</div>
</div>
<% include footer %>
+3 -4
View File
@@ -8,9 +8,11 @@
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<!-- Theme CSS -->
<link href="<%= adminOrigin %>/theme.css" rel="stylesheet">
<!-- Custom Fonts -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
<!-- jQuery-->
<script src="<%= adminOrigin %>/3rdparty/js/jquery.min.js"></script>
@@ -22,9 +24,6 @@
<script src="<%= adminOrigin %>/3rdparty/js/angular.min.js"></script>
<script src="<%= adminOrigin %>/3rdparty/js/angular-loader.min.js"></script>
<!-- Theme CSS -->
<link href="<%= adminOrigin %>/theme.css" rel="stylesheet">
</head>
<body class="oauth">
+2 -1
View File
@@ -13,13 +13,14 @@ app.controller('Controller', [function () {}]);
</script>
<center>
<h1>Hello <%= user.username %> create a new password</h1>
<h1>Hello <%= user.username %>, set a new password</h1>
</center>
<div class="container" ng-app="Application" ng-controller="Controller">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form action="/api/v1/session/password/reset" method="post" name="resetForm" autocomplete="off" role="form" novalidate>
<input type="password" style="display: none;">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
+3 -3
View File
@@ -3,14 +3,14 @@
<!-- tester -->
<center>
<h1>Reset your password successful</h1>
<h1>Password reset successful</h1>
</center>
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<p>An email was sent to you with a link to create a new password.</p>
If you have not received any email after some time, maybe you have misspelled your email address, simply try again <a href="/api/v1/session/password/resetRequest.html">here</a>.
<p>An email was sent to you with a link to set a new password.</p>
If you have not received any email, simply <a href="/api/v1/session/password/resetRequest.html">try again</a>.
</div>
</div>
</div>
-46
View File
@@ -1,46 +0,0 @@
<% include header %>
<!-- tester -->
<script>
'use strict';
// very basic angular app
var app = angular.module('Application', []);
app.controller('Controller', [function () {}]);
</script>
<center>
<h1>Hello <%= user.username %> create a password</h1>
</center>
<div class="container" ng-app="Application" ng-controller="Controller">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form action="/api/v1/session/password/reset" method="post" name="setupForm" autocomplete="off" role="form" novalidate>
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
<div class="form-group" ng-class="{ 'has-error': setupForm.password.$dirty && setupForm.password.$invalid }">
<label class="control-label" for="inputPassword">New Password</label>
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
</div>
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-maxlength="30" ng-minlength="8" autofocus required>
</div>
<div class="form-group" ng-class="{ 'has-error': setupForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
<div class="control-label" ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
<small ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
</div>
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
</form>
</div>
</div>
</div>
<% include footer %>
+1 -1
View File
@@ -126,7 +126,7 @@ function authenticate(req, res, next) {
clientdb.getByAppIdAndType(result.id, clientdb.TYPE_PROXY, function (error, result) {
if (error) {
console.error('Unkonwn OAuth client.', error);
console.error('Unknown OAuth client.', error);
return res.send(500, 'Unknown OAuth client.');
}
+1 -4
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
var config = require('./config.js'),
@@ -13,8 +11,6 @@ exports = module.exports = {
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'data/addons'),
DNS_IN_SYNC_FILE: path.join(config.baseDir(), 'data/dns_in_sync'),
COLLECTD_APPCONFIG_DIR: path.join(config.baseDir(), 'data/collectd/collectd.conf.d'),
DATA_DIR: path.join(config.baseDir(), 'data'),
@@ -26,6 +22,7 @@ exports = module.exports = {
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/avatar.png'),
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
FIRST_RUN_FILE: path.join(config.baseDir(), 'data/box/first_run'),
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json'),
+19 -10
View File
@@ -44,12 +44,12 @@ function removeInternalAppFields(app) {
health: app.health,
location: app.location,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy,
lastBackupId: app.lastBackupId,
manifest: app.manifest,
portBindings: app.portBindings,
iconUrl: app.iconUrl,
fqdn: app.fqdn
fqdn: app.fqdn,
memoryLimit: app.memoryLimit
};
}
@@ -76,7 +76,10 @@ function getAppBySubdomain(req, res, next) {
}
function getApps(req, res, next) {
apps.getAll(function (error, allApps) {
assert.strictEqual(typeof req.user, 'object');
var func = req.user.admin ? apps.getAll : apps.getAllByUser.bind(null, req.user);
func(function (error, allApps) {
if (error) return next(new HttpError(500, error));
allApps = allApps.map(removeInternalAppFields);
@@ -116,19 +119,19 @@ function installApp(req, res, next) {
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required'));
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
if (data.cert && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (data.key && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
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'));
// 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 oauthproxy:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.oauthProxy, data.manifest);
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.oauthProxy, data.icon || null, data.cert || null, data.key || null, 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, 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.'));
@@ -160,15 +163,15 @@ function configureApp(req, res, next) {
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required'));
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
if (data.cert && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (data.key && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
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'));
debug('Configuring app id:%s location:%s bindings:%j accessRestriction:%j oauthProxy:%s', req.params.id, data.location, data.portBindings, data.accessRestriction, data.oauthProxy);
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.oauthProxy, data.cert || null, data.key || null, function (error) {
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.cert || null, data.key || null, data.memoryLimit || 0, 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.'));
@@ -378,7 +381,13 @@ function exec(req, res, next) {
function listBackups(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.listBackups(req.params.id, function (error, result) {
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'));
apps.listBackups(page, perPage, req.params.id, function (error, result) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
+23 -10
View File
@@ -1,22 +1,25 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
get: get,
create: create
create: create,
download: download
};
var backups = require('../backups.js'),
var assert = require('assert'),
backups = require('../backups.js'),
BackupsError = require('../backups.js').BackupsError,
cloudron = require('../cloudron.js'),
CloudronError = require('../cloudron.js').CloudronError,
debug = require('debug')('box:routes/backups'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function get(req, res, next) {
backups.getAllPaged(1, 5, function (error, result) {
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'));
backups.getPaged(page, perPage, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
@@ -27,10 +30,20 @@ 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
cloudron.backup(function (error) {
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
backups.backup(function (error) {
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
function download(req, res, next) {
assert.strictEqual(typeof req.params.backupId, 'string');
backups.getRestoreUrl(req.params.backupId, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
}
+4 -16
View File
@@ -10,7 +10,6 @@ exports = module.exports = {
getProgress: getProgress,
getConfig: getConfig,
update: update,
migrate: migrate,
feedback: feedback
};
@@ -129,24 +128,13 @@ function update(req, res, next) {
});
}
function migrate(req, res, next) {
if (typeof req.body.size !== 'string') return next(new HttpError(400, 'size must be string'));
if (typeof req.body.region !== 'string') return next(new HttpError(400, 'region must be string'));
debug('Migration requested', req.body.size, req.body.region);
cloudron.migrate(req.body.size, req.body.region, function (error) {
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK && req.body.type !== mailer.FEEDBACK_TYPE_TICKET && req.body.type !== mailer.FEEDBACK_TYPE_APP) return next(new HttpError(400, 'type must be either "ticket", "feedback" or "app"'));
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"'));
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'));
+1 -1
View File
@@ -53,4 +53,4 @@ function apps(req, res, next) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { apps: result }));
});
}
}
+67
View File
@@ -0,0 +1,67 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
get: get,
list: list,
create: create,
remove: remove
};
var assert = require('assert'),
groups = require('../groups.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
groups = require('../groups.js'),
GroupError = groups.GroupError;
function create(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be string'));
groups.create(req.body.name, function (error, group) {
if (error && error.reason === GroupError.BAD_NAME) return next(new HttpError(400, error.message));
if (error && error.reason === GroupError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
if (error) return next(new HttpError(500, error));
var groupInfo = {
id: group.id,
name: group.name
};
next(new HttpSuccess(201, groupInfo));
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.groupId, 'string');
groups.getWithMembers(req.params.groupId, function (error, result) {
if (error && error.reason === GroupError.NOT_FOUND) return next(new HttpError(404, 'No such group'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
}
function list(req, res, next) {
groups.getAll(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { groups: result }));
});
}
function remove(req, res, next) {
assert.strictEqual(typeof req.params.groupId, 'string');
groups.remove(req.params.groupId, function (error) {
if (error && error.reason === GroupError.NOT_FOUND) return next(new HttpError(404, 'Group not found'));
if (error && error.reason === GroupError.NOT_ALLOWED) return next(new HttpError(409, 'Group deletion not allowed'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
+4 -4
View File
@@ -2,14 +2,14 @@
exports = module.exports = {
apps: require('./apps.js'),
backups: require('./backups.js'),
clients: require('./clients.js'),
cloudron: require('./cloudron.js'),
developer: require('./developer.js'),
graphs: require('./graphs.js'),
groups: require('./groups.js'),
internal: require('./internal.js'),
oauth2: require('./oauth2.js'),
settings: require('./settings.js'),
clients: require('./clients.js'),
backups: require('./backups.js'),
internal: require('./internal.js'),
user: require('./user.js')
};
+19 -6
View File
@@ -1,13 +1,14 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
backup: backup,
update: update
update: update,
retire: retire
};
var cloudron = require('../cloudron.js'),
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'),
HttpError = require('connect-lastmile').HttpError,
@@ -18,8 +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
cloudron.backup(function (error) {
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
backups.backup(function (error) {
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
@@ -39,3 +40,15 @@ function update(req, res, next) {
});
}
function retire(req, res, next) {
debug('triggering retire');
// 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
cloudron.retire(function (error) {
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
+60 -17
View File
@@ -17,7 +17,6 @@ var assert = require('assert'),
querystring = require('querystring'),
util = require('util'),
session = require('connect-ensure-login'),
settings = require('../settings.js'),
tokendb = require('../tokendb'),
appdb = require('../appdb'),
url = require('url'),
@@ -58,7 +57,7 @@ gServer.grant(oauth2orize.grant.code({ scopeSeparator: ',' }, function (client,
var code = hat(256);
var expiresAt = Date.now() + 60 * 60000; // 1 hour
authcodedb.add(code, client.id, user.username, expiresAt, function (error) {
authcodedb.add(code, client.id, user.id, expiresAt, function (error) {
if (error) return callback(error);
debug('grant code: new auth code for client %s code %s', client.id, code);
@@ -268,19 +267,56 @@ function passwordSentSite(req, res) {
renderTemplate(res, 'password_reset_sent', { adminOrigin: config.adminOrigin(), title: 'Cloudron Password Reset' });
}
// -> GET /api/v1/session/password/setup.html
function passwordSetupSite(req, res, next) {
if (!req.query.reset_token) return next(new HttpError(400, 'Missing reset_token'));
function renderAccountSetupSite(res, req, userObject, error) {
renderTemplate(res, 'account_setup', {
adminOrigin: config.adminOrigin(),
user: userObject,
error: error,
csrf: req.csrfToken(),
resetToken: req.query.reset_token || req.body.resetToken,
title: 'Cloudron Password Setup'
});
}
user.getByResetToken(req.query.reset_token, function (error, user) {
if (error) return next(new HttpError(401, 'Invalid reset_token'));
// -> GET /api/v1/session/account/setup.html
function accountSetupSite(req, res) {
if (!req.query.reset_token) return sendError(req, res, 'Missing Reset Token');
renderTemplate(res, 'password_setup', {
adminOrigin: config.adminOrigin(),
user: user,
csrf: req.csrfToken(),
resetToken: req.query.reset_token,
title: 'Cloudron Password Setup'
user.getByResetToken(req.query.reset_token, function (error, userObject) {
if (error) return sendError(req, res, 'Invalid Reset Token');
renderAccountSetupSite(res, req, userObject, '');
});
}
// -> POST /api/v1/session/account/setup
function accountSetup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'Missing username'));
if (typeof req.body.displayName !== 'string') return next(new HttpError(400, 'Missing displayName'));
debug('acountSetup: with token %s.', req.body.resetToken);
user.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return sendError(req, res, 'Invalid Reset Token');
userObject.username = req.body.username;
userObject.displayName = req.body.displayName;
user.update(userObject.id, userObject.username, userObject.email, userObject.displayName, 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));
// setPassword clears the resetToken
user.setPassword(userObject.id, req.body.password, function (error, result) {
if (error && error.reason === UserError.BAD_PASSWORD) return renderAccountSetupSite(res, req, userObject, 'Password invalid');
if (error) return next(new HttpError(500, error));
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
});
});
});
}
@@ -314,8 +350,11 @@ function passwordReset(req, res, next) {
user.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid resetToken'));
if (!userObject.username) return next(new HttpError(401, 'No username set'));
// setPassword clears the resetToken
user.setPassword(userObject.id, req.body.password, function (error, result) {
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(406, 'Password does not meet the requirements'));
if (error) return next(new HttpError(500, error));
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
@@ -374,14 +413,17 @@ var authorization = [
if (type === clientdb.TYPE_ADMIN) return next();
if (type === clientdb.TYPE_EXTERNAL) return next();
if (type === clientdb.TYPE_SIMPLE_AUTH) return sendError(req, res, 'Unkonwn OAuth client.');
if (type === clientdb.TYPE_SIMPLE_AUTH) return sendError(req, res, 'Unknown OAuth client.');
appdb.get(req.oauth2.client.appId, function (error, appObject) {
if (error) return sendErrorPageOrRedirect(req, res, 'Invalid request. Unknown app for this client_id.');
if (!apps.hasAccessTo(appObject, req.oauth2.user)) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
apps.hasAccessTo(appObject, req.oauth2.user, function (error, access) {
if (error) return sendError(req, res, 'Internal error');
if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
next();
next();
});
});
},
gServer.decision({ loadTransaction: false })
@@ -456,8 +498,9 @@ exports = module.exports = {
passwordResetRequest: passwordResetRequest,
passwordSentSite: passwordSentSite,
passwordResetSite: passwordResetSite,
passwordSetupSite: passwordSetupSite,
passwordReset: passwordReset,
accountSetupSite: accountSetupSite,
accountSetup: accountSetup,
authorization: authorization,
token: token,
scope: scope,
+1312 -1315
View File
File diff suppressed because it is too large Load Diff
+6 -33
View File
@@ -19,15 +19,17 @@ var appdb = require('../../appdb.js'),
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
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),
userdb._clear,
database._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
@@ -51,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 */, false /* oauthProxy */, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, callback);
},
function createSettings(callback) {
@@ -72,35 +74,6 @@ describe('Backups API', function () {
before(setup);
after(cleanup);
describe('get', function () {
it('cannot get backups with appstore superagent failing', function (done) {
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=BACKUP_TOKEN').reply(401, {});
superagent.get(SERVER_URL + '/api/v1/backups')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(503);
expect(req.isDone()).to.be.ok();
done();
});
});
it('can get backups', function (done) {
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=BACKUP_TOKEN').reply(200, { backups: ['foo', 'bar']});
superagent.get(SERVER_URL + '/api/v1/backups')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(req.isDone()).to.be.ok();
expect(res.body.backups).to.be.an(Array);
expect(res.body.backups[0]).to.eql('foo');
expect(res.body.backups[1]).to.eql('bar');
done();
});
});
});
describe('create', function () {
it('fails due to mising token', function (done) {
superagent.post(SERVER_URL + '/api/v1/backups')
@@ -122,7 +95,7 @@ describe('Backups API', 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', SessionToken: 'sessionToken' } });
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } });
superagent.post(SERVER_URL + '/api/v1/backups')
.query({ access_token: token })
+17 -5
View File
@@ -8,6 +8,7 @@
var async = require('async'),
config = require('../../config.js'),
clientdb = require('../../clientdb.js'),
database = require('../../database.js'),
oauth2 = require('../oauth2.js'),
expect = require('expect.js'),
@@ -20,7 +21,7 @@ var async = require('async'),
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null; // authentication token
function cleanup(done) {
@@ -173,6 +174,8 @@ describe('OAuth Clients API', function () {
expect(result.body.redirectURI).to.be.a('string');
expect(result.body.clientSecret).to.be.a('string');
expect(result.body.scope).to.be.a('string');
expect(result.body.type).to.equal(clientdb.TYPE_EXTERNAL);
done();
});
});
@@ -412,7 +415,7 @@ describe('Clients', function () {
server.start.bind(server),
database._clear.bind(null),
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
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')
@@ -427,7 +430,16 @@ describe('Clients', function () {
// stash for further use
token = result.body.token;
callback();
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
USER_0.id = result.body.id;
callback();
});
});
}
], done);
@@ -531,7 +543,7 @@ describe('Clients', function () {
expect(result.statusCode).to.equal(200);
expect(result.body.tokens.length).to.eql(1);
expect(result.body.tokens[0].identifier).to.eql('user-' + USER_0.username);
expect(result.body.tokens[0].identifier).to.eql('user-' + USER_0.id);
done();
});
@@ -584,7 +596,7 @@ describe('Clients', function () {
expect(result.statusCode).to.equal(200);
expect(result.body.tokens.length).to.eql(1);
expect(result.body.tokens[0].identifier).to.eql('user-' + USER_0.username);
expect(result.body.tokens[0].identifier).to.eql('user-' + USER_0.id);
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token })
+8 -179
View File
@@ -18,14 +18,14 @@ var async = require('async'),
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null; // authentication token
var server;
function setup(done) {
nock.cleanAll();
config._reset();
config.set('version', '0.5.0');
config.set('fqdn', 'localhost');
server.start(done);
}
@@ -33,6 +33,8 @@ function cleanup(done) {
database._clear(function (error) {
expect(error).to.not.be.ok();
config._reset();
server.stop(done);
});
}
@@ -231,7 +233,9 @@ describe('Cloudron', function () {
});
it('succeeds', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/localhost?token=' + config.token()).reply(200, { box: { region: 'sfo', size: '1gb' }});
var scope = nock(config.apiServerOrigin())
.get('/api/v1/boxes/localhost?token=' + config.token())
.reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }});
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
.query({ access_token: token })
@@ -258,181 +262,6 @@ describe('Cloudron', function () {
});
describe('migrate', function () {
before(function (done) {
async.series([
setup,
function (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, {});
config._reset();
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(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
},
function setupBackupConfig(callback) {
superagent.post(SERVER_URL + '/api/v1/settings/backup_config')
.send({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
callback();
});
}
], done);
});
after(cleanup);
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with missing size', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with wrong size type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 4, region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with missing region', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with wrong region type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 4, password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails when in wrong state', function (done) {
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope1 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(409, {});
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
it('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(202, {});
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
});
describe('feedback', function () {
before(function (done) {
async.series([
@@ -514,7 +343,7 @@ describe('Cloudron', function () {
it('succeeds with app type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'app', subject: 'some subject', description: 'some description' })
.send({ type: 'app_missing', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
+33 -2
View File
@@ -17,7 +17,7 @@ var async = require('async'),
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null; // authentication token
var server;
@@ -297,7 +297,16 @@ describe('Developer API', function () {
it('fails with unknown username', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME.toUpperCase(), password: PASSWORD })
.send({ username: USERNAME + USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with unknown email', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME + EMAIL, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -324,6 +333,17 @@ describe('Developer API', function () {
});
});
it('with uppercase username succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(result.body.token).to.be.a('string');
done();
});
});
it('with email succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: EMAIL, password: PASSWORD })
@@ -334,5 +354,16 @@ describe('Developer API', function () {
done();
});
});
it('with uppercase email succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: EMAIL.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(result.body.token).to.be.a('string');
done();
});
});
});
});
+256
View File
@@ -0,0 +1,256 @@
/* jslint node:true */
/* 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'),
groups = require('../../groups.js'),
superagent = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js'),
tokendb = require('../../tokendb.js'),
nock = require('nock'),
userdb = require('../../userdb.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var USERNAME_1 = 'user', PASSWORD_1 = 'Foobar?1337', EMAIL_1 ='happy@me.com';
var token, token_1 = null;
var userId, userId_1 = null;
var server;
function setup(done) {
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;
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
userId = result.body.id;
callback();
});
});
},
function (callback) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1, email: EMAIL_1, invite: false })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
token_1 = tokendb.generateToken();
userId_1 = result.body.id;
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, '*', callback);
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('Groups API', function () {
before(setup);
after(cleanup);
describe('list', function () {
it('cannot get groups without token', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups')
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
done();
});
});
it('cannot get groups as normal user', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups')
.query({ access_token: token_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('can get groups', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.groups).to.be.an(Array);
expect(res.body.groups.length).to.be(1);
expect(res.body.groups[0].name).to.eql('admin');
done();
});
});
});
describe('create', function () {
it('fails due to mising token', function (done) {
superagent.post(SERVER_URL + '/api/v1/groups')
.send({ name: 'externals'})
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/groups')
.query({ access_token: token })
.send({ name: 'externals'})
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
done();
});
});
it('fails for already exists', function (done) {
superagent.post(SERVER_URL + '/api/v1/groups')
.query({ access_token: token })
.send({ name: 'externals'})
.end(function (error, result) {
expect(result.statusCode).to.equal(409);
done();
});
});
});
describe('get', function () {
it('cannot get non-existing group', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups/nope')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('cannot get existing group with normal user', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups/admin')
.query({ access_token: token_1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('can get existing group', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups/admin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.name).to.be('admin');
expect(result.body.userIds.length).to.be(1);
expect(result.body.userIds[0]).to.be(userId);
done();
});
});
});
describe('remove', function () {
it('cannot remove without token', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/externals')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('can remove empty group', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/externals')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('cannot remove non-empty group', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/admin')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(409);
done();
});
});
});
describe('Set groups', function () {
before(function (done) {
async.series([
groups.create.bind(null, 'group0'),
groups.create.bind(null, 'group1')
], done);
});
it('cannot add user to invalid group', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/set_groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin', 'something' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('can add user to valid group', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/set_groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin', 'group0', 'group1' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('can remove last user from admin', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/set_groups')
.query({ access_token: token })
.send({ groupIds: [ 'group0', 'group1' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(403); // not allowed
done();
});
});
});
});
+81 -20
View File
@@ -138,10 +138,9 @@ describe('OAuth2', function () {
describe('flow', function () {
var USER_0 = {
id: uuid.v4(),
username: 'someusername',
username: 'someUSERname',
password: '@#45Strongpassword',
email: 'some@email.com',
admin: true,
email: 'some@EMAIL.com',
salt: 'somesalt',
createdAt: (new Date()).toUTCString(),
modifiedAt: (new Date()).toUTCString(),
@@ -156,7 +155,7 @@ describe('OAuth2', function () {
location: 'test',
portBindings: {},
accessRestriction: null,
oauthProxy: true
memoryLimit: 0
};
var APP_1 = {
@@ -166,7 +165,7 @@ describe('OAuth2', function () {
location: 'test1',
portBindings: {},
accessRestriction: { users: [ 'foobar' ] },
oauthProxy: true
memoryLimit: 0
};
var APP_2 = {
@@ -176,7 +175,17 @@ describe('OAuth2', function () {
location: 'test2',
portBindings: {},
accessRestriction: { users: [ USER_0.id ] },
oauthProxy: true
memoryLimit: 0
};
var APP_3 = {
id: 'app3',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test3',
portBindings: {},
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
memoryLimit: 0
};
// unknown app
@@ -269,6 +278,16 @@ describe('OAuth2', function () {
scope: 'profile'
};
// app with accessRestriction allowing group
var CLIENT_9 = {
id: 'cid-client9',
appId: APP_3.id,
type: clientdb.TYPE_OAUTH,
clientSecret: 'secret9',
redirectURI: 'http://redirect9',
scope: 'profile'
};
// make csrf always succeed for testing
oauth2.csrf = function (req, res, next) {
req.csrfToken = function () { return hat(256); };
@@ -288,11 +307,13 @@ describe('OAuth2', function () {
clientdb.add.bind(null, CLIENT_6.id, CLIENT_6.appId, CLIENT_6.type, CLIENT_6.clientSecret, CLIENT_6.redirectURI, CLIENT_6.scope),
clientdb.add.bind(null, CLIENT_7.id, CLIENT_7.appId, CLIENT_7.type, CLIENT_7.clientSecret, CLIENT_7.redirectURI, CLIENT_7.scope),
clientdb.add.bind(null, CLIENT_8.id, CLIENT_8.appId, CLIENT_8.type, CLIENT_8.clientSecret, CLIENT_8.redirectURI, CLIENT_8.scope),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.oauthProxy),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.oauthProxy),
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),
function (callback) {
user.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, true, '', false, function (error, userObject) {
user.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, function (error, userObject) {
expect(error).to.not.be.ok();
// update the global objects to reflect the new user id
@@ -778,7 +799,7 @@ describe('OAuth2', function () {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(body.indexOf('Unkonwn OAuth client.')).to.not.equal(-1);
expect(body.indexOf('Unknown OAuth client.')).to.not.equal(-1);
done();
});
@@ -802,6 +823,21 @@ describe('OAuth2', function () {
});
});
it('fails for grant type code with accessRestriction (group)', function (done) { // USER_0 is not an admin
startAuthorizationFlow(CLIENT_9, 'code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_9.redirectURI + '&client_id=' + CLIENT_9.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(body.indexOf('No access to this app.')).to.not.equal(-1);
done();
});
});
});
it('fails for grant type token due to accessRestriction', function (done) {
startAuthorizationFlow(CLIENT_6, 'token', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_6.redirectURI + '&client_id=' + CLIENT_6.id + '&response_type=token';
@@ -825,7 +861,7 @@ describe('OAuth2', function () {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(body.indexOf('Unkonwn OAuth client.')).to.not.equal(-1);
expect(body.indexOf('Unknown OAuth client.')).to.not.equal(-1);
done();
});
@@ -847,7 +883,14 @@ describe('OAuth2', function () {
expect(foo.access_token).to.be.a('string');
expect(foo.token_type).to.eql('Bearer');
done();
// Ensure the token is also usable
superagent.get(SERVER_URL + '/api/v1/profile?access_token=' + foo.access_token, function (error, result) {
expect(error).to.not.be.ok();
expect(result.status).to.eql(200);
expect(result.body.username).to.equal(USER_0.username.toLowerCase());
done();
});
});
});
});
@@ -1227,7 +1270,14 @@ describe('OAuth2', function () {
expect(body.access_token).to.be.a('string');
expect(body.token_type).to.eql('Bearer');
done();
// Ensure the token is also usable
superagent.get(SERVER_URL + '/api/v1/profile?access_token=' + body.access_token, function (error, result) {
expect(error).to.not.be.ok();
expect(result.status).to.eql(200);
expect(result.body.username).to.equal(USER_0.username.toLowerCase());
done();
});
});
});
});
@@ -1282,31 +1332,33 @@ describe('Password', function () {
it('reset request succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/password/resetRequest.html')
.end(function (error, result) {
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
expect(result.statusCode).to.equal(200);
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
done();
});
});
it('setup fails due to missing reset_token', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/password/setup.html')
superagent.get(SERVER_URL + '/api/v1/session/account/setup.html')
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(result.statusCode).to.equal(200);
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
done();
});
});
it('setup fails due to invalid reset_token', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/password/setup.html')
superagent.get(SERVER_URL + '/api/v1/session/account/setup.html')
.query({ reset_token: hat(256) })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
expect(result.statusCode).to.equal(200);
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
done();
});
});
it('setup succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/password/setup.html')
superagent.get(SERVER_URL + '/api/v1/session/account/setup.html')
.query({ reset_token: USER_0.resetToken })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
@@ -1407,6 +1459,15 @@ describe('Password', function () {
});
});
it('fails due to weak password', function (done) {
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: 'foobar', resetToken: USER_0.resetToken })
.end(function (error, result) {
expect(result.statusCode).to.equal(406);
done();
});
});
it('succeeds', function (done) {
var scope = nock(config.adminOrigin())
.filteringPath(function (path) {
+2 -2
View File
@@ -22,7 +22,7 @@ var appdb = require('../../appdb.js'),
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var server;
@@ -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 */, false /* oauthProxy */, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, callback);
}
], done);
}
+109 -11
View File
@@ -21,7 +21,7 @@ describe('SimpleAuth API', function () {
var SERVER_URL = 'http://localhost:' + config.get('port');
var SIMPLE_AUTH_ORIGIN = 'http://localhost:' + config.get('simpleAuthPort');
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var USERNAME = 'superaDMin', PASSWORD = 'Foobar?1337', EMAIL ='silly@ME.com';
var APP_0 = {
id: 'app0',
@@ -30,7 +30,7 @@ describe('SimpleAuth API', function () {
location: 'test0',
portBindings: {},
accessRestriction: { users: [ 'foobar', 'someone'] },
oauthProxy: true
memoryLimit: 0
};
var APP_1 = {
@@ -39,8 +39,8 @@ describe('SimpleAuth API', function () {
manifest: { version: '0.1.0', addons: { } },
location: 'test1',
portBindings: {},
accessRestriction: { users: [ 'foobar', USERNAME, 'someone' ] },
oauthProxy: true
accessRestriction: { users: [ 'foobar', 'someone' ] },
memoryLimit: 0
};
var APP_2 = {
@@ -50,7 +50,17 @@ describe('SimpleAuth API', function () {
location: 'test2',
portBindings: {},
accessRestriction: null,
oauthProxy: true
memoryLimit: 0
};
var APP_3 = {
id: 'app3',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test3',
portBindings: {},
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
memoryLimit: 0
};
var CLIENT_0 = {
@@ -98,6 +108,15 @@ describe('SimpleAuth API', function () {
scope: 'user,profile'
};
var CLIENT_5 = {
id: 'someclientid5',
appId: APP_3.id,
type: clientdb.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret5',
redirectURI: '',
scope: 'user,profile'
};
before(function (done) {
async.series([
server.start.bind(server),
@@ -119,7 +138,14 @@ describe('SimpleAuth API', function () {
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
callback();
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: result.body.token}).end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.eql(200);
APP_1.accessRestriction.users.push(result.body.id);
callback();
});
});
},
@@ -128,9 +154,11 @@ describe('SimpleAuth API', function () {
clientdb.add.bind(null, CLIENT_2.id, CLIENT_2.appId, CLIENT_2.type, CLIENT_2.clientSecret, CLIENT_2.redirectURI, CLIENT_2.scope),
clientdb.add.bind(null, CLIENT_3.id, CLIENT_3.appId, CLIENT_3.type, CLIENT_3.clientSecret, CLIENT_3.redirectURI, CLIENT_3.scope),
clientdb.add.bind(null, CLIENT_4.id, CLIENT_4.appId, CLIENT_4.type, CLIENT_4.clientSecret, CLIENT_4.redirectURI, CLIENT_4.scope),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.oauthProxy),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.oauthProxy)
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)
], done);
});
@@ -288,6 +316,7 @@ describe('SimpleAuth API', function () {
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.displayName).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
@@ -295,7 +324,41 @@ describe('SimpleAuth API', function () {
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME);
expect(result.body.username).to.eql(USERNAME.toLowerCase());
expect(result.body.email).to.eql(EMAIL.toLowerCase());
done();
});
});
});
it('succeeds for allowed app with email', function (done) {
var body = {
clientId: CLIENT_2.id,
username: EMAIL,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.displayName).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME.toLowerCase());
expect(result.body.email).to.eql(EMAIL.toLowerCase());
done();
});
@@ -319,6 +382,7 @@ describe('SimpleAuth API', function () {
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.displayName).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
@@ -326,7 +390,41 @@ describe('SimpleAuth API', function () {
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME);
expect(result.body.username).to.eql(USERNAME.toLowerCase());
expect(result.body.email).to.eql(EMAIL.toLowerCase());
done();
});
});
});
it('succeeds for app with group accessRestriction', function (done) {
var body = {
clientId: CLIENT_5.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.displayName).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME.toLowerCase());
expect(result.body.email).to.eql(EMAIL.toLowerCase());
done();
});
+2 -2
View File
@@ -68,7 +68,7 @@ start_mongodb() {
start_mail() {
docker rm -f mail 2>/dev/null 1>&2 || true
docker run -dP --name=mail -e DOMAIN_NAME="localhost" \
docker run -dP --name=mail -e MAIL_SERVER_NAME="server.local" -e MAIL_DOMAIN="server.local" \
--read-only -v /tmp -v /run \
-v /tmp/maildata:/app/data "${MAIL_IMAGE}" >/dev/null
}
@@ -79,7 +79,7 @@ start_mongodb
start_mail
echo -n "Waiting for addons to start"
for i in {1..20}; do
for i in {1..10}; do
echo -n "."
sleep 1
done
+193 -98
View File
@@ -10,6 +10,7 @@ var config = require('../../config.js'),
database = require('../../database.js'),
tokendb = require('../../tokendb.js'),
expect = require('expect.js'),
groups = require('../../groups.js'),
mailer = require('../../mailer.js'),
superagent = require('superagent'),
nock = require('nock'),
@@ -18,10 +19,10 @@ var config = require('../../config.js'),
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME_0 = 'admin', PASSWORD = 'Foobar?1337', EMAIL = 'silly@me.com', EMAIL_0_NEW = 'stupid@me.com';
var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'tao@zen.mac';
var USERNAME_2 = 'userTheSecond', EMAIL_2 = 'user@foo.bar';
var USERNAME_3 = 'userTheThird', EMAIL_3 = 'user3@foo.bar';
var USERNAME_0 = 'superaDmIn', PASSWORD = 'Foobar?1337', EMAIL_0 = 'silLY@me.com', EMAIL_0_NEW = 'stupID@me.com', DISPLAY_NAME_0_NEW = 'New Name';
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) {
@@ -30,7 +31,11 @@ function setup(done) {
mailer._clearMailQueue();
userdb._clear(done);
userdb._clear(function (error) {
expect(error).to.eql(null);
groups.create('somegroupid', done);
});
});
}
@@ -56,7 +61,7 @@ function checkMails(number, done) {
describe('User API', function () {
this.timeout(5000);
var user_0 = null;
var user_0, user_1, user_2, user_3 = null;
var token = null;
var token_1 = tokendb.generateToken();
var token_2 = tokendb.generateToken();
@@ -100,8 +105,9 @@ describe('User API', function () {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME_0, password: PASSWORD, email: EMAIL })
.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
@@ -109,7 +115,16 @@ describe('User API', function () {
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
done(err);
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
expect(error).to.eql(null);
expect(result.status).to.equal(200);
// stash for further use
user_0 = result.body;
done();
});
});
});
@@ -122,17 +137,24 @@ describe('User API', function () {
});
});
it('can get userInfo with token', function (done) {
it('cannot get userInfo by username', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0);
expect(res.body.email).to.equal(EMAIL);
expect(res.body.admin).to.be.ok();
expect(res.statusCode).to.equal(404);
// stash for further use
user_0 = res.body;
done();
});
});
it('can get userInfo with token', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.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.toLowerCase());
expect(res.body.admin).to.be.ok();
done();
});
@@ -157,19 +179,19 @@ describe('User API', function () {
});
it('can get userInfo with token', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0);
expect(res.body.email).to.equal(EMAIL);
expect(res.body.username).to.equal(USERNAME_0.toLowerCase());
expect(res.body.email).to.equal(EMAIL_0.toLowerCase());
expect(res.body.admin).to.be.ok();
done();
});
});
it('cannot get userInfo only with basic auth', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id)
.auth(USERNAME_0, PASSWORD)
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
@@ -178,7 +200,7 @@ describe('User API', function () {
});
it('cannot get userInfo with invalid token (token length)', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: 'x' + token })
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
@@ -187,7 +209,7 @@ describe('User API', function () {
});
it('cannot get userInfo with invalid token (wrong token)', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token.toUpperCase() })
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
@@ -196,13 +218,14 @@ describe('User API', function () {
});
it('can get userInfo with token in auth header', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id)
.set('Authorization', 'Bearer ' + token)
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0);
expect(res.body.email).to.equal(EMAIL);
expect(res.body.username).to.equal(USERNAME_0.toLowerCase());
expect(res.body.email).to.equal(EMAIL_0.toLowerCase());
expect(res.body.admin).to.be.ok();
expect(res.body.displayName).to.be.a('string');
expect(res.body.password).to.not.be.ok();
expect(res.body.salt).to.not.be.ok();
done();
@@ -210,7 +233,7 @@ describe('User API', function () {
});
it('cannot get userInfo with invalid token in auth header', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id)
.set('Authorization', 'Bearer ' + 'x' + token)
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
@@ -219,7 +242,7 @@ describe('User API', function () {
});
it('cannot get userInfo with invalid token (wrong token)', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id)
.set('Authorization', 'Bearer ' + 'x' + token.toUpperCase())
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
@@ -233,13 +256,15 @@ describe('User API', function () {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1, email: EMAIL_1, invite: true })
.end(function (err, res) {
expect(err).to.not.be.ok();
expect(res.statusCode).to.equal(201);
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
user_1 = result.body;
checkMails(2, function () {
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, tokendb.PREFIX_USER + USERNAME_1, 'test-client-id', Date.now() + 10000, '*', done);
tokendb.add(token_1, tokendb.PREFIX_USER + user_1.id, 'test-client-id', Date.now() + 10000, '*', done);
});
});
});
@@ -260,7 +285,7 @@ describe('User API', function () {
it('reinvite second user succeeds', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/invite')
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
@@ -271,53 +296,48 @@ describe('User API', function () {
});
it('set second user as admin succeeds', function (done) {
// TODO is USERNAME_1 in body and url redundant?
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
superagent.put(SERVER_URL + '/api/v1/users/' + user_1.id + '/set_groups')
.query({ access_token: token })
.send({ username: USERNAME_1, admin: true })
.send({ groupIds: [ groups.ADMIN_GROUP_ID ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
superagent.get(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.admin).to.equal(true);
done();
});
});
});
it('remove first user from admins succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
.query({ access_token: token_1 })
.send({ username: USERNAME_0, admin: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('remove second user by first, now normal, user fails', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
it('remove itself from admins fails', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id + '/set_groups')
.query({ access_token: token })
.send({ password: PASSWORD })
.send({ groupIds: [ 'somegroupid' ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('remove second user from admins and thus last admin fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
.query({ access_token: token_1 })
.send({ username: USERNAME_1, admin: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('reset first user as admin succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
.query({ access_token: token_1 })
.send({ username: USERNAME_0, admin: true })
it('remove second user from admins succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_1.id + '/set_groups')
.query({ access_token: token })
.send({ groupIds: [ 'somegroupid' ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
superagent.get(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.admin).to.equal(false);
done();
});
});
});
@@ -357,31 +377,44 @@ describe('User API', function () {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2, email: EMAIL_2, invite: false })
.end(function (error, res) {
expect(res.statusCode).to.equal(201);
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
user_2 = result.body;
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_3, email: EMAIL_3, invite: true })
.end(function (error, res) {
expect(res.statusCode).to.equal(201);
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
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 + USERNAME_2, 'test-client-id', Date.now() + 10000, '*', done);
tokendb.add(token_2, tokendb.PREFIX_USER + user_2.id, 'test-client-id', Date.now() + 10000, '*', done);
});
});
});
});
it('second user userInfo', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
it('second user userInfo fails for first 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 })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.username).to.equal(USERNAME_2);
expect(result.body.email).to.equal(EMAIL_2);
expect(result.body.username).to.equal(USERNAME_2.toLowerCase());
expect(result.body.email).to.equal(EMAIL_2.toLowerCase());
expect(result.body.admin).to.not.be.ok();
done();
@@ -391,16 +424,25 @@ describe('User API', function () {
it('create user with same username should fail', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2, email: EMAIL, invite: false })
.send({ username: USERNAME_2, email: EMAIL_0, invite: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('list users', function (done) {
it('list users fails for normal user', function (done) {
superagent.get(SERVER_URL + '/api/v1/users')
.query({ access_token: token_2 })
.end(function (error, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('list users succeeds for admin', function (done) {
superagent.get(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.end(function (error, res) {
expect(error).to.be(null);
expect(res.statusCode).to.equal(200);
@@ -421,7 +463,7 @@ describe('User API', function () {
});
it('user removes himself is not allowed', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_0)
superagent.del(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
@@ -431,7 +473,7 @@ describe('User API', function () {
});
it('admin cannot remove normal user without giving a password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
@@ -440,7 +482,7 @@ describe('User API', function () {
});
it('admin cannot remove normal user with empty password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.send({ password: '' })
.end(function (err, res) {
@@ -450,7 +492,7 @@ describe('User API', function () {
});
it('admin cannot remove normal user with giving wrong password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.send({ password: PASSWORD + PASSWORD })
.end(function (err, res) {
@@ -460,7 +502,7 @@ describe('User API', function () {
});
it('admin removes normal user', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
@@ -470,7 +512,7 @@ describe('User API', function () {
});
it('admin removes himself should not be allowed', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_0)
superagent.del(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
@@ -481,51 +523,104 @@ describe('User API', function () {
// Change email
it('change email fails due to missing token', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.send({ password: PASSWORD, email: EMAIL_0_NEW })
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('change email fails due to missing password', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
it('change email fails due to invalid email', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ email: EMAIL_0_NEW })
.send({ email: 'foo@bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('change email fails due to wrong password', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.send({ password: PASSWORD+PASSWORD, email: EMAIL_0_NEW })
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 email fails due to invalid email', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
it('change user succeeds without email nor displayName', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ password: PASSWORD, email: 'foo@bar' })
.send({})
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(result.statusCode).to.equal(204);
done();
});
});
it('change email succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.send({ password: PASSWORD, email: EMAIL_0_NEW })
it('change email for own user succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_2.id)
.query({ access_token: token_2 })
.send({ email: EMAIL_2_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done(error);
superagent.get(SERVER_URL + '/api/v1/users/' + user_2.id)
.query({ access_token: token_2 })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_2.toLowerCase());
expect(res.body.email).to.equal(EMAIL_2_NEW.toLowerCase());
expect(res.body.admin).to.equal(false);
expect(res.body.displayName).to.equal('');
done();
});
});
});
it('change email as admin for other user succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_2.id)
.query({ access_token: token })
.send({ email: EMAIL_2 })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/users/' + user_2.id)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_2.toLowerCase());
expect(res.body.email).to.equal(EMAIL_2.toLowerCase());
expect(res.body.admin).to.equal(false);
expect(res.body.displayName).to.equal('');
done();
});
});
});
it('change displayName succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ displayName: DISPLAY_NAME_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.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.toLowerCase());
expect(res.body.admin).to.be.ok();
expect(res.body.displayName).to.equal(DISPLAY_NAME_0_NEW);
done();
});
});
});
+85 -42
View File
@@ -9,15 +9,16 @@ exports = module.exports = {
list: listUser,
create: createUser,
changePassword: changePassword,
changeAdmin: changeAdmin,
remove: removeUser,
verifyPassword: verifyPassword,
requireAdmin: requireAdmin,
sendInvite: sendInvite
sendInvite: sendInvite,
setGroups: setGroups
};
var assert = require('assert'),
generatePassword = require('../password.js').generate,
groups = require('../groups.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
user = require('../user.js'),
@@ -34,27 +35,35 @@ function profile(req, res, next) {
if (req.user.tokenType === tokendb.TYPE_USER || req.user.tokenType === tokendb.TYPE_DEV) {
result.username = req.user.username;
result.email = req.user.email;
result.admin = req.user.admin;
}
result.displayName = req.user.displayName;
next(new HttpSuccess(200, result));
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 createUser(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if (typeof req.body.invite !== 'boolean') return next(new HttpError(400, 'invite must be boolean'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
var username = req.body.username;
var password = generatePassword();
var email = req.body.email;
var sendInvite = req.body.invite;
var username = req.body.username || '';
var displayName = req.body.displayName || '';
user.create(username, password, email, displayName, false /* admin */, req.user /* creator */, sendInvite, function (error, user) {
user.create(username, password, email, displayName, { 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'));
@@ -65,43 +74,40 @@ function createUser(req, res, next) {
var userInfo = {
id: user.id,
username: user.username,
displayName: user.displayName,
email: user.email,
admin: user.admin,
resetToken: user.resetToken
};
next(new HttpSuccess(201, { userInfo: userInfo }));
next(new HttpSuccess(201, userInfo ));
});
}
function update(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
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'));
if (req.user.id !== req.params.userId && !req.user.admin) return next(new HttpError(403, 'Not allowed'));
user.update(req.user.id, req.user.username, req.body.email, function (error) {
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
user.get(req.params.userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
user.update(req.params.userId, result.username, req.body.email || result.email, req.body.displayName || result.displayName, 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));
function changeAdmin(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'API call requires a username.'));
if (typeof req.body.admin !== 'boolean') return next(new HttpError(400, 'API call requires an admin setting.'));
user.changeAdmin(req.body.username, req.body.admin, function (error) {
if (error && error.reason === UserError.NOT_ALLOWED) return next(new HttpError(403, 'Last admin'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
next(new HttpSuccess(204));
});
});
}
@@ -133,17 +139,25 @@ function listUser(req, res, next) {
function info(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
assert.strictEqual(typeof req.user, 'object');
if (req.user.id !== req.params.userId && !req.user.admin) return next(new HttpError(403, 'Not allowed'));
user.get(req.params.userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {
id: result.id,
username: result.username,
email: result.email,
admin: result.admin
}));
groups.isMember(groups.ADMIN_GROUP_ID, req.params.userId, function (error, isAdmin) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {
id: result.id,
username: result.username,
email: result.email,
admin: isAdmin,
displayName: result.displayName
}));
});
});
}
@@ -157,11 +171,16 @@ function removeUser(req, res, next) {
if (req.user.id === req.params.userId) return next(new HttpError(403, 'Not allowed to remove yourself.'));
user.remove(req.params.userId, function (error) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
user.get(req.params.userId, function (error, userObject) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
user.remove(userObject, 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));
next(new HttpSuccess(204));
});
});
}
@@ -173,12 +192,19 @@ function verifyPassword(req, res, next) {
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
user.verify(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'));
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
if (error) return next(new HttpError(500, error));
next();
// 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();
});
});
}
@@ -196,10 +222,27 @@ function requireAdmin(req, res, next) {
function sendInvite(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
user.sendInvite(req.params.userId, function (error) {
user.sendInvite(req.params.userId, function (error, result) {
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(200, {}));
next(new HttpSuccess(200, { resetToken: result }));
});
}
function setGroups(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.userId, 'string');
if (!Array.isArray(req.body.groupIds)) return next(new HttpError(400, 'API call requires a groups array.'));
// this route is only allowed for admins, so req.user has to be an admin
if (req.user.id === req.params.userId && req.body.groupIds.indexOf(groups.ADMIN_GROUP_ID) === -1) return next(new HttpError(403, 'Admin removing itself from admins is not allowed'));
user.setGroups(req.params.userId, req.body.groupIds, function (error) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'One or more groups not found'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
+35 -17
View File
@@ -12,37 +12,56 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
exit 0
fi
if [ $# -lt 3 ]; then
echo "Usage: backupapp.sh <appid> <url> <key> [aws session token]"
if [ $# -lt 8 ]; then
echo "Usage: backupapp.sh <appid> <s3 config url> <s3 data url> <access key id> <access key> <session token> <region> <password>"
exit 1
fi
readonly DATA_DIR="${HOME}/data"
app_id="$1"
backup_url="$2"
backup_key="$3"
session_token="$4"
# env vars used by the awscli
readonly app_id="$1"
readonly s3_config_url="$2"
readonly s3_data_url="$3"
export AWS_ACCESS_KEY_ID="$4"
export AWS_SECRET_ACCESS_KEY="$5"
export AWS_SESSION_TOKEN="$6"
export AWS_DEFAULT_REGION="$7"
readonly password="$8"
readonly now=$(date "+%Y-%m-%dT%H:%M:%S")
readonly app_data_dir="${DATA_DIR}/${app_id}"
readonly app_data_snapshot="${DATA_DIR}/snapshots/${app_id}-${now}"
btrfs subvolume snapshot -r "${app_data_dir}" "${app_data_snapshot}"
# Upload config.json first because uploading tarball might take a lot of time, leading to token expiry
for try in `seq 1 5`; do
echo "Uploading backup to ${backup_url} (try ${try})"
echo "Uploading config.json to ${s3_config_url} (try ${try})"
error_log=$(mktemp)
headers=("-H" "Content-Type:")
# federated tokens in CaaS case need session token
if [ ! -z "$session_token" ]; then
headers=(${headers[@]} "-H" "x-amz-security-token: ${session_token}")
# use aws instead of curl because curl will always read entire stream memory to set Content-Length
# aws will do multipart upload
if cat "${app_data_snapshot}/config.json" \
| aws s3 cp - "${s3_config_url}" 2>"${error_log}"; then
break
fi
cat "${error_log}" && rm "${error_log}"
done
if tar -cvzf - -C "${app_data_snapshot}" . \
| openssl aes-256-cbc -e -pass "pass:${backup_key}" \
| curl --fail -X PUT ${headers[@]} --data-binary @- "${backup_url}" 2>"${error_log}"; then
if [[ ${try} -eq 5 ]]; then
echo "Backup failed uploading config.json"
btrfs subvolume delete "${app_data_snapshot}"
exit 1
fi
for try in `seq 1 5`; do
echo "Uploading backup to ${s3_data_url} (try ${try})"
error_log=$(mktemp)
if tar -czf - -C "${app_data_snapshot}" . \
| openssl aes-256-cbc -e -pass "pass:${password}" \
| aws s3 cp - "${s3_data_url}" 2>"${error_log}"; then
break
fi
cat "${error_log}" && rm "${error_log}"
@@ -51,9 +70,8 @@ done
btrfs subvolume delete "${app_data_snapshot}"
if [[ ${try} -eq 5 ]]; then
echo "Backup failed"
echo "Backup failed uploading backup tarball"
exit 1
else
echo "Backup successful"
fi

Some files were not shown because too many files have changed in this diff Show More