Compare commits

..

357 Commits

Author SHA1 Message Date
Girish Ramakrishnan 060d9e88ef Pass manifest to backupApp 2016-06-14 11:32:29 -07:00
Girish Ramakrishnan 9cf497a87d 0.15.2 changes 2016-06-13 23:31:35 -07:00
Girish Ramakrishnan b174765992 delete unused addonsa fter backup 2016-06-13 23:07:41 -07:00
Girish Ramakrishnan 9c2d217176 fix typo 2016-06-13 23:04:43 -07:00
Girish Ramakrishnan 3197349058 Fix app backup before updates
we were passing the current manifest to the backup code which meant that
the app version and manifest was incorrect.
2016-06-13 21:19:29 -07:00
Girish Ramakrishnan 5f3378878e remove lastBackupConfig 2016-06-13 19:19:28 -07:00
Girish Ramakrishnan 53cd45496b parse the response 2016-06-13 18:28:51 -07:00
Girish Ramakrishnan 942339435a return config correctly 2016-06-13 18:04:22 -07:00
Girish Ramakrishnan 2bd6519795 add assert 2016-06-13 18:02:57 -07:00
Girish Ramakrishnan 1763c36a0b restore from the backup's config.json
To summarize what we are doing is that restore is simply getting old data and
old code. Config is not changed. If config is required, then it has to come
in the restore REST parameter. Otherwise, there is too much magic.

https://blog.smartserver.io/2016/06/13/app-restore/
2016-06-13 16:54:59 -07:00
Girish Ramakrishnan 2c0eb33625 use apps.getAppConfig when generating config.json 2016-06-13 15:11:49 -07:00
Girish Ramakrishnan 040b9993c7 refactor code into getAppConfig 2016-06-13 15:07:15 -07:00
Girish Ramakrishnan 8f21126697 add a way to get the restore config (config.json) 2016-06-13 15:04:27 -07:00
Girish Ramakrishnan 716d29165c store altDomain in backupConfig 2016-06-13 13:14:04 -07:00
Girish Ramakrishnan a2ec308155 pass the lastBackupId explicity as the backup to restore to 2016-06-13 10:13:54 -07:00
Girish Ramakrishnan b82610ba00 pass data argument to restore 2016-06-13 10:08:58 -07:00
Johannes Zellner ed4674cd14 Disable the trial popup 2016-06-13 16:51:28 +02:00
Johannes Zellner 4e9dc75a37 Replace DatabaseError with ClientsError where applicable 2016-06-13 14:43:56 +02:00
Johannes Zellner f284b4cd83 Use clients.get() instead of clientdb.get() 2016-06-13 13:51:14 +02:00
Johannes Zellner 15cf83b37c Fixup the built-in client setup for tests 2016-06-13 13:48:43 +02:00
Johannes Zellner 0eff8911ee Do not use DatabaseError in routes clients.js 2016-06-13 13:29:39 +02:00
Girish Ramakrishnan 814a0ce3a6 wait for 10mins before sending out emails about app being down 2016-06-11 18:36:38 -07:00
Girish Ramakrishnan b3e1c221b7 bump infra to force a reconfigure for existing pr cloudrons 2016-06-11 13:49:55 -07:00
Girish Ramakrishnan dc31946e50 move webdav block outside location
when inside location, nginx is redirecting to 127.0.0.1 (no clue why)
2016-06-11 12:05:16 -07:00
Girish Ramakrishnan 36bbb98970 0.15.1 changes 2016-06-10 12:40:48 -07:00
Girish Ramakrishnan ea4cea9733 add tag on blur 2016-06-10 12:35:57 -07:00
Girish Ramakrishnan 7c06937a57 Add @fqdn hint to email aliases 2016-06-10 12:04:40 -07:00
Girish Ramakrishnan 597704d3ed remove the plain input email aliases 2016-06-10 11:58:25 -07:00
Girish Ramakrishnan 63290b9936 Final fixes to taginput 2016-06-10 11:58:00 -07:00
Girish Ramakrishnan 324222b040 Fix template code style 2016-06-09 16:11:57 -07:00
Girish Ramakrishnan f37b92da04 taginput: remove autocomplete. we don't use it 2016-06-09 10:34:42 -07:00
Girish Ramakrishnan 0de3b8fbdb taginput: add tag input control for email aliases 2016-06-09 10:34:29 -07:00
Johannes Zellner f0cb3f94cb Fixup the token ui to allow removal of user added clients 2016-06-09 16:17:14 +02:00
Johannes Zellner 1508a5c6b9 Add tokendb.delByClientId() 2016-06-09 15:42:52 +02:00
Johannes Zellner 9b9db6acf1 Only the rest api shall not allow to remove those 2016-06-09 15:35:46 +02:00
Johannes Zellner 001bf94773 Remove unused require 2016-06-09 15:35:20 +02:00
Johannes Zellner 0160c12965 Allow to distinguish between built-in auth clients and external ones 2016-06-09 15:35:00 +02:00
Johannes Zellner d08397336d Add test for addon auth clients deletion 2016-06-09 15:12:30 +02:00
Johannes Zellner 880754877d Prevent the rest api to delete addon auth clients 2016-06-09 14:44:38 +02:00
Johannes Zellner 984a191e4c Use the variable correctly 2016-06-09 14:24:53 +02:00
Girish Ramakrishnan cdca43311b wording and other minor fixes 2016-06-08 17:06:27 -07:00
Girish Ramakrishnan 020b47841a 0.15.1 changes 2016-06-08 16:42:00 -07:00
Girish Ramakrishnan 3f602c8a04 verifyWithUsername and not as id 2016-06-08 15:54:22 -07:00
Girish Ramakrishnan dea0c5642d bump mail container 2016-06-08 13:16:44 -07:00
Girish Ramakrishnan 3d2b75860b minor fixes to session ui 2016-06-08 13:04:22 -07:00
Girish Ramakrishnan 0da754a14b fix singular/plural 2016-06-08 12:52:19 -07:00
Girish Ramakrishnan 3d408c8c90 fix wording 2016-06-08 12:43:15 -07:00
Girish Ramakrishnan 40348a5132 Merge branch '0.15' 2016-06-08 11:56:24 -07:00
Girish Ramakrishnan 9a177d9e46 fix typo 2016-06-08 10:14:59 -07:00
Girish Ramakrishnan 6f1df9980d minor wording changes 2016-06-08 09:58:30 -07:00
Girish Ramakrishnan 0c9d331f47 more changes 2016-06-08 08:46:18 -07:00
Girish Ramakrishnan f9db24e162 Fix autoupdate detection logic
We should be comparing existing manifest ports with new manifest ports.
The user chosen bindings itself doesn't matter.
2016-06-08 08:45:40 -07:00
Johannes Zellner 385bf3561b Remove unused function in platform.js 2016-06-08 15:06:03 +02:00
Johannes Zellner 4304f20fe0 Fix some log output 2016-06-08 15:05:43 +02:00
Johannes Zellner 509083265f fix setup_infra.sh to accomodate ifconfig differences 2016-06-08 15:01:28 +02:00
Johannes Zellner 1b9dbd06c8 Fix typo 2016-06-08 14:55:14 +02:00
Johannes Zellner d6482414bb Fixup the clientdb tests 2016-06-08 14:49:54 +02:00
Johannes Zellner 194b9b35bd We now have 3 built-in api clients 2016-06-08 14:48:03 +02:00
Johannes Zellner 6b9acb4722 Preserve the built-in clients on db clean 2016-06-08 14:47:21 +02:00
Johannes Zellner 08c3cb9376 Insert the default auth clients for tests 2016-06-08 14:37:41 +02:00
Johannes Zellner 79631ba996 Provide a fallback for the redirect uri 2016-06-08 14:30:42 +02:00
Johannes Zellner 4776a005a5 Remove redundant client TYPE_*s 2016-06-08 14:09:06 +02:00
Johannes Zellner e954df2120 Issue developer tokens with cid-cli 2016-06-08 13:39:31 +02:00
Johannes Zellner 526a62a20e Fix the async iterator 2016-06-08 13:35:32 +02:00
Johannes Zellner e2432d002f Count activeTokens correctly 2016-06-08 13:31:17 +02:00
Johannes Zellner 6e4d6d1099 Do not show application token listing if there is nothing to show 2016-06-08 13:03:59 +02:00
Johannes Zellner fc2d1d61d7 Also logout the webadmin session 2016-06-08 13:03:21 +02:00
Johannes Zellner 4c4ae08b44 Allow users to see issued tokens and revoke them all in one 2016-06-08 12:56:48 +02:00
Johannes Zellner 401c0e1b44 Special handling for better ui for sdk tokens 2016-06-08 11:55:40 +02:00
Johannes Zellner e431bd6040 Fix typo 2016-06-08 11:36:01 +02:00
Johannes Zellner a69cd204d6 Handle sdk and cli clients just like the webadmin 2016-06-08 11:33:08 +02:00
Johannes Zellner 3c3de6205e Add test case for blocking cid-webadmin deletion 2016-06-08 11:27:10 +02:00
Johannes Zellner 16444f775d Prevent deletion of the built-in clients 2016-06-08 11:24:02 +02:00
Johannes Zellner 2676658b5d Add auth client cid-sdk and cid-cli 2016-06-08 11:20:06 +02:00
Girish Ramakrishnan fbb8a842c1 do not require password 2016-06-08 00:07:41 -07:00
Girish Ramakrishnan 62b586e8dd fix require path 2016-06-07 20:57:39 -07:00
Girish Ramakrishnan 313d98ef70 add a route to check for updates quickly 2016-06-07 20:24:41 -07:00
Girish Ramakrishnan 06448f146d fix typo 2016-06-07 20:09:53 -07:00
Girish Ramakrishnan 064d950f87 add new tests for field validation 2016-06-07 16:00:02 -07:00
Girish Ramakrishnan 3236ce9cd6 check if both are null 2016-06-07 15:36:45 -07:00
Johannes Zellner f74b22645f Token view is now admin only 2016-06-07 22:56:27 +02:00
Johannes Zellner 3540f2c197 Move token management to separate view for admins only 2016-06-07 22:54:53 +02:00
Girish Ramakrishnan 3231fe7874 use user.get which already set admin flag 2016-06-07 10:03:08 -07:00
Girish Ramakrishnan dc8fd2eab3 do not use userdb directly 2016-06-07 10:01:14 -07:00
Girish Ramakrishnan 3ae388602c fix wording 2016-06-07 09:27:42 -07:00
Johannes Zellner 733187f3c4 Also show redirectURI for developers to use 2016-06-07 16:21:33 +02:00
Johannes Zellner 02d2a7058e Remove whitespace in scope input 2016-06-07 16:15:53 +02:00
Johannes Zellner 25003bcf40 Add placeholder text in scope input 2016-06-07 16:12:56 +02:00
Johannes Zellner 234caa60eb Add form validation for scopes 2016-06-07 16:10:33 +02:00
Johannes Zellner a0227b6043 Remove now unused localhost test client
We can now simply use the regular APIs to do local development against a Cloudron
2016-06-07 16:03:50 +02:00
Johannes Zellner 46ac6c4918 Offset the footer in the console views 2016-06-07 15:58:53 +02:00
Johannes Zellner 4afde79297 Fix error message 2016-06-07 15:56:22 +02:00
Johannes Zellner 17d48f3fce Specify the expiration on the client. Currently this is 100 years.
I am not sure if this is the best approach, or if we should introduce a magic value instead.
2016-06-07 15:54:32 +02:00
Johannes Zellner facdabcc8d Mention the token expiration in the ui 2016-06-07 15:54:09 +02:00
Johannes Zellner 691803f10b Allow optional expiresAt to be set on token creation 2016-06-07 15:47:13 +02:00
Johannes Zellner 8144c6d086 Patch up the token delete button with the route 2016-06-07 15:38:42 +02:00
Johannes Zellner 290ab6cc7d Fix typo 2016-06-07 15:38:30 +02:00
Johannes Zellner 8e5af17e5d Add route to delete a single token 2016-06-07 15:34:27 +02:00
Johannes Zellner d9d94faf75 Refresh the client on token actions 2016-06-07 15:15:56 +02:00
Johannes Zellner 0201ab19e4 Pass in the client object 2016-06-07 14:47:56 +02:00
Johannes Zellner 721fe74f3c The route creates the subobject 2016-06-07 14:47:47 +02:00
Johannes Zellner 96eeb247a1 Add rest api to create a new token for a client 2016-06-07 14:29:37 +02:00
Johannes Zellner 6261231593 add ui for token generation 2016-06-07 14:19:20 +02:00
Johannes Zellner d62d2b17fe Select tokens and secrets with single click 2016-06-07 14:10:20 +02:00
Johannes Zellner 89cef4f050 Show tokens for admins 2016-06-07 14:07:41 +02:00
Johannes Zellner 8602e033c5 Only show active clients for non-admins 2016-06-07 13:46:45 +02:00
Johannes Zellner 3598d89b12 ?all is gone in clients route 2016-06-07 13:17:02 +02:00
Johannes Zellner ffd552583c Patch up the client remove ui 2016-06-07 13:12:53 +02:00
Johannes Zellner 9eabc9d266 Fixup the wording 2016-06-07 12:50:52 +02:00
Johannes Zellner edf8cd736e Add modal for client removal 2016-06-07 12:48:12 +02:00
Johannes Zellner c5ebe2c2bf Fixup the panel padding 2016-06-07 12:48:00 +02:00
Johannes Zellner 5d0ccc0dd7 Show correct token count 2016-06-07 12:37:18 +02:00
Johannes Zellner 4147455654 Fetch tokens for each client separately 2016-06-07 12:37:04 +02:00
Johannes Zellner f3436a99a2 Fixup the client list details 2016-06-07 12:28:33 +02:00
Johannes Zellner 70d569e2e8 List all oauth clients in webadmin 2016-06-07 12:26:14 +02:00
Johannes Zellner 684625fbaf Remove unused clients.getAllWithDetailsByUserId() 2016-06-07 12:25:48 +02:00
Johannes Zellner c8b9ae542c Simply return oauth clients instead of join with tokendb 2016-06-07 12:15:25 +02:00
Johannes Zellner af29c1ba86 Handle external api client requests separately 2016-06-07 12:00:18 +02:00
Johannes Zellner 207e81345f Log event for external login 2016-06-07 11:59:54 +02:00
Johannes Zellner d880731351 Support ?all query param for oauth clients get route 2016-06-07 11:18:30 +02:00
Johannes Zellner e603cfe96e Add clients.getAllWithDetails() 2016-06-07 11:17:29 +02:00
Johannes Zellner 5b93a2870f Add clientdb.getAllWithTokenCount() 2016-06-07 11:17:14 +02:00
Johannes Zellner 1214300800 Consistent code styling 2016-06-07 11:08:35 +02:00
Johannes Zellner 8159334cbf Avoid more inlining 2016-06-07 10:55:36 +02:00
Johannes Zellner 78135c807a Be consistent in client.js add -> create 2016-06-07 10:49:11 +02:00
Johannes Zellner bfa33e4d8e Fixup a linter issue 2016-06-07 10:48:36 +02:00
Johannes Zellner 8b23174769 add client.addOAuthClient() 2016-06-07 10:45:50 +02:00
Johannes Zellner a078c94b97 Ensure autofocus is handled 2016-06-07 10:43:33 +02:00
Johannes Zellner c86392cd60 Add modal dialog to create API clients 2016-06-07 10:42:54 +02:00
Johannes Zellner f0e9256d46 Avoid style inlining 2016-06-07 10:10:58 +02:00
Girish Ramakrishnan 0cd4e4f03a update now takes appStoreId 2016-06-04 20:51:17 -07:00
Girish Ramakrishnan 1766da9174 update code path now takes appStoreId 2016-06-04 20:05:29 -07:00
Girish Ramakrishnan dbdcf1ec27 pass data object to update 2016-06-04 19:12:36 -07:00
Girish Ramakrishnan c916ea2589 fix style 2016-06-04 18:56:53 -07:00
Girish Ramakrishnan 5540b5f545 remove unused require 2016-06-04 18:55:31 -07:00
Girish Ramakrishnan 1e38190e68 setting falsy values for cert/key removes it 2016-06-04 18:30:05 -07:00
Girish Ramakrishnan 8f3553090f make args optional in configure 2016-06-04 18:07:06 -07:00
Girish Ramakrishnan cc0f5a1f03 fix configure arg insanity 2016-06-04 16:32:27 -07:00
Girish Ramakrishnan a1c531d2a8 better type checking in configure and make accessRestriction optional 2016-06-04 16:27:50 -07:00
Girish Ramakrishnan 57cb3b04d7 generate 2048-bit keys 2016-06-04 15:59:53 -07:00
Girish Ramakrishnan a49cf98a8d do not allow appId to be set
this is some legacy code
2016-06-04 13:40:43 -07:00
Girish Ramakrishnan da6cab8dd6 we return 400 now 2016-06-04 13:32:41 -07:00
Girish Ramakrishnan 3b7cfdd7db better type checking 2016-06-04 13:31:18 -07:00
Girish Ramakrishnan f9251c8b37 sending manifest is now redundant 2016-06-04 13:23:13 -07:00
Girish Ramakrishnan 4068ff5f21 add TODO note to validate accessRestriction 2016-06-04 13:20:10 -07:00
Girish Ramakrishnan ee073c91a3 return BAD_FIELD if app was not found 2016-06-04 13:15:38 -07:00
Girish Ramakrishnan 9e8742ca87 download manifest from appstore when appStoreId is provided 2016-06-04 01:07:43 -07:00
Girish Ramakrishnan 7f99fe2399 appStoreId has empty string default 2016-06-03 23:58:09 -07:00
Girish Ramakrishnan bfe8df35df toLowerCase in one place 2016-06-03 23:54:46 -07:00
Girish Ramakrishnan e2848d3e08 fix apps.install insane arg list 2016-06-03 23:35:55 -07:00
Girish Ramakrishnan bc823b4a75 make checkManifestConstraints return AppsError 2016-06-03 22:19:09 -07:00
Girish Ramakrishnan c24f780722 make validateAccessRestriction and validateMemory return AppsError 2016-06-03 22:16:55 -07:00
Girish Ramakrishnan 0d51ec9920 make validatePortBindings return AppsError 2016-06-03 22:15:02 -07:00
Girish Ramakrishnan e07e544029 make validateHostname return AppsError 2016-06-03 22:14:08 -07:00
Girish Ramakrishnan 5aff55c5ca typo when stashing altDomain 2016-06-03 19:50:01 -07:00
Girish Ramakrishnan 5ebc29746d fix failing tests 2016-06-03 19:14:16 -07:00
Girish Ramakrishnan 8fc44e6bc9 remove redundant checks 2016-06-03 19:08:47 -07:00
Girish Ramakrishnan 44f4872134 remove dead comments 2016-06-03 17:55:05 -07:00
Girish Ramakrishnan 49dd584a41 return expiresAt as ISO-string for API consistency 2016-06-03 10:11:09 -07:00
Girish Ramakrishnan 6d8f1f90d4 add note to change expires to TIMESTAMP type 2016-06-03 10:07:30 -07:00
Girish Ramakrishnan c1ded66c1a make download_url a post route 2016-06-03 09:23:15 -07:00
Johannes Zellner 4df49a82e5 Some clientdb.TYPE_ oversight in clients.js 2016-06-03 15:28:04 +02:00
Johannes Zellner 92e6ee9539 The clientSecret is now only ever created in the clients.js 2016-06-03 15:11:08 +02:00
Johannes Zellner 3ad2a2a5ca Fixup the unit tests 2016-06-03 15:07:44 +02:00
Johannes Zellner 226537de04 Move client TYPE_* to clients.js 2016-06-03 15:05:00 +02:00
Johannes Zellner 41b324eb2d Remove clientdb usage in addons.js 2016-06-03 14:56:45 +02:00
Johannes Zellner 1360729e97 Don't use clientdb directly from auth.js and apptask.js 2016-06-03 14:52:59 +02:00
Johannes Zellner 725e1debcc Provide getByAppIdAndType() by clients.js 2016-06-03 14:47:06 +02:00
Johannes Zellner 201efa70b7 use clients instead of clientdb in oauth2.js 2016-06-03 14:38:58 +02:00
Johannes Zellner c52d0369fa Provide better feedback on invalid scopes 2016-06-03 13:53:33 +02:00
Johannes Zellner b4dfad3aa3 Fixup the unit tests after removing PREFIX_USER 2016-06-03 13:09:26 +02:00
Johannes Zellner 7667cdc66d PREFIX_USER finally gone 2016-06-03 13:01:23 +02:00
Johannes Zellner 3a9a667890 Make all token grants without PREFIX_USER 2016-06-03 13:01:05 +02:00
Johannes Zellner 304cfed5a9 Result of password setting is now a plain token identifier 2016-06-03 13:00:07 +02:00
Johannes Zellner 778c583a52 Activation hands out a token without PREFIX_USER now 2016-06-03 12:59:13 +02:00
Johannes Zellner f988bb4d14 Do not use PREFIX_USER for token managment 2016-06-03 12:58:39 +02:00
Johannes Zellner 7057f1aaa2 All token identifiers are now plain user ids 2016-06-03 12:54:59 +02:00
Johannes Zellner e06f5f88b8 Remove the token types 2016-06-03 12:54:34 +02:00
Johannes Zellner 03cd3f0b6f Remove attached tokenType on req.user 2016-06-03 12:53:11 +02:00
Johannes Zellner 615f875169 Remove PREFIX_DEV for developer tokens 2016-06-03 12:52:10 +02:00
Johannes Zellner f27ba04a00 Add test case for developer tokens 2016-06-03 11:11:11 +02:00
Johannes Zellner 3e0006a327 Allow tokens with SCOPE_ROLE_SDK through without a password 2016-06-03 11:10:59 +02:00
Johannes Zellner 558ca42ae8 Issue developer tokens with SCOPE_ROLE_SDK 2016-06-03 11:10:22 +02:00
Johannes Zellner 9d8a803185 Handle scope roles in scope checks 2016-06-03 11:09:48 +02:00
Johannes Zellner 105047b0c4 Add SCOPE_ROLE_SDK 2016-06-03 11:08:35 +02:00
Johannes Zellner e335aa5dee Check for sdk token instead of token type DEV 2016-06-03 10:17:52 +02:00
Johannes Zellner 10163733db Separate the scope checking 2016-06-03 10:10:58 +02:00
Girish Ramakrishnan 251fad8514 add test for groupIds in listing api 2016-06-03 00:14:52 -07:00
Girish Ramakrishnan 036740f97b filter out correct fields in the route code 2016-06-03 00:04:17 -07:00
Girish Ramakrishnan f4958d936c return groupIds in get user route 2016-06-03 00:00:11 -07:00
Girish Ramakrishnan 80ca69a128 user.update does not need the user object 2016-06-02 23:53:06 -07:00
Girish Ramakrishnan 097d23c412 move logic to model code 2016-06-02 23:29:43 -07:00
Girish Ramakrishnan 13a1213b0d make group listing API return member userIds 2016-06-02 21:07:33 -07:00
Girish Ramakrishnan 76fe2bf531 add note to fix precision at some point 2016-06-02 19:43:23 -07:00
Girish Ramakrishnan 50c4e4c91e log event only after lock is acquired 2016-06-02 19:26:58 -07:00
Girish Ramakrishnan 46441d1814 cloudron.update is not exposed 2016-06-02 19:23:21 -07:00
Girish Ramakrishnan a4e73be834 pass auditSource for certificate renewal 2016-06-02 18:54:45 -07:00
Girish Ramakrishnan 6be0d0814d pass auditSource from cron.js 2016-06-02 18:51:50 -07:00
Girish Ramakrishnan e30d71921e pass auditSource for app autoupdater 2016-06-02 18:49:56 -07:00
Girish Ramakrishnan a49c78f32c make box autoupdate generate eventlog 2016-06-02 18:47:09 -07:00
Girish Ramakrishnan b077223e58 fix scope name 2016-06-02 17:49:54 -07:00
Girish Ramakrishnan d2864dfe56 rename root scope to cloudron scope (for lack of better scope name) 2016-06-02 16:51:14 -07:00
Girish Ramakrishnan 6d08af35a8 give developer token root scope 2016-06-02 15:58:40 -07:00
Girish Ramakrishnan 54f9d653f7 fix error messages 2016-06-02 14:41:21 -07:00
Girish Ramakrishnan 8d65f93fa4 return error.message 2016-06-02 14:40:29 -07:00
Girish Ramakrishnan 462440bb30 do not check for password in profile route
This is already checked by the verifyPassword middleware based on
the token type.

When using dev tokens, this check barfs for lack of password field
even when none is required.
2016-06-02 14:26:01 -07:00
Girish Ramakrishnan 65261dc4d5 add time_zone setter route 2016-06-02 13:54:07 -07:00
Girish Ramakrishnan 54ead09aac make the name API work
currently this only works for the main webadmin (and not for
nakeddomain, error etc) but that's fine.
2016-06-02 13:25:02 -07:00
Girish Ramakrishnan 28b3550214 use error.message 2016-06-02 13:00:23 -07:00
Girish Ramakrishnan e2e70da4c5 restrict length to 32 2016-06-02 12:51:49 -07:00
Johannes Zellner 7326ea27ca Only set username and displayName after successful update 2016-06-02 21:12:02 +02:00
Girish Ramakrishnan 1fe00f7f80 do not use verbs in resource url 2016-06-02 12:01:48 -07:00
Girish Ramakrishnan e9e9d6000d remove token check for user.update to work with dev tokens 2016-06-02 11:29:59 -07:00
Girish Ramakrishnan 6dccb3655f add no groups available message in edit user dialog 2016-06-02 10:55:34 -07:00
Girish Ramakrishnan c3113bd74d go back to step2 if activation fails 2016-06-02 10:40:06 -07:00
Girish Ramakrishnan e79119b72a 0.15.0 changes 2016-06-02 10:32:10 -07:00
Johannes Zellner 086cfdc1e6 Disabled form fields are not POSTed
I did not know about that fact, one has to use readonly
2016-06-02 16:12:32 +02:00
Johannes Zellner 1f091d3b4b We have to let angular know 2016-06-02 16:06:15 +02:00
Johannes Zellner 892fa4b2ec We still require the username to be sent always 2016-06-02 16:01:25 +02:00
Johannes Zellner a87b4b207c Adhere to already set username in user setup view 2016-06-02 15:47:58 +02:00
Johannes Zellner bdd14022d6 Report user conflict message all the way through the rest routes 2016-06-02 15:41:07 +02:00
Johannes Zellner 3d40cf03b1 Pass down the reason why the user conflicts 2016-06-02 15:39:21 +02:00
Johannes Zellner 594be7dbbd Allow the userdb code to distinguish between username or email duplicates 2016-06-02 15:34:27 +02:00
Johannes Zellner a52e2ffc23 Distinguish between username and email conflict 2016-06-02 15:19:35 +02:00
Johannes Zellner 8eeee712aa Remove unused require 2016-06-02 14:14:16 +02:00
Johannes Zellner 0f62faa198 All our tokens are now representing an user with a profile 2016-06-02 14:13:57 +02:00
Johannes Zellner bfd66cf309 Remove unused token PREFIX_APP 2016-06-02 14:07:41 +02:00
Johannes Zellner c2f7d61e34 Remove unused token TYPE_APP 2016-06-02 14:07:19 +02:00
Johannes Zellner d5d5e356ae Add error handling for invalid usernames 2016-06-02 13:52:44 +02:00
Johannes Zellner 531752cd43 Use placeholder for description in username and displayName fields 2016-06-02 13:52:33 +02:00
Johannes Zellner 9eac56578c Allow admins to set the username and displayName optionally on user creation 2016-06-02 13:32:45 +02:00
Johannes Zellner d06398dbfd Move webdav nginx fixes into app endpoint
Not sure if this will now still work with oauth proxy though.
2016-06-02 09:49:01 +02:00
Girish Ramakrishnan 60ce6b69ee profile updates must be POST 2016-06-02 00:31:41 -07:00
Girish Ramakrishnan 4fcc7fe99f updateUser is POST 2016-06-02 00:27:06 -07:00
Girish Ramakrishnan 82cd215ffa merge bad fields and pass error.message correctly in REST responses 2016-06-02 00:12:21 -07:00
Girish Ramakrishnan 1dcea84068 fix typo 2016-06-01 23:43:21 -07:00
Girish Ramakrishnan 4107252bfe developer mode is true by default 2016-06-01 23:13:12 -07:00
Girish Ramakrishnan 9cc6cb56f7 fix error message 2016-06-01 19:38:42 -07:00
Girish Ramakrishnan 48b99a4203 enable developer mode by default
also emphasize on the api aspect
2016-06-01 18:57:15 -07:00
Girish Ramakrishnan 824767adbb enabled -> isEnabled 2016-06-01 18:21:02 -07:00
Girish Ramakrishnan 3d84880d92 keep limits in sync with the nginx config 2016-06-01 18:09:23 -07:00
Girish Ramakrishnan dfa08469d6 set timeouts explicitly 2016-06-01 17:33:28 -07:00
Girish Ramakrishnan d798073d95 fix comment of default_server 2016-06-01 17:28:15 -07:00
Girish Ramakrishnan 41632b8c11 fix favicon of naked domain 2016-06-01 17:27:39 -07:00
Girish Ramakrishnan 6ccc46717e remove unused middleware 2016-06-01 16:59:51 -07:00
Girish Ramakrishnan 2495caf2eb remove unused redis module 2016-06-01 16:55:58 -07:00
Girish Ramakrishnan ae9c104a8b remove unused bytes module 2016-06-01 16:54:25 -07:00
Girish Ramakrishnan 683f371778 remove serve-favicon
favicon is served up as /api/v1/cloudron/avatar
2016-06-01 16:52:28 -07:00
Girish Ramakrishnan eb29bdd575 document keepalive_timeout 2016-06-01 16:51:52 -07:00
Girish Ramakrishnan b13de298bf Add some REST api tests 2016-06-01 16:33:18 -07:00
Johannes Zellner 47978436c2 Set Destination header for webdav in nginx proxy 2016-06-01 18:49:50 +02:00
Johannes Zellner 71b5cc4702 Fix invite mail wording 2016-06-01 18:45:31 +02:00
Girish Ramakrishnan 5a9e32d41a hide aliases field if no username is set 2016-06-01 06:33:29 -07:00
Girish Ramakrishnan b03e4db8d5 check for null username 2016-05-31 21:38:51 -07:00
Girish Ramakrishnan 663ff2410a user.update must become a post route 2016-05-31 11:51:56 -07:00
Girish Ramakrishnan f763759008 return empty groupIds 2016-05-31 11:49:59 -07:00
Girish Ramakrishnan 69aa11d6c6 send pretty json 2016-05-31 11:14:59 -07:00
Girish Ramakrishnan 65041743c5 update to connect-lastmile@0.1.0 2016-05-31 10:48:37 -07:00
Girish Ramakrishnan 76214d3d7a no variable named infra_version 2016-05-30 19:45:59 -07:00
Girish Ramakrishnan be83a967fc node require will not work without json extension 2016-05-30 17:03:56 -07:00
Girish Ramakrishnan 119e095710 actually change ownership 2016-05-30 15:51:52 -07:00
Girish Ramakrishnan 5df3a41988 INFRA_VERSION may not exist 2016-05-30 14:48:41 -07:00
Girish Ramakrishnan a34b611e20 make INFRA_VERSION writable by yellowtent user 2016-05-30 12:52:39 -07:00
Girish Ramakrishnan 75c1731443 do not add app mailboxes to database
a) we don't allow .app pattern in database for aliases and mailboxes
b) the addons already know about app names separately
2016-05-30 01:38:43 -07:00
Girish Ramakrishnan 9e36b7abf4 load addon vars for existing infra case 2016-05-30 01:06:41 -07:00
Girish Ramakrishnan b37226d4d1 fix ui issues 2016-05-30 00:07:58 -07:00
Girish Ramakrishnan 311efe5d10 test: add test for getting aliases 2016-05-29 23:23:03 -07:00
Girish Ramakrishnan ebdd6d8a31 add missing require 2016-05-29 23:15:55 -07:00
Girish Ramakrishnan 3ee9f70113 return alias info in mailbox response 2016-05-29 23:14:24 -07:00
Girish Ramakrishnan adfc069e16 allow user alias to be set for custom domains 2016-05-29 23:02:01 -07:00
Girish Ramakrishnan 31fd0d711a add setAliases 2016-05-29 22:45:48 -07:00
Girish Ramakrishnan a6a852cfae remove bogus debug 2016-05-29 22:01:21 -07:00
Girish Ramakrishnan e9b3e22e86 check version of existing infra 2016-05-29 21:58:04 -07:00
Girish Ramakrishnan 564d61bcf5 fix typo 2016-05-29 21:31:49 -07:00
Girish Ramakrishnan 5582ac7402 add some debugs 2016-05-29 21:28:55 -07:00
Girish Ramakrishnan a05b6ad78d delete mailbox on user delete 2016-05-29 21:02:51 -07:00
Girish Ramakrishnan ec71390d0b autocreate mailbox when username is available 2016-05-29 19:14:01 -07:00
Girish Ramakrishnan 68a3862ee5 add create and remove mailbox 2016-05-29 18:56:40 -07:00
Girish Ramakrishnan a9f70d8363 add mailbox search endpoint 2016-05-29 18:24:54 -07:00
Girish Ramakrishnan e91539d79a add a todo 2016-05-29 18:08:16 -07:00
Girish Ramakrishnan 5546bfbf0e add mailbox ldap auth point 2016-05-29 17:25:23 -07:00
Girish Ramakrishnan 803d47b426 refactor authenticate path into a middleware 2016-05-29 17:16:52 -07:00
Girish Ramakrishnan e4c0192243 rename to appUserBind since it is tailored for apps 2016-05-29 17:07:48 -07:00
Girish Ramakrishnan d5b5289e0c Add mailbox importer for existing users and apps
this should prevent conflicts of mailboxes from the get-go.
2016-05-28 02:07:43 -07:00
Girish Ramakrishnan 2909aad72a use async.series 2016-05-28 01:59:48 -07:00
Girish Ramakrishnan cafbb31e78 push aliases to mail container on startup 2016-05-28 01:53:25 -07:00
Girish Ramakrishnan 080128539c set/unset aliases on the mail container 2016-05-28 01:33:20 -07:00
Girish Ramakrishnan cf93a99a4e add a note about mailboxes 2016-05-27 22:28:56 -07:00
Girish Ramakrishnan ce927bfa22 alias
also remove id since it's not useful for mailbox case (not like
mailbox can be renamed and we need a fixed it)
2016-05-27 22:20:08 -07:00
Girish Ramakrishnan 6993a9c7e7 add more mailbox route test 2016-05-27 18:23:14 -07:00
Girish Ramakrishnan 84d04cce16 initial mailboxes route 2016-05-27 18:17:57 -07:00
Girish Ramakrishnan f735fd8172 add mailboxes tests 2016-05-27 17:43:25 -07:00
Girish Ramakrishnan 53e28db1d6 add note on accessRestriction 2016-05-27 11:10:36 -07:00
Girish Ramakrishnan 77457d1ea9 initial mailbox db and model code 2016-05-27 10:36:47 -07:00
Girish Ramakrishnan 161b7cf76b add mailboxes table 2016-05-26 21:08:20 -07:00
Girish Ramakrishnan 01b6defd24 mount mail container /run into data 2016-05-26 15:18:10 -07:00
Girish Ramakrishnan badc524ff2 '-' has special meaning haraka
so do '.app' instead
2016-05-26 10:58:30 -07:00
Girish Ramakrishnan b3f53099f0 allow only alpha numerals in username 2016-05-25 21:36:20 -07:00
Girish Ramakrishnan a28560cdc0 0.14.2 changes 2016-05-24 20:17:20 -07:00
Girish Ramakrishnan 4afdf50736 finally finally finally the tests are working 2016-05-24 20:04:37 -07:00
Girish Ramakrishnan 078e36f07f turns out we cannot use sudo since it asks for password 2016-05-24 19:59:47 -07:00
Girish Ramakrishnan 3b8e15a61c check for node in path 2016-05-24 18:32:15 -07:00
Girish Ramakrishnan 67682c5d27 check for test image 2016-05-24 17:44:19 -07:00
Girish Ramakrishnan 48e3b8ebf9 provide a dummy config for tests 2016-05-24 17:36:54 -07:00
Girish Ramakrishnan 2072dedf66 bump mail addon version 2016-05-24 16:46:54 -07:00
Girish Ramakrishnan 51f43ecc27 use infra_version.js in checkInstall 2016-05-24 16:46:27 -07:00
Girish Ramakrishnan 2347a7ced2 admin email is a platform property 2016-05-24 16:36:56 -07:00
Girish Ramakrishnan b2cadaf95c load vars files after the platform is created 2016-05-24 16:28:59 -07:00
Girish Ramakrishnan 957f787701 setup mail addon root credentials 2016-05-24 16:18:21 -07:00
Girish Ramakrishnan ad48067bb2 setup_infra now uses infra_version.js
INFRA_VERSION is now removed. Note that DATA_DIR/INFRA_VERSION
still exists.
2016-05-24 16:16:03 -07:00
Girish Ramakrishnan 12b6c46558 use infra_version.js in splashpage 2016-05-24 13:31:45 -07:00
Girish Ramakrishnan b4ba17c599 use the constant 2016-05-24 13:23:41 -07:00
Girish Ramakrishnan 7fb28662c1 remove old images in platform.js 2016-05-24 13:23:41 -07:00
Girish Ramakrishnan 4845db538a use infra_version.js in platform.js 2016-05-24 13:23:41 -07:00
Girish Ramakrishnan 8429985253 use infra_version.js in addons.js 2016-05-24 13:23:41 -07:00
Girish Ramakrishnan aff9ff47bc use infra_version.js in baseimage script 2016-05-24 13:23:38 -07:00
Girish Ramakrishnan 9b3077eca3 add infra_version.js 2016-05-24 13:02:38 -07:00
Girish Ramakrishnan 39396cb3ab call callback if provided 2016-05-24 11:39:05 -07:00
Girish Ramakrishnan 364f0ead51 debug out the cmd 2016-05-24 11:26:11 -07:00
Girish Ramakrishnan 5ac1d5575c platform.js: minor refactor 2016-05-24 10:58:18 -07:00
Girish Ramakrishnan e5a030baff move platform cleanup bits to javascript 2016-05-24 10:52:55 -07:00
Girish Ramakrishnan a100837e69 Add helpers to restore/reconfigure all apps 2016-05-24 10:44:45 -07:00
Girish Ramakrishnan ffacf17a42 add paths.INFRA_VERSION_FILE 2016-05-24 10:26:08 -07:00
Girish Ramakrishnan f5d37b6443 add ini module 2016-05-24 10:25:33 -07:00
Girish Ramakrishnan d71d09c1ba Add shell.execSync 2016-05-24 10:22:39 -07:00
Girish Ramakrishnan c1a2444dfa move container creation to platform.js 2016-05-24 09:40:26 -07:00
Girish Ramakrishnan ef40aae3ba set adminEmail to no-reply@localhost for tests 2016-05-24 00:54:38 -07:00
Girish Ramakrishnan 9570086c87 add config.smtpPort 2016-05-24 00:53:42 -07:00
Girish Ramakrishnan 57a823a698 make tests work 2016-05-24 00:44:01 -07:00
Girish Ramakrishnan ec0ee07b17 test: email works now 2016-05-23 23:17:38 -07:00
Girish Ramakrishnan 3d7545133e wait 30 secs in the beginning 2016-05-23 23:16:02 -07:00
Girish Ramakrishnan bcc752469a remove containers after the test 2016-05-23 22:47:40 -07:00
Girish Ramakrishnan da85f4c096 stop ldap server in test 2016-05-23 21:59:06 -07:00
Girish Ramakrishnan 3b740a5651 make tests work 2016-05-23 20:41:00 -07:00
Girish Ramakrishnan 7eb202f19a test: use the test-app instead of duplicating the checks in the tests 2016-05-23 20:17:11 -07:00
Girish Ramakrishnan 8dbd4c8527 use 1024 bit keys
Stacked error: error:04075070:rsa routines:RSA_sign:digest too big for rsa key
2016-05-23 19:31:57 -07:00
Girish Ramakrishnan 88f2ce554d bump to mail 0.13.1 with the auth fix 2016-05-23 18:57:59 -07:00
Girish Ramakrishnan 57888659a6 Update test app version 2016-05-23 18:31:12 -07:00
Girish Ramakrishnan ebdefa7f18 test: oauth addon 2016-05-23 17:34:25 -07:00
Girish Ramakrishnan 569150f602 tests: create a self-signed cert 2016-05-23 16:40:18 -07:00
Girish Ramakrishnan 6ccb806628 make apps-test pass 2016-05-23 16:31:02 -07:00
Girish Ramakrishnan ae807b28b6 test: let server start the infra
otherwise, deps like dkim keys need to be setup in tests as well
2016-05-23 15:53:51 -07:00
Girish Ramakrishnan 00726b01e2 pass -no-run-if-empty instead 2016-05-23 15:50:36 -07:00
Girish Ramakrishnan f5b777ab33 add route tests for username 2016-05-23 15:00:21 -07:00
Girish Ramakrishnan d84e584222 add some username tests 2016-05-23 14:56:09 -07:00
Girish Ramakrishnan 31e452e1cc test: mixed case reserved name 2016-05-23 14:52:29 -07:00
Girish Ramakrishnan e015b9bd7a 0.14.1 changes 2016-05-23 12:29:49 -07:00
Girish Ramakrishnan 10e0cbcebc do not set allowHalfOpen (otherwise we have to end socket ourself) 2016-05-23 10:50:04 -07:00
Girish Ramakrishnan 2768c3a336 acme: configure prod based on caas or acme 2016-05-23 09:48:17 -07:00
Girish Ramakrishnan 37512c4cac Wrap the stdin stream to indicate EOF
The docker exec protocol supports half-closing to signal that the stdin
is finished. The CLI tool tried to do this by closing it's half of the
socket when the stdin finished. Unfortunately, this does not work because
nginx immediately terminates a half-close :/ Node itself has no problem.

http://mailman.nginx.org/pipermail/nginx/2008-September/007388.html
seems to support the hypothesis. Basically, for HTTP and websockets
there is no notion of half-close.

Websocket protocol itself has no half-close as well:
http://www.lenholgate.com/blog/2011/07/websockets---i-miss-the-tcp-half-close.html
http://doc.akka.io/docs/akka/2.4.5/scala/http/client-side/websocket-support.html

The fix is to implement our own protocol that wrap stdin. We put a length
header for every payload. When we hit EOF, the length is set to 0. The server
sees this 0 length header and closes the exec container socket.
2016-05-22 22:27:49 -07:00
Girish Ramakrishnan 0aaaa866e4 Add a whole bunch of magic for docker.exec to work 2016-05-22 00:27:32 -07:00
Girish Ramakrishnan 53cb7fe687 debug out cmd 2016-05-19 15:54:35 -07:00
Girish Ramakrishnan da42f2f00c fix boolean logic 2016-05-19 15:54:35 -07:00
Girish Ramakrishnan 27d2daae93 leave a note in nginx config 2016-05-19 12:27:54 -07:00
Girish Ramakrishnan 42cc8249f8 reserve usernames with -app in them 2016-05-18 21:45:02 -07:00
Girish Ramakrishnan de055492ef set username restriction to 2 chars 2016-05-18 11:05:45 -07:00
Girish Ramakrishnan efa3ccaffe fix crash because of missing error handling 2016-05-18 10:00:32 -07:00
103 changed files with 8202 additions and 3722 deletions
+22
View File
@@ -513,3 +513,25 @@
[0.14.0]
- You have mail :-)
[0.14.1]
- 2-character usernames are now allowed
- Make cloudron CLI push/pull more robust
[0.14.2]
- Update mail addon
[0.15.0]
- [REST API](https://cloudron.io/references/api.html) is now in public beta
- Enable Developer mode by default for new Cloudrons
- Reverse proxy fixes for apps exposing a WebDav server
- Allow admins to optionally set the username and displayName on user creation
- Fix app autoupdate logic to detect if one or more in-use port bindings was removed
[0.15.1]
- Fix mail connectivity from IPv6 clients
- Add API token management UI
- Improved UI to enter email aliases
[0.15.2]
- Allow restoring apps from any previous backup
+2 -2
View File
@@ -137,8 +137,8 @@ while true; do
sleep 30
done
echo "Copying INFRA_VERSION"
$scp22 "${SCRIPT_DIR}/../src/INFRA_VERSION" root@${server_ip}:.
echo "Copying infra_version.js"
$scp22 "${SCRIPT_DIR}/../src/infra_version.js" root@${server_ip}:.
echo "Copying box source"
cd "${SOURCE_DIR}"
+14 -37
View File
@@ -19,12 +19,6 @@ function die {
[[ "$(systemd --version 2>&1)" == *"systemd 229"* ]] || die "Expecting systemd to be 229"
if [ -f "${SOURCE_DIR}/INFRA_VERSION" ]; then
source "${SOURCE_DIR}/INFRA_VERSION"
else
echo "No INFRA_VERSION found, skip pulling docker images"
fi
if [ ${SELFHOSTED} == 0 ]; then
echo "!! Initializing Ubuntu image for CaaS"
else
@@ -156,30 +150,22 @@ update-grub
# now add the user to the docker group
usermod "${USER}" -a -G docker
if [ -z $(echo "${INFRA_VERSION}") ]; then
echo "Skip pulling base docker images"
else
echo "=== Pulling base docker images ==="
docker pull "${BASE_IMAGE}"
echo "==== Install nodejs ===="
# Cannot use anything above 4.1.1 - https://github.com/nodejs/node/issues/3803
mkdir -p /usr/local/node-4.1.1
curl -sL https://nodejs.org/dist/v4.1.1/node-v4.1.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-4.1.1
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 "=== Pulling mysql addon image ==="
docker pull "${MYSQL_IMAGE}"
echo "==== Downloading docker images ===="
images=$(node -e "var i = require('${SOURCE_DIR}/infra_version.js'); console.log(i.baseImage, Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
echo "=== Pulling postgresql addon image ==="
docker pull "${POSTGRESQL_IMAGE}"
echo "=== Pulling redis addon image ==="
docker pull "${REDIS_IMAGE}"
echo "=== Pulling mongodb addon image ==="
docker pull "${MONGODB_IMAGE}"
echo "=== Pulling graphite docker images ==="
docker pull "${GRAPHITE_IMAGE}"
echo "=== Pulling mail ==="
docker pull "${MAIL_IMAGE}"
fi
echo "Pulling images: ${images}"
for image in ${images}; do
docker pull "${image}"
done
echo "==== Install nginx ===="
apt-get -y install nginx-full
@@ -210,15 +196,6 @@ echo "==== Install logrotate ==="
apt-get install -y cron logrotate
systemctl enable cron
echo "==== Install nodejs ===="
# Cannot use anything above 4.1.1 - https://github.com/nodejs/node/issues/3803
mkdir -p /usr/local/node-4.1.1
curl -sL https://nodejs.org/dist/v4.1.1/node-v4.1.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-4.1.1
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
chown "${USER}:${USER}" -R "${INSTALLER_SOURCE_DIR}"
@@ -0,0 +1,24 @@
'use strict';
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE mailboxes(' +
'name VARCHAR(128) NOT NULL,' +
'aliasTarget VARCHAR(128),' +
'creationTime TIMESTAMP,' +
'PRIMARY KEY (name))';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE mailboxes', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,25 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
// imports mailbox entries for existing users
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
function addUserMailboxes(done) {
db.all('SELECT username FROM users', function (error, results) {
if (error) return done(error);
async.eachSeries(results, function (r, next) {
if (!r.username) return next();
db.runSql('INSERT INTO mailboxes (name) VALUES (?)', [ r.username ], next);
}, done);
});
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,16 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN lastBackupConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN lastBackupConfigJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
+16 -6
View File
@@ -38,7 +38,7 @@ CREATE TABLE IF NOT EXISTS tokens(
identifier VARCHAR(128) NOT NULL,
clientId VARCHAR(128),
scope VARCHAR(512) NOT NULL,
expires BIGINT NOT NULL,
expires BIGINT NOT NULL, // FIXME: make this a timestamp
PRIMARY KEY(accessToken));
CREATE TABLE IF NOT EXISTS clients(
@@ -62,16 +62,15 @@ CREATE TABLE IF NOT EXISTS apps(
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
location VARCHAR(128) NOT NULL UNIQUE,
dnsRecordId VARCHAR(512),
accessRestrictionJson TEXT,
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
oauthProxy BOOLEAN DEFAULT 0,
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
memoryLimit BIGINT DEFAULT 0,
altDomain VARCHAR(256),
lastBackupId VARCHAR(128),
lastBackupConfigJson TEXT, // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
lastBackupId VARCHAR(128), // tracks last valid backup, can be removed
oldConfigJson TEXT, // used to pass old config for apptask
oldConfigJson TEXT, // used to pass old config for apptask, can be removed when we use a queue
PRIMARY KEY(id));
@@ -86,7 +85,7 @@ CREATE TABLE IF NOT EXISTS authcodes(
authCode VARCHAR(128) NOT NULL UNIQUE,
userId VARCHAR(128) NOT NULL,
clientId VARCHAR(128) NOT NULL,
expiresAt BIGINT NOT NULL,
expiresAt BIGINT NOT NULL, // ## FIXME: make this a timestamp
PRIMARY KEY(authCode));
CREATE TABLE IF NOT EXISTS settings(
@@ -115,6 +114,17 @@ CREATE TABLE IF NOT EXISTS eventlog(
action VARCHAR(128) NOT NULL,
source JSON, /* { userId, username, ip }. userId can be null for cron,sysadmin */
data JSON, /* free flowing json based on action */
creationTime TIMESTAMP, /* FIXME: precision must be TIMESTAMP(2) */
PRIMARY KEY (id));
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
aliasTarget VARCHAR(128), /* the target name type is an alias */
creationTime TIMESTAMP,
PRIMARY KEY (id));
+4221 -2216
View File
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -16,10 +16,9 @@
"async": "^1.2.1",
"aws-sdk": "^2.1.46",
"body-parser": "^1.13.1",
"bytes": "^2.3.0",
"cloudron-manifestformat": "^2.4.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "0.0.13",
"connect-lastmile": "^0.1.0",
"connect-timeout": "^1.5.0",
"cookie-parser": "^1.3.5",
"cookie-session": "^1.1.0",
@@ -33,6 +32,7 @@
"express": "^4.12.4",
"express-session": "^1.11.3",
"hat": "0.0.3",
"ini": "^1.3.4",
"json": "^9.0.3",
"ldapjs": "^0.7.1",
"mime": "^1.3.4",
@@ -56,7 +56,6 @@
"proxy-middleware": "^0.13.0",
"safetydance": "^0.1.1",
"semver": "^4.3.6",
"serve-favicon": "^2.2.0",
"split": "^1.0.0",
"superagent": "^1.8.3",
"supererror": "^0.7.1",
@@ -89,7 +88,6 @@
"mocha": "*",
"nock": "^3.4.0",
"node-sass": "^3.0.0-alpha.0",
"redis": "^2.4.2",
"request": "^2.65.0",
"sinon": "^1.12.2",
"yargs": "^3.15.0"
+6 -5
View File
@@ -9,8 +9,6 @@ readonly BOX_SRC_DIR="/home/yellowtent/box"
readonly DATA_DIR="/home/yellowtent/data"
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
source "${script_dir}/../src/INFRA_VERSION" # this injects INFRA_VERSION
echo "Setting up nginx update page"
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
@@ -24,13 +22,16 @@ rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}"
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
# create nginx config
infra_version="none"
[[ -f "${DATA_DIR}/INFRA_VERSION" ]] && infra_version=$(cat "${DATA_DIR}/INFRA_VERSION")
if [[ "${arg_retire}" == "true" || "${infra_version}" != "${INFRA_VERSION}" ]]; then
readonly current_infra=$(node -e "console.log(require('${script_dir}/../src/infra_version.js').version);")
existing_infra="none"
[[ -f "${DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
if [[ "${arg_retire}" == "true" || "${existing_infra}" != "${current_infra}" ]]; then
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire} existing: ${existing_infra} current: ${current_infra}"
rm -f ${DATA_DIR}/nginx/applications/*
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
else
echo "Show progress bar only on admin domain for normal update"
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
fi
+10 -8
View File
@@ -120,6 +120,7 @@ fi
set_progress "33" "Changing ownership"
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
chown "${USER}:${USER}" "${DATA_DIR}/INFRA_VERSION" || true
chown "${USER}:${USER}" "${DATA_DIR}"
set_progress "65" "Creating cloudron.conf"
@@ -135,7 +136,6 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"fqdn": "${arg_fqdn}",
"isCustomDomain": ${arg_is_custom_domain},
"boxVersionsUrl": "${arg_box_versions_url}",
"adminEmail": "\"Cloudron\" <no-reply@${arg_fqdn}>",
"provider": "${arg_provider}",
"database": {
"hostname": "localhost",
@@ -188,18 +188,20 @@ if [[ ! -z "${arg_tls_config}" ]]; then
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
fi
# Add webadmin oauth client
# The domain might have changed, therefor we have to update the record
# !!! This needs to be in sync with the webadmin, specifically login_callback.js
echo "Add webadmin oauth cient"
ADMIN_SCOPES="root,developer,profile,users,apps,settings"
echo "Add webadmin api cient"
readonly ADMIN_SCOPES="cloudron,developer,profile,users,apps,settings"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"webadmin\", \"admin\", \"secret-webadmin\", \"${admin_origin}\", \"${ADMIN_SCOPES}\")" box
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"Settings\", \"built-in\", \"secret-webadmin\", \"${admin_origin}\", \"${ADMIN_SCOPES}\")" box
echo "Add localhost test oauth client"
ADMIN_SCOPES="root,developer,profile,users,apps,settings"
echo "Add SDK api client"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-test\", \"test\", \"test\", \"secret-test\", \"http://127.0.0.1:5000\", \"${ADMIN_SCOPES}\")" box
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-sdk\", \"SDK\", \"built-in\", \"secret-sdk\", \"${admin_origin}\", \"*,roleSdk\")" box
echo "Add cli api client"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-cli\", \"Cloudron Tool\", \"built-in\", \"secret-cli\", \"${admin_origin}\", \"*,roleSdk\")" box
set_progress "80" "Starting Cloudron"
systemctl start cloudron.target
+13
View File
@@ -44,6 +44,18 @@ server {
return 307 <%= adminOrigin %>/appstatus.html?referrer=https://$host$request_uri;
}
<% if ( endpoint === 'app' ) { %>
# For some reason putting this webdav block inside location does not work
# http://serverfault.com/questions/121766/webdav-rename-fails-on-an-apache-mod-dav-install-behind-nginx
if ($request_method ~ ^(COPY|MOVE)$) {
set $destination $http_destination;
}
if ($destination ~* ^https(.+)$) {
set $destination http$1;
}
proxy_set_header Destination $destination;
<% } %>
location / {
# 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;
@@ -59,6 +71,7 @@ server {
client_max_body_size 1m;
}
# the read timeout is between successive reads and not the whole connection
location ~ ^/api/v1/apps/.*/exec$ {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 30m;
+12 -11
View File
@@ -24,7 +24,14 @@ http {
sendfile on;
keepalive_timeout 65;
# timeout for client to finish sending headers
client_header_timeout 30s;
# timeout for reading client request body (successive read timeout and not whole body!)
client_body_timeout 60s;
# keep-alive connections timeout in 65s. this is because many browsers timeout in 60 seconds
keepalive_timeout 65s;
# HTTP server
server {
@@ -50,22 +57,15 @@ http {
}
}
# We have to enable https for nginx to read in the vhost in http request
# and send a 404. This is a side-effect of using wildcard DNS
# This server handles the naked domain for custom domains.
# It can also be used for wildcard subdomain 404. This feature is not used by the Cloudron itself
# because box always sets up DNS records for app subdomains.
server {
listen 443 default_server;
ssl on;
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;
@@ -79,6 +79,7 @@ http {
rewrite ^/$ /nakeddomain.html break;
}
# required for /api/v1/cloudron/avatar
location /api/ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 1m;
-23
View File
@@ -1,23 +0,0 @@
#!/bin/bash
# If you change the infra version, be sure to put a warning
# in the change log
INFRA_VERSION=32
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well
BASE_IMAGE=cloudron/base:0.8.1
MYSQL_IMAGE=cloudron/mysql:0.11.0
POSTGRESQL_IMAGE=cloudron/postgresql:0.10.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.13.0
GRAPHITE_IMAGE=cloudron/graphite:0.8.0
MYSQL_REPO=cloudron/mysql
POSTGRESQL_REPO=cloudron/postgresql
MONGODB_REPO=cloudron/mongodb
REDIS_REPO=cloudron/redis # if you change this, fix src/addons.js as well
MAIL_REPO=cloudron/mail
GRAPHITE_REPO=cloudron/graphite
+23 -49
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
initialize: initialize,
setupAddons: setupAddons,
teardownAddons: teardownAddons,
backupAddons: backupAddons,
@@ -21,23 +19,22 @@ exports = module.exports = {
var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
certificates = require('./certificates.js'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
ClientsError = clients.ClientsError,
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
dockerConnection = docker.connection,
fs = require('fs'),
generatePassword = require('password-generator'),
hat = require('hat'),
infra = require('./infra_version.js'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
util = require('util'),
uuid = require('node-uuid');
util = require('util');
var NOOP = function (app, options, callback) { return callback(); };
@@ -124,8 +121,7 @@ var KNOWN_ADDONS = {
}
};
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
SETUP_INFRA_CMD = path.join(__dirname, 'scripts/setup_infra.sh');;
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh');
function debugApp(app, args) {
assert(!app || typeof app === 'object');
@@ -134,17 +130,6 @@ function debugApp(app, args) {
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function initialize(callback) {
if (process.env.BOX_ENV === 'test') return callback();
debug('initializing addon infrastructure');
certificates.getAdminCertificatePath(function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
shell.sudo('seutp_infra', [ SETUP_INFRA_CMD, paths.DATA_DIR, config.fqdn(), config.adminFqdn(), certFilePath, keyFilePath, config.database().name, config.database().password ], callback);
});
}
function setupAddons(app, addons, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addons || typeof addons === 'object');
@@ -298,22 +283,18 @@ function setupOauth(app, options, callback) {
assert.strictEqual(typeof callback, 'function');
var appId = app.id;
var id = 'cid-' + uuid.v4();
var clientSecret = hat(256);
var redirectURI = 'https://' + config.appFqdn(app.location);
var scope = 'profile';
debugApp(app, 'setupOauth: id:%s clientSecret:%s', id, clientSecret);
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
if (error && error.reason !== ClientsError.NOT_FOUND) return callback(error);
clientdb.delByAppIdAndType(appId, clientdb.TYPE_OAUTH, function (error) { // remove existing creds
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
clientdb.add(id, appId, clientdb.TYPE_OAUTH, clientSecret, redirectURI, scope, function (error) {
clients.add(appId, clients.TYPE_OAUTH, redirectURI, scope, function (error, result) {
if (error) return callback(error);
var env = [
'OAUTH_CLIENT_ID=' + id,
'OAUTH_CLIENT_SECRET=' + clientSecret,
'OAUTH_CLIENT_ID=' + result.id,
'OAUTH_CLIENT_SECRET=' + result.clientSecret,
'OAUTH_ORIGIN=' + config.adminOrigin()
];
@@ -331,8 +312,8 @@ function teardownOauth(app, options, callback) {
debugApp(app, 'teardownOauth');
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_OAUTH, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
clients.delByAppIdAndType(app.id, clients.TYPE_OAUTH, function (error) {
if (error && error.reason !== ClientsError.NOT_FOUND) console.error(error);
appdb.unsetAddonConfig(app.id, 'oauth', callback);
});
@@ -344,15 +325,12 @@ function setupSimpleAuth(app, options, callback) {
assert.strictEqual(typeof callback, 'function');
var appId = app.id;
var id = 'cid-' + uuid.v4();
var scope = 'profile';
debugApp(app, 'setupSimpleAuth: id:%s', id);
clients.delByAppIdAndType(app.id, clients.TYPE_SIMPLE_AUTH, function (error) { // remove existing creds
if (error && error.reason !== ClientsError.NOT_FOUND) return callback(error);
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_SIMPLE_AUTH, function (error) { // remove existing creds
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
clientdb.add(id, appId, clientdb.TYPE_SIMPLE_AUTH, '', '', scope, function (error) {
clients.add(appId, clients.TYPE_SIMPLE_AUTH, '', scope, function (error, result) {
if (error) return callback(error);
var env = [
@@ -360,7 +338,7 @@ function setupSimpleAuth(app, options, callback) {
'SIMPLE_AUTH_PORT=' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_URL=http://172.17.0.1:' + config.get('simpleAuthPort'), // obsolete, remove
'SIMPLE_AUTH_ORIGIN=http://172.17.0.1:' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_CLIENT_ID=' + id
'SIMPLE_AUTH_CLIENT_ID=' + result.id
];
debugApp(app, 'Setting simple auth addon config to %j', env);
@@ -377,8 +355,8 @@ function teardownSimpleAuth(app, options, callback) {
debugApp(app, 'teardownSimpleAuth');
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_SIMPLE_AUTH, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
clients.delByAppIdAndType(app.id, clients.TYPE_SIMPLE_AUTH, function (error) {
if (error && error.reason !== ClientsError.NOT_FOUND) console.error(error);
appdb.unsetAddonConfig(app.id, 'simpleauth', callback);
});
@@ -450,8 +428,7 @@ function setupSendMail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// FIXME: to can conflict with a real user!
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '-app';
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '.app';
var cmd = [ '/addons/mail/service.sh', 'add-send', from ];
@@ -471,8 +448,7 @@ function teardownSendMail(app, options, callback) {
debugApp(app, 'Tearing down sendmail');
// FIXME: to can conflict with a real user!
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '-app';
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '.app';
var cmd = [ '/addons/mail/service.sh', 'remove-send', from ];
@@ -492,8 +468,7 @@ function setupRecvMail(app, options, callback) {
debugApp(app, 'Setting up recvmail');
// FIXME: to can conflict with a real user!
var to = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '-app';
var to = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '.app';
var cmd = [ '/addons/mail/service.sh', 'add-recv', to ];
@@ -511,8 +486,7 @@ function teardownRecvMail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// FIXME: to can conflict with a real user!
var to = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '-app';
var to = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '.app';
var cmd = [ '/addons/mail/service.sh', 'remove-recv', to ];
@@ -767,7 +741,7 @@ function setupRedis(app, options, callback) {
name: 'redis-' + app.id,
Hostname: 'redis-' + app.location,
Tty: true,
Image: 'cloudron/redis:0.8.0', // if you change this, fix src/INFRA_VERSION as well
Image: infra.images.redis.tag,
Cmd: null,
Volumes: {
'/tmp': {},
+1 -8
View File
@@ -58,7 +58,7 @@ var assert = require('assert'),
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain' ].join(',');
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -69,10 +69,6 @@ function postProcess(result) {
result.manifest = safe.JSON.parse(result.manifestJson);
delete result.manifestJson;
assert(result.lastBackupConfigJson === null || typeof result.lastBackupConfigJson === 'string');
result.lastBackupConfig = safe.JSON.parse(result.lastBackupConfigJson);
delete result.lastBackupConfigJson;
assert(result.oldConfigJson === null || typeof result.oldConfigJson === 'string');
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
delete result.oldConfigJson;
@@ -284,9 +280,6 @@ function updateWithConstraints(id, app, constraints, callback) {
if (p === 'manifest') {
fields.push('manifestJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p === 'lastBackupConfig') {
fields.push('lastBackupConfigJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p === 'oldConfig') {
fields.push('oldConfigJson = ?');
values.push(JSON.stringify(app[p]));
+1 -1
View File
@@ -17,7 +17,7 @@ exports = module.exports = {
};
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
var UNHEALTHY_THRESHOLD = 3 * 60 * 1000; // 3 minutes
var UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
var gHealthInfo = { }; // { time, emailSent }
var gRunTimeout = null;
var gDockerEventStream = null;
+318 -222
View File
@@ -30,7 +30,12 @@ exports = module.exports = {
checkManifestConstraints: checkManifestConstraints,
autoupdateApps: autoupdateApps,
updateApps: updateApps,
restoreInstalledApps: restoreInstalledApps,
configureInstalledApps: configureInstalledApps,
getAppConfig: getAppConfig,
// exported for testing
_validateHostname: validateHostname,
@@ -43,6 +48,7 @@ var addons = require('./addons.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = backups.BackupsError,
certificates = require('./certificates.js'),
config = require('./config.js'),
constants = require('./constants.js'),
@@ -62,6 +68,7 @@ var addons = require('./addons.js'),
superagent = require('superagent'),
taskmanager = require('./taskmanager.js'),
util = require('util'),
uuid = require('node-uuid'),
validator = require('validator');
// http://dustinsenos.com/articles/customErrorsInNode
@@ -105,14 +112,14 @@ AppsError.BAD_CERTIFICATE = 'Invalid certificate';
function validateHostname(location, fqdn) {
var RESERVED_LOCATIONS = [ constants.ADMIN_LOCATION, constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION, constants.MAIL_LOCATION, constants.POSTMAN_LOCATION ];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new Error(location + ' is reserved');
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
if (location === '') return null; // bare location
if ((location.length + 1 /*+ hyphen */ + fqdn.indexOf('.')) > 63) return new Error('Hostname length cannot be greater than 63');
if (location.match(/^[A-Za-z0-9-]+$/) === null) return new Error('Hostname can only contain alphanumerics and hyphen');
if (location[0] === '-' || location[location.length-1] === '-') return new Error('Hostname cannot start or end with hyphen');
if (location.length + 1 /* hyphen */ + fqdn.length > 253) return new Error('FQDN length exceeds 253 characters');
if ((location.length + 1 /*+ hyphen */ + fqdn.indexOf('.')) > 63) return new AppsError(AppsError.BAD_FIELD, 'Hostname length cannot be greater than 63');
if (location.match(/^[A-Za-z0-9-]+$/) === null) return new AppsError(AppsError.BAD_FIELD, 'Hostname can only contain alphanumerics and hyphen');
if (location[0] === '-' || location[location.length-1] === '-') return new AppsError(AppsError.BAD_FIELD, 'Hostname cannot start or end with hyphen');
if (location.length + 1 /* hyphen */ + fqdn.length > 253) return new AppsError(AppsError.BAD_FIELD, 'FQDN length exceeds 253 characters');
return null;
}
@@ -137,6 +144,7 @@ function validatePortBindings(portBindings, tcpPorts) {
2020, /* install server */
config.get('port'), /* app server (lo) */
config.get('sysadminPort'), /* sysadmin app server (lo) */
config.get('smtpPort'), /* internal smtp port (lo) */
config.get('ldapPort'), /* ldap server (lo) */
config.get('oauthProxyPort'), /* oauth proxy server (lo) */
config.get('simpleAuthPort'), /* simple auth server (lo) */
@@ -151,8 +159,8 @@ function validatePortBindings(portBindings, tcpPorts) {
for (env in portBindings) {
if (!/^[a-zA-Z0-9_]+$/.test(env)) return new AppsError(AppsError.BAD_FIELD, env + ' is not valid environment variable');
if (!Number.isInteger(portBindings[env])) return new Error(portBindings[env] + ' is not an integer');
if (portBindings[env] <= 0 || portBindings[env] > 65535) return new Error(portBindings[env] + ' is out of range');
if (!Number.isInteger(portBindings[env])) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not an integer');
if (portBindings[env] <= 0 || portBindings[env] > 65535) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is out of range');
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
}
@@ -175,19 +183,20 @@ function validateAccessRestriction(accessRestriction) {
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');
if (!Array.isArray(accessRestriction.users)) return new AppsError(AppsError.BAD_FIELD, 'users array property required');
if (!accessRestriction.users.every(function (e) { return typeof e === 'string'; })) return new AppsError(AppsError.BAD_FIELD, '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');
if (!Array.isArray(accessRestriction.groups)) return new AppsError(AppsError.BAD_FIELD, 'groups array property required');
if (!accessRestriction.groups.every(function (e) { return typeof e === 'string'; })) return new AppsError(AppsError.BAD_FIELD, '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');
if (noUsers && noGroups) return new AppsError(AppsError.BAD_FIELD, 'users and groups array cannot both be empty');
// TODO: maybe validate if the users and groups actually exist
return null;
}
@@ -202,8 +211,8 @@ function validateMemoryLimit(manifest, memoryLimit) {
// 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');
if (memoryLimit < min) return new AppsError(AppsError.BAD_FIELD, 'memoryLimit too small');
if (memoryLimit > max) return new AppsError(AppsError.BAD_FIELD, 'memoryLimit too large');
return null;
}
@@ -230,6 +239,17 @@ function getDuplicateErrorDetails(location, portBindings, error) {
return new AppsError(AppsError.ALREADY_EXISTS);
}
function getAppConfig(app) {
return {
manifest: app.manifest,
location: app.location,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
altDomain: app.altDomain
};
}
function getIconUrlSync(app) {
var iconPath = paths.APPICONS_DIR + '/' + app.id + '.png';
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
@@ -343,145 +363,176 @@ function purchase(appStoreId, callback) {
});
}
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, cert, key, memoryLimit, altDomain, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'object');
assert(!icon || typeof icon === 'string');
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof memoryLimit, 'number');
assert(altDomain === null || typeof altDomain === 'string');
function downloadManifest(appStoreId, manifest, callback) {
if (!appStoreId && !manifest) return callback(new AppsError(AppsError.BAD_FIELD, 'Neither manifest nor appStoreId provided'));
if (!appStoreId) return callback(null, '', manifest);
var parts = appStoreId.split('@');
var url = config.apiServerOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : '');
debug('downloading manifest from %s', url);
superagent.get(url).end(function (error, result) {
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message));
if (result.statusCode !== 200) return callback(new AppsError(AppsError.BAD_FIELD, util.format('Failed to get app info from store.', result.statusCode, result.text)));
callback(null, parts[0], result.body.manifest);
});
}
function install(data, auditSource, callback) {
assert(data && typeof data === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error: ' + error.message));
var location = data.location.toLowerCase(),
portBindings = data.portBindings || null,
accessRestriction = data.accessRestriction || null,
icon = data.icon || null,
cert = data.cert || null,
key = data.key || null,
memoryLimit = data.memoryLimit || 0,
altDomain = data.altDomain || null;
error = checkManifestConstraints(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
assert(data.appStoreId || data.manifest); // atleast one of them is required
error = validateHostname(location, config.fqdn());
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = validatePortBindings(portBindings, manifest.tcpPorts);
if (error) return callback(error);
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;
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
// singleUser mode requires accessRestriction to contain exactly one user
if (manifest.singleUser && accessRestriction === null) return callback(new AppsError(AppsError.USER_REQUIRED));
if (manifest.singleUser && accessRestriction.users.length !== 1) return callback(new AppsError(AppsError.USER_REQUIRED));
if (icon) {
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
}
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
debug('Will install app with id : ' + appId);
purchase(appStoreId, function (error) {
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, memoryLimit, altDomain, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error: ' + error.message));
// 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));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
error = checkManifestConstraints(manifest);
if (error) return callback(error);
error = validateHostname(location, config.fqdn());
if (error) return callback(error);
error = validatePortBindings(portBindings, manifest.tcpPorts);
if (error) return callback(error);
error = validateAccessRestriction(accessRestriction);
if (error) return callback(error);
error = validateMemoryLimit(manifest, memoryLimit);
if (error) return callback(error);
// memoryLimit might come in as 0 if not specified
memoryLimit = memoryLimit || manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
// singleUser mode requires accessRestriction to contain exactly one user
if (manifest.singleUser && accessRestriction === null) return callback(new AppsError(AppsError.USER_REQUIRED));
if (manifest.singleUser && accessRestriction.users.length !== 1) return callback(new AppsError(AppsError.USER_REQUIRED));
if (icon) {
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
}
taskmanager.restartAppTask(appId);
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest });
var appId = uuid.v4();
debug('Will install app with id : ' + appId);
callback(null);
purchase(appStoreId, function (error) {
if (error) return callback(error);
appdb.add(appId, appStoreId, manifest, location, portBindings, accessRestriction, memoryLimit, altDomain, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// 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));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
}
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest });
callback(null, { id : appId });
});
});
});
}
function configure(appId, location, portBindings, accessRestriction, cert, key, memoryLimit, altDomain, auditSource, callback) {
function configure(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'object');
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof memoryLimit, 'number');
assert(altDomain === null || typeof altDomain === 'string');
assert(data && typeof data === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var error = validateHostname(location, config.fqdn());
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = validateAccessRestriction(accessRestriction);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
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));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
var location, portBindings, values = { };
if ('location' in data) {
location = values.location = data.location.toLowerCase();
error = validateHostname(values.location, config.fqdn());
if (error) return callback(error);
} else {
location = app.location;
}
var values = {
location: location.toLowerCase(),
accessRestriction: accessRestriction,
portBindings: portBindings,
memoryLimit: memoryLimit,
altDomain: altDomain,
if ('accessRestriction' in data) {
values.accessRestriction = data.accessRestriction;
error = validateAccessRestriction(values.accessRestriction);
if (error) return callback(error);
}
oldConfig: {
location: app.location,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
altDomain: altDomain
if ('altDomain' in data) {
values.altDomain = data.altDomain;
if (values.altDomain !== null && !validator.isFQDN(values.altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
}
if ('portBindings' in data) {
portBindings = values.portBindings = data.portBindings;
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
if (error) return callback(error);
} else {
portBindings = app.portBindings;
}
if ('memoryLimit' in data) {
values.memoryLimit = data.memoryLimit;
error = validateMemoryLimit(app.manifest, values.memoryLimit);
if (error) return callback(error);
// memoryLimit might come in as 0 if not specified
values.memoryLimit = values.memoryLimit || app.memoryLimit || app.manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
}
// save cert to data/box/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
error = certificates.validateCertificate(data.cert, data.key, config.appFqdn(location));
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
} else { // remove existing cert/key
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'))) debug('Error removing key: ' + safe.error.message);
}
};
}
values.oldConfig = getAppConfig(app);
debug('Will configure app with id:%s values:%j', appId, values);
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -494,76 +545,75 @@ function configure(appId, location, portBindings, accessRestriction, cert, key,
});
}
function update(appId, force, manifest, portBindings, icon, auditSource, callback) {
function update(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof force, 'boolean');
assert(manifest && typeof manifest === 'object');
assert(typeof portBindings === 'object'); // can be null
assert(!icon || typeof icon === 'string');
assert(data && typeof data === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will update app with id:%s', appId);
var error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
error = checkManifestConstraints(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed:' + error.message));
var values = { };
error = validatePortBindings(portBindings, manifest.tcpPorts);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
if (icon) {
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
error = checkManifestConstraints(manifest);
if (error) return callback(error);
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
}
values.manifest = manifest;
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
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 = '';
if ('portBindings' in data) {
values.portBindings = data.portBindings;
error = validatePortBindings(data.portBindings, values.manifest.tcpPorts);
if (error) return callback(error);
}
// 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;
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
var values = {
appStoreId: appStoreId,
manifest: manifest,
portBindings: portBindings,
memoryLimit: memoryLimit,
oldConfig: {
manifest: app.manifest,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction,
memoryLimit: app.memoryLimit,
altDomain: app.altDomain
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APPICONS_DIR, appId + '.png'));
}
};
}
appdb.setInstallationCommand(appId, force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails('' /* location cannot conflict */, portBindings, error));
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
// 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 !== values.manifest.id) {
if (!data.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 it as a dev app
values.appStoreId = '';
}
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest });
// Ensure we update the memory limit in case the new app requires more memory as a minimum
if (values.manifest.memoryLimit && app.memoryLimit < values.manifest.memoryLimit) {
values.memoryLimit = values.manifest.memoryLimit;
}
callback(null);
values.oldConfig = getAppConfig(app);
appdb.setInstallationCommand(appId, data.force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails('' /* location cannot conflict */, values.portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest });
callback(null);
});
});
});
}
@@ -613,8 +663,9 @@ function getLogs(appId, lines, follow, callback) {
});
}
function restore(appId, auditSource, callback) {
function restore(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -624,43 +675,36 @@ function restore(appId, auditSource, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// restore without a backup is the same as re-install
var restoreConfig = app.lastBackupConfig, values = { };
if (restoreConfig) {
// re-validate because this new box version may not accept old configs.
// if we restore location, it should be validated here as well
error = checkManifestConstraints(restoreConfig.manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
// for empty or null backupId, use existing manifest to mimic a reinstall
var func = data.backupId ? backups.getRestoreConfig.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
error = validatePortBindings(restoreConfig.portBindings, restoreConfig.manifest.tcpPorts); // maybe new ports got reserved now
if (error) return callback(error);
// ## should probably query new location, access restriction from user
values = {
manifest: restoreConfig.manifest,
portBindings: restoreConfig.portBindings,
memoryLimit: restoreConfig.memoryLimit,
oldConfig: {
location: app.location,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
manifest: app.manifest,
altDomain: app.altDomain
}
};
}
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
func(function (error, restoreConfig) {
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));
taskmanager.restartAppTask(appId);
if (!restoreConfig) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(restoreConfig.manifest);
if (error) return callback(error);
callback(null);
var values = {
lastBackupId: data.backupId || null, // when null, apptask simply reinstalls
manifest: restoreConfig.manifest,
oldConfig: getAppConfig(app)
};
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
callback(null);
});
});
});
}
@@ -717,14 +761,16 @@ function stop(appId, callback) {
}
function checkManifestConstraints(manifest) {
if (!manifest.dockerImage) return new Error('Missing dockerImage'); // dockerImage is optional in manifest
assert(manifest && typeof manifest === 'object');
if (!manifest.dockerImage) return new AppsError(AppsError.BAD_FIELD, 'Missing dockerImage'); // dockerImage is optional in manifest
if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) {
return new Error('Box version exceeds Apps maxBoxVersion');
return new AppsError(AppsError.BAD_FIELD, 'Box version exceeds Apps maxBoxVersion');
}
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, config.version())) {
return new Error('minBoxVersion exceeds Box version');
return new AppsError(AppsError.BAD_FIELD, 'minBoxVersion exceeds Box version');
}
return null;
@@ -748,10 +794,14 @@ function exec(appId, options, callback) {
var container = docker.connection.getContainer(app.containerId);
var execOptions = {
var execOptions = {
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
// A pseudo tty is a terminal which processes can detect (for example, disable colored output)
// Creating a pseudo terminal also assigns a terminal driver which detects control sequences
// When passing binary data, tty must be disabled. In addition, the stdout/stderr becomes a single
// unified stream because of the nature of a tty (see https://github.com/docker/docker/issues/19696)
Tty: options.tty,
Cmd: cmd
};
@@ -761,9 +811,18 @@ function exec(appId, options, callback) {
var startOptions = {
Detach: false,
Tty: options.tty,
stdin: true // this is a dockerode option that enabled openStdin in the modem
// hijacking upgrades the docker connection from http to tcp. because of this upgrade,
// we can work with half-close connections (not defined in http). this way, the client
// can properly signal that stdin is EOF by closing it's side of the socket. In http,
// the whole connection will be dropped when stdin get EOF.
// https://github.com/apocas/dockerode/commit/b4ae8a03707fad5de893f302e4972c1e758592fe
hijack: true,
stream: true,
stdin: true,
stdout: true,
stderr: true
};
exec.start(startOptions, function(error, stream) {
exec.start(startOptions, function(error, stream /* in hijacked mode, this is a net.socket */) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (options.rows && options.columns) {
@@ -776,23 +835,25 @@ function exec(appId, options, callback) {
});
}
function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { manifest } }
function updateApps(updateInfo, auditSource, callback) { // updateInfo is { appId -> { manifest } }
assert.strictEqual(typeof updateInfo, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
function canAutoupdateApp(app, newManifest) {
var tcpPorts = newManifest.tcpPorts || { };
var newTcpPorts = newManifest.newTcpPorts || { };
var oldTcpPorts = app.manifest.tcpPorts || { };
var portBindings = app.portBindings; // this is never null
if (Object.keys(tcpPorts).length === 0 && Object.keys(portBindings).length === 0) return null;
if (Object.keys(tcpPorts).length === 0) return new Error('tcpPorts is now empty but portBindings is not');
if (Object.keys(portBindings).length === 0) return new Error('portBindings is now empty but tcpPorts is not');
for (var env in tcpPorts) {
if (!(env in portBindings)) return new Error(env + ' is required from user');
for (var env in newTcpPorts) {
if (!(env in oldTcpPorts)) return new Error(env + ' is required from user');
}
// it's fine if one or more keys got removed
for (env in portBindings) {
if (!(env in newTcpPorts)) return new Error(env + ' was in use but new update removes it');
}
// it's fine if one or more (unused) keys got removed
return null;
}
@@ -811,8 +872,11 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
return iteratorDone();
}
update(appId, false /* force */, updateInfo[appId].manifest, app.portBindings,
null /* icon */, { userId: null, username: 'autoupdater' }, function (error) {
var data = {
manifest: updateInfo[appId].manifest
};
update(appId, data, auditSource, function (error) {
if (error) debug('Error initiating autoupdate of %s. %s', appId, error.message);
iteratorDone(null);
@@ -859,3 +923,35 @@ function listBackups(page, perPage, appId, callback) {
});
});
}
function restoreInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
if (app.installationState !== appdb.ISTATE_INSTALLED) return iteratorDone();
debug('marking %s for restore', app.location || app.id);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { oldConfig: null }, iteratorDone);
}, callback);
});
}
function configureInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
if (app.installationState !== appdb.ISTATE_INSTALLED) return iteratorDone();
debug('marking %s for reconfigure', app.location || app.id);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { oldConfig: null }, iteratorDone);
}, callback);
});
}
+10 -12
View File
@@ -36,15 +36,14 @@ var addons = require('./addons.js'),
async = require('async'),
backups = require('./backups.js'),
certificates = require('./certificates.js'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
config = require('./config.js'),
ClientsError = clients.ClientsError,
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apptask'),
docker = require('./docker.js'),
ejs = require('ejs'),
fs = require('fs'),
hat = require('hat'),
manifestFormat = require('cloudron-manifestformat'),
net = require('net'),
nginx = require('./nginx.js'),
@@ -57,7 +56,6 @@ var addons = require('./addons.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
util = require('util'),
uuid = require('node-uuid'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
@@ -163,20 +161,18 @@ function allocateOAuthProxyCredentials(app, callback) {
if (!nginx.requiresOAuthProxy(app)) return callback(null);
var id = 'cid-' + uuid.v4();
var clientSecret = hat(256);
var redirectURI = 'https://' + config.appFqdn(app.location);
var scope = 'profile';
clientdb.add(id, app.id, clientdb.TYPE_PROXY, clientSecret, redirectURI, scope, callback);
clients.add(app.id, clients.TYPE_PROXY, redirectURI, scope, callback);
}
function removeOAuthProxyCredentials(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_PROXY, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) {
clients.delByAppIdAndType(app.id, clients.TYPE_PROXY, function (error) {
if (error && error.reason !== ClientsError.NOT_FOUND) {
debugApp(app, 'Error removing OAuth client id', error);
return callback(error);
}
@@ -443,7 +439,7 @@ function backup(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
backups.backupApp.bind(null, app, app.manifest.addons),
backups.backupApp.bind(null, app, app.manifest),
// done!
function (callback) {
@@ -629,7 +625,6 @@ function update(app, callback) {
removeCollectdProfile.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
addons.teardownAddons.bind(null, app, unusedAddons),
function deleteImageIfChanged(done) {
if (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage) return done();
@@ -642,10 +637,13 @@ function update(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '30, Backup app' }),
backups.backupApp.bind(null, app, app.oldConfig.manifest.addons)
backups.backupApp.bind(null, app, app.oldConfig.manifest)
], next);
},
// only delete unused addons after backup
addons.teardownAddons.bind(null, app, unusedAddons),
updateApp.bind(null, app, { installationProgress: '45, Downloading icon' }),
downloadIcon.bind(null, app),
+9 -35
View File
@@ -10,17 +10,16 @@ exports = module.exports = {
var assert = require('assert'),
BasicStrategy = require('passport-http').BasicStrategy,
BearerStrategy = require('passport-http-bearer').Strategy,
clientdb = require('./clientdb'),
clients = require('./clients'),
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
ClientsError = clients.ClientsError,
DatabaseError = require('./databaseerror'),
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'),
userdb = require('./userdb'),
UserError = user.UserError,
_ = require('underscore');
@@ -32,7 +31,7 @@ function initialize(callback) {
});
passport.deserializeUser(function(userId, callback) {
userdb.get(userId, function (error, result) {
user.get(userId, function (error, result) {
if (error) return callback(error);
var md5 = crypto.createHash('md5').update(result.email.toLowerCase()).digest('hex');
@@ -67,8 +66,8 @@ function initialize(callback) {
debug('BasicStrategy: detected client id %s instead of username:password', username);
// username is actually client id here
// password is client secret
clientdb.get(username, function (error, client) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
clients.get(username, function (error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.clientSecret != password) return callback(null, false);
return callback(null, client);
@@ -85,8 +84,8 @@ function initialize(callback) {
}));
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
clientdb.get(clientId, function(error, client) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
clients.get(clientId, function(error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) { return callback(error); }
if (client.clientSecret != clientSecret) { return callback(null, false); }
return callback(null, client);
@@ -101,37 +100,12 @@ function initialize(callback) {
// scopes here can define what capabilities that token carries
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
var info = { scope: token.scope };
var tokenType;
if (token.identifier.indexOf(tokendb.PREFIX_DEV) === 0) {
token.identifier = token.identifier.slice(tokendb.PREFIX_DEV.length);
tokenType = tokendb.TYPE_DEV;
} else if (token.identifier.indexOf(tokendb.PREFIX_APP) === 0) {
tokenType = tokendb.TYPE_APP;
return callback(null, { id: token.identifier.slice(tokendb.PREFIX_APP.length), tokenType: tokenType }, info);
} else if (token.identifier.indexOf(tokendb.PREFIX_USER) === 0) {
tokenType = tokendb.TYPE_USER;
token.identifier = token.identifier.slice(tokendb.PREFIX_USER.length);
} else {
// legacy tokens assuming a user access token
tokenType = tokendb.TYPE_USER;
}
userdb.get(token.identifier, function (error, user) {
user.get(token.identifier, function (error, user) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// amend the tokenType of the token owner
user.tokenType = tokenType;
// 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);
});
callback(null, user, info);
});
});
}));
+57 -34
View File
@@ -7,6 +7,7 @@ exports = module.exports = {
getByAppIdPaged: getByAppIdPaged,
getRestoreUrl: getRestoreUrl,
getRestoreConfig: getRestoreConfig,
ensureBackup: ensureBackup,
@@ -36,6 +37,7 @@ var addons = require('./addons.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
settings = require('./settings.js'),
superagent = require('superagent'),
util = require('util'),
webhooks = require('./webhooks.js');
@@ -135,12 +137,13 @@ function getBoxBackupCredentials(appBackupIds, callback) {
});
}
function getAppBackupCredentials(app, callback) {
function getAppBackupCredentials(app, manifest, callback) {
assert.strictEqual(typeof app, 'object');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
var now = new Date();
var filebase = util.format('appbackup_%s_%s-v%s', app.id, now.toISOString(), app.manifest.version);
var filebase = util.format('appbackup_%s_%s-v%s', app.id, now.toISOString(), manifest.version);
var configFilename = filebase + '.json', dataFilename = filebase + '.tar.gz';
settings.getBackupConfig(function (error, backupConfig) {
@@ -161,6 +164,32 @@ function getAppBackupCredentials(app, callback) {
});
}
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
function getRestoreConfig(backupId, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function');
var configFile = backupId.replace(/\.tar\.gz$/, '.json');
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getRestoreUrl(backupConfig, configFile, function (error, result) {
if (error) return callback(error);
superagent.get(result.url).buffer(true).end(function (error, response) {
if (error && !error.response) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (response.statusCode !== 200) return callback(new Error('Invalid response code when getting config.json : ' + response.statusCode));
var config = safe.JSON.parse(response.text);
if (!config) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error in config:' + safe.error.message));
return callback(null, config);
});
});
});
}
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
function getRestoreUrl(backupId, callback) {
assert.strictEqual(typeof backupId, 'string');
@@ -185,14 +214,15 @@ function getRestoreUrl(backupId, callback) {
});
}
function copyLastBackup(app, callback) {
function copyLastBackup(app, manifest, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof app.lastBackupId, 'string');
assert(manifest && typeof manifeset === 'object');
assert.strictEqual(typeof callback, 'function');
var now = new Date();
var toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, now.toISOString(), app.manifest.version);
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', app.id, now.toISOString(), app.manifest.version);
var toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, now.toISOString(), manifest.version);
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', app.id, now.toISOString(), manifest.version);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
@@ -267,11 +297,12 @@ function canBackupApp(app) {
// 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) {
function reuseOldAppBackup(app, manifest, callback) {
assert.strictEqual(typeof app.lastBackupId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
copyLastBackup(app, function (error, newBackupId) {
copyLastBackup(app, manifest, function (error, newBackupId) {
if (error) return callback(error);
debugApp(app, 'reuseOldAppBackup: reused old backup %s as %s', app.lastBackupId, newBackupId);
@@ -280,12 +311,12 @@ function reuseOldAppBackup(app, callback) {
});
}
function createNewAppBackup(app, addonsToBackup, callback) {
function createNewAppBackup(app, manifest, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
getAppBackupCredentials(app, function (error, result) {
getAppBackupCredentials(app, manifest, function (error, result) {
if (error) return callback(error);
debugApp(app, 'createNewAppBackup: backup url:%s backup config url:%s', result.s3DataUrl, result.s3ConfigUrl);
@@ -294,14 +325,14 @@ function createNewAppBackup(app, addonsToBackup, callback) {
result.sessionToken, result.region, result.backupKey ];
async.series([
addons.backupAddons.bind(null, app, addonsToBackup),
addons.backupAddons.bind(null, app, manifest.addons),
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) {
backupdb.add({ id: result.id, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ] }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, result.id);
@@ -310,13 +341,12 @@ function createNewAppBackup(app, addonsToBackup, callback) {
});
}
function setRestorePoint(appId, lastBackupId, lastBackupConfig, callback) {
function setRestorePoint(appId, lastBackupId, 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) {
appdb.update(appId, { lastBackupId: lastBackupId }, 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));
@@ -324,12 +354,12 @@ function setRestorePoint(appId, lastBackupId, lastBackupConfig, callback) {
});
}
function backupApp(app, addonsToBackup, callback) {
function backupApp(app, manifest, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
var appConfig = null, backupFunction;
var backupFunction;
if (!canBackupApp(app)) {
if (!app.lastBackupId) {
@@ -337,17 +367,11 @@ function backupApp(app, addonsToBackup, callback) {
return callback(new BackupsError(BackupsError.BAD_STATE, 'App not healthy and never backed up previously'));
}
appConfig = app.lastBackupConfig;
backupFunction = reuseOldAppBackup.bind(null, app);
backupFunction = reuseOldAppBackup.bind(null, app, manifest);
} else {
appConfig = {
manifest: app.manifest,
location: app.location,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction,
memoryLimit: app.memoryLimit
};
backupFunction = createNewAppBackup.bind(null, app, addonsToBackup);
var appConfig = apps.getAppConfig(app);
appConfig.manifest = manifest;
backupFunction = createNewAppBackup.bind(null, app, manifest);
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
return callback(safe.error);
@@ -359,7 +383,7 @@ function backupApp(app, addonsToBackup, callback) {
debugApp(app, 'backupApp: successful id:%s', backupId);
setRestorePoint(app.id, backupId, appConfig, function (error) {
setRestorePoint(app.id, backupId, function (error) {
if (error) return callback(error);
return callback(null, backupId);
@@ -386,7 +410,7 @@ function backupBoxAndApps(auditSource, callback) {
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
++processed;
backupApp(app, app.manifest.addons, function (error, backupId) {
backupApp(app, app.manifest, function (error, backupId) {
if (error && error.reason !== BackupsError.BAD_STATE) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
@@ -433,8 +457,8 @@ function backup(auditSource, callback) {
callback(null);
}
function ensureBackup(callback) {
callback = callback || NOOP_CALLBACK;
function ensureBackup(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
getPaged(1, 1, function (error, backups) {
if (error) {
@@ -447,8 +471,7 @@ function ensureBackup(callback) {
return callback(null);
}
var eventSource = { userId: null, username: 'cron' };
backup(eventSource, callback);
backup(auditSource, callback);
});
}
+15 -12
View File
@@ -2,7 +2,7 @@
exports = module.exports = {
installAdminCertificate: installAdminCertificate,
autoRenew: autoRenew,
renewAll: renewAll,
setFallbackCertificate: setFallbackCertificate,
setAdminCertificate: setAdminCertificate,
CertificatesError: CertificatesError,
@@ -69,7 +69,8 @@ function getApi(app, callback) {
var api = !app.altDomain && tlsConfig.provider === 'caas' ? caas : acme;
var options = { };
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
// used by acme backend to determine the LE origin.
options.prod = (api === caas) ? !config.isDev() : tlsConfig.provider.match(/.*-prod/) !== null;
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
@@ -123,9 +124,11 @@ function isExpiringSync(certFilePath, hours) {
return result.status === 1; // 1 - expired 0 - not expired
}
function autoRenew(callback) {
debug('autoRenew: Checking certificates for renewal');
callback = callback || NOOP_CALLBACK;
function renewAll(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('renewAll: Checking certificates for renewal');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
@@ -139,7 +142,7 @@ function autoRenew(callback) {
var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.key');
if (!safe.fs.existsSync(keyFilePath)) {
debug('autoRenew: no existing key file for %s. skipping', appDomain);
debug('renewAll: no existing key file for %s. skipping', appDomain);
continue;
}
@@ -148,7 +151,7 @@ function autoRenew(callback) {
}
}
debug('autoRenew: %j needs to be renewed', expiringApps.map(function (a) { return a.altDomain || config.appFqdn(a.location); }));
debug('renewAll: %j needs to be renewed', expiringApps.map(function (a) { return a.altDomain || config.appFqdn(a.location); }));
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
var domain = app.altDomain || config.appFqdn(app.location);
@@ -156,28 +159,28 @@ function autoRenew(callback) {
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('autoRenew: renewing cert for %s with options %j', domain, apiOptions);
debug('renewAll: 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');
var errorMessage = error ? error.message : '';
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, { userId: null, username: 'cron' }, { domain: domain, errorMessage: errorMessage });
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: domain, errorMessage: errorMessage });
mailer.certificateRenewed(domain, errorMessage);
if (error) {
debug('autoRenew: could not renew cert for %s because %s', domain, error);
debug('renewAll: 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);
debug('renewAll: 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);
debug('renewAll: certificate for %s renewed', domain);
}
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
+19 -14
View File
@@ -5,6 +5,7 @@
exports = module.exports = {
get: get,
getAll: getAll,
getAllWithTokenCount: getAllWithTokenCount,
getAllWithTokenCountByIdentifier: getAllWithTokenCountByIdentifier,
add: add,
del: del,
@@ -14,13 +15,7 @@ exports = module.exports = {
delByAppId: delByAppId,
delByAppIdAndType: delByAppIdAndType,
_clear: clear,
TYPE_EXTERNAL: 'external',
TYPE_OAUTH: 'addon-oauth',
TYPE_SIMPLE_AUTH: 'addon-simpleauth',
TYPE_PROXY: 'addon-proxy',
TYPE_ADMIN: 'admin'
_clear: clear
};
var assert = require('assert'),
@@ -52,14 +47,24 @@ function getAll(callback) {
});
}
function getAllWithTokenCount(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId GROUP BY clients.id', [], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
});
}
function getAllWithTokenCountByIdentifier(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId WHERE tokens.identifier=? GROUP BY clients.id', [ identifier ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
callback(null, results);
});
}
@@ -71,7 +76,7 @@ function getByAppId(appId, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null, result[0]);
callback(null, result[0]);
});
}
@@ -84,7 +89,7 @@ function getByAppIdAndType(appId, type, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null, result[0]);
callback(null, result[0]);
});
}
@@ -127,7 +132,7 @@ function delByAppId(appId, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null);
callback(null);
});
}
@@ -140,17 +145,17 @@ function delByAppIdAndType(appId, type, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null);
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM clients WHERE appId!="webadmin"', function (error) {
database.query('DELETE FROM clients WHERE id!="cid-webadmin" AND id!="cid-sdk" AND id!="cid-cli"', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
return callback(null);
callback(null);
});
}
+113 -24
View File
@@ -6,16 +6,32 @@ exports = module.exports = {
add: add,
get: get,
del: del,
getAllWithDetailsByUserId: getAllWithDetailsByUserId,
getAll: getAll,
getByAppIdAndType: getByAppIdAndType,
getClientTokensByUserId: getClientTokensByUserId,
delClientTokensByUserId: delClientTokensByUserId,
delByAppIdAndType: delByAppIdAndType,
addClientTokenByUserId: addClientTokenByUserId,
delToken: delToken,
// keep this in sync with start.sh ADMIN_SCOPES that generates the cid-webadmin
SCOPE_APPS: 'apps',
SCOPE_DEVELOPER: 'developer',
SCOPE_PROFILE: 'profile',
SCOPE_ROOT: 'root',
SCOPE_CLOUDRON: 'cloudron',
SCOPE_SETTINGS: 'settings',
SCOPE_USERS: 'users'
SCOPE_USERS: 'users',
// roles are handled just like the above scopes, they are parallel to scopes
// scopes enclose API groups, roles specify the usage role
SCOPE_ROLE_SDK: 'roleSdk',
// client type enums
TYPE_EXTERNAL: 'external',
TYPE_BUILT_IN: 'built-in',
TYPE_OAUTH: 'addon-oauth',
TYPE_SIMPLE_AUTH: 'addon-simpleauth',
TYPE_PROXY: 'addon-proxy'
};
var assert = require('assert'),
@@ -23,7 +39,6 @@ var assert = require('assert'),
hat = require('hat'),
appdb = require('./appdb.js'),
tokendb = require('./tokendb.js'),
constants = require('./constants.js'),
async = require('async'),
clientdb = require('./clientdb.js'),
DatabaseError = require('./databaseerror.js'),
@@ -50,6 +65,10 @@ function ClientsError(reason, errorOrMessage) {
util.inherits(ClientsError, Error);
ClientsError.INVALID_SCOPE = 'Invalid scope';
ClientsError.INVALID_CLIENT = 'Invalid client';
ClientsError.INVALID_TOKEN = 'Invalid token';
ClientsError.NOT_FOUND = 'Not found';
ClientsError.INTERNAL_ERROR = 'Internal Error';
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
function validateScope(scope) {
assert.strictEqual(typeof scope, 'string');
@@ -58,16 +77,17 @@ function validateScope(scope) {
exports.SCOPE_APPS,
exports.SCOPE_DEVELOPER,
exports.SCOPE_PROFILE,
exports.SCOPE_ROOT,
exports.SCOPE_CLOUDRON,
exports.SCOPE_SETTINGS,
exports.SCOPE_USERS
exports.SCOPE_USERS,
'*', // includes all scopes, but not roles
exports.SCOPE_ROLE_SDK
];
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE);
if (scope === '*') return null;
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE, 'Empty scope not allowed');
var allValid = scope.split(',').every(function (s) { return VALID_SCOPES.indexOf(s) !== -1; });
if (!allValid) return new ClientsError(ClientsError.INVALID_SCOPE);
if (!allValid) return new ClientsError(ClientsError.INVALID_SCOPE, 'Invalid scope. Available scopes are ' + VALID_SCOPES.join(', '));
return null;
}
@@ -79,6 +99,9 @@ function add(appId, type, redirectURI, scope, callback) {
assert.strictEqual(typeof scope, 'string');
assert.strictEqual(typeof callback, 'function');
// allow whitespace
scope = scope.split(',').map(function (s) { return s.trim(); }).join(',');
var error = validateScope(scope);
if (error) return callback(error);
@@ -106,6 +129,7 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.get(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null, result);
});
@@ -116,24 +140,24 @@ function del(id, callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.del(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null, result);
});
}
function getAllWithDetailsByUserId(userId, callback) {
assert.strictEqual(typeof userId, 'string');
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.getAllWithTokenCountByIdentifier(tokendb.PREFIX_USER + userId, function (error, results) {
clientdb.getAll(function (error, results) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, []);
if (error) return callback(error);
var tmp = [];
async.each(results, function (record, callback) {
if (record.type === clientdb.TYPE_ADMIN) {
record.name = constants.ADMIN_NAME;
record.location = constants.ADMIN_LOCATION;
if (record.type === exports.TYPE_EXTERNAL || record.type === exports.TYPE_BUILT_IN) {
// the appId in this case holds the name
record.name = record.appId;
tmp.push(record);
@@ -142,14 +166,13 @@ function getAllWithDetailsByUserId(userId, callback) {
appdb.get(record.appId, function (error, result) {
if (error) {
console.error('Failed to get app details for oauth client', result, error);
console.error('Failed to get app details for oauth client', record.appId, error);
return callback(null); // ignore error so we continue listing clients
}
if (record.type === clientdb.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === clientdb.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
if (record.type === clientdb.TYPE_SIMPLE_AUTH) record.name = result.manifest.title + ' Simple Auth';
if (record.type === clientdb.TYPE_EXTERNAL) record.name = result.manifest.title + ' external';
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
if (record.type === exports.TYPE_SIMPLE_AUTH) record.name = result.manifest.title + ' Simple Auth';
record.location = result.location;
@@ -164,15 +187,27 @@ function getAllWithDetailsByUserId(userId, callback) {
});
}
function getByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
clientdb.getByAppIdAndType(appId, type, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null, result);
});
}
function getClientTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.getByIdentifierAndClientId(tokendb.PREFIX_USER + userId, clientId, function (error, result) {
tokendb.getByIdentifierAndClientId(userId, clientId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) {
// this can mean either that there are no tokens or the clientId is actually unknown
clientdb.get(clientId, function (error/*, result*/) {
get(clientId, function (error/*, result*/) {
if (error) return callback(error);
callback(null, []);
});
@@ -188,10 +223,10 @@ function delClientTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.delByIdentifierAndClientId(tokendb.PREFIX_USER + userId, clientId, function (error) {
tokendb.delByIdentifierAndClientId(userId, clientId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) {
// this can mean either that there are no tokens or the clientId is actually unknown
clientdb.get(clientId, function (error/*, result*/) {
get(clientId, function (error/*, result*/) {
if (error) return callback(error);
callback(null);
});
@@ -201,3 +236,57 @@ function delClientTokensByUserId(clientId, userId, callback) {
callback(null);
});
}
function delByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
clientdb.delByAppIdAndType(appId, type, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null);
});
}
function addClientTokenByUserId(clientId, userId, expiresAt, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof expiresAt, 'number');
assert.strictEqual(typeof callback, 'function');
get(clientId, function (error, result) {
if (error) return callback(error);
var token = tokendb.generateToken();
tokendb.add(token, userId, result.id, expiresAt, result.scope, function (error) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null, {
accessToken: token,
identifier: userId,
clientId: result.id,
scope: result.id,
expires: expiresAt
});
});
});
}
function delToken(clientId, tokenId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof tokenId, 'string');
assert.strictEqual(typeof callback, 'function');
get(clientId, function (error, result) {
if (error) return callback(error);
tokendb.del(tokenId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.INVALID_TOKEN, 'Invalid token'));
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null);
});
});
}
+20 -34
View File
@@ -12,7 +12,6 @@ exports = module.exports = {
sendHeartbeat: sendHeartbeat,
updateToLatest: updateToLatest,
update: update,
reboot: reboot,
retire: retire,
@@ -31,7 +30,7 @@ var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:cloudron'),
@@ -54,9 +53,8 @@ var apps = require('./apps.js'),
updateChecker = require('./updatechecker.js'),
user = require('./user.js'),
UserError = user.UserError,
userdb = require('./userdb.js'),
util = require('util'),
uuid = require('node-uuid');
user = require('./user.js'),
util = require('util');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update',
@@ -92,10 +90,6 @@ CloudronError.BAD_FIELD = 'Field error';
CloudronError.INTERNAL_ERROR = 'Internal Error';
CloudronError.EXTERNAL_ERROR = 'External Error';
CloudronError.ALREADY_PROVISIONED = 'Already Provisioned';
CloudronError.BAD_USERNAME = 'Bad username';
CloudronError.BAD_EMAIL = 'Bad email';
CloudronError.BAD_PASSWORD = 'Bad password';
CloudronError.BAD_NAME = 'Bad name';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
CloudronError.NOT_FOUND = 'Not found';
@@ -232,19 +226,17 @@ function activate(username, password, email, displayName, ip, auditSource, callb
user.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
if (error && error.reason === UserError.BAD_USERNAME) return callback(new CloudronError(CloudronError.BAD_USERNAME));
if (error && error.reason === UserError.BAD_PASSWORD) return callback(new CloudronError(CloudronError.BAD_PASSWORD));
if (error && error.reason === UserError.BAD_EMAIL) return callback(new CloudronError(CloudronError.BAD_EMAIL));
if (error && error.reason === UserError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
clientdb.getByAppIdAndType('webadmin', clientdb.TYPE_ADMIN, function (error, result) {
clients.get('cid-webadmin', function (error, result) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
// Also generate a token so the admin creation can also act as a login
var token = tokendb.generateToken();
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(token, tokendb.PREFIX_USER + userObject.id, result.id, expires, '*', function (error) {
tokendb.add(token, userObject.id, result.id, expires, '*', function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
// EE API is sync. do not keep the REST API reponse waiting
@@ -261,7 +253,7 @@ function activate(username, password, email, displayName, ip, auditSource, callb
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
userdb.count(function (error, count) {
user.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
@@ -502,8 +494,9 @@ function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
function update(boxUpdateInfo, callback) {
function update(boxUpdateInfo, auditSource, callback) {
assert.strictEqual(typeof boxUpdateInfo, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (!boxUpdateInfo) return callback(null);
@@ -511,6 +504,8 @@ function update(boxUpdateInfo, callback) {
var error = locker.lock(locker.OP_BOX_UPDATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
// ensure tools can 'wait' on progress
progress.set(progress.UPDATE, 0, 'Starting');
@@ -549,9 +544,7 @@ function updateToLatest(auditSource, callback) {
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
update(boxUpdateInfo, callback);
update(boxUpdateInfo, auditSource, callback);
}
function doShortCircuitUpdate(boxUpdateInfo, callback) {
@@ -661,23 +654,16 @@ function installAppBundle(callback) {
}
async.eachSeries(bundle, function (appInfo, iteratorCallback) {
var appstoreId = appInfo.appstoreId;
var parts = appstoreId.split('@');
debug('autoInstall: installing %s at %s', appInfo.appstoreId, appInfo.location);
var url = config.apiServerOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : '');
var data = {
appStoreId: appInfo.appstoreId,
location: appInfo.location,
portBindings: appInfo.portBindings || null,
accessRestriction: appInfo.accessRestriction || null,
};
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 */,
null /* altDomain */, { userId: null, username: 'autoinstaller' }, iteratorCallback);
});
apps.install(data, { userId: null, username: 'autoinstaller' }, iteratorCallback);
}, function (error) {
if (error) debug('autoInstallApps: ', error);
+1 -7
View File
@@ -31,7 +31,6 @@ exports = module.exports = {
mailFqdn: mailFqdn,
appFqdn: appFqdn,
zoneName: zoneName,
adminEmail: adminEmail,
isDev: isDev,
@@ -74,11 +73,11 @@ function initConfig() {
data.fqdn = 'localhost';
data.token = null;
data.adminEmail = null;
data.boxVersionsUrl = null;
data.version = null;
data.isCustomDomain = false;
data.webServerOrigin = null;
data.smtpPort = 2525; // // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.oauthProxyPort = 3003;
@@ -101,7 +100,6 @@ function initConfig() {
name: 'boxtest'
};
data.token = 'APPSTORE_TOKEN';
data.adminEmail = 'test@cloudron.foo';
} else {
assert(false, 'Unknown environment. This should not happen!');
}
@@ -140,10 +138,6 @@ function get(key) {
return safe.query(data, key);
}
function adminEmail() {
return get('adminEmail');
}
function apiServerOrigin() {
return get('apiServerOrigin');
}
+5 -4
View File
@@ -30,6 +30,7 @@ var gAutoupdaterJob = null,
gCheckDiskSpaceJob = null;
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
var AUDIT_SOURCE = { userId: null, username: 'cron' };
// cron format
// Seconds: 0-59
@@ -66,7 +67,7 @@ function recreateJobs(unusedTimeZone, callback) {
if (gBackupJob) gBackupJob.stop();
gBackupJob = new CronJob({
cronTime: '00 00 */4 * * *', // every 4 hours
onTick: backups.ensureBackup,
onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
@@ -122,7 +123,7 @@ function recreateJobs(unusedTimeZone, callback) {
if (gCertificateRenewJob) gCertificateRenewJob.stop();
gCertificateRenewJob = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: certificates.autoRenew,
onTick: certificates.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
@@ -154,10 +155,10 @@ function autoupdatePatternChanged(pattern) {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.update(updateInfo.box, NOOP_CALLBACK);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, NOOP_CALLBACK);
apps.updateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No auto updates available');
}
+2 -1
View File
@@ -120,7 +120,8 @@ function clear(callback) {
require('./groupdb.js')._clear,
require('./userdb.js')._clear,
require('./settingsdb.js')._clear,
require('./eventlogdb.js')._clear
require('./eventlogdb.js')._clear,
require('./mailboxdb.js')._clear
], callback);
}
+6 -4
View File
@@ -5,7 +5,7 @@
exports = module.exports = {
DeveloperError: DeveloperError,
enabled: enabled,
isEnabled: isEnabled,
setEnabled: setEnabled,
issueDeveloperToken: issueDeveloperToken,
getNonApprovedApps: getNonApprovedApps
@@ -13,6 +13,7 @@ exports = module.exports = {
var assert = require('assert'),
config = require('./config.js'),
clients = require('./clients.js'),
debug = require('debug')('box:developer'),
eventlog = require('./eventlog.js'),
tokendb = require('./tokendb.js'),
@@ -42,7 +43,7 @@ util.inherits(DeveloperError, Error);
DeveloperError.INTERNAL_ERROR = 'Internal Error';
DeveloperError.EXTERNAL_ERROR = 'External Error';
function enabled(callback) {
function isEnabled(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getDeveloperMode(function (error, enabled) {
@@ -72,13 +73,14 @@ function issueDeveloperToken(user, auditSource, callback) {
var token = tokendb.generateToken();
var expiresAt = Date.now() + 24 * 60 * 60 * 1000; // 1 day
var scopes = '*,' + clients.SCOPE_ROLE_SDK;
tokendb.add(token, tokendb.PREFIX_DEV + user.id, '', expiresAt, 'developer,apps,settings,users,profile', function (error) {
tokendb.add(token, user.id, 'cid-cli', expiresAt, scopes, function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { authType: 'cli', userId: user.id, username: user.username });
callback(null, { token: token, expiresAt: expiresAt });
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
});
}
+14
View File
@@ -4,6 +4,7 @@ exports = module.exports = {
get: get,
getWithMembers: getWithMembers,
getAll: getAll,
getAllWithMembers: getAllWithMembers,
add: add,
del: del,
count: count,
@@ -65,6 +66,19 @@ function getAll(callback) {
});
}
function getAllWithMembers(callback) {
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' GROUP BY groups.id', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; });
callback(null, results);
});
}
function add(id, name, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof name, 'string');
+16 -5
View File
@@ -10,6 +10,7 @@ exports = module.exports = {
get: get,
getWithMembers: getWithMembers,
getAll: getAll,
getAllWithMembers: getAllWithMembers,
getMembers: getMembers,
addMember: addMember,
@@ -51,7 +52,7 @@ 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.BAD_FIELD = 'Field error';
GroupError.NOT_EMPTY = 'Not Empty';
GroupError.NOT_ALLOWED = 'Not Allowed';
@@ -59,12 +60,12 @@ 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 (name.length <= 2) return new GroupError(GroupError.BAD_FIELD, 'name must be atleast 2 chars');
if (name.length >= 200) return new GroupError(GroupError.BAD_FIELD, '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 (!/^[A-Za-z0-9_-]*$/.test(name)) return new GroupError(GroupError.BAD_FIELD, 'name can only have A-Za-z0-9_-');
if (RESERVED.indexOf(name) !== -1) return new GroupError(GroupError.BAD_NAME, 'name is reserved');
if (RESERVED.indexOf(name) !== -1) return new GroupError(GroupError.BAD_FIELD, 'name is reserved');
return null;
}
@@ -133,6 +134,16 @@ function getAll(callback) {
});
}
function getAllWithMembers(callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getAllWithMembers(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');
+21
View File
@@ -0,0 +1,21 @@
'use strict';
// WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
// These constants are used in the installer script as well
// Do not require anything here!
exports = module.exports = {
'version': 36,
'baseImage': 'cloudron/base:0.8.1',
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.11.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.10.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.9.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.8.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.13.2' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.8.0' }
}
};
+80 -20
View File
@@ -12,7 +12,9 @@ var assert = require('assert'),
eventlog = require('./eventlog.js'),
user = require('./user.js'),
UserError = user.UserError,
ldap = require('ldapjs');
ldap = require('ldapjs'),
mailboxes = require('./mailboxes.js'),
MailboxError = mailboxes.MailboxError;
var gServer = null;
@@ -66,7 +68,8 @@ function userSearch(req, res, next) {
cn: entry.id,
uid: entry.id,
mail: entry.email,
mailAlternateAddress: entry.username + '@' + config.fqdn(), // only valid when incoming mail enabled
// TODO: check mailboxes before we send this
mailAlternateAddress: entry.username + '@' + config.fqdn(),
displayname: displayName,
givenName: firstName,
username: entry.username,
@@ -130,7 +133,40 @@ function groupSearch(req, res, next) {
});
}
function userBind(req, res, next) {
function mailboxSearch(req, res, next) {
debug('mailbox search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
mailboxes.getAll(function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
result.forEach(function (entry) {
var dn = ldap.parseDN('cn=' + entry.name + ',ou=mailboxes,dc=cloudron');
// TODO: send aliases
var obj = {
dn: dn.toString(),
attributes: {
objectclass: ['mailbox'],
objectcategory: 'mailbox',
cn: entry.name,
uid: entry.name,
mail: entry.name + '@' + config.fqdn()
}
};
// 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(obj.attributes)) {
res.send(obj);
}
});
res.end();
});
}
function authenticateUser(req, res, next) {
debug('user bind: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
// extract the common name which might have different attribute names
@@ -145,7 +181,7 @@ function userBind(req, res, next) {
var parts = commonName.split('@');
if (parts[1] === config.fqdn()) { // internal email, verify with username
commonName = parts[0];
api = user.verify;
api = user.verifyWithUsername;
} else { // external email
api = user.verifyWithEmail;
}
@@ -155,34 +191,55 @@ function userBind(req, res, next) {
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) {
api(commonName, req.credentials || '', function (error, user) {
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));
getAppByRequest(req, function (error, app) {
if (error) return next(error);
req.user = user;
if (!app) {
debug('no app found for this container, allow access');
return res.end();
}
next();
});
}
apps.hasAccessTo(app, userObject, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
function authorizeUserForApp(req, res, next) {
assert(req.user);
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
// We simply authorize the user to access a mailbox by his own name
getAppByRequest(req, function (error, app) {
if (error) return next(error);
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: userObject.id });
if (!app) {
debug('no app found for this container, allow access');
return res.end();
}
res.end();
});
apps.hasAccessTo(app, req.user, 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()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: req.user.id });
res.end();
});
});
}
function authorizeUserForMailbox(req, res, next) {
assert(req.user);
mailboxes.get(req.user.username, function (error) {
if (error && error.reason === MailboxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: req.user.username }, { userId: req.user.username });
res.end();
});
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -190,7 +247,10 @@ function start(callback) {
gServer.search('ou=users,dc=cloudron', userSearch);
gServer.search('ou=groups,dc=cloudron', groupSearch);
gServer.bind('ou=users,dc=cloudron', userBind);
gServer.bind('ou=users,dc=cloudron', authenticateUser, authorizeUserForApp);
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch);
gServer.bind('ou=mailboxes,dc=cloudron', authenticateUser, authorizeUserForMailbox);
// this is the bind for addons (after bind, they might search and authenticate)
gServer.bind('ou=addons,dc=cloudron', function(req, res, next) {
+1 -1
View File
@@ -2,7 +2,7 @@
Dear Admin,
User with name <%= user.email %> was added in the Cloudron at <%= fqdn %>.
User with email <%= user.email %> was added in the Cloudron at <%= fqdn %>.
You are receiving this email because you are an Admin of the Cloudron at <%= fqdn %>.
+127
View File
@@ -0,0 +1,127 @@
'use strict';
exports = module.exports = {
add: add,
del: del,
get: get,
getAll: getAll,
getAliases: getAliases,
setAliases: setAliases,
_clear: clear
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
util = require('util');
var MAILBOX_FIELDS = [ 'name', 'aliasTarget', 'creationTime' ].join(',');
function add(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name) VALUES (?)', [ name ], 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 mailboxes', [], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function del(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mailboxes WHERE name=? OR aliasTarget = ?', [ name, name ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
function postProcess(result) {
result.aliases = result.aliases ? result.aliases.split(',') : [ ];
}
function get(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
var query = 'SELECT m1.name, m1.creationTime, GROUP_CONCAT(m2.name) AS aliases ' +
'FROM mailboxes as m1 ' +
'LEFT OUTER JOIN mailboxes as m2 ON m1.name = m2.aliasTarget ' +
'WHERE m1.name=? AND m1.aliasTarget IS NULL ' +
'GROUP BY m1.name';
database.query(query, [ name ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(results[0]);
callback(null, results[0]);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
var query = 'SELECT m1.name, m1.creationTime, GROUP_CONCAT(m2.name) AS aliases ' +
'FROM mailboxes as m1 ' +
'LEFT OUTER JOIN mailboxes as m2 ON m1.name = m2.aliasTarget ' +
'WHERE m1.aliasTarget IS NULL ' +
'GROUP BY m1.name';
database.query(query, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
callback(null, results);
});
}
function setAliases(name, aliases, callback) {
assert.strictEqual(typeof name, 'string');
assert(util.isArray(aliases));
assert.strictEqual(typeof callback, 'function');
// also cleanup the groupMembers table
var queries = [];
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ?', args: [ name ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, aliasTarget) VALUES (?, ?)', args: [ alias, name ] });
});
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function getAliases(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT name FROM mailboxes WHERE aliasTarget=? ORDER BY name', [ name ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results = results.map(function (r) { return r.name; });
callback(null, results);
});
}
+198
View File
@@ -0,0 +1,198 @@
'use strict';
exports = module.exports = {
add: add,
del: del,
get: get,
getAll: getAll,
setAliases: setAliases,
getAliases: getAliases,
setupAliases: setupAliases,
MailboxError: MailboxError
};
var assert = require('assert'),
async = require('async'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:mailboxes'),
docker = require('./docker.js'),
mailboxdb = require('./mailboxdb.js'),
util = require('util');
function MailboxError(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(MailboxError, Error);
MailboxError.ALREADY_EXISTS = 'already exists';
MailboxError.BAD_FIELD = 'Field error';
MailboxError.NOT_FOUND = 'not found';
MailboxError.INTERNAL_ERROR = 'internal error';
MailboxError.EXTERNAL_ERROR = 'external error';
function validateName(name) {
var RESERVED_NAMES = [ 'no-reply', 'postmaster', 'mailer-daemon' ];
if (!name.length) return new MailboxError(MailboxError.BAD_FIELD, "name cannot be empty");
if (name.length < 2) return new MailboxError(MailboxError.BAD_FIELD, 'name too small');
if (name.length > 127) return new MailboxError(MailboxError.BAD_FIELD, 'name too long');
if (RESERVED_NAMES.indexOf(name) !== -1) return new MailboxError(MailboxError.BAD_FIELD, 'name is reserved');
if (/[^a-zA-Z0-9.]/.test(name)) return new MailboxError(MailboxError.BAD_FIELD, 'name can only contain alphanumerals and dot');
if (name.indexOf('.app') !== -1) return new MailboxError(MailboxError.BAD_FIELD, 'alias pattern is reserved for apps');
return null;
}
function add(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
name = name.toLowerCase();
var error = validateName(name);
if (error) return callback(error);
mailboxdb.add(name, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailboxError(MailboxError.ALREADY_EXISTS));
if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error));
debug('Added mailbox %s', name);
var mailbox = {
name: name
};
callback(null, mailbox);
});
}
function pushAlias(name, aliases, callback) {
if (process.env.BOX_ENV === 'test') return callback();
var cmd = [ '/addons/mail/service.sh', 'set-alias', name ].concat(aliases);
debug('pushing alias for %s : %j', name, aliases);
docker.execContainer('mail', cmd, { }, function (error) {
if (error) return callback(new MailboxError(MailboxError.EXTERNAL_ERROR, error));
callback();
});
}
function del(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
pushAlias(name, [ ], function (error) {
if (error) return callback(error);
mailboxdb.del(name, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailboxError(MailboxError.NOT_FOUND));
if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error));
debug('deleted mailbox %s', name);
callback();
});
});
}
function get(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.get(name, function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailboxError(MailboxError.NOT_FOUND));
if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error));
callback(null, mailbox);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
mailboxdb.getAll(function (error, results) {
if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error));
callback(null, results);
});
}
function setAliases(name, aliases, callback) {
assert.strictEqual(typeof name, 'string');
assert(util.isArray(aliases));
assert.strictEqual(typeof callback, 'function');
for (var i = 0; i < aliases.length; i++) {
aliases[i] = aliases[i].toLowerCase();
var error = validateName(aliases[i]);
if (error) return callback(error);
}
pushAlias(name, aliases, function (error) {
if (error) return callback(error);
mailboxdb.setAliases(name, aliases, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailboxError(MailboxError.ALREADY_EXISTS, error.message))
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailboxError(MailboxError.NOT_FOUND));
if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error));
callback(null);
});
});
}
function getAliases(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getAliases(name, function (error, aliases) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailboxError(MailboxError.NOT_FOUND));
if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error));
callback(null, aliases);
});
}
// push aliases to the mail container on startup
function setupAliases(callback) {
assert.strictEqual(typeof callback, 'function');
getAll(function (error, mailboxes) {
if (error) return callback(error);
async.each(mailboxes, function (mailbox, iteratorDone) {
getAliases(mailbox.name, function (error, aliases) {
if (error) return iteratorDone(error);
if (aliases.length === 0) return iteratorDone();
pushAlias(mailbox.name, aliases, iteratorDone);
});
}, callback)
});
}
+16 -15
View File
@@ -41,6 +41,7 @@ var assert = require('assert'),
ejs = require('ejs'),
nodemailer = require('nodemailer'),
path = require('path'),
platform = require('./platform.js'),
safe = require('safetydance'),
smtpTransport = require('nodemailer-smtp-transport'),
users = require('./user.js'),
@@ -113,7 +114,7 @@ function getTxtRecords(callback) {
function checkDns() {
getTxtRecords(function (error, records) {
if (error || !records) {
debug('checkDns: DNS error or no records looking up TXT records for %s %s', config.adminFqdn(), error, records);
debug('checkDns: DNS error or no records looking up TXT records for %s %s', config.fqdn(), error, records);
gCheckDnsTimerId = setTimeout(checkDns, 60000);
return;
}
@@ -159,10 +160,10 @@ function sendMails(queue) {
var transport = nodemailer.createTransport(smtpTransport({
host: mailServerIp,
port: 2525, // this value comes from mail container
port: config.get('smtpPort'),
auth: {
user: 'no-reply', // derive from adminEmail
pass: 'supersecret'
user: platform.mailConfig().username,
pass: platform.mailConfig().password
}
}));
@@ -222,7 +223,7 @@ function mailUserEventToAdmins(user, event) {
adminEmails = _.difference(adminEmails, [ user.email ]);
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: adminEmails.join(', '),
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' }),
@@ -248,7 +249,7 @@ function sendInvite(user, invitor) {
};
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: user.email,
subject: util.format('Welcome to Cloudron %s', config.fqdn()),
text: render('welcome_user.ejs', templateData)
@@ -271,7 +272,7 @@ function userAdded(user, inviteSent) {
var inviteLink = inviteSent ? null : config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken;
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: adminEmails.join(', '),
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' }),
@@ -306,7 +307,7 @@ function passwordReset(user) {
var resetLink = config.adminOrigin() + '/api/v1/session/password/reset.html?reset_token=' + user.resetToken;
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: user.email,
subject: 'Password Reset Request',
text: render('password_reset.ejs', { fqdn: config.fqdn(), user: user, resetLink: resetLink, format: 'text' })
@@ -324,7 +325,7 @@ function appDied(app) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
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' })
@@ -342,7 +343,7 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
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' })
@@ -360,7 +361,7 @@ function appUpdateAvailable(app, updateInfo) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
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, updateInfo: updateInfo, format: 'text' })
@@ -374,7 +375,7 @@ function outOfDiskSpace(message) {
assert.strictEqual(typeof message, 'string');
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
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' })
@@ -388,7 +389,7 @@ function certificateRenewed(domain, message) {
assert.strictEqual(typeof message, 'string');
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
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' })
@@ -404,7 +405,7 @@ function unexpectedExit(program, context) {
assert.strictEqual(typeof context, 'string');
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: 'admin@cloudron.io',
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
text: render('unexpected_exit.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
@@ -426,7 +427,7 @@ function sendFeedback(user, type, subject, description) {
type === exports.FEEDBACK_TYPE_APP_ERROR);
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
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'})
-1
View File
@@ -4,7 +4,6 @@ exports = module.exports = {
cookieParser: require('cookie-parser'),
cors: require('./cors'),
csrf: require('csurf'),
favicon: require('serve-favicon'),
json: require('body-parser').json,
morgan: require('morgan'),
proxy: require('proxy-middleware'),
+11 -1
View File
@@ -8,7 +8,10 @@
// very basic angular app
var app = angular.module('Application', []);
app.controller('Controller', [function () {}]);
app.controller('Controller', ['$scope', function ($scope) {
$scope.username = '<%= (user && user.username) ? user.username : '' %>';
$scope.displayName = '<%= (user && user.displayName) ? user.displayName : '' %>';
}]);
</script>
@@ -28,6 +31,12 @@ app.controller('Controller', [function () {}]);
<center><p class="has-error"><%= error %></p></center>
<% if (user && user.username) { %>
<div class="form-group"">
<label class="control-label">Username</label>
<input type="text" class="form-control" ng-model="username" name="username" readonly required>
</div>
<% } else { %>
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) }">
<label class="control-label">Username</label>
<div class="control-label" ng-show="setupForm.username.$dirty && setupForm.username.$invalid">
@@ -37,6 +46,7 @@ app.controller('Controller', [function () {}]);
</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>
+2 -2
View File
@@ -7,7 +7,7 @@ exports = module.exports = {
var appdb = require('./appdb.js'),
assert = require('assert'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:proxy'),
@@ -124,7 +124,7 @@ function authenticate(req, res, next) {
return res.send(500, 'Unknown app.');
}
clientdb.getByAppIdAndType(result.id, clientdb.TYPE_PROXY, function (error, result) {
clients.getByAppIdAndType(result.id, clients.TYPE_PROXY, function (error, result) {
if (error) {
console.error('Unknown OAuth client.', error);
return res.send(500, 'Unknown OAuth client.');
+3 -1
View File
@@ -27,5 +27,7 @@ exports = module.exports = {
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json'),
ACME_CHALLENGES_DIR: path.join(config.baseDir(), 'data/acme'),
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'data/box/acme/acme.key')
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'data/box/acme/acme.key'),
INFRA_VERSION_FILE: path.join(config.baseDir(), 'data/INFRA_VERSION')
};
+108
View File
@@ -0,0 +1,108 @@
'use strict';
exports = module.exports = {
initialize: initialize,
mailConfig: mailConfig
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
certificates = require('./certificates.js'),
debug = require('debug')('box:platform'),
fs = require('fs'),
infra = require('./infra_version.js'),
ini = require('ini'),
mailboxes = require('./mailboxes.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
util = require('util');
var SETUP_INFRA_CMD = path.join(__dirname, 'scripts/setup_infra.sh');
var gAddonVars = null;
function initialize(callback) {
if (process.env.BOX_ENV === 'test' && !process.env.CREATE_INFRA) return callback();
debug('initializing addon infrastructure');
var existingInfra = { version: 'none' };
if (fs.existsSync(paths.INFRA_VERSION_FILE)) {
existingInfra = safe.JSON.parse(fs.readFileSync(paths.INFRA_VERSION_FILE, 'utf8'));
if (!existingInfra) existingInfra = { version: 'legacy' };
}
if (infra.version === existingInfra.version) {
debug('platform is uptodate at version %s', infra.version);
return loadAddonVars(callback);
}
debug('Updating infrastructure from %s to %s', existingInfra.version, infra.version);
async.series([
stopContainers,
startAddons,
removeOldImages,
existingInfra.version === 'none' ? apps.restoreInstalledApps : apps.configureInstalledApps,
loadAddonVars,
mailboxes.setupAliases,
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra))
], callback);
}
function removeOldImages(callback) {
debug('removing old addon images');
for (var imageName in infra.images) {
var image = infra.images[imageName];
debug('cleaning up images of %j', image);
var cmd = 'docker images "%s" | tail -n +2 | awk \'{ print $1 ":" $2 }\' | grep -v "%s" | xargs --no-run-if-empty docker rmi';
shell.execSync('removeOldImagesSync', util.format(cmd, image.repo, image.tag));
}
callback();
}
function stopContainers(callback) {
// TODO: be nice and stop addons cleanly (example, shutdown commands)
debug('stopping existing containers');
shell.execSync('stopContainersSync', 'docker ps -qa | xargs --no-run-if-empty docker rm -f');
callback();
}
function startAddons(callback) {
assert.strictEqual(typeof callback, 'function');
certificates.getAdminCertificatePath(function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
shell.sudo('setup_infra', [ SETUP_INFRA_CMD, paths.DATA_DIR, config.fqdn(), config.adminFqdn(), certFilePath, keyFilePath ], function (error) {
callback(error);
});
});
}
function loadAddonVars(callback) {
gAddonVars = {
mail: ini.parse(fs.readFileSync(paths.DATA_DIR + '/addons/mail_vars.sh', 'utf8')),
postgresql: ini.parse(fs.readFileSync(paths.DATA_DIR + '/addons/postgresql_vars.sh', 'utf8')),
mysql: ini.parse(fs.readFileSync(paths.DATA_DIR + '/addons/mysql_vars.sh', 'utf8')),
mongodb: ini.parse(fs.readFileSync(paths.DATA_DIR + '/addons/mongodb_vars.sh', 'utf8'))
};
callback();
}
function mailConfig() {
if (!gAddonVars) return { username: 'no-reply', from: 'no-reply@' + config.fqdn(), password: 'doesnotwork' }; // for tests which don't run infra
return {
username: gAddonVars.mail.MAIL_ROOT_USERNAME,
from: '"Cloudron" <' + gAddonVars.mail.MAIL_ROOT_USERNAME + '@' + config.fqdn() + '>',
password: gAddonVars.mail.MAIL_ROOT_PASSWORD
};
}
+79 -52
View File
@@ -28,8 +28,7 @@ var apps = require('../apps.js'),
HttpSuccess = require('connect-lastmile').HttpSuccess,
paths = require('../paths.js'),
safe = require('safetydance'),
util = require('util'),
uuid = require('node-uuid');
util = require('util');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -90,82 +89,74 @@ function getAppIcon(req, res, next) {
});
}
/*
* Installs an app
* @bodyparam {string} appStoreId The id of the app to be installed
* @bodyparam {manifest} manifest The app manifest
* @bodyparam {string} password The user's password
* @bodyparam {string} location The subdomain where the app is to be installed
* @bodyparam {object} portBindings map from environment variable name to (public) host port. can be null.
If a value in manifest.tcpPorts is missing in portBindings, the port/service is disabled
* @bodyparam {icon} icon Base64 encoded image
*/
function installApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
var data = req.body;
if (!data) return next(new HttpError(400, 'Cannot parse data field'));
if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest is required'));
if (typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId is required'));
// atleast one
if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
// required
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'));
// optional
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
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'));
// falsy values in cert and key unset the cert
if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (data.cert && 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'));
// falsy value in altDomain unsets it
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
// allow tests to provide an appId for testing
var appId = (process.env.BOX_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4();
debug('Installing app id:%s data:%j', data);
debug('Installing app id:%s storeid:%s loc:%s port:%j accessRestriction:%j memoryLimit:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.memoryLimit, data.manifest);
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.icon || null, data.cert || null, data.key || null, data.memoryLimit || 0, data.altDomain || null, auditSource(req), function (error) {
apps.install(data, auditSource(req), function (error, app) {
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.'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.USER_REQUIRED) return next(new HttpError(400, 'accessRestriction must specify one user'));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { id: appId } ));
next(new HttpSuccess(202, app));
});
}
/*
* Configure an app
* @bodyparam {string} password The user's password
* @bodyparam {string} location The subdomain where the app is to be installed
* @bodyparam {object} portBindings map from env to (public) host port. can be null.
If a value in manifest.tcpPorts is missing in portBindings, the port/service is disabled
*/
function configureApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var data = req.body;
if (!data) return next(new HttpError(400, 'Cannot parse data field'));
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 (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 ('location' in data && typeof data.location !== 'string') return next(new HttpError(400, 'location must be string'));
if ('portBindings' in data && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if ('accessRestriction' in data && typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
// falsy values in cert and key unset the cert
if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (data.cert && 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'));
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
debug('Configuring app id:%s location:%s bindings:%j accessRestriction:%j memoryLimit:%s', req.params.id, data.location, data.portBindings, data.accessRestriction, data.memoryLimit);
debug('Configuring app id:%s data:%j', req.params.id, data);
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.cert || null, data.key || null, data.memoryLimit || 0, data.altDomain || null, auditSource(req), function (error) {
apps.configure(req.params.id, data, auditSource(req), function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
@@ -180,14 +171,21 @@ function configureApp(req, res, next) {
}
function restoreApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var data = req.body;
debug('Restore app id:%s', req.params.id);
apps.restore(req.params.id, auditSource(req), function (error) {
if (!('backupId' in req.body)) return next(new HttpError(400, 'backupId is required'));
if (data.backupId !== null && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null'));
apps.restore(req.params.id, data, auditSource(req), function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { }));
@@ -209,10 +207,6 @@ function backupApp(req, res, next) {
});
}
/*
* Uninstalls an app
* @bodyparam {string} id The id of the app to be uninstalled
*/
function uninstallApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
@@ -260,15 +254,18 @@ function updateApp(req, res, next) {
var data = req.body;
if (!data) return next(new HttpError(400, 'Cannot parse data field'));
if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest is required'));
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
// atleast one
if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
if ('portBindings' in data && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
debug('Update app id:%s to manifest:%j with portBindings:%j', req.params.id, data.manifest, data.portBindings);
apps.update(req.params.id, data.force || false, data.manifest, data.portBindings || null, data.icon, auditSource(req), function (error) {
apps.update(req.params.id, req.body, auditSource(req), function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
@@ -338,15 +335,37 @@ function getLogs(req, res, next) {
});
}
function demuxStream(stream, stdin) {
var header = null;
stream.on('readable', function() {
header = header || stream.read(4);
while (header !== null) {
var length = header.readUInt32BE(0);
if (length === 0) {
header = null;
return stdin.end(); // EOF
}
var payload = stream.read(length);
if (payload === null) break;
stdin.write(payload);
header = stream.read(4);
}
});
}
function exec(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('Execing into app id:%s', req.params.id);
debug('Execing into app id:%s and cmd:%s', req.params.id, req.query.cmd);
var cmd = null;
if (req.query.cmd) {
cmd = safe.JSON.parse(req.query.cmd);
if (!util.isArray(cmd) && cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1'));
if (!util.isArray(cmd) || cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1'));
}
var columns = req.query.columns ? parseInt(req.query.columns, 10) : null;
@@ -367,8 +386,16 @@ function exec(req, res, next) {
req.clearTimeout();
res.sendUpgradeHandshake();
// When tty is disabled, the duplexStream has 2 separate streams. When enabled, it has stdout/stderr merged.
duplexStream.pipe(res.socket);
res.socket.pipe(duplexStream);
if (tty) {
res.socket.pipe(duplexStream); // in tty mode, the client always waits for server to exit
} else {
demuxStream(res.socket, duplexStream);
res.socket.on('error', function () { duplexStream.end(); });
res.socket.on('end', function () { duplexStream.end(); });
}
});
}
+2 -2
View File
@@ -3,7 +3,7 @@
exports = module.exports = {
get: get,
create: create,
download: download
createDownloadUrl: createDownloadUrl
};
var assert = require('assert'),
@@ -43,7 +43,7 @@ function create(req, res, next) {
});
}
function download(req, res, next) {
function createDownloadUrl(req, res, next) {
assert.strictEqual(typeof req.params.backupId, 'string');
backups.getRestoreUrl(req.params.backupId, function (error, result) {
+52 -15
View File
@@ -4,16 +4,16 @@ exports = module.exports = {
add: add,
get: get,
del: del,
getAllByUserId: getAllByUserId,
getAll: getAll,
addClientToken: addClientToken,
getClientTokens: getClientTokens,
delClientTokens: delClientTokens
delClientTokens: delClientTokens,
delToken: delToken
};
var assert = require('assert'),
clientdb = require('../clientdb.js'),
clients = require('../clients.js'),
ClientsError = clients.ClientsError,
DatabaseError = require('../databaseerror.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
validUrl = require('valid-url');
@@ -27,8 +27,8 @@ function add(req, res, next) {
if (typeof data.scope !== 'string' || !data.scope) return next(new HttpError(400, 'scope is required'));
if (!validUrl.isWebUri(data.redirectURI)) return next(new HttpError(400, 'redirectURI must be a valid uri'));
clients.add(data.appId, clientdb.TYPE_EXTERNAL, data.redirectURI, data.scope, function (error, result) {
if (error && error.reason === ClientsError.INVALID_SCOPE) return next(new HttpError(400, 'Invalid scope'));
clients.add(data.appId, clients.TYPE_EXTERNAL, data.redirectURI, data.scope, function (error, result) {
if (error && error.reason === ClientsError.INVALID_SCOPE) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, result));
});
@@ -38,7 +38,7 @@ function get(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
clients.get(req.params.clientId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(404, 'No such client'));
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
@@ -47,26 +47,49 @@ function get(req, res, next) {
function del(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
clients.del(req.params.clientId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(404, 'no such client'));
clients.get(req.params.clientId, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204, result));
// we do not allow to use the REST API to delete addon clients
if (result.type !== clients.TYPE_EXTERNAL) return next(new HttpError(405, 'Deleting app addon clients is not allowed.'));
clients.del(req.params.clientId, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === ClientsError.NOT_ALLOWED) return next(new HttpError(405, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204, result));
});
});
}
function getAllByUserId(req, res, next) {
clients.getAllWithDetailsByUserId(req.user.id, function (error, result) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return next(new HttpError(500, error));
function getAll(req, res, next) {
clients.getAll(function (error, result) {
if (error && error.reason !== ClientsError.NOT_FOUND) return next(new HttpError(500, error));
next(new HttpSuccess(200, { clients: result }));
});
}
function addClientToken(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
assert.strictEqual(typeof req.user, 'object');
var expiresAt = req.query.expiresAt ? parseInt(req.query.expiresAt, 10) : Date.now() + 24 * 60 * 60 * 1000; // default 1 day;
if (isNaN(expiresAt) || expiresAt <= Date.now()) return next(new HttpError(400, 'expiresAt must be a timestamp in the future'));
clients.addClientTokenByUserId(req.params.clientId, req.user.id, expiresAt, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { token: result }));
});
}
function getClientTokens(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
assert.strictEqual(typeof req.user, 'object');
clients.getClientTokensByUserId(req.params.clientId, req.user.id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(404, 'no such client'));
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { tokens: result }));
});
@@ -77,8 +100,22 @@ function delClientTokens(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
clients.delClientTokensByUserId(req.params.clientId, req.user.id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(404, 'no such client'));
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
function delToken(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
assert.strictEqual(typeof req.params.tokenId, 'string');
assert.strictEqual(typeof req.user, 'object');
clients.delToken(req.params.clientId, req.params.tokenId, function (error) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === ClientsError.INVALID_TOKEN) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
+15 -5
View File
@@ -8,10 +8,12 @@ exports = module.exports = {
getProgress: getProgress,
getConfig: getConfig,
update: update,
feedback: feedback
feedback: feedback,
checkForUpdates: checkForUpdates
};
var assert = require('assert'),
async = require('async'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
config = require('../config.js'),
@@ -20,7 +22,8 @@ var assert = require('assert'),
HttpSuccess = require('connect-lastmile').HttpSuccess,
progress = require('../progress.js'),
mailer = require('../mailer.js'),
superagent = require('superagent');
superagent = require('superagent'),
updateChecker = require('../updatechecker.js');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -55,9 +58,7 @@ function activate(req, res, next) {
cloudron.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
if (error && error.reason === CloudronError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CloudronError.BAD_USERNAME) return next(new HttpError(400, 'Bad username'));
if (error && error.reason === CloudronError.BAD_PASSWORD) return next(new HttpError(400, 'Bad password'));
if (error && error.reason === CloudronError.BAD_EMAIL) return next(new HttpError(400, 'Bad email'));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
// only in caas case do we have to notify the api server about activation
@@ -131,6 +132,15 @@ function update(req, res, next) {
});
}
function checkForUpdates(req, res, next) {
async.series([
updateChecker.checkAppUpdates,
updateChecker.checkBoxUpdates
], function () {
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
});
}
function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
+1 -1
View File
@@ -19,7 +19,7 @@ function auditSource(req) {
}
function enabled(req, res, next) {
developer.enabled(function (error, enabled) {
developer.isEnabled(function (error, enabled) {
if (enabled) return next();
next(new HttpError(412, 'Developer mode not enabled'));
});
+2 -2
View File
@@ -20,7 +20,7 @@ function create(req, res, next) {
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.BAD_FIELD) 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));
@@ -45,7 +45,7 @@ function get(req, res, next) {
}
function list(req, res, next) {
groups.getAll(function (error, result) {
groups.getAllWithMembers(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { groups: result }));
+2 -1
View File
@@ -9,9 +9,10 @@ exports = module.exports = {
eventlog: require('./eventlog.js'),
graphs: require('./graphs.js'),
groups: require('./groups.js'),
mailboxes: require('./mailboxes.js'),
oauth2: require('./oauth2.js'),
profile: require('./profile.js'),
settings: require('./settings.js'),
sysadmin: require('./sysadmin.js'),
settings: require('./settings.js'),
user: require('./user.js')
};
+91
View File
@@ -0,0 +1,91 @@
'use strict';
exports = module.exports = {
list: list,
get: get,
remove: remove,
create: create,
setAliases: setAliases,
getAliases: getAliases
};
var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
mailboxes = require('../mailboxes.js'),
MailboxError = mailboxes.MailboxError,
util = require('util');
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'));
mailboxes.add(req.body.name, function (error, mailbox) {
if (error && error.reason === MailboxError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === MailboxError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, mailbox));
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.mailboxId, 'string');
mailboxes.get(req.params.mailboxId, function (error, result) {
if (error && error.reason === MailboxError.NOT_FOUND) return next(new HttpError(404, 'No such mailbox'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
}
function list(req, res, next) {
mailboxes.getAll(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { mailboxes: result }));
});
}
function remove(req, res, next) {
assert.strictEqual(typeof req.params.mailboxId, 'string');
mailboxes.del(req.params.mailboxId, function (error) {
if (error && error.reason === MailboxError.NOT_FOUND) return next(new HttpError(404, 'Mailbox not found'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
function setAliases(req, res, next) {
assert.strictEqual(typeof req.params.mailboxId, 'string');
if (!util.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array'));
for (var i = 0; i < req.body.aliases.length; i++) {
if (typeof req.body.aliases[i] !== 'string') return next(new HttpError(400, 'alias must be a string'));
}
mailboxes.setAliases(req.params.mailboxId, req.body.aliases, function (error) {
if (error && error.reason === MailboxError.NOT_FOUND) return next(new HttpError(404, 'No such mailbox'));
if (error && error.reason === MailboxError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === MailboxError.ALREADY_EXISTS) return next(new HttpError(409, 'One or more alias already exist'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function getAliases(req, res, next) {
assert.strictEqual(typeof req.params.mailboxId, 'string');
mailboxes.getAliases(req.params.mailboxId, function (error, aliases) {
if (error && error.reason === MailboxError.NOT_FOUND) return next(new HttpError(404, 'No such mailbox'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { aliases: aliases }));
});
}
+55 -33
View File
@@ -4,9 +4,9 @@ var appdb = require('../appdb'),
apps = require('../apps'),
assert = require('assert'),
authcodedb = require('../authcodedb'),
clientdb = require('../clientdb'),
clients = require('../clients'),
ClientsError = clients.ClientsError,
config = require('../config.js'),
constants = require('../constants.js'),
DatabaseError = require('../databaseerror'),
debug = require('debug')('box:routes/oauth2'),
eventlog = require('../eventlog.js'),
@@ -21,7 +21,8 @@ var appdb = require('../appdb'),
url = require('url'),
user = require('../user.js'),
UserError = user.UserError,
util = require('util');
util = require('util'),
_ = require('underscore');
function auditSource(req, appId) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -41,7 +42,7 @@ gServer.serializeClient(function (client, callback) {
});
gServer.deserializeClient(function (id, callback) {
clientdb.get(id, callback);
clients.get(id, callback);
});
@@ -76,7 +77,7 @@ gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client,
var token = tokendb.generateToken();
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(token, tokendb.PREFIX_USER + user.id, client.id, expires, client.scope, function (error) {
tokendb.add(token, user.id, client.id, expires, client.scope, function (error) {
if (error) return callback(error);
debug('grant token: new access token for client %s token %s', client.id, token);
@@ -106,7 +107,7 @@ gServer.exchange(oauth2orize.exchange.code(function (client, code, redirectURI,
var token = tokendb.generateToken();
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(token, tokendb.PREFIX_USER + authCode.userId, authCode.clientId, expires, client.scope, function (error) {
tokendb.add(token, authCode.userId, authCode.clientId, expires, client.scope, function (error) {
if (error) return callback(error);
debug('exchange: new access token for client %s token %s', client.id, token);
@@ -201,13 +202,13 @@ function loginForm(req, res) {
});
}
clientdb.get(u.query.client_id, function (error, result) {
clients.get(u.query.client_id, function (error, result) {
if (error) return sendError(req, res, 'Unknown OAuth client');
switch (result.type) {
case clientdb.TYPE_ADMIN: return render(constants.ADMIN_NAME, '/api/v1/cloudron/avatar');
case clientdb.TYPE_EXTERNAL: return render('External Application', '/api/v1/cloudron/avatar');
case clientdb.TYPE_SIMPLE_AUTH: return sendError(req, res, 'Unknown OAuth client');
case clients.TYPE_BUILT_IN: return render(result.appId, '/api/v1/cloudron/avatar');
case clients.TYPE_EXTERNAL: return render(result.appId, '/api/v1/cloudron/avatar');
case clients.TYPE_SIMPLE_AUTH: return sendError(req, res, 'Unknown OAuth client');
default: break;
}
@@ -306,16 +307,20 @@ function accountSetup(req, res, next) {
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, auditSource(req), function (error) {
var data = _.pick(req.body, 'username', 'displayName');
user.update(userObject.id, data, auditSource(req), function (error) {
if (error && error.reason === UserError.ALREADY_EXISTS) return renderAccountSetupSite(res, req, userObject, 'Username already exists');
if (error && error.reason === UserError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error && error.reason === UserError.NOT_FOUND) return renderAccountSetupSite(res, req, userObject, 'No such user');
if (error) return next(new HttpError(500, error));
userObject.username = req.body.username;
userObject.displayName = req.body.displayName;
// setPassword clears the resetToken
user.setPassword(userObject.id, req.body.password, function (error, result) {
if (error && error.reason === UserError.BAD_PASSWORD) return renderAccountSetupSite(res, req, userObject, 'Password invalid');
if (error && error.reason === UserError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error) return next(new HttpError(500, error));
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
@@ -357,7 +362,7 @@ function passwordReset(req, res, next) {
// 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 && error.reason === UserError.BAD_FIELD) return next(new HttpError(406, error.message));
if (error) return next(new HttpError(500, error));
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
@@ -399,8 +404,8 @@ var authorization = [
gServer.authorization({}, function (clientId, redirectURI, callback) {
debug('authorization: client %s with callback to %s.', clientId, redirectURI);
clientdb.get(clientId, function (error, client) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
clients.get(clientId, function (error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// ignore the origin passed into form the client, but use the one from the clientdb
@@ -414,12 +419,12 @@ var authorization = [
// Handle our different types of oauth clients
var type = req.oauth2.client.type;
if (type === clientdb.TYPE_ADMIN) {
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, 'admin'), { userId: req.oauth2.user.id });
if (type === clients.TYPE_EXTERNAL || type === clients.TYPE_BUILT_IN) {
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id });
return next();
} else if (type === clients.TYPE_SIMPLE_AUTH) {
return sendError(req, res, 'Unknown OAuth client.');
}
if (type === clientdb.TYPE_EXTERNAL) return next();
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.');
@@ -451,6 +456,31 @@ var token = [
gServer.errorHandler()
];
// tests if all requestedScopes are attached to the request
function validateRequestedScopes(req, requestedScopes) {
assert.strictEqual(typeof req, 'object');
assert(Array.isArray(requestedScopes));
if (!req.authInfo || !req.authInfo.scope) return new Error('No scope found');
var scopes = req.authInfo.scope.split(',');
// check for roles separately
if (requestedScopes.indexOf(clients.SCOPE_ROLE_SDK) !== -1 && scopes.indexOf(clients.SCOPE_ROLE_SDK) === -1) {
return new Error('Missing required scope role "' + clients.SCOPE_ROLE_SDK + '"');
}
if (scopes.indexOf('*') !== -1) return null;
for (var i = 0; i < requestedScopes.length; ++i) {
if (scopes.indexOf(requestedScopes[i]) === -1) {
debug('scope: missing scope "%s".', requestedScopes[i]);
return new Error('Missing required scope "' + requestedScopes[i] + '"');
}
}
return null;
}
// The scope middleware provides an auth middleware for routes.
//
@@ -470,17 +500,8 @@ function scope(requestedScope) {
return [
passport.authenticate(['bearer'], { session: false }),
function (req, res, next) {
if (!req.authInfo || !req.authInfo.scope) return next(new HttpError(401, 'No scope found'));
if (req.authInfo.scope === '*') return next();
var scopes = req.authInfo.scope.split(',');
for (var i = 0; i < requestedScopes.length; ++i) {
if (scopes.indexOf(requestedScopes[i]) === -1) {
debug('scope: missing scope "%s".', requestedScopes[i]);
return next(new HttpError(401, 'Missing required scope "' + requestedScopes[i] + '"'));
}
}
var error = validateRequestedScopes(req, requestedScopes);
if (error) return next(new HttpError(401, error.message));
next();
}
@@ -511,6 +532,7 @@ exports = module.exports = {
accountSetup: accountSetup,
authorization: authorization,
token: token,
validateRequestedScopes: validateRequestedScopes,
scope: scope,
csrf: csrf
};
+19 -31
View File
@@ -12,8 +12,8 @@ var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
user = require('../user.js'),
tokendb = require('../tokendb.js'),
UserError = user.UserError;
UserError = user.UserError,
_ = require('underscore');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -23,26 +23,18 @@ function auditSource(req) {
function get(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
var result = {};
result.id = req.user.id;
result.tokenType = req.user.tokenType;
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
if (error) return next(new HttpError(500, error));
if (req.user.tokenType === tokendb.TYPE_USER || req.user.tokenType === tokendb.TYPE_DEV) {
result.username = req.user.username;
result.email = req.user.email;
result.displayName = req.user.displayName;
result.showTutorial = req.user.showTutorial;
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
if (error) return next(new HttpError(500, error));
result.admin = isAdmin;
next(new HttpSuccess(200, result));
});
} else {
next(new HttpSuccess(200, result));
}
next(new HttpSuccess(200, {
id: req.user.id,
username: req.user.username,
email: req.user.email,
admin: isAdmin,
displayName: req.user.displayName,
showTutorial: req.user.showTutorial
}));
});
}
function update(req, res, next) {
@@ -52,12 +44,11 @@ function update(req, res, next) {
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'));
var data = _.pick(req.body, 'email', 'displayName');
user.update(req.user.id, req.user.username, req.body.email || req.user.email, req.body.displayName || req.user.displayName, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
user.update(req.user.id, data, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
@@ -69,13 +60,10 @@ function changePassword(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.user, 'object');
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires the users old password.'));
if (typeof req.body.newPassword !== 'string') return next(new HttpError(400, 'API call requires the users new password.'));
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
if (typeof req.body.newPassword !== 'string') return next(new HttpError(400, 'newPassword must be a string'));
user.setPassword(req.user.id, req.body.newPassword, function (error) {
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Wrong password'));
if (error) return next(new HttpError(500, error));
+20 -2
View File
@@ -17,6 +17,7 @@ exports = module.exports = {
setBackupConfig: setBackupConfig,
getTimeZone: getTimeZone,
setTimeZone: setTimeZone,
setCertificate: setCertificate,
setAdminCertificate: setAdminCertificate
@@ -45,7 +46,7 @@ function setAutoupdatePattern(req, res, next) {
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setAutoupdatePattern(req.body.pattern, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, 'Invalid pattern'));
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
@@ -58,8 +59,9 @@ function setCloudronName(req, res, next) {
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name is required'));
settings.setCloudronName(req.body.name, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, 'Invalid name'));
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
@@ -67,6 +69,7 @@ function setCloudronName(req, res, next) {
function getCloudronName(req, res, next) {
settings.getCloudronName(function (error, name) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { name: name }));
});
}
@@ -74,10 +77,24 @@ function getCloudronName(req, res, next) {
function getTimeZone(req, res, next) {
settings.getTimeZone(function (error, tz) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { timeZone: tz }));
});
}
function setTimeZone(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.timeZone !== 'string') return next(new HttpError(400, 'timeZone is required'));
settings.setTimeZone(req.body.timeZone, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function setCloudronAvatar(req, res, next) {
assert.strictEqual(typeof req.files, 'object');
@@ -86,6 +103,7 @@ function setCloudronAvatar(req, res, next) {
settings.setCloudronAvatar(avatar, function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
+271 -319
View File
@@ -12,7 +12,7 @@ var appdb = require('../../appdb.js'),
path = require('path'),
async = require('async'),
child_process = require('child_process'),
clientdb = require('../../clientdb.js'),
clients = require('../../clients.js'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
@@ -23,18 +23,18 @@ var appdb = require('../../appdb.js'),
http = require('http'),
https = require('https'),
js2xml = require('js2xmlparser'),
ldap = require('../../ldap.js'),
net = require('net'),
nock = require('nock'),
paths = require('../../paths.js'),
redis = require('redis'),
superagent = require('superagent'),
safe = require('safetydance'),
server = require('../../server.js'),
settings = require('../../settings.js'),
simpleauth = require('../../simpleauth.js'),
superagent = require('superagent'),
taskmanager = require('../../taskmanager.js'),
tokendb = require('../../tokendb.js'),
url = require('url'),
util = require('util'),
uuid = require('node-uuid'),
_ = require('underscore');
@@ -42,9 +42,9 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '11.0.0';
var TEST_IMAGE_TAG = '15.0.0';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
var APP_STORE_ID = 'test', APP_ID;
var APP_LOCATION = 'appslocation';
@@ -112,62 +112,21 @@ describe('Apps', function () {
var imageCreated = false;
before(function (done) {
console.log('Starting addons, this can take 10 seconds');
safe.fs.unlinkSync(paths.DATA_DIR + '/INFRA_VERSION');
safe.fs.writeFileSync(paths.DATA_DIR + '/cert', 'utf8');
safe.fs.writeFileSync(paths.DATA_DIR + '/key', 'utf8');
var args = [
path.resolve(__dirname + '/../../scripts/setup_infra.sh'),
paths.DATA_DIR,
config.fqdn(),
config.adminFqdn(),
paths.DATA_DIR + '/cert',
paths.DATA_DIR + '/key',
config.database().name,
'"' + config.database().password + '"' // can be empty...
];
child_process.exec('sudo ' + args.join(' '), { stdio: 'pipe' }, function (error) {
if (error) return done(error);
dockerProxy = startDockerProxy(function interceptor(req, res) {
if (req.method === 'POST' && req.url === '/images/create?fromImage=' + encodeURIComponent(TEST_IMAGE_REPO) + '&tag=' + TEST_IMAGE_TAG) {
imageCreated = true;
res.writeHead(200);
res.end();
return true;
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE + '?force=false&noprune=false') {
imageDeleted = true;
res.writeHead(200);
res.end();
return true;
}
return false;
}, done);
});
});
after(function (done) {
child_process.exec('docker ps | awk \'{print $1}\' | xargs docker rm -f', function () {
dockerProxy.close(done);
});
});
/*
Individual sub category setup and cleanup
*/
function setup(done) {
config._reset();
process.env.CREATE_INFRA = 1;
safe.fs.unlinkSync(paths.INFRA_VERSION_FILE);
child_process.execSync('docker ps -qa | xargs --no-run-if-empty docker rm -f');
async.series([
// first clear, then start server. otherwise, taskmanager spins up tasks for obsolete appIds
database.initialize,
database._clear,
server.start.bind(server),
ldap.start,
simpleauth.start,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
@@ -206,58 +165,104 @@ describe('Apps', function () {
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, tokendb.PREFIX_USER + USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
},
function (callback) {
dockerProxy = startDockerProxy(function interceptor(req, res) {
if (req.method === 'POST' && req.url === '/images/create?fromImage=' + encodeURIComponent(TEST_IMAGE_REPO) + '&tag=' + TEST_IMAGE_TAG) {
imageCreated = true;
res.writeHead(200);
res.end();
return true;
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE + '?force=false&noprune=false') {
imageDeleted = true;
res.writeHead(200);
res.end();
return true;
}
return false;
}, callback);
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
], done);
}
], function (error) {
if (error) return done(error);
console.log('This test can take ~30 seconds to start as it waits for infra to be ready');
setTimeout(done, 30000);
});
});
after(function (done) {
delete process.env.CREATE_INFRA;
// child_process.execSync('docker ps -qa | xargs --no-run-if-empty docker rm -f');
dockerProxy.close(function () { });
function cleanup(done) {
// db is not cleaned up here since it's too late to call it after server.stop. if called before server.stop taskmanager apptasks are unhappy :/
async.series([
taskmanager.stopPendingTasks,
taskmanager.waitForPendingTasks,
server.stop,
ldap.stop,
simpleauth.stop,
config._reset,
], done);
}
});
describe('App API', function () {
this.timeout(50000);
before(setup);
after(function (done) {
APP_ID = null;
cleanup(done);
appdb._clear(done); // TODO: test proper uninstall (requires mock for aws)
});
it('app install fails - missing manifest', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('manifest is required');
expect(res.body.message).to.eql('appStoreId or manifest is required');
done();
});
});
it('app install fails - missing appId', function (done) {
it('app install fails - null manifest', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, password: PASSWORD })
.send({ manifest: null, password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('appStoreId is required');
expect(res.body.message).to.eql('appStoreId or manifest is required');
done();
});
});
it('app install fails - invalid json', function (done) {
it('app install fails - bad manifest format', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: 'epic', password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('manifest must be an object');
done();
});
});
it('app install fails - empty appStoreId format', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: null, appStoreId: '', password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('appStoreId or manifest is required');
done();
});
});
it('app install fails - invalid json', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send('garbage')
@@ -270,7 +275,7 @@ describe('Apps', function () {
it('app install fails - invalid location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('Hostname can only contain alphanumerics and hyphen');
@@ -281,7 +286,7 @@ describe('Apps', function () {
it('app install fails - invalid location type', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('location is required');
@@ -292,7 +297,7 @@ describe('Apps', function () {
it('app install fails - reserved admin location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.ADMIN_LOCATION, accessRestriction: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: constants.ADMIN_LOCATION, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.ADMIN_LOCATION + ' is reserved');
@@ -303,7 +308,7 @@ describe('Apps', function () {
it('app install fails - reserved api location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.API_LOCATION, accessRestriction: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: constants.API_LOCATION, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
@@ -314,7 +319,7 @@ describe('Apps', function () {
it('app install fails - portBindings must be object', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: 23, accessRestriction: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: 23, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('portBindings must be an object');
@@ -325,7 +330,7 @@ describe('Apps', function () {
it('app install fails - accessRestriction is required', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {} })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {} })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -336,7 +341,7 @@ describe('Apps', function () {
it('app install fails - accessRestriction type is wrong', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: '' })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -347,7 +352,7 @@ describe('Apps', function () {
it('app install fails - accessRestriction no users not allowed', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST_1, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: null })
.send({ manifest: APP_MANIFEST_1, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction must specify one user');
@@ -358,7 +363,7 @@ describe('Apps', function () {
it('app install fails - accessRestriction too many users not allowed', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST_1, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: { users: [ 'one', 'two' ] } })
.send({ manifest: APP_MANIFEST_1, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: { users: [ 'one', 'two' ] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction must specify one user');
@@ -369,50 +374,64 @@ describe('Apps', function () {
it('app install fails for non admin', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token_1 })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('app install fails due to purchase failure', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(402, {});
it('app install fails because manifest download fails', function (done) {
var fake = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(404, {});
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(fake.isDone()).to.be.ok();
done();
});
});
it('app install fails due to purchase failure', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(402, {});
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(402);
expect(fake.isDone()).to.be.ok();
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
it('app install succeeds with purchase', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake.isDone()).to.be.ok();
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
it('app install fails because of conflicting location', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
expect(fake.isDone()).to.be.ok();
done();
});
});
@@ -510,23 +529,23 @@ describe('Apps', function () {
});
it('app install succeeds already purchased', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(200, {});
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION_2, portBindings: null, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION_2, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake.isDone()).to.be.ok();
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
it('app install succeeds without password but developer token', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
settings.setDeveloperMode(true, function (error) {
expect(error).to.be(null);
@@ -535,7 +554,7 @@ describe('Apps', function () {
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
// overwrite non dev token
@@ -543,11 +562,10 @@ describe('Apps', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
expect(fake.isDone()).to.be.ok();
APP_ID = res.body.id;
done();
});
@@ -578,8 +596,6 @@ describe('Apps', function () {
imageCreated = false;
async.series([
setup,
function (callback) {
apiHockInstance
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
@@ -608,7 +624,6 @@ describe('Apps', function () {
APP_ID = null;
async.series([
cleanup,
apiHockServer.close.bind(apiHockServer),
awsHockServer.close.bind(awsHockServer)
], done);
@@ -617,7 +632,8 @@ describe('Apps', function () {
var appResult = null /* the json response */, appEntry = null /* entry from database */;
it('can install test app', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(200, {});
var count = 0;
function checkInstallStatus() {
@@ -634,12 +650,13 @@ describe('Apps', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake.isDone()).to.be.ok();
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
expect(res.body.id).to.be.a('string');
expect(res.body.id).to.be.eql(APP_ID);
APP_ID = res.body.id;
checkInstallStatus();
});
});
@@ -669,14 +686,7 @@ describe('Apps', function () {
expect(data.Config.Env).to.contain('APP_ORIGIN=https://' + config.appFqdn(APP_LOCATION));
expect(data.Config.Env).to.contain('APP_DOMAIN=' + config.appFqdn(APP_LOCATION));
expect(data.Config.Hostname).to.be(APP_LOCATION);
clientdb.getByAppIdAndType(appResult.id, clientdb.TYPE_OAUTH, function (error, client) {
expect(error).to.not.be.ok();
expect(client.id.length).to.be(40); // cid- + 32 hex chars (128 bits) + 4 hyphens
expect(client.clientSecret.length).to.be(64); // 32 hex chars (256 bits)
expect(data.Config.Env).to.contain('OAUTH_CLIENT_ID=' + client.id);
expect(data.Config.Env).to.contain('OAUTH_CLIENT_SECRET=' + client.clientSecret);
done();
});
done();
});
});
@@ -722,6 +732,65 @@ describe('Apps', function () {
});
});
it('installation - app responnds to http request', function (done) {
superagent.get('http://localhost:' + appEntry.httpPort).end(function (err, res) {
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
expect(res.body.status).to.be('OK');
done();
});
});
it('installation - oauth addon config', function (done) {
var appContainer = docker.getContainer(appEntry.containerId);
appContainer.inspect(function (error, data) {
expect(error).to.not.be.ok();
clients.getByAppIdAndType(APP_ID, clients.TYPE_OAUTH, function (error, client) {
expect(error).to.not.be.ok();
expect(client.id.length).to.be(40); // cid- + 32 hex chars (128 bits) + 4 hyphens
expect(client.clientSecret.length).to.be(64); // 32 hex chars (256 bits)
expect(data.Config.Env).to.contain('OAUTH_CLIENT_ID=' + client.id);
expect(data.Config.Env).to.contain('OAUTH_CLIENT_SECRET=' + client.clientSecret);
done();
});
});
});
it('installation - app can populate addons', function (done) {
superagent.get('http://localhost:' + appEntry.httpPort + '/populate_addons').end(function (err, res) {
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
for (var key in res.body) {
expect(res.body[key]).to.be('OK');
}
done();
});
});
it('installation - app can check addons', function (done) {
console.log('This test can take a while as it waits for scheduler addon to tick');
async.retry({ times: 15, interval: 6000 }, function (callback) {
superagent.get('http://localhost:' + appEntry.httpPort + '/check_addons')
.query({ username: USERNAME, password: PASSWORD })
.end(function (err, res) {
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
delete res.body.recvmail; // unclear why dovecot mail delivery won't work
delete res.body.stdenv; // cannot access APP_ORIGIN
for (var key in res.body) {
if (res.body[key] !== 'OK') return callback('Not done yet: ' + JSON.stringify(res.body));
}
callback();
});
}, done);
});
var redisIp, exportedRedisPort;
it('installation - redis addon created', function (done) {
@@ -739,129 +808,6 @@ describe('Apps', function () {
});
});
it('installation - redis addon config', function (done) {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
var redisUrl = null;
data.Config.Env.forEach(function (env) { if (env.indexOf('REDIS_URL=') === 0) redisUrl = env.split('=')[1]; });
expect(redisUrl).to.be.ok();
var urlp = url.parse(redisUrl);
var password = urlp.auth.split(':')[1];
expect(data.Config.Env).to.contain('REDIS_PORT=6379');
expect(data.Config.Env).to.contain('REDIS_HOST=redis-' + APP_ID);
expect(data.Config.Env).to.contain('REDIS_PASSWORD=' + password);
expect(urlp.hostname).to.be('redis-' + APP_ID);
var client = redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
client.on('error', done);
client.set('key', 'value');
client.get('key', function (err, reply) {
expect(err).to.not.be.ok();
expect(reply.toString()).to.be('value');
client.end();
done();
});
});
});
it('installation - mysql addon config', function (done) {
var appContainer = docker.getContainer(appEntry.containerId);
appContainer.inspect(function (error, data) {
var mysqlUrl = null;
data.Config.Env.forEach(function (env) { if (env.indexOf('MYSQL_URL=') === 0) mysqlUrl = env.split('=')[1]; });
expect(mysqlUrl).to.be.ok();
var urlp = url.parse(mysqlUrl);
var username = urlp.auth.split(':')[0];
var password = urlp.auth.split(':')[1];
var dbname = urlp.path.substr(1);
expect(data.Config.Env).to.contain('MYSQL_PORT=3306');
expect(data.Config.Env).to.contain('MYSQL_HOST=mysql');
expect(data.Config.Env).to.contain('MYSQL_USERNAME=' + username);
expect(data.Config.Env).to.contain('MYSQL_PASSWORD=' + password);
expect(data.Config.Env).to.contain('MYSQL_DATABASE=' + dbname);
var cmd = util.format('mysql -h %s -u%s -p%s --database=%s -e "CREATE TABLE IF NOT EXISTS foo (id INT);"',
'mysql', username, password, dbname);
child_process.exec('docker exec ' + appContainer.id + ' ' + cmd, { timeout: 5000 }, function (error, stdout, stderr) {
expect(!error).to.be.ok();
expect(stdout.length).to.be(0);
// expect(stderr.length).to.be(0); // "Warning: Using a password on the command line interface can be insecure."
done();
});
});
});
it('installation - postgresql addon config', function (done) {
var appContainer = docker.getContainer(appEntry.containerId);
appContainer.inspect(function (error, data) {
var postgresqlUrl = null;
data.Config.Env.forEach(function (env) { if (env.indexOf('POSTGRESQL_URL=') === 0) postgresqlUrl = env.split('=')[1]; });
expect(postgresqlUrl).to.be.ok();
var urlp = url.parse(postgresqlUrl);
var username = urlp.auth.split(':')[0];
var password = urlp.auth.split(':')[1];
var dbname = urlp.path.substr(1);
expect(data.Config.Env).to.contain('POSTGRESQL_PORT=5432');
expect(data.Config.Env).to.contain('POSTGRESQL_HOST=postgresql');
expect(data.Config.Env).to.contain('POSTGRESQL_USERNAME=' + username);
expect(data.Config.Env).to.contain('POSTGRESQL_PASSWORD=' + password);
expect(data.Config.Env).to.contain('POSTGRESQL_DATABASE=' + dbname);
var cmd = util.format('bash -c "PGPASSWORD=%s psql -q -h %s -U%s --dbname=%s -e \'CREATE TABLE IF NOT EXISTS foo (id INT);\'"',
password, 'postgresql', username, dbname);
child_process.exec('docker exec ' + appContainer.id + ' ' + cmd, { timeout: 5000 }, function (error, stdout, stderr) {
expect(!error).to.be.ok();
expect(stdout.length).to.be(0);
expect(stderr.length).to.be(0);
done();
});
});
});
it('installation - mongodb addon config', function (done) {
var appContainer = docker.getContainer(appEntry.containerId);
appContainer.inspect(function (error, data) {
var mongodbUrl = null;
data.Config.Env.forEach(function (env) { if (env.indexOf('MONGODB_URL=') === 0) mongodbUrl = env.split('=')[1]; });
expect(mongodbUrl).to.be.ok();
var urlp = url.parse(mongodbUrl);
var username = urlp.auth.split(':')[0];
var password = urlp.auth.split(':')[1];
var dbname = urlp.path.substr(1);
expect(data.Config.Env).to.contain('MONGODB_PORT=27017');
expect(data.Config.Env).to.contain('MONGODB_HOST=mongodb');
expect(data.Config.Env).to.contain('MONGODB_USERNAME=' + username);
expect(data.Config.Env).to.contain('MONGODB_PASSWORD=' + password);
expect(data.Config.Env).to.contain('MONGODB_DATABASE=' + dbname);
var cmd = util.format('mongo --quiet -u %s -p %s %s:%s/%s --eval "db.collection.insert({ item: 34 })"',
username, password, 'mongodb', 27017, dbname);
child_process.exec('docker exec ' + appContainer.id + ' ' + cmd, { timeout: 5000 }, function (error) {
expect(!error).to.be.ok();
done();
});
});
});
it('installation - scheduler', function (done) {
async.retry({ times: 100, interval: 1000 }, function (retryCallback) {
if (fs.existsSync(paths.DATA_DIR + '/' + APP_ID + '/data/every_minute.env')) return retryCallback();
retryCallback(new Error('not run yet'));
}, done);
});
xit('logs - stdout and stderr', function (done) {
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logs')
.query({ access_token: token })
@@ -981,6 +927,27 @@ describe('Apps', function () {
checkStartState();
});
it('installation - app can check addons', function (done) {
this.timeout(120000);
async.retry({ times: 15, interval: 6000 }, function (callback) {
superagent.get('http://localhost:' + appEntry.httpPort + '/check_addons')
.query({ username: USERNAME, password: PASSWORD })
.end(function (err, res) {
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
delete res.body.recvmail; // unclear why dovecot mail delivery won't work
delete res.body.stdenv; // cannot access APP_ORIGIN
for (var key in res.body) {
if (res.body[key] !== 'OK') return callback('Not done yet: ' + JSON.stringify(res.body));
}
callback();
});
}, done);
});
it('can uninstall app', function (done) {
var count = 0;
function checkUninstallStatus() {
@@ -1064,8 +1031,6 @@ describe('Apps', function () {
APP_ID = uuid.v4();
async.series([
setup,
function (callback) {
config.set('fqdn', 'test.foobar.com');
callback();
@@ -1102,7 +1067,6 @@ describe('Apps', function () {
after(function (done) {
APP_ID = null;
async.series([
cleanup,
apiHockServer.close.bind(apiHockServer),
awsHockServer.close.bind(awsHockServer)
], done);
@@ -1111,7 +1075,8 @@ describe('Apps', function () {
var appResult = null, appEntry = null;
it('can install test app', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
var count = 0;
function checkInstallStatus() {
@@ -1128,11 +1093,12 @@ describe('Apps', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake.isDone()).to.be.ok();
expect(res.body.id).to.equal(APP_ID);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
APP_ID = res.body.id;
checkInstallStatus();
});
});
@@ -1220,6 +1186,39 @@ describe('Apps', function () {
});
});
it('installation - app can populate addons', function (done) {
superagent.get('http://localhost:' + appEntry.httpPort + '/populate_addons').end(function (err, res) {
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
for (var key in res.body) {
expect(res.body[key]).to.be('OK');
}
done();
});
});
it('installation - app can check addons', function (done) {
this.timeout(120000);
async.retry({ times: 15, interval: 6000 }, function (callback) {
superagent.get('http://localhost:' + appEntry.httpPort + '/check_addons')
.query({ username: USERNAME, password: PASSWORD })
.end(function (err, res) {
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
delete res.body.recvmail; // unclear why dovecot mail delivery won't work
delete res.body.stdenv; // cannot access APP_ORIGIN
for (var key in res.body) {
if (res.body[key] !== 'OK') return callback('Not done yet: ' + JSON.stringify(res.body));
}
callback();
});
}, done);
});
var redisIp, exportedRedisPort;
it('installation - redis addon created', function (done) {
@@ -1237,37 +1236,6 @@ describe('Apps', function () {
});
});
it('installation - redis addon config', function (done) {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
var redisUrl = null;
data.Config.Env.forEach(function (env) { if (env.indexOf('REDIS_URL=') === 0) redisUrl = env.split('=')[1]; });
expect(redisUrl).to.be.ok();
var urlp = url.parse(redisUrl);
expect(urlp.hostname).to.be('redis-' + APP_ID);
var password = urlp.auth.split(':')[1];
expect(data.Config.Env).to.contain('REDIS_PORT=6379');
expect(data.Config.Env).to.contain('REDIS_HOST=redis-' + APP_ID);
expect(data.Config.Env).to.contain('REDIS_PASSWORD=' + password);
function checkRedis() {
var client = redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
client.on('error', done);
client.set('key', 'value');
client.get('key', function (err, reply) {
expect(err).to.not.be.ok();
expect(reply.toString()).to.be('value');
client.end();
done();
});
}
setTimeout(checkRedis, 1000); // the bridge network takes time to come up?
});
});
function checkConfigureStatus(count, done) {
assert.strictEqual(typeof count, 'number');
assert.strictEqual(typeof done, 'function');
@@ -1283,20 +1251,20 @@ describe('Apps', function () {
});
}
it('cannot reconfigure app with missing location', function (done) {
it('cannot reconfigure app with bad location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.send({ password: PASSWORD, location: 1234, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot reconfigure app with missing accessRestriction', function (done) {
it('cannot reconfigure app with bad accessRestriction', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 } })
.send({ password: PASSWORD, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1306,7 +1274,7 @@ describe('Apps', function () {
it('cannot reconfigure app with only the cert, no key', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1 })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1316,27 +1284,27 @@ describe('Apps', function () {
it('cannot reconfigure app with only the key, no cert', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, key: validKey1 })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, key: validKey1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot reconfigure app with cert not bein a string', function (done) {
it('cannot reconfigure app with cert not being a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: 1234, key: validKey1 })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: 1234, key: validKey1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot reconfigure app with key not bein a string', function (done) {
it('cannot reconfigure app with key not being a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1, key: 1234 })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, cert: validCert1, key: 1234 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1346,7 +1314,7 @@ describe('Apps', function () {
it('non admin cannot reconfigure app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token_1 })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
@@ -1356,7 +1324,7 @@ describe('Apps', function () {
it('can reconfigure app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 } })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
checkConfigureStatus(0, done);
@@ -1400,47 +1368,31 @@ describe('Apps', function () {
});
});
it('redis addon works after reconfiguration', function (done) {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
var redisUrl = null;
data.Config.Env.forEach(function (env) { if (env.indexOf('REDIS_URL=') === 0) redisUrl = env.split('=')[1]; });
expect(redisUrl).to.be.ok();
it('installation - app can check addons', function (done) {
this.timeout(120000);
async.retry({ times: 15, interval: 6000 }, function (callback) {
superagent.get('http://localhost:' + appEntry.httpPort + '/check_addons')
.query({ username: USERNAME, password: PASSWORD })
.end(function (err, res) {
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
var urlp = url.parse(redisUrl);
var password = urlp.auth.split(':')[1];
delete res.body.recvmail; // unclear why dovecot mail delivery won't work
delete res.body.stdenv; // cannot access APP_ORIGIN
expect(urlp.hostname).to.be('redis-' + APP_ID);
for (var key in res.body) {
if (res.body[key] !== 'OK') return callback('Not done yet: ' + JSON.stringify(res.body));
}
expect(data.Config.Env).to.contain('REDIS_PORT=6379');
expect(data.Config.Env).to.contain('REDIS_HOST=redis-' + APP_ID);
expect(data.Config.Env).to.contain('REDIS_PASSWORD=' + password);
var client = redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
client.on('error', done);
client.set('key', 'value');
client.get('key', function (err, reply) {
expect(err).to.not.be.ok();
expect(reply.toString()).to.be('value');
client.end();
done();
callback();
});
});
});
it('scheduler works after reconfiguration', function (done) {
async.retry({ times: 100, interval: 1000 }, function (callback) {
var data = safe.fs.readFileSync(paths.DATA_DIR + '/' + APP_ID + '/data/every_minute.env', 'utf8');
if (data && data.indexOf('ECHO_SERVER_PORT=7172') !== -1) return callback();
callback(new Error('not run yet'));
}, done);
});
it('can reconfigure app with custom certificate', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1, key: validKey1 })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1, key: validKey1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
checkConfigureStatus(0, done);
+51 -6
View File
@@ -8,7 +8,7 @@
var async = require('async'),
config = require('../../config.js'),
clientdb = require('../../clientdb.js'),
clients = require('../../clients.js'),
database = require('../../database.js'),
oauth2 = require('../oauth2.js'),
expect = require('expect.js'),
@@ -174,7 +174,7 @@ 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);
expect(result.body.type).to.equal(clients.TYPE_EXTERNAL);
done();
});
@@ -291,6 +291,14 @@ describe('OAuth Clients API', function () {
scope: 'profile'
};
var CLIENT_1 = {
id: '',
appId: 'someAppId-1',
redirectURI: 'http://some.callback1',
scope: 'profile',
type: clients.TYPE_OAUTH
};
before(function (done) {
async.series([
server.start.bind(null),
@@ -387,6 +395,44 @@ describe('OAuth Clients API', function () {
});
});
});
it('fails for cid-webadmin', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(405);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
done();
});
});
});
it('fails for addon auth client', function (done) {
clients.add(CLIENT_1.appId, CLIENT_1.type, CLIENT_1.redirectURI, CLIENT_1.scope, function (error, result) {
expect(error).to.equal(null);
CLIENT_1.id = result.id;
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_1.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(405);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_1.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
done();
});
});
});
});
});
});
});
@@ -489,8 +535,7 @@ describe('Clients', function () {
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.clients.length).to.eql(1);
expect(result.body.clients[0].tokenCount).to.eql(1);
expect(result.body.clients.length).to.eql(3);
done();
});
@@ -543,7 +588,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.id);
expect(result.body.tokens[0].identifier).to.eql(USER_0.id);
done();
});
@@ -596,7 +641,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.id);
expect(result.body.tokens[0].identifier).to.eql(USER_0.id);
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token })
+60 -4
View File
@@ -327,7 +327,7 @@ describe('Developer API', function () {
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
@@ -338,7 +338,7 @@ describe('Developer API', function () {
.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(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
@@ -349,7 +349,7 @@ describe('Developer API', function () {
.send({ username: EMAIL, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
@@ -360,10 +360,66 @@ describe('Developer API', function () {
.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(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
});
});
describe('sdk tokens are valid without password checks', function () {
var token_normal, token_sdk;
before(function (done) {
async.series([
setup,
settings.setDeveloperMode.bind(null, true),
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, {});
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();
token_normal = result.body.token;
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
token_sdk = result.body.token;
callback();
});
});
},
], done);
});
after(cleanup);
it('fails with non sdk token', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password').query({ access_token: token_normal }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password').query({ access_token: token_sdk }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
});
});
+1 -1
View File
@@ -67,7 +67,7 @@ function setup(done) {
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, tokendb.PREFIX_USER + USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
}
], done);
+41 -9
View File
@@ -6,18 +6,15 @@
'use strict';
var appdb = require('../../appdb.js'),
async = require('async'),
var 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');
nock = require('nock');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -117,6 +114,9 @@ describe('Groups API', function () {
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');
expect(res.body.groups[0].userIds).to.be.an(Array);
expect(res.body.groups[0].userIds.length).to.be(1);
expect(res.body.groups[0].userIds[0]).to.be(userId);
done();
});
});
@@ -224,7 +224,7 @@ describe('Groups API', function () {
});
it('cannot add user to invalid group', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/set_groups')
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin', 'something' ]})
.end(function (error, result) {
@@ -234,7 +234,7 @@ describe('Groups API', function () {
});
it('can add user to valid group', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/set_groups')
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin', 'group0', 'group1' ]})
.end(function (error, result) {
@@ -243,8 +243,8 @@ describe('Groups API', function () {
});
});
it('can remove last user from admin', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/set_groups')
it('cannot remove self from admin', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'group0', 'group1' ]})
.end(function (error, result) {
@@ -252,5 +252,37 @@ describe('Groups API', function () {
done();
});
});
it('can add another user to admin', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId_1 + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('lists members of admin 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.userIds.length).to.be(2);
expect(result.body.userIds[0]).to.be(userId);
expect(result.body.userIds[1]).to.be(userId_1);
done();
});
});
it('remove activation user from admin', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
.query({ access_token: token_1 })
.send({ groupIds: [ 'group0', 'group1' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(204); // user_1 is still admin, so we can remove the other person
done();
});
});
});
});
+211
View File
@@ -0,0 +1,211 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
server = require('../../server.js'),
nock = require('nock'),
userdb = require('../../userdb.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var MAILBOX_ID = 'mailbox';
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var server;
function setup(done) {
config.set('fqdn', 'foobar.com');
async.series([
server.start.bind(server),
userdb._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('Mailbox API', function () {
this.timeout(10000);
before(setup);
after(cleanup);
it('cannot create a mailbox without name param', function (done) {
superagent.post(SERVER_URL + '/api/v1/mailboxes')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot create a mailbox without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/mailboxes')
.send({ name: MAILBOX_ID })
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
done();
});
});
it('cannot create invalid mailbox', function (done) {
superagent.post(SERVER_URL + '/api/v1/mailboxes')
.query({ access_token: token })
.send({ name: 'no-reply' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('can create mailbox', function (done) {
superagent.post(SERVER_URL + '/api/v1/mailboxes')
.query({ access_token: token })
.send({ name: MAILBOX_ID })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
it('can get mailbox', function (done) {
superagent.get(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.name).to.equal(MAILBOX_ID);
expect(res.body.creationTime).to.be.ok();
done();
});
});
it('cannot set with invalid alias', function (done) {
superagent.put(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases')
.query({ access_token: token })
.send({ aliases: [ 'a' ]})
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot set with invalid type', function (done) {
superagent.put(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases')
.query({ access_token: token })
.send({ aliases: [ 'apple', 34 ]})
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('can set aliases of mailbox', function (done) {
superagent.put(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases')
.query({ access_token: token })
.send({ aliases: [ 'alias1', 'alias2' ]})
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
});
it('can list mailboxes', function (done) {
superagent.get(SERVER_URL + '/api/v1/mailboxes')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.mailboxes).to.be.an(Array);
expect(res.body.mailboxes[0].name).to.be(MAILBOX_ID);
done();
});
});
it('can get aliases', function (done) {
superagent.get(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.aliases).to.be.an(Array);
expect(res.body.aliases[0]).to.be('alias1');
expect(res.body.aliases[1]).to.be('alias2');
done();
});
});
it('can add another mailbox', function (done) {
superagent.post(SERVER_URL + '/api/v1/mailboxes')
.query({ access_token: token })
.send({ name: MAILBOX_ID + '2' })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
it('cannot alias existing mailbox', function (done) {
superagent.put(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases')
.query({ access_token: token })
.send({ aliases: [ MAILBOX_ID + '2' ]})
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('can delete mailbox', function (done) {
superagent.del(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID)
.query({ access_token: token })
.send({ name: MAILBOX_ID })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('cannot delete random mailbox', function (done) {
superagent.del(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID)
.query({ access_token: token })
.send({ name: MAILBOX_ID })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
});
+11 -10
View File
@@ -18,6 +18,7 @@ var expect = require('expect.js'),
querystring = require('querystring'),
database = require('../../database.js'),
clientdb = require('../../clientdb.js'),
clients = require('../../clients.js'),
userdb = require('../../userdb.js'),
user = require('../../user.js'),
appdb = require('../../appdb.js'),
@@ -197,7 +198,7 @@ describe('OAuth2', function () {
var CLIENT_0 = {
id: 'cid-client0',
appId: 'appid-app0',
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret0',
redirectURI: 'http://redirect0',
scope: 'profile'
@@ -207,7 +208,7 @@ describe('OAuth2', function () {
var CLIENT_1 = {
id: 'cid-client1',
appId: 'appid-app1',
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret1',
redirectURI: 'http://redirect1',
scope: 'profile'
@@ -217,7 +218,7 @@ describe('OAuth2', function () {
var CLIENT_2 = {
id: 'cid-client2',
appId: APP_0.id,
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret2',
redirectURI: 'http://redirect2',
scope: 'profile'
@@ -227,7 +228,7 @@ describe('OAuth2', function () {
var CLIENT_3 = {
id: 'cid-client3',
appId: APP_0.id,
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret3',
redirectURI: 'http://redirect1',
scope: 'profile'
@@ -237,7 +238,7 @@ describe('OAuth2', function () {
var CLIENT_4 = {
id: 'cid-client4',
appId: 'appid-app4',
type: clientdb.TYPE_PROXY,
type: clients.TYPE_PROXY,
clientSecret: 'secret4',
redirectURI: 'http://redirect4',
scope: 'profile'
@@ -247,7 +248,7 @@ describe('OAuth2', function () {
var CLIENT_5 = {
id: 'cid-client5',
appId: APP_0.id,
type: clientdb.TYPE_PROXY,
type: clients.TYPE_PROXY,
clientSecret: 'secret5',
redirectURI: 'http://redirect5',
scope: 'profile'
@@ -257,7 +258,7 @@ describe('OAuth2', function () {
var CLIENT_6 = {
id: 'cid-client6',
appId: APP_1.id,
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret6',
redirectURI: 'http://redirect6',
scope: 'profile'
@@ -267,7 +268,7 @@ describe('OAuth2', function () {
var CLIENT_7 = {
id: 'cid-client7',
appId: APP_2.id,
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret7',
redirectURI: 'http://redirect7',
scope: 'profile'
@@ -277,7 +278,7 @@ describe('OAuth2', function () {
var CLIENT_8 = {
id: 'cid-client8',
appId: APP_2.id,
type: clientdb.TYPE_SIMPLE_AUTH,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'secret8',
redirectURI: 'http://redirect8',
scope: 'profile'
@@ -287,7 +288,7 @@ describe('OAuth2', function () {
var CLIENT_9 = {
id: 'cid-client9',
appId: APP_3.id,
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret9',
redirectURI: 'http://redirect9',
scope: 'profile'
+14 -14
View File
@@ -115,7 +115,7 @@ describe('Profile API', function () {
var token = tokendb.generateToken();
var expires = Date.now() - 2000; // 1 sec
tokendb.add(token, tokendb.PREFIX_USER + user_0.id, null, expires, '*', function (error) {
tokendb.add(token, user_0.id, null, expires, '*', function (error) {
expect(error).to.not.be.ok();
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
@@ -153,7 +153,7 @@ describe('Profile API', function () {
after(cleanup);
it('change email fails due to missing token', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
@@ -162,7 +162,7 @@ describe('Profile API', function () {
});
it('change email fails due to invalid email', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ email: 'foo@bar' })
.end(function (error, result) {
@@ -172,7 +172,7 @@ describe('Profile API', function () {
});
it('change user succeeds without email nor displayName', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({})
.end(function (error, result) {
@@ -182,7 +182,7 @@ describe('Profile API', function () {
});
it('change email succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
@@ -203,7 +203,7 @@ describe('Profile API', function () {
});
it('change displayName succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ displayName: DISPLAY_NAME_0_NEW })
.end(function (error, result) {
@@ -229,7 +229,7 @@ describe('Profile API', function () {
after(cleanup);
it('fails due to missing current password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ newPassword: 'some wrong password' })
.end(function (err, res) {
@@ -239,7 +239,7 @@ describe('Profile API', function () {
});
it('fails due to missing new password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD })
.end(function (err, res) {
@@ -249,7 +249,7 @@ describe('Profile API', function () {
});
it('fails due to wrong password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
.end(function (err, res) {
@@ -259,7 +259,7 @@ describe('Profile API', function () {
});
it('fails due to invalid password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'five' })
.end(function (err, res) {
@@ -269,7 +269,7 @@ describe('Profile API', function () {
});
it('succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
.end(function (err, res) {
@@ -284,7 +284,7 @@ describe('Profile API', function () {
after(cleanup);
it('fails due to missing showTutorial', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/tutorial')
superagent.post(SERVER_URL + '/api/v1/profile/tutorial')
.query({ access_token: token_0 })
.send({})
.end(function (err, res) {
@@ -294,7 +294,7 @@ describe('Profile API', function () {
});
it('fails due to wrong showTutorial type', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/tutorial')
superagent.post(SERVER_URL + '/api/v1/profile/tutorial')
.query({ access_token: token_0 })
.send({ showTutorial: 'true' })
.end(function (err, res) {
@@ -304,7 +304,7 @@ describe('Profile API', function () {
});
it('succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/tutorial')
superagent.post(SERVER_URL + '/api/v1/profile/tutorial')
.query({ access_token: token_0 })
.send({ showTutorial: false })
.end(function (err, res) {
+88
View File
@@ -0,0 +1,88 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
net = require('net'),
nock = require('nock'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var server;
function setup(done) {
config.setVersion('1.2.3');
async.series([
server.start.bind(server),
database._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('REST API', function () {
before(setup);
after(cleanup);
it('does not crash with invalid JSON', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.set('content-type', 'application/json')
.send("some invalid non-strict json")
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(result.body.message).to.be('Bad JSON');
done();
});
});
it('does not crash with invalid string', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.set('content-type', 'application/x-www-form-urlencoded')
.send("some string")
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
});
+9 -8
View File
@@ -6,9 +6,10 @@
'use strict';
var clientdb = require('../../clientdb.js'),
appdb = require('../../appdb.js'),
var appdb = require('../../appdb.js'),
async = require('async'),
clientdb = require('../../clientdb.js'),
clients = require('../../clients.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
@@ -70,7 +71,7 @@ describe('SimpleAuth API', function () {
var CLIENT_0 = {
id: 'someclientid',
appId: 'someappid',
type: clientdb.TYPE_SIMPLE_AUTH,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret',
redirectURI: '',
scope: 'user,profile'
@@ -79,7 +80,7 @@ describe('SimpleAuth API', function () {
var CLIENT_1 = {
id: 'someclientid1',
appId: APP_0.id,
type: clientdb.TYPE_SIMPLE_AUTH,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret1',
redirectURI: '',
scope: 'user,profile'
@@ -88,7 +89,7 @@ describe('SimpleAuth API', function () {
var CLIENT_2 = {
id: 'someclientid2',
appId: APP_1.id,
type: clientdb.TYPE_SIMPLE_AUTH,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret2',
redirectURI: '',
scope: 'user,profile'
@@ -97,7 +98,7 @@ describe('SimpleAuth API', function () {
var CLIENT_3 = {
id: 'someclientid3',
appId: APP_2.id,
type: clientdb.TYPE_SIMPLE_AUTH,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret3',
redirectURI: '',
scope: 'user,profile'
@@ -106,7 +107,7 @@ describe('SimpleAuth API', function () {
var CLIENT_4 = {
id: 'someclientid4',
appId: APP_2.id,
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'someclientsecret4',
redirectURI: '',
scope: 'user,profile'
@@ -115,7 +116,7 @@ describe('SimpleAuth API', function () {
var CLIENT_5 = {
id: 'someclientid5',
appId: APP_3.id,
type: clientdb.TYPE_SIMPLE_AUTH,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret5',
redirectURI: '',
scope: 'user,profile'
+60 -13
View File
@@ -1,4 +1,3 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
@@ -21,7 +20,7 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME_0 = 'superaDmIn', PASSWORD = 'Foobar?1337', EMAIL_0 = 'silLY@me.com', EMAIL_0_NEW = 'stupID@me.com', DISPLAY_NAME_0_NEW = 'New Name';
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 USERNAME_3 = 'ut', EMAIL_3 = 'user3@FOO.bar';
function setup(done) {
server.start(function (error) {
@@ -161,7 +160,7 @@ describe('User API', function () {
var token = tokendb.generateToken();
var expires = Date.now() + 2000; // 1 sec
tokendb.add(token, tokendb.PREFIX_USER + user_0.id, null, expires, '*', function (error) {
tokendb.add(token, user_0.id, null, expires, '*', function (error) {
expect(error).to.not.be.ok();
setTimeout(function () {
@@ -261,7 +260,7 @@ describe('User API', function () {
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 + user_1.id, 'test-client-id', Date.now() + 10000, '*', done);
tokendb.add(token_1, user_1.id, 'test-client-id', Date.now() + 10000, '*', done);
});
});
});
@@ -293,7 +292,7 @@ describe('User API', function () {
});
it('set second user as admin succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_1.id + '/set_groups')
superagent.put(SERVER_URL + '/api/v1/users/' + user_1.id + '/groups')
.query({ access_token: token })
.send({ groupIds: [ groups.ADMIN_GROUP_ID ] })
.end(function (err, res) {
@@ -310,8 +309,24 @@ describe('User API', function () {
});
});
it('list groupIds when listing users', 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);
expect(res.body.users).to.be.an('array');
res.body.users.forEach(function (user) {
expect(user.admin).to.be(true);
expect(user.groupIds).to.eql([ groups.ADMIN_GROUP_ID ]);
});
done();
});
});
it('remove itself from admins fails', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id + '/set_groups')
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'somegroupid' ] })
.end(function (err, res) {
@@ -321,7 +336,7 @@ describe('User API', function () {
});
it('remove second user from admins succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_1.id + '/set_groups')
superagent.put(SERVER_URL + '/api/v1/users/' + user_1.id + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'somegroupid' ] })
.end(function (err, res) {
@@ -368,6 +383,26 @@ describe('User API', function () {
});
});
it('create user reserved name fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: 'no-reply' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('create user with short name fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: 'n' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('create second and third user', function (done) {
mailer._clearMailQueue();
@@ -441,12 +476,24 @@ describe('User API', function () {
expect(user.email).to.be.ok();
expect(user.password).to.not.be.ok();
expect(user.salt).to.not.be.ok();
expect(user.groupIds).to.be.an(Array);
expect(user.admin).to.be.a('boolean');
});
done();
});
});
it('remove random user fails', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/randomid')
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('user removes himself is not allowed', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
@@ -508,7 +555,7 @@ describe('User API', function () {
// Change email
it('change email fails due to missing token', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id)
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
@@ -517,7 +564,7 @@ describe('User API', function () {
});
it('change email fails due to invalid email', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ email: 'foo@bar' })
.end(function (error, result) {
@@ -527,7 +574,7 @@ describe('User API', function () {
});
it('change user succeeds without email nor displayName', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({})
.end(function (error, result) {
@@ -537,7 +584,7 @@ describe('User API', function () {
});
it('change email succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_2.id)
superagent.post(SERVER_URL + '/api/v1/users/' + user_2.id)
.query({ access_token: token })
.send({ email: EMAIL_2_NEW })
.end(function (error, result) {
@@ -558,7 +605,7 @@ describe('User API', function () {
});
it('change email as admin for other user succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_2.id)
superagent.post(SERVER_URL + '/api/v1/users/' + user_2.id)
.query({ access_token: token })
.send({ email: EMAIL_2 })
.end(function (error, result) {
@@ -579,7 +626,7 @@ describe('User API', function () {
});
it('change displayName succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ displayName: DISPLAY_NAME_0_NEW })
.end(function (error, result) {
+32 -39
View File
@@ -13,13 +13,16 @@ exports = module.exports = {
};
var assert = require('assert'),
clients = require('../clients.js'),
generatePassword = require('../password.js').generate,
groups = require('../groups.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
oauth2 = require('./oauth2.js'),
user = require('../user.js'),
tokendb = require('../tokendb.js'),
UserError = user.UserError;
UserError = user.UserError,
_ = require('underscore');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -41,11 +44,8 @@ function create(req, res, next) {
var displayName = req.body.displayName || '';
user.create(username, password, email, displayName, auditSource(req), { invitor: req.user, sendInvite: sendInvite }, function (error, user) {
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, 'Invalid username'));
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, 'Invalid email'));
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, 'Invalid password'));
if (error && error.reason === UserError.BAD_FIELD) 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.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
var userInfo = {
@@ -54,6 +54,7 @@ function create(req, res, next) {
displayName: user.displayName,
email: user.email,
admin: user.admin,
groupIds: [ ],
resetToken: user.resetToken
};
@@ -68,30 +69,29 @@ function update(req, res, next) {
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 ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a 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.get(req.params.userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
user.update(req.params.userId, req.body, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
user.update(req.params.userId, result.username, req.body.email || result.email, req.body.displayName || result.displayName, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
next(new HttpSuccess(204));
});
}
function list(req, res, next) {
user.list(function (error, result) {
user.list(function (error, results) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { users: result }));
var users = results.map(function (result) {
return _.pick(result, 'id', 'username', 'email', 'displayName', 'groupIds', 'admin');
});
next(new HttpSuccess(200, { users: users }));
});
}
@@ -105,17 +105,14 @@ function get(req, res, next) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
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
}));
});
next(new HttpSuccess(200, {
id: result.id,
username: result.username,
displayName: result.displayName,
email: result.email,
admin: result.admin,
groupIds: result.groupIds
}));
});
}
@@ -129,24 +126,20 @@ function remove(req, res, next) {
if (req.user.id === req.params.userId) return next(new HttpError(403, 'Not allowed to remove yourself.'));
user.get(req.params.userId, function (error, userObject) {
user.remove(req.params.userId, auditSource(req), function (error) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
user.remove(userObject, auditSource(req), function (error) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
next(new HttpSuccess(204));
});
}
function verifyPassword(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
// developers are allowed through without password
if (req.user.tokenType === tokendb.TYPE_DEV) return next();
// using an 'sdk' token we skip password checks
var error = oauth2.validateRequestedScopes(req, [ clients.SCOPE_ROLE_SDK ]);
if (!error) return next();
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
+26 -71
View File
@@ -12,42 +12,17 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
exit 0
fi
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${script_dir}/../INFRA_VERSION" # this injects INFRA_VERSION
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly infra_version="${script_dir}/../infra_version.js"
readonly data_dir="$1"
readonly fqdn="$2"
readonly mail_fqdn="$3"
readonly mail_tls_cert="$4"
readonly mail_tls_key="$5"
readonly db_name="$6"
readonly db_password="$7"
# removing containers ensures containers are launched with latest config updates
# restore code in appatask does not delete old containers
infra_version="none"
[[ -f "${data_dir}/INFRA_VERSION" ]] && infra_version=$(cat "${data_dir}/INFRA_VERSION")
if [[ "${infra_version}" == "${INFRA_VERSION}" ]]; then
echo "Infrastructure is upto date"
exit 0
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
echo "${existing_containers}" | xargs docker rm -f
fi
# a hack to 'refresh' images when testing with hotfix --recreate-infra
if [[ -z "${infra_version}" ]]; then
echo "Removing existing images"
docker rmi "${BASE_IMAGE}" "${MYSQL_IMAGE}" "${POSTGRESQL_IMAGE}" "${MONGODB_IMAGE}" "${REDIS_IMAGE}" "${MAIL_IMAGE}" "${GRAPHITE_IMAGE}" || true
fi
# graphite
readonly graphite_image=$(node -e "console.log(require('${infra_version}').images.graphite.tag);")
graphite_container_id=$(docker run --restart=always -d --name="graphite" \
-m 75m \
--memory-swap 150m \
@@ -56,15 +31,19 @@ graphite_container_id=$(docker run --restart=always -d --name="graphite" \
-p 127.0.0.1:8000:8000 \
-v "${data_dir}/graphite:/app/data" \
--read-only -v /tmp -v /run \
"${GRAPHITE_IMAGE}")
"${graphite_image}")
echo "Graphite container id: ${graphite_container_id}"
if docker images "${GRAPHITE_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${GRAPHITE_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old graphite images"
fi
# mail (note: 2525 is hardcoded in mail container and app use this port)
# 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 uses /app/data for backed up data and /run for restart-able data
readonly mail_image=$(node -e "console.log(require('${infra_version}').images.mail.tag);")
mail_addon_root_password=$(pwgen -1 -s)
cat > "${data_dir}/addons/mail_vars.sh" <<EOF
MAIL_ROOT_USERNAME=no-reply
MAIL_ROOT_PASSWORD=${mail_addon_root_password}
EOF
mail_container_id=$(docker run --restart=always -d --name="mail" \
-m 75m \
--memory-swap 150m \
@@ -72,25 +51,25 @@ mail_container_id=$(docker run --restart=always -d --name="mail" \
-e "MAIL_DOMAIN=${fqdn}" \
-e "MAIL_SERVER_NAME=${mail_fqdn}" \
-v "${data_dir}/box/mail:/app/data" \
-v "${data_dir}/mail:/run" \
-v "${data_dir}/addons/mail_vars.sh:/etc/mail/mail_vars.sh:ro" \
-v "${mail_tls_key}:/etc/tls_key.pem:ro" \
-v "${mail_tls_cert}:/etc/tls_cert.pem:ro" \
-p 587:2525 \
-p 993:9993 \
-p 4190:4190 \
-p 25:2525 \
--read-only -v /tmp -v /run \
"${MAIL_IMAGE}")
--read-only -v /tmp \
"${mail_image}")
echo "Mail container id: ${mail_container_id}"
if docker images "${MAIL_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${MAIL_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old mail images"
fi
# mysql
readonly mysql_image=$(node -e "console.log(require('${infra_version}').images.mysql.tag);")
mysql_addon_root_password=$(pwgen -1 -s)
docker0_ip=$(/sbin/ifconfig docker0 | grep "inet addr" | awk -F: '{print $2}' | awk '{print $1}')
docker0_ip=$(/sbin/ifconfig docker0 | grep "inet " | awk -F: '{print $2}' | awk '{print $1}')
cat > "${data_dir}/addons/mysql_vars.sh" <<EOF
readonly MYSQL_ROOT_PASSWORD='${mysql_addon_root_password}'
readonly MYSQL_ROOT_HOST='${docker0_ip}'
MYSQL_ROOT_PASSWORD='${mysql_addon_root_password}'
MYSQL_ROOT_HOST='${docker0_ip}'
EOF
mysql_container_id=$(docker run --restart=always -d --name="mysql" \
-m 256m \
@@ -99,16 +78,14 @@ mysql_container_id=$(docker run --restart=always -d --name="mysql" \
-v "${data_dir}/mysql:/var/lib/mysql" \
-v "${data_dir}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
--read-only -v /tmp -v /run \
"${MYSQL_IMAGE}")
"${mysql_image}")
echo "MySQL container id: ${mysql_container_id}"
if docker images "${MYSQL_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${MYSQL_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old mysql images"
fi
# postgresql
readonly postgresql_image=$(node -e "console.log(require('${infra_version}').images.postgresql.tag);")
postgresql_addon_root_password=$(pwgen -1 -s)
cat > "${data_dir}/addons/postgresql_vars.sh" <<EOF
readonly POSTGRESQL_ROOT_PASSWORD='${postgresql_addon_root_password}'
POSTGRESQL_ROOT_PASSWORD='${postgresql_addon_root_password}'
EOF
postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \
-m 100m \
@@ -117,16 +94,14 @@ postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \
-v "${data_dir}/postgresql:/var/lib/postgresql" \
-v "${data_dir}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
--read-only -v /tmp -v /run \
"${POSTGRESQL_IMAGE}")
"${postgresql_image}")
echo "PostgreSQL container id: ${postgresql_container_id}"
if docker images "${POSTGRESQL_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${POSTGRESQL_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old postgresql images"
fi
# mongodb
readonly mongodb_image=$(node -e "console.log(require('${infra_version}').images.mongodb.tag);")
mongodb_addon_root_password=$(pwgen -1 -s)
cat > "${data_dir}/addons/mongodb_vars.sh" <<EOF
readonly MONGODB_ROOT_PASSWORD='${mongodb_addon_root_password}'
MONGODB_ROOT_PASSWORD='${mongodb_addon_root_password}'
EOF
mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \
-m 100m \
@@ -135,25 +110,5 @@ mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \
-v "${data_dir}/mongodb:/var/lib/mongodb" \
-v "${data_dir}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
--read-only -v /tmp -v /run \
"${MONGODB_IMAGE}")
"${mongodb_image}")
echo "Mongodb container id: ${mongodb_container_id}"
if docker images "${MONGODB_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${MONGODB_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old mongodb images"
fi
# redis
if docker images "${REDIS_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${REDIS_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old redis images"
fi
# only touch apps in installed state. any other state is just resumed by the taskmanager
if [[ "${infra_version}" == "none" ]]; then
# if no existing infra was found (for new, upgraded and restored cloudons), download app backups
echo "Marking installed apps for restore"
mysql -u root --password="${db_password}" -e 'UPDATE apps SET installationState = "pending_restore", oldConfigJson = NULL WHERE installationState = "installed"' ${db_name}
else
# if existing infra was found, just mark apps for reconfiguration
mysql -u root --password="${db_password}" -e 'UPDATE apps SET installationState = "pending_configure", oldConfigJson = NULL WHERE installationState = "installed"' ${db_name}
fi
echo -n "${INFRA_VERSION}" > "${data_dir}/INFRA_VERSION"
+35 -21
View File
@@ -5,8 +5,7 @@ exports = module.exports = {
stop: stop
};
var addons = require('./addons.js'),
assert = require('assert'),
var assert = require('assert'),
async = require('async'),
auth = require('./auth.js'),
certificates = require('./certificates.js'),
@@ -22,6 +21,7 @@ var addons = require('./addons.js'),
middleware = require('./middleware'),
passport = require('passport'),
path = require('path'),
platform = require('./platform.js'),
routes = require('./routes/index.js'),
taskmanager = require('./taskmanager.js');
@@ -32,7 +32,7 @@ function initializeExpressSync() {
var app = express();
var httpServer = http.createServer(app);
var QUERY_LIMIT = '10mb', // max size for json and urlencoded queries
var QUERY_LIMIT = '1mb', // max size for json and urlencoded queries (see also client_max_body_size in nginx)
FIELD_LIMIT = 2 * 1024 * 1024; // max fields that can appear in multipart
var REQUEST_TIMEOUT = 10000; // timeout for all requests (see also setTimeout on the httpServer)
@@ -43,6 +43,7 @@ function initializeExpressSync() {
app.set('views', path.join(__dirname, 'oauth2views'));
app.set('view options', { layout: true, debug: false });
app.set('view engine', 'ejs');
app.set('json spaces', 2); // pretty json
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
@@ -62,13 +63,13 @@ function initializeExpressSync() {
.use(middleware.lastMile());
// NOTE: these limits have to be in sync with nginx limits
var FILE_SIZE_LIMIT = '1mb', // max file size that can be uploaded
var FILE_SIZE_LIMIT = '1mb', // max file size that can be uploaded (see also client_max_body_size in nginx)
FILE_TIMEOUT = 60 * 1000; // increased timeout for file uploads (1 min)
var multipart = middleware.multipart({ maxFieldsSize: FIELD_LIMIT, limit: FILE_SIZE_LIMIT, timeout: FILE_TIMEOUT });
// scope middleware implicitly also adds bearer token verification
var rootScope = routes.oauth2.scope(clients.SCOPE_ROOT);
var cloudronScope = routes.oauth2.scope(clients.SCOPE_CLOUDRON);
var profileScope = routes.oauth2.scope(clients.SCOPE_PROFILE);
var usersScope = routes.oauth2.scope(clients.SCOPE_USERS);
var appsScope = routes.oauth2.scope(clients.SCOPE_APPS);
@@ -90,28 +91,29 @@ function initializeExpressSync() {
router.post('/api/v1/developer/login', routes.developer.enabled, routes.developer.login);
router.get ('/api/v1/developer/apps', developerScope, routes.developer.enabled, routes.developer.apps);
// private routes
router.get ('/api/v1/cloudron/config', rootScope, routes.cloudron.getConfig);
router.post('/api/v1/cloudron/update', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update);
router.post('/api/v1/cloudron/reboot', rootScope, routes.cloudron.reboot);
router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs);
// cloudron routes
router.get ('/api/v1/cloudron/config', cloudronScope, routes.cloudron.getConfig);
router.post('/api/v1/cloudron/update', cloudronScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update);
router.post('/api/v1/cloudron/check_for_updates', cloudronScope, routes.user.requireAdmin, routes.cloudron.checkForUpdates);
router.post('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.reboot);
router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.graphs.getGraphs);
// feedback
router.post('/api/v1/cloudron/feedback', usersScope, routes.cloudron.feedback);
// profile api, working off the user behind the provided token
router.get ('/api/v1/profile', profileScope, routes.profile.get);
router.put ('/api/v1/profile', profileScope, routes.profile.update);
router.put ('/api/v1/profile/password', profileScope, routes.user.verifyPassword, routes.profile.changePassword);
router.put ('/api/v1/profile/tutorial', profileScope, routes.profile.setShowTutorial);
router.post('/api/v1/profile', profileScope, routes.profile.update);
router.post('/api/v1/profile/password', profileScope, routes.user.verifyPassword, routes.profile.changePassword);
router.post('/api/v1/profile/tutorial', profileScope, routes.profile.setShowTutorial);
// user routes only for admins
// user routes
router.get ('/api/v1/users', usersScope, routes.user.requireAdmin, routes.user.list);
router.post('/api/v1/users', usersScope, routes.user.requireAdmin, routes.user.create);
router.get ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.get);
router.del ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.user.remove);
router.put ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.update);
router.put ('/api/v1/users/:userId/set_groups', usersScope, routes.user.requireAdmin, routes.user.setGroups);
router.post('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.update);
router.put ('/api/v1/users/:userId/groups', usersScope, routes.user.requireAdmin, routes.user.setGroups);
router.post('/api/v1/users/:userId/invite', usersScope, routes.user.requireAdmin, routes.user.sendInvite);
// Group management
@@ -120,6 +122,14 @@ function initializeExpressSync() {
router.get ('/api/v1/groups/:groupId', usersScope, routes.user.requireAdmin, routes.groups.get);
router.del ('/api/v1/groups/:groupId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.groups.remove);
// Mailbox management
router.get ('/api/v1/mailboxes', usersScope, routes.user.requireAdmin, routes.mailboxes.list);
router.post('/api/v1/mailboxes', usersScope, routes.user.requireAdmin, routes.mailboxes.create);
router.get ('/api/v1/mailboxes/:mailboxId', usersScope, routes.user.requireAdmin, routes.mailboxes.get);
router.del ('/api/v1/mailboxes/:mailboxId', usersScope, routes.user.requireAdmin, routes.mailboxes.remove);
router.put ('/api/v1/mailboxes/:mailboxId/aliases', usersScope, routes.user.requireAdmin, routes.mailboxes.setAliases);
router.get ('/api/v1/mailboxes/:mailboxId/aliases', usersScope, routes.user.requireAdmin, routes.mailboxes.getAliases);
// form based login routes used by oauth2 frame
router.get ('/api/v1/session/login', csrf, routes.oauth2.loginForm);
router.post('/api/v1/session/login', csrf, routes.oauth2.login);
@@ -136,13 +146,15 @@ function initializeExpressSync() {
// oauth2 routes
router.get ('/api/v1/oauth/dialog/authorize', routes.oauth2.authorization);
router.post('/api/v1/oauth/token', routes.oauth2.token);
router.get ('/api/v1/oauth/clients', settingsScope, routes.clients.getAllByUserId);
router.get ('/api/v1/oauth/clients', settingsScope, routes.clients.getAll);
router.post('/api/v1/oauth/clients', routes.developer.enabled, settingsScope, routes.clients.add);
router.get ('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.get);
router.post('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.add);
router.del ('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.del);
router.get ('/api/v1/oauth/clients/:clientId/tokens', settingsScope, routes.clients.getClientTokens);
router.post('/api/v1/oauth/clients/:clientId/tokens', routes.developer.enabled, settingsScope, routes.clients.addClientToken);
router.del ('/api/v1/oauth/clients/:clientId/tokens', settingsScope, routes.clients.delClientTokens);
router.del ('/api/v1/oauth/clients/:clientId/tokens/:tokenId', settingsScope, routes.clients.delToken);
// app routes
router.get ('/api/v1/apps', appsScope, routes.apps.getApps);
@@ -176,6 +188,7 @@ function initializeExpressSync() {
router.post('/api/v1/settings/certificate', settingsScope, routes.user.requireAdmin, routes.settings.setCertificate);
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.user.requireAdmin, routes.settings.setAdminCertificate);
router.get ('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.getTimeZone);
router.post('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.setTimeZone);
// eventlog route
router.get('/api/v1/eventlog', settingsScope, routes.user.requireAdmin, routes.eventlog.get);
@@ -183,9 +196,10 @@ function initializeExpressSync() {
// backup routes
router.get ('/api/v1/backups', settingsScope, routes.user.requireAdmin, routes.backups.get);
router.post('/api/v1/backups', settingsScope, routes.user.requireAdmin, routes.backups.create);
router.get ('/api/v1/backups/:backupId', appsScope, routes.user.requireAdmin, routes.backups.download);
router.post('/api/v1/backups/:backupId/download_url', appsScope, routes.user.requireAdmin, routes.backups.createDownloadUrl);
// disable server timeout. we use the timeout middleware to handle timeouts on a route level
// disable server socket "idle" timeout. we use the timeout middleware to handle timeouts on a route level
// we rely on nginx for timeouts on the TCP level (see client_header_timeout)
httpServer.setTimeout(0);
// upgrade handler
@@ -219,7 +233,7 @@ function initializeSysadminExpressSync() {
var app = express();
var httpServer = http.createServer(app);
var QUERY_LIMIT = '10mb'; // max size for json and urlencoded queries
var QUERY_LIMIT = '1mb'; // max size for json and urlencoded queries
var REQUEST_TIMEOUT = 10000; // timeout for all requests
var json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json
@@ -257,7 +271,7 @@ function start(callback) {
database.initialize,
cloudron.initialize, // keep this here because it reads activation state that others depend on
certificates.installAdminCertificate, // keep this before cron to block heartbeats until cert is ready
addons.initialize, // starts the addons
platform.initialize,
taskmanager.initialize,
mailer.initialize,
cron.initialize,
+8 -2
View File
@@ -51,6 +51,7 @@ var assert = require('assert'),
config = require('./config.js'),
CronJob = require('cron').CronJob,
DatabaseError = require('./databaseerror.js'),
moment = require('moment-timezone'),
paths = require('./paths.js'),
safe = require('safetydance'),
settingsdb = require('./settingsdb.js'),
@@ -62,7 +63,7 @@ var gDefaults = (function () {
result[exports.AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *';
result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles';
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
result[exports.DEVELOPER_MODE_KEY] = false;
result[exports.DEVELOPER_MODE_KEY] = true;
result[exports.DNS_CONFIG_KEY] = { };
result[exports.BACKUP_CONFIG_KEY] = { };
result[exports.TLS_CONFIG_KEY] = { provider: 'caas' };
@@ -132,6 +133,8 @@ function setTimeZone(tz, callback) {
assert.strictEqual(typeof tz, 'string');
assert.strictEqual(typeof callback, 'function');
if (moment.tz.names().indexOf(tz) === -1) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Bad timeZone'));
settingsdb.set(exports.TIME_ZONE_KEY, tz, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
@@ -166,7 +169,10 @@ function setCloudronName(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
if (!name) return callback(new SettingsError(SettingsError.BAD_FIELD));
if (!name) return callback(new SettingsError(SettingsError.BAD_FIELD, 'name is empty'));
// some arbitrary restrictions (for sake of ui layout)
if (name.length > 32) return callback(new SettingsError(SettingsError.BAD_FIELD, 'name cannot exceed 32 characters'));
settingsdb.set(exports.CLOUDRON_NAME_KEY, name, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
+11 -1
View File
@@ -2,7 +2,8 @@
exports = module.exports = {
sudo: sudo,
exec: exec
exec: exec,
execSync: execSync
};
var assert = require('assert'),
@@ -13,6 +14,15 @@ var assert = require('assert'),
var SUDO = '/usr/bin/sudo';
function execSync(tag, cmd, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof cmd, 'string');
debug(cmd);
child_process.execSync(cmd, { stdio: 'inherit' });
if (callback) return callback();
}
function exec(tag, file, args, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof file, 'string');
+3 -4
View File
@@ -8,7 +8,6 @@ exports = module.exports = {
var apps = require('./apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
ClientsError = clients.ClientsError,
config = require('./config.js'),
@@ -38,7 +37,7 @@ function loginLogic(clientId, username, password, callback) {
if (error) return callback(error);
// only allow simple auth clients
if (clientObject.type !== clientdb.TYPE_SIMPLE_AUTH) return callback(new ClientsError(ClientsError.INVALID_CLIENT));
if (clientObject.type !== clients.TYPE_SIMPLE_AUTH) return callback(new ClientsError(ClientsError.INVALID_CLIENT));
var authFunction = (username.indexOf('@') === -1) ? user.verifyWithUsername : user.verifyWithEmail;
authFunction(username, password, function (error, userObject) {
@@ -54,7 +53,7 @@ function loginLogic(clientId, username, password, callback) {
var accessToken = tokendb.generateToken();
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(accessToken, tokendb.PREFIX_USER + userObject.id, clientId, expires, clientObject.scope, function (error) {
tokendb.add(accessToken, userObject.id, clientId, expires, clientObject.scope, function (error) {
if (error) return callback(error);
debug('login: new access token for client %s and user %s: %s', clientId, username, accessToken);
@@ -87,7 +86,7 @@ function login(req, res, next) {
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required'));
loginLogic(req.body.clientId, req.body.username, req.body.password, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(401, 'Unknown client'));
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(401, 'Unknown client'));
if (error && error.reason === ClientsError.INVALID_CLIENT) return next(new HttpError(401, 'Unkown client'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(401, 'Forbidden'));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(401, 'Unkown app'));
+1 -1
View File
@@ -43,7 +43,7 @@ SubdomainError.NOT_FOUND = 'No such domain';
SubdomainError.EXTERNAL_ERROR = 'External error';
SubdomainError.STILL_BUSY = 'Still busy';
SubdomainError.MISSING_CREDENTIALS = 'Missing credentials';
SubdomainError.INTERNAL_ERROR = 'Missing credentials';
SubdomainError.INTERNAL_ERROR = 'Internal error';
SubdomainError.ACCESS_DENIED = 'Access denied';
// choose which subdomain backend we use for test purpose we use route53
+53
View File
@@ -326,4 +326,57 @@ describe('Apps', function () {
});
});
});
describe('configureInstalledApps', function () {
before(function (done) {
async.series([
appdb.update.bind(null, APP_0.id, { installationState: appdb.ISTATE_INSTALLED }),
appdb.update.bind(null, APP_1.id, { installationState: appdb.ISTATE_ERROR }),
appdb.update.bind(null, APP_2.id, { installationState: appdb.ISTATE_INSTALLED })
], done);
});
it('can mark apps for reconfigure', function (done) {
apps.configureInstalledApps(function (error) {
expect(error).to.be(null);
apps.getAll(function (error, apps) {
expect(apps[0].installationState).to.be(appdb.ISTATE_PENDING_CONFIGURE);
expect(apps[0].oldConfig).to.be(null);
expect(apps[1].installationState).to.be(appdb.ISTATE_ERROR);
expect(apps[2].installationState).to.be(appdb.ISTATE_PENDING_CONFIGURE);
expect(apps[2].oldConfig).to.be(null);
done();
});
});
});
});
describe('restoreInstalledApps', function () {
before(function (done) {
async.series([
appdb.update.bind(null, APP_0.id, { installationState: appdb.ISTATE_INSTALLED }),
appdb.update.bind(null, APP_1.id, { installationState: appdb.ISTATE_ERROR }),
appdb.update.bind(null, APP_2.id, { installationState: appdb.ISTATE_INSTALLED })
], done);
});
it('can mark apps for reconfigure', function (done) {
apps.restoreInstalledApps(function (error) {
expect(error).to.be(null);
apps.getAll(function (error, apps) {
expect(apps[0].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
expect(apps[0].oldConfig).to.be(null);
expect(apps[1].installationState).to.be(appdb.ISTATE_ERROR);
expect(apps[2].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
expect(apps[2].oldConfig).to.be(null);
done();
});
});
});
});
});
+1 -1
View File
@@ -31,7 +31,7 @@ var MANIFEST = {
"contactEmail": "support@cloudron.io",
"version": "0.1.0",
"manifestVersion": 1,
"dockerImage": "cloudron/test:8.0.0",
"dockerImage": "cloudron/test:15.0.0",
"healthCheckPath": "/",
"httpPort": 7777,
"tcpPorts": {
+14 -31
View File
@@ -3,9 +3,7 @@
set -eu
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
readonly TEST_IMAGE="cloudron/test:11.0.0"
source ${SOURCE_DIR}/src/INFRA_VERSION
readonly TEST_IMAGE="cloudron/test:15.0.0"
# reset sudo timestamp to avoid wrong success
sudo -k || sudo --reset-timestamp
@@ -35,37 +33,22 @@ for script in "${scripts[@]}"; do
fi
done
# setup_infra requires node to be in path for the root user
if ! test -x /usr/bin/node 2>/dev/null; then
echo "node is not in PATH for the root user. Create a symlink to /usr/bin/node possibly"
exit 1
fi
image_missing=""
if ! docker inspect "${TEST_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${TEST_IMAGE}"
image_missing="true"
fi
images=$(node -e "var i = require('${SOURCE_DIR}/src/infra_version.js'); console.log(Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join('\n'));"; echo $TEST_IMAGE)
if ! docker inspect "${REDIS_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${REDIS_IMAGE}"
image_missing="true"
fi
if ! docker inspect "${MYSQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${MYSQL_IMAGE}"
image_missing="true"
fi
if ! docker inspect "${POSTGRESQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${POSTGRESQL_IMAGE}"
image_missing="true"
fi
if ! docker inspect "${MONGODB_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${MONGODB_IMAGE}"
image_missing="true"
fi
if ! docker inspect "${MAIL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${MAIL_IMAGE}"
image_missing="true"
fi
for image in ${images}; do
if ! docker inspect "${image}" >/dev/null 2>/dev/null; then
echo "docker pull ${image}"
image_missing="true"
fi
done
if [[ "${image_missing}" == "true" ]]; then
echo "Pull above images before running tests"
+141 -19
View File
@@ -16,6 +16,7 @@ var appdb = require('../appdb.js'),
eventlogdb = require('../eventlogdb.js'),
expect = require('expect.js'),
hat = require('hat'),
mailboxdb = require('../mailboxdb.js'),
settingsdb = require('../settingsdb.js'),
tokendb = require('../tokendb.js'),
userdb = require('../userdb.js'),
@@ -85,10 +86,28 @@ describe('database', function () {
userdb.add(USER_2.id, USER_2, done);
});
it('cannot add same user again', function (done) {
userdb.add(USER_0.id, USER_0, function (error) {
it('cannot add user width same email again', function (done) {
var tmp = JSON.parse(JSON.stringify(USER_0));
tmp.id = 'somethingelse';
tmp.username = 'somethingelse';
userdb.add(tmp.id, tmp, function (error) {
expect(error).to.be.ok();
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
expect(error.message).to.equal('email already exists');
done();
});
});
it('cannot add user width same username again', function (done) {
var tmp = JSON.parse(JSON.stringify(USER_0));
tmp.id = 'somethingelse';
tmp.email = 'somethingelse@not.taken';
userdb.add(tmp.id, tmp, function (error) {
expect(error).to.be.ok();
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
expect(error.message).to.equal('username already exists');
done();
});
});
@@ -184,6 +203,24 @@ describe('database', function () {
});
});
it('can update the user with already existing email', function (done) {
userdb.update(USER_0.id, { email: USER_2.email }, function (error) {
expect(error).to.be.ok();
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
expect(error.message).to.equal('email already exists');
done();
});
});
it('can update the user with already existing username', function (done) {
userdb.update(USER_0.id, { username: USER_2.username }, function (error) {
expect(error).to.be.ok();
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
expect(error.message).to.equal('username already exists');
done();
});
});
it('cannot update with null field', function () {
expect(function () {
userdb.update(USER_0.id, { email: null }, function () {});
@@ -329,21 +366,21 @@ describe('database', function () {
describe('token', function () {
var TOKEN_0 = {
accessToken: tokendb.generateToken(),
identifier: tokendb.PREFIX_USER + '0',
identifier: '0',
clientId: 'clientid-0',
expires: Date.now() + 60 * 60000,
scope: '*'
};
var TOKEN_1 = {
accessToken: tokendb.generateToken(),
identifier: tokendb.PREFIX_USER + '1',
identifier: '1',
clientId: 'clientid-1',
expires: Number.MAX_SAFE_INTEGER,
scope: '*'
};
var TOKEN_2 = {
accessToken: tokendb.generateToken(),
identifier: tokendb.PREFIX_USER + '2',
identifier: '2',
clientId: 'clientid-2',
expires: Date.now(),
scope: '*'
@@ -470,16 +507,30 @@ describe('database', function () {
it('delByIdentifierAndClientId succeeds', function (done) {
tokendb.delByIdentifierAndClientId(TOKEN_0.identifier, TOKEN_0.clientId, function (error) {
expect(error).to.be(null);
done();
tokendb.get(TOKEN_0.accessToken, function (error, result) {
expect(error).to.be.a(DatabaseError);
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
expect(result).to.not.be.ok();
done();
});
});
});
it('get of previously deleted token fails', function (done) {
tokendb.get(TOKEN_0.accessToken, function (error, result) {
expect(error).to.be.a(DatabaseError);
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
expect(result).to.not.be.ok();
done();
it('delByClientId succeeds', function (done) {
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) {
expect(error).to.be(null);
tokendb.delByClientId(TOKEN_0.clientId, function (error, result) {
expect(error).to.not.be.ok();
tokendb.get(TOKEN_0.accessToken, function (error, result) {
expect(error).to.be.a(DatabaseError);
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
expect(result).to.not.be.ok();
done();
});
});
});
});
});
@@ -500,7 +551,6 @@ describe('database', function () {
health: null,
accessRestriction: null,
lastBackupId: null,
lastBackupConfig: null,
oldConfig: null,
memoryLimit: 4294967296,
altDomain: null
@@ -520,7 +570,6 @@ describe('database', function () {
health: null,
accessRestriction: { users: [ 'foobar' ] },
lastBackupId: null,
lastBackupConfig: null,
oldConfig: null,
memoryLimit: 0,
altDomain: null
@@ -812,7 +861,7 @@ describe('database', function () {
var CLIENT_0 = {
id: 'cid-0',
appId: 'someappid_0',
type: clientdb.TYPE_OAUTH,
type: 'typeisastring',
clientSecret: 'secret-0',
redirectURI: 'http://foo.bar',
scope: '*'
@@ -821,7 +870,7 @@ describe('database', function () {
var CLIENT_1 = {
id: 'cid-1',
appId: 'someappid_1',
type: clientdb.TYPE_OAUTH,
type: 'typeisastring',
clientSecret: 'secret-',
redirectURI: 'http://foo.bar',
scope: '*'
@@ -883,9 +932,9 @@ describe('database', function () {
clientdb.getAll(function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(Array);
expect(result.length).to.equal(3); // one of them is webadmin
expect(result[0]).to.eql(CLIENT_0);
expect(result[1]).to.eql(CLIENT_1);
expect(result.length).to.equal(5); // three built-in clients
expect(result[3]).to.eql(CLIENT_0);
expect(result[4]).to.eql(CLIENT_1);
done();
});
});
@@ -1123,5 +1172,78 @@ describe('database', function () {
});
});
});
describe('mailboxes', function () {
it('add succeeds', function (done) {
mailboxdb.add('support', function (error, mailbox) {
expect(error).to.be(null);
done();
});
});
it('cannot add dup entry', function (done) {
mailboxdb.add('support', function (error, mailbox) {
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
done();
});
});
it('get succeeds', function (done) {
mailboxdb.get('support', function (error, mailbox) {
expect(error).to.be(null);
expect(mailbox.name).to.be('support');
expect(mailbox.creationTime).to.be.a(Date);
done();
});
});
it('getAll succeeds', function (done) {
mailboxdb.getAll(function (error, results) {
expect(error).to.be(null);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
expect(results[0].name).to.be('support');
done();
});
});
it('can set alias', function (done) {
mailboxdb.setAliases('support2', [ 'support2', 'help' ], function (error) {
expect(error).to.be(null);
done();
});
});
it('can get alias', function (done) {
mailboxdb.getAliases('support2', function (error, results) {
expect(error).to.be(null);
expect(results.length).to.be(2);
expect(results[0]).to.be('help');
expect(results[1]).to.be('support2')
done();
});
});
it('unset aliases', function (done) {
mailboxdb.setAliases('support2', [ ], function (error) {
expect(error).to.be(null);
mailboxdb.getAliases('support2', function (error, results) {
expect(error).to.be(null);
expect(results.length).to.be(0);
done();
});
});
});
it('del succeeds', function (done) {
mailboxdb.del('support', function (error) {
expect(error).to.be(null);
done();
});
});
});
});
+4 -4
View File
@@ -53,28 +53,28 @@ describe('Groups', function () {
it('cannot create group - too small', function (done) {
groups.create('a', function (error) {
expect(error.reason).to.be(GroupError.BAD_NAME);
expect(error.reason).to.be(GroupError.BAD_FIELD);
done();
});
});
it('cannot create group - too big', function (done) {
groups.create(new Array(256).join('a'), function (error) {
expect(error.reason).to.be(GroupError.BAD_NAME);
expect(error.reason).to.be(GroupError.BAD_FIELD);
done();
});
});
it('cannot create group - bad name', function (done) {
groups.create('bad:name', function (error) {
expect(error.reason).to.be(GroupError.BAD_NAME);
expect(error.reason).to.be(GroupError.BAD_FIELD);
done();
});
});
it('cannot create group - reserved', function (done) {
groups.create('users', function (error) {
expect(error.reason).to.be(GroupError.BAD_NAME);
expect(error.reason).to.be(GroupError.BAD_FIELD);
done();
});
});
+2 -2
View File
@@ -30,14 +30,14 @@ describe('janitor', function () {
var TOKEN_0 = {
accessToken: tokendb.generateToken(),
identifier: tokendb.PREFIX_USER + '0',
identifier: '0',
clientId: 'clientid-0',
expires: Date.now() + 60 * 60 * 1000,
scope: '*'
};
var TOKEN_1 = {
accessToken: tokendb.generateToken(),
identifier: tokendb.PREFIX_USER + '1',
identifier: '1',
clientId: 'clientid-1',
expires: Date.now() - 1000,
scope: '*',
+5 -3
View File
@@ -53,7 +53,6 @@ var APP_0 = {
health: null,
accessRestriction: null,
lastBackupId: null,
lastBackupConfig: null,
oldConfig: null,
memoryLimit: 4294967296
};
@@ -139,8 +138,11 @@ function setup(done) {
}
function cleanup(done) {
dockerProxy.close(function () {
database._clear(done);
async.series([
ldapServer.stop,
database._clear
], function () {
dockerProxy.close(function () { done(); }); // some strange error
});
}
+136
View File
@@ -0,0 +1,136 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var database = require('../database.js'),
expect = require('expect.js'),
mailboxes = require('../mailboxes.js'),
MailboxError = mailboxes.MailboxError,
hat = require('hat');
function setup(done) {
// ensure data/config/mount paths
database.initialize(function (error) {
expect(error).to.be(null);
database._clear(done);
});
}
function cleanup(done) {
database._clear(done);
}
var MAILBOX_NAME = 'test';
describe('Mailboxes', function () {
before(setup);
after(cleanup);
it('cannot create mailbox - too small', function (done) {
mailboxes.add('a', function (error) {
expect(error.reason).to.be(MailboxError.BAD_FIELD);
done();
});
});
it('cannot create mailbox - too big', function (done) {
mailboxes.add(new Array(129).join('a'), function (error) {
expect(error.reason).to.be(MailboxError.BAD_FIELD);
done();
});
});
it('cannot create mailbox - bad name', function (done) {
mailboxes.add('bad:name', function (error) {
expect(error.reason).to.be(MailboxError.BAD_FIELD);
done();
});
});
it('cannot create mailbox - reserved', function (done) {
mailboxes.add('no-reply', function (error) {
expect(error.reason).to.be(MailboxError.BAD_FIELD);
done();
});
});
it('can create valid mailbox', function (done) {
mailboxes.add(MAILBOX_NAME, function (error) {
expect(error).to.be(null);
done();
});
});
it('cannot add existing mailbox', function (done) {
mailboxes.add(MAILBOX_NAME, function (error) {
expect(error.reason).to.be(MailboxError.ALREADY_EXISTS);
done();
});
});
it('cannot get invalid mailbox', function (done) {
mailboxes.get('sometrandom', function (error) {
expect(error.reason).to.be(MailboxError.NOT_FOUND);
done();
});
});
it('can get valid mailbox', function (done) {
mailboxes.get(MAILBOX_NAME, function (error, group) {
expect(error).to.be(null);
expect(group.name).to.equal(MAILBOX_NAME);
done();
});
});
it('can set aliases', function (done) {
mailboxes.setAliases(MAILBOX_NAME, [ 'alias1', 'alias2' ], function (error) {
expect(error).to.be(null);
done();
});
});
it('can set subset alias', function (done) {
mailboxes.setAliases(MAILBOX_NAME, [ 'alias1' ], function (error) {
expect(error).to.be(null);
done();
});
});
it('can get aliases', function (done) {
mailboxes.getAliases(MAILBOX_NAME, function (error, aliases) {
expect(error).to.be(null);
expect(aliases[0]).to.be('alias1');
done();
});
});
it('can get aliases from mailbox', function (done) {
mailboxes.get(MAILBOX_NAME, function (error, group) {
expect(error).to.be(null);
expect(group.name).to.equal(MAILBOX_NAME);
expect(group.aliases.length).to.be(1);
expect(group.aliases[0]).to.be('alias1');
done();
});
});
it('cannot set self-referential alias', function (done) {
mailboxes.setAliases(MAILBOX_NAME, [ MAILBOX_NAME ], function (error) {
expect(error.reason).to.be(MailboxError.ALREADY_EXISTS);
done();
});
});
it('cannot delete invalid mailbox', function (done) {
mailboxes.del('random', function (error) {
expect(error.reason).to.be(MailboxError.NOT_FOUND);
done();
});
});
});
+1 -2
View File
@@ -1,4 +1,3 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
@@ -62,7 +61,7 @@ describe('Settings', function () {
it('can get default developer mode', function (done) {
settings.getDeveloperMode(function (error, enabled) {
expect(error).to.be(null);
expect(enabled).to.equal(false);
expect(enabled).to.equal(true);
done();
});
});
+17 -4
View File
@@ -13,8 +13,21 @@ mkdir -p $HOME/.cloudron_test
cd $HOME/.cloudron_test
mkdir -p data/appdata data/box/appicons data/mail data/nginx/cert data/nginx/applications data/collectd/collectd.conf.d data/addons configs data/box/certs data/box/mail/dkim/localhost data/box/mail/dkim/foobar.com
webadmin_scopes="root,profile,users,apps,settings"
webadmin_origin="https://${ADMIN_LOCATION}-localhost"
mysql --user=root --password="" \
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"webadmin\", \"admin\", \"secret-webadmin\", \"${webadmin_origin}\", \"${webadmin_scopes}\")" boxtest
# put cert
openssl req -x509 -newkey rsa:2048 -keyout data/nginx/cert/host.key -out data/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes
webadmin_scopes="cloudron,profile,users,apps,settings"
webadmin_origin="https://${ADMIN_LOCATION}-localhost"
# !!!!!! check clientdb.js clear() to not nuke those entries
echo "Add webadmin api client"
mysql --user=root --password="" \
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"Settings\", \"built-in\", \"secret-webadmin\", \"${webadmin_origin}\", \"${webadmin_scopes}\")" boxtest
echo "Add SDK api client"
mysql --user=root --password="" \
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-sdk\", \"SDK\", \"built-in\", \"secret-sdk\", \"${webadmin_origin}\", \"*,roleSdk\")" boxtest
echo "Add cli api client"
mysql --user=root --password="" \
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-cli\", \"Cloudron Tool\", \"built-in\", \"secret-cli\", \"${webadmin_origin}\", \"*,roleSdk\")" boxtest
+15
View File
@@ -47,5 +47,20 @@ describe('shell', function () {
done();
});
});
it('execSync a valid program', function (done) {
shell.execSync('test', 'ls -l | wc -c');
done();
});
it('execSync throws for invalid program', function (done) {
expect(function () { shell.execSync('test', 'cannotexist') }).to.throwException();
done();
});
it('execSync throws for failed program', function (done) {
expect(function () { shell.execSync('test', 'false'); }).to.throwException();
done();
});
});
+81 -14
View File
@@ -1,4 +1,3 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
@@ -86,7 +85,7 @@ describe('User', function () {
user.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
expect(error.reason).to.equal(UserError.BAD_FIELD);
done();
});
@@ -96,7 +95,7 @@ describe('User', function () {
user.create(USERNAME, 'thisiseightch%$234arslong', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
expect(error.reason).to.equal(UserError.BAD_FIELD);
done();
});
@@ -106,7 +105,7 @@ describe('User', function () {
user.create(USERNAME, 'foobaRASDF%', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
expect(error.reason).to.equal(UserError.BAD_FIELD);
done();
});
@@ -116,7 +115,7 @@ describe('User', function () {
user.create(USERNAME, 'foobaRASDF23423', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
expect(error.reason).to.equal(UserError.BAD_FIELD);
done();
});
@@ -126,17 +125,47 @@ describe('User', function () {
user.create('admin', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_USERNAME);
expect(error.reason).to.equal(UserError.BAD_FIELD);
done();
});
});
it('fails due to reserved username', function (done) {
user.create('AdMiN', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
user.create('Mailer-Daemon', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_USERNAME);
expect(error.reason).to.equal(UserError.BAD_FIELD);
done();
});
});
it('fails due to short username', function (done) {
user.create('Z', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_FIELD);
done();
});
});
it('fails due to long username', function (done) {
user.create(new Array(257).fill('Z').join(''), PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_FIELD);
done();
});
});
it('fails due to reserved pattern', function (done) {
user.create('maybe-app', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UserError.BAD_FIELD);
done();
});
@@ -191,7 +220,7 @@ describe('User', function () {
user.create(USERNAME, '', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).not.to.be.ok();
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
expect(error.reason).to.equal(UserError.BAD_FIELD);
done();
});
@@ -404,7 +433,8 @@ describe('User', function () {
after(cleanupUsers);
it('fails due to unknown userid', function (done) {
user.update(USERNAME, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, AUDIT_SOURCE, function (error) {
var data = { username: USERNAME_NEW, email: EMAIL_NEW, displayName: DISPLAY_NAME_NEW };
user.update(USERNAME, data, AUDIT_SOURCE, function (error) {
expect(error).to.be.a(UserError);
expect(error.reason).to.equal(UserError.NOT_FOUND);
@@ -413,16 +443,19 @@ describe('User', function () {
});
it('fails due to invalid email', function (done) {
user.update(userObject.id, USERNAME_NEW, 'brokenemailaddress', DISPLAY_NAME_NEW, AUDIT_SOURCE, function (error) {
var data = { username: USERNAME_NEW, email: 'brokenemailaddress', displayName: DISPLAY_NAME_NEW };
user.update(userObject.id, data, AUDIT_SOURCE, function (error) {
expect(error).to.be.a(UserError);
expect(error.reason).to.equal(UserError.BAD_EMAIL);
expect(error.reason).to.equal(UserError.BAD_FIELD);
done();
});
});
it('succeeds', function (done) {
user.update(userObject.id, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, AUDIT_SOURCE, function (error) {
var data = { username: USERNAME_NEW, email: EMAIL_NEW, displayName: DISPLAY_NAME_NEW };
user.update(userObject.id, data, AUDIT_SOURCE, function (error) {
expect(error).to.not.be.ok();
user.get(userObject.id, function (error, result) {
@@ -438,7 +471,9 @@ describe('User', function () {
});
it('succeeds with same data', function (done) {
user.update(userObject.id, USERNAME_NEW, EMAIL_NEW, DISPLAY_NAME_NEW, AUDIT_SOURCE, function (error) {
var data = { username: USERNAME_NEW, email: EMAIL_NEW, displayName: DISPLAY_NAME_NEW };
user.update(userObject.id, data, AUDIT_SOURCE, function (error) {
expect(error).to.not.be.ok();
user.get(userObject.id, function (error, result) {
@@ -543,6 +578,19 @@ describe('User', function () {
});
});
describe('count', function () {
before(createOwner);
after(cleanupUsers);
it('succeeds', function (done) {
user.count(function (error, count) {
expect(error).to.not.be.ok();
expect(count).to.be(1);
done();
});
});
});
describe('set password', function () {
before(createOwner);
after(cleanupUsers);
@@ -648,4 +696,23 @@ describe('User', function () {
});
});
});
describe('remove', function () {
before(createOwner);
after(cleanupUsers);
it('fails for unkown user', function (done) {
user.remove('unknown', { }, function (error) {
expect(error.reason).to.be(UserError.NOT_FOUND);
done();
});
});
it('can remove valid user', function (done) {
user.remove(userObject.id, { }, function (error) {
expect(error).to.be(null);
done();
});
});
});
});
+13 -8
View File
@@ -7,20 +7,13 @@ exports = module.exports = {
get: get,
add: add,
del: del,
delByClientId: delByClientId,
getByIdentifier: getByIdentifier,
delByIdentifier: delByIdentifier,
getByIdentifierAndClientId: getByIdentifierAndClientId,
delByIdentifierAndClientId: delByIdentifierAndClientId,
delExpired: delExpired,
TYPE_USER: 'user',
TYPE_DEV: 'developer',
TYPE_APP: 'appliation',
PREFIX_USER: 'user-',
PREFIX_DEV: 'dev-',
PREFIX_APP: 'app-',
_clear: clear
};
@@ -77,6 +70,18 @@ function del(accessToken, callback) {
});
}
function delByClientId(clientId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM tokens WHERE clientId = ?', [ clientId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null);
});
}
function getByIdentifier(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
+71 -41
View File
@@ -5,6 +5,7 @@ exports = module.exports = {
list: listUsers,
create: createUser,
count: count,
verify: verify,
verifyWithUsername: verifyWithUsername,
verifyWithEmail: verifyWithEmail,
@@ -23,14 +24,16 @@ exports = module.exports = {
};
var assert = require('assert'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
crypto = require('crypto'),
debug = require('debug')('box:user'),
DatabaseError = require('./databaseerror.js'),
eventlog = require('./eventlog.js'),
groups = require('./groups.js'),
GroupError = groups.GroupError,
hat = require('hat'),
mailer = require('./mailer.js'),
mailboxes = require('./mailboxes.js'),
tokendb = require('./tokendb.js'),
userdb = require('./userdb.js'),
util = require('util'),
@@ -43,6 +46,8 @@ var CRYPTO_SALT_SIZE = 64; // 512-bit salt
var CRYPTO_ITERATIONS = 10000; // iterations
var CRYPTO_KEY_LENGTH = 512; // bits
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function UserError(reason, errorOrMessage) {
@@ -69,9 +74,6 @@ UserError.ALREADY_EXISTS = 'Already Exists';
UserError.NOT_FOUND = 'Not Found';
UserError.WRONG_PASSWORD = 'Wrong User or Password';
UserError.BAD_FIELD = 'Bad field';
UserError.BAD_USERNAME = 'Bad username';
UserError.BAD_EMAIL = 'Bad email';
UserError.BAD_PASSWORD = 'Bad password';
UserError.BAD_TOKEN = 'Bad token';
function validateUsername(username) {
@@ -83,10 +85,16 @@ function validateUsername(username) {
// allow empty usernames
if (username === '') return null;
if (username.length <= 2) return new UserError(UserError.BAD_USERNAME, 'Username must be atleast 3 chars');
if (username.length > 256) return new UserError(UserError.BAD_USERNAME, 'Username too long');
if (username.length <= 1) return new UserError(UserError.BAD_FIELD, 'Username must be atleast 2 chars');
if (username.length > 256) return new UserError(UserError.BAD_FIELD, 'Username too long');
if (RESERVED_USERNAMES.indexOf(username) !== -1) return new UserError(UserError.BAD_USERNAME, 'Username is reserved');
if (RESERVED_USERNAMES.indexOf(username) !== -1) return new UserError(UserError.BAD_FIELD, 'Username is reserved');
// +/- can be tricky in emails
if (/[^a-zA-Z0-9.]/.test(username)) return new UserError(UserError.BAD_FIELD, 'Username can only contain alphanumerals and dot');
// app emails are sent using the .app suffix
if (username.indexOf('.app') !== -1) return new UserError(UserError.BAD_FIELD, 'Username pattern is reserved for apps');
return null;
}
@@ -94,7 +102,7 @@ function validateUsername(username) {
function validateEmail(email) {
assert.strictEqual(typeof email, 'string');
if (!validator.isEmail(email)) return new UserError(UserError.BAD_EMAIL, 'Invalid email');
if (!validator.isEmail(email)) return new UserError(UserError.BAD_FIELD, 'Invalid email');
return null;
}
@@ -137,7 +145,7 @@ function createUser(username, password, email, displayName, auditSource, options
if (error) return callback(error);
error = validatePassword(password);
if (error) return callback(new UserError(UserError.BAD_PASSWORD, error.message));
if (error) return callback(new UserError(UserError.BAD_FIELD, error.message));
error = validateEmail(email);
if (error) return callback(error);
@@ -166,10 +174,11 @@ function createUser(username, password, email, displayName, auditSource, options
};
userdb.add(user.id, user, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.ALREADY_EXISTS));
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.ALREADY_EXISTS, error.message));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_ADD, auditSource, { userId: user.id, email: user.email });
if (username) mailboxes.add(username, NOOP_CALLBACK);
callback(null, user);
@@ -243,35 +252,48 @@ function verifyWithEmail(email, password, callback) {
});
}
function removeUser(user, auditSource, callback) {
assert.strictEqual(typeof user, 'object');
function removeUser(userId, auditSource, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
userdb.del(user.id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
getUser(userId, function (error, user) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_USER_REMOVE, auditSource, { userId: user.id });
userdb.del(userId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
callback(null);
eventlog.add(eventlog.ACTION_USER_REMOVE, auditSource, { userId: userId });
if (user.username) mailboxes.del(user.username, NOOP_CALLBACK);
mailer.userRemoved(user);
callback(null);
mailer.userRemoved(user);
});
});
}
function listUsers(callback) {
assert.strictEqual(typeof callback, 'function');
userdb.getAllWithGroupIds(function (error, result) {
userdb.getAllWithGroupIds(function (error, results) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
var allUsers = result.map(function (obj) {
var u = _.pick(obj, 'id', 'username', 'email', 'displayName', 'groupIds');
u.admin = u.groupIds.indexOf(groups.ADMIN_GROUP_ID) !== -1;
return u;
results.forEach(function (result) {
result.admin = result.groupIds.indexOf(groups.ADMIN_GROUP_ID) !== -1;
});
return callback(null, allUsers);
return callback(null, results);
});
}
function count(callback) {
assert.strictEqual(typeof callback, 'function');
userdb.count(function (error, count) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
callback(null, count);
});
}
@@ -287,6 +309,7 @@ function getUser(userId, callback) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
result.groupIds = groupIds;
result.admin = groupIds.indexOf(groups.ADMIN_GROUP_ID) !== -1;
return callback(null, result);
});
@@ -308,29 +331,36 @@ function getByResetToken(resetToken, callback) {
});
}
function updateUser(userId, username, email, displayName, auditSource, callback) {
function updateUser(userId, data, auditSource, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
username = username.toLowerCase();
email = email.toLowerCase();
var error;
data = _.pick(data, 'email', 'displayName', 'username');
var error = validateUsername(username);
if (error) return callback(error);
if (_.isEmpty(data)) return callback();
error = validateEmail(email);
if (error) return callback(error);
if (data.username) {
data.username = data.username.toLowerCase();
error = validateUsername(data.username);
if (error) return callback(error);
}
userdb.update(userId, { username: username, email: email, displayName: displayName }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.ALREADY_EXISTS, error));
if (data.email) {
data.email = data.email.toLowerCase();
error = validateEmail(data.email);
if (error) return callback(error);
}
userdb.update(userId, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.ALREADY_EXISTS, error.message));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, error));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_UPDATE, auditSource, { userId: userId });
if (data.username) mailboxes.add(data.username, NOOP_CALLBACK); // TODO: do this only when username actually changes
callback(null);
});
@@ -406,7 +436,7 @@ function setPassword(userId, newPassword, callback) {
assert.strictEqual(typeof callback, 'function');
var error = validatePassword(newPassword);
if (error) return callback(new UserError(UserError.BAD_PASSWORD, error.message));
if (error) return callback(new UserError(UserError.BAD_FIELD, error.message));
userdb.get(userId, function (error, user) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
@@ -425,13 +455,13 @@ function setPassword(userId, newPassword, callback) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
// Also generate a token so the new user can get logged in immediately
clientdb.getByAppIdAndType('webadmin', clientdb.TYPE_ADMIN, function (error, result) {
clients.get('cid-webadmin', function (error, result) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
var token = tokendb.generateToken();
var expiresAt = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(token, tokendb.PREFIX_USER + user.id, result.id, expiresAt, '*', function (error) {
tokendb.add(token, user.id, result.id, expiresAt, '*', function (error) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
callback(null, { token: token, expiresAt: expiresAt });
@@ -451,11 +481,11 @@ function createOwner(username, password, email, displayName, auditSource, callba
assert.strictEqual(typeof callback, 'function');
// This is only not allowed for the owner
if (username === '') return callback(new UserError(UserError.BAD_USERNAME, 'Username cannot be empty'));
if (username === '') return callback(new UserError(UserError.BAD_FIELD, 'Username cannot be empty'));
userdb.count(function (error, count) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
if (count !== 0) return callback(new UserError(UserError.ALREADY_EXISTS));
if (count !== 0) return callback(new UserError(UserError.ALREADY_EXISTS, 'Owner already exists'));
createUser(username, password, email, displayName, auditSource, { owner: true }, function (error, user) {
if (error) return callback(error);
+20 -2
View File
@@ -143,7 +143,16 @@ function add(userId, user, callback) {
var data = [ userId, user.username || null, user.password, user.email, user.salt, user.createdAt, user.modifiedAt, user.resetToken, user.displayName, user.showTutorial ];
database.query('INSERT INTO users (id, username, password, email, salt, createdAt, modifiedAt, resetToken, displayName, showTutorial) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', data, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error && error.code === 'ER_DUP_ENTRY') {
var msg = error.message;
if (error.message.indexOf('users_email') !== -1) {
msg = 'email already exists';
} else if (error.message.indexOf('users_username') !== -1) {
msg = 'username already exists';
}
return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, msg));
}
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
@@ -216,7 +225,16 @@ function update(userId, user, callback) {
args.push(userId);
database.query('UPDATE users SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error && error.code === 'ER_DUP_ENTRY') {
var msg = error.message;
if (error.message.indexOf('users_email') !== -1) {
msg = 'email already exists';
} else if (error.message.indexOf('users_username') !== -1) {
msg = 'username already exists';
}
return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, msg));
}
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
+5 -3
View File
@@ -4,6 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
<title> Cloudron </title>
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
@@ -173,7 +174,7 @@
<span class="icon-bar"></span>
</button>
<a class="navbar-brand navbar-brand-icon" href="#/"><img ng-src="{{ client.avatar }}" width="40" height="40"/></a>
<a class="navbar-brand" href="#/">Cloudron</a>
<a class="navbar-brand" href="#/">{{ config.cloudronName || 'Cloudron' }}</a>
</div>
<!-- /.navbar-header -->
@@ -201,6 +202,7 @@
<li ng-show="user.admin"><a href="#/activity"><i class="fa fa-list-alt fa-fw"></i> Activity</a></li>
<li><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
<li ng-show="user.admin && config.isCustomDomain"><a href="#/certs"><i class="fa fa-certificate fa-fw"></i> DNS & Certs</a></li>
<li ng-show="user.admin"><a href="#/tokens"><i class="fa fa-key fa-fw"></i> API Access</a></li>
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
<li class="divider"></li>
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li>
@@ -217,14 +219,14 @@
</div>
</div>
<div class="upgrade hide">
<!-- <div class="upgrade hide">
<div class="content">
<h4>Your Cloudron trial ends soon</h4>
<p>To keep your Cloudron, just <a href="https://cloudron.io/console.html#/billing" target="_blank">setup a payment method at cloudron.io</a> or <a href="mailto: support@cloudron.io">send us an email</a>.</p>
</div>
<div class="trigger">Want to keep your Cloudron?</div>
</div>
</div> -->
<!-- Footer -->
<footer class="text-center">
+90 -10
View File
@@ -245,8 +245,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
Client.prototype.installApp = function (id, manifest, title, config, callback) {
var that = this;
var data = {
appStoreId: id,
manifest: manifest,
appStoreId: id + '@' + manifest.version,
location: config.location,
portBindings: config.portBindings,
accessRestriction: config.accessRestriction,
@@ -270,8 +269,8 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.restoreApp = function (appId, password, callback) {
var data = { password: password };
Client.prototype.restoreApp = function (appId, backupId, password, callback) {
var data = { password: password, backupId: backupId };
$http.post(client.apiOrigin + '/api/v1/apps/' + appId + '/restore', data).success(function (data, status) {
if (status !== 202) return callback(new ClientError(status, data));
callback(null);
@@ -306,7 +305,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
};
Client.prototype.updateApp = function (id, manifest, portBindings, password, callback) {
$http.post(client.apiOrigin + '/api/v1/apps/' + id + '/update', { manifest: manifest, password: password, portBindings: portBindings }).success(function (data, status) {
$http.post(client.apiOrigin + '/api/v1/apps/' + id + '/update', { appStoreId: manifest.id + '@' + manifest.version, password: password, portBindings: portBindings }).success(function (data, status) {
if (status !== 202) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
@@ -417,8 +416,15 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.getMailboxes = function (callback) {
$http.get(client.apiOrigin + '/api/v1/mailboxes').success(function (data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data.mailboxes);
}).error(defaultErrorHandler(callback));
};
Client.prototype.setGroups = function (userId, groupIds, callback) {
$http.put(client.apiOrigin + '/api/v1/users/' + userId + '/set_groups', { groupIds: groupIds }).success(function (data, status) {
$http.put(client.apiOrigin + '/api/v1/users/' + userId + '/groups', { groupIds: groupIds }).success(function (data, status) {
if (status !== 204) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
@@ -523,6 +529,40 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.createOAuthClient = function (appId, scope, redirectURI, callback) {
var data = {
appId: appId,
scope: scope,
redirectURI: redirectURI
};
$http.post(client.apiOrigin + '/api/v1/oauth/clients', data).success(function(data, status) {
if (status !== 201 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data.clients);
}).error(defaultErrorHandler(callback));
};
Client.prototype.delOAuthClient = function (id, callback) {
$http.delete(client.apiOrigin + '/api/v1/oauth/clients/' + id).success(function(data, status) {
if (status !== 204) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.createTokenByClientId = function (id, expiresAt, callback) {
$http.post(client.apiOrigin + '/api/v1/oauth/clients/' + id + '/tokens?expiresAt=' + expiresAt).success(function(data, status) {
if (status !== 201) return callback(new ClientError(status, data));
callback(null, data.token);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getTokensByClientId = function (id, callback) {
$http.get(client.apiOrigin + '/api/v1/oauth/clients/' + id + '/tokens').success(function(data, status) {
if (status !== 200) return callback(new ClientError(status, data));
callback(null, data.tokens);
}).error(defaultErrorHandler(callback));
};
Client.prototype.delTokensByClientId = function (id, callback) {
$http.delete(client.apiOrigin + '/api/v1/oauth/clients/' + id + '/tokens').success(function(data, status) {
if (status !== 204) return callback(new ClientError(status, data));
@@ -530,6 +570,13 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.delToken = function (clientId, tokenId, callback) {
$http.delete(client.apiOrigin + '/api/v1/oauth/clients/' + clientId + '/tokens/' + tokenId).success(function(data, status) {
if (status !== 204) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.update = function (password, callback) {
$http.post(client.apiOrigin + '/api/v1/cloudron/update', { password: password }).success(function(data, status) {
if (status !== 202 || typeof data !== 'object') return callback(new ClientError(status, data));
@@ -593,6 +640,39 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.createMailbox = function (name, callback) {
var data = {
name: name
};
$http.post(client.apiOrigin + '/api/v1/mailboxes', data).success(function(data, status) {
if (status !== 201 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data);
}).error(defaultErrorHandler(callback));
};
Client.prototype.removeMailbox = function (name, callback) {
var data = {
name: name
};
$http({ method: 'DELETE', url: client.apiOrigin + '/api/v1/mailboxes/' + name, data: data, headers: { 'Content-Type': 'application/json' }}).success(function(data, status) {
if (status !== 204) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.setAliases = function (name, aliases, callback) {
var data = {
aliases: aliases
};
$http.put(client.apiOrigin + '/api/v1/mailboxes/' + name + '/aliases', data).success(function(data, status) {
if (status !== 200) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.createUser = function (username, email, displayName, sendInvite, callback) {
var data = {
username: username,
@@ -613,7 +693,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
displayName: user.displayName
};
$http.put(client.apiOrigin + '/api/v1/users/' + user.id, data).success(function(data, status) {
$http.post(client.apiOrigin + '/api/v1/users/' + user.id, data).success(function(data, status) {
if (status !== 204) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
@@ -636,7 +716,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
newPassword: newPassword
};
$http.put(client.apiOrigin + '/api/v1/profile/password', data).success(function(data, status) {
$http.post(client.apiOrigin + '/api/v1/profile/password', data).success(function(data, status) {
if (status !== 204) return callback(new ClientError(status, data));
callback(null, data);
}).error(defaultErrorHandler(callback));
@@ -782,11 +862,11 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
Client.prototype.setShowTutorial = function (show, callback) {
var data = { showTutorial: show };
$http.put(client.apiOrigin + '/api/v1/profile/tutorial', data).success(function (data, status) {
$http.post(client.apiOrigin + '/api/v1/profile/tutorial', data).success(function (data, status) {
if (status !== 204) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
}
};
client = new Client();
return client;
+89
View File
@@ -54,6 +54,9 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/support', {
controller: 'SupportController',
templateUrl: 'views/support.html'
}).when('/tokens', {
controller: 'TokensController',
templateUrl: 'views/tokens.html'
}).otherwise({ redirectTo: '/'});
}]);
@@ -94,6 +97,12 @@ app.filter('installSuccess', function () {
};
});
app.filter('activeOAuthClients', function () {
return function (clients, user) {
return clients.filter(function (c) { return user.admin || (c.activeTokens && c.activeTokens.length > 0); });
};
});
app.filter('installationActive', function () {
return function(app) {
if (app.installationState === ISTATES.ERROR) return false;
@@ -307,3 +316,83 @@ app.directive('ngClickSelect', function () {
}
};
});
// https://codepen.io/webmatze/pen/isuHh
app.directive('tagInput', function () {
return {
restrict: 'E',
scope: {
inputTags: '=taglist'
},
link: function ($scope, element, attrs) {
$scope.defaultWidth = 200;
$scope.tagText = ''; // current tag being edited
$scope.placeholder = attrs.placeholder;
$scope.tagArray = function () {
if ($scope.inputTags === undefined) {
return [];
}
return $scope.inputTags.split(',').filter(function (tag) {
return tag !== '';
});
};
$scope.addTag = function () {
var tagArray;
if ($scope.tagText.length === 0) {
return;
}
tagArray = $scope.tagArray();
tagArray.push($scope.tagText);
$scope.inputTags = tagArray.join(',');
return $scope.tagText = '';
};
$scope.deleteTag = function (key) {
var tagArray;
tagArray = $scope.tagArray();
if (tagArray.length > 0 && $scope.tagText.length === 0 && key === undefined) {
tagArray.pop();
} else {
if (key !== undefined) {
tagArray.splice(key, 1);
}
}
return $scope.inputTags = tagArray.join(',');
};
$scope.$watch('tagText', function (newVal, oldVal) {
var tempEl;
if (!(newVal === oldVal && newVal === undefined)) {
tempEl = $('<span>' + newVal + '</span>').appendTo('body');
$scope.inputWidth = tempEl.width() + 5;
if ($scope.inputWidth < $scope.defaultWidth) {
$scope.inputWidth = $scope.defaultWidth;
}
return tempEl.remove();
}
});
element.bind('keydown', function (e) {
var key = e.which;
if (key === 9 || key === 13) {
e.preventDefault();
}
if (key === 8) {
return $scope.$apply('deleteTag()');
}
});
element.bind('keyup', function (e) {
var key = e.which;
if (key === 9 || key === 13 || key === 188) {
e.preventDefault();
return $scope.$apply('addTag()');
}
});
},
template:
'<div class="tag-input-container">' +
'<div class="input-tag" data-ng-repeat="tag in tagArray()">' +
'{{tag}}' +
'<div class="delete-tag" data-ng-click="deleteTag($index)">&times;</div>' +
'</div>' +
'<input type="text" data-ng-model="tagText" ng-blur="addTag()" placeholder="{{placeholder}}"/>' +
'</div>'
};
});
+17 -13
View File
@@ -188,26 +188,30 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
// wait till the view has loaded until showing a modal dialog
Client.onConfig(function (config) {
if (!config.billing) {
setTimeout(function () {
$('.upgrade')[0].classList.remove('hide');
// if (!config.billing) {
// setTimeout(function () {
// $('.upgrade')[0].classList.remove('hide');
$('.upgrade .trigger').hover(function () {
$('.upgrade .content')[0].classList.add('active');
$('.upgrade .trigger')[0].classList.add('active');
});
// $('.upgrade .trigger').hover(function () {
// $('.upgrade .content')[0].classList.add('active');
// $('.upgrade .trigger')[0].classList.add('active');
// });
$('.upgrade').hover(function () {}, function () {
$('.upgrade .content')[0].classList.remove('active');
$('.upgrade .trigger')[0].classList.remove('active');
});
}, 2000);
}
// $('.upgrade').hover(function () {}, function () {
// $('.upgrade .content')[0].classList.remove('active');
// $('.upgrade .trigger')[0].classList.remove('active');
// });
// }, 2000);
// }
// check if we are actually updating
if (config.progress.update && config.progress.update.percent !== -1) {
window.location.href = '/update.html';
}
if (config.cloudronName) {
document.title = config.cloudronName;
}
});
+1 -1
View File
@@ -225,7 +225,7 @@ app.controller('FinishController', ['$scope', '$location', 'Wizard', 'Client', f
Client.createAdmin(Wizard.username, Wizard.password, Wizard.email, Wizard.displayName, Wizard.setupToken, function (error) {
if (error) {
console.error('Internal error', error);
window.location.href = '/error.html';
$location.path('/step2').search('error', error.message);
return;
}
+2
View File
@@ -6,6 +6,8 @@
<title> Cloudron </title>
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet" type="text/css">
+54 -1
View File
@@ -137,6 +137,10 @@ html {
}
}
.panel-body {
padding: 15px 0;
}
// ----------------------------
// Apps view
// ----------------------------
@@ -369,10 +373,16 @@ html {
margin-bottom: 0;
}
.section-header {
max-width: 720px;
margin: 0 auto;
}
.card {
background-color: white;
max-width: 600px;
max-width: 720px;
margin: 0 auto;
margin-bottom: 15px;
padding: 10px 15px;
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
}
@@ -1044,3 +1054,46 @@ $graphs-success-alt: lighten(#27CE65, 20%);
width: 200px;
}
}
// ----------------------------
// Tag Input
// ----------------------------
// https://codepen.io/webmatze/pen/isuHh
.tag-input-container {
input {
display: inline-block;
float: left;
height: 18px;
padding: 0px;
font-size: 14px;
line-height: 18px;
color: black;
border: 0px;
margin: 1px;
&:focus {
outline: 0;
box-shadow: 0px;
}
}
.input-tag {
padding: 2px 4px;
line-height: 12px;
font-size: 11px;
background-color: #e3eaf6;
display: inline-block;
float: left;
border-radius: 2px;
margin: 2px 5px 2px 0px;
border: 1px solid #a9b6d2;
.delete-tag {
display: inline-block;
font-size: 12px;
cursor: pointer;
padding: 0px 2px;
&:hover {
background-color: #96b4d2;
}
}
}
}
+17 -26
View File
@@ -101,7 +101,7 @@
<br/>
<div style="max-width: 600px; margin: 0 auto;">
<div class="section-header">
<div class="text-left">
<h1>Account</h1>
</div>
@@ -140,39 +140,30 @@
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;">
<br/>
<div class="section-header">
<div class="text-left">
<h3>Application Access</h3>
<h3>Sessions</h3>
</div>
</div>
<!-- we will always at least have the webadmin token here, so activeClients always will have one entry with at least one token -->
<div class="card" ng-repeat="client in activeClients" style="margin-bottom: 15px;" ng-hide="client.tokenCount === 0">
<div class="card">
<div class="grid-item-top">
<div class="row">
<div class="col-xs-12">
<h4 class="text-muted">{{client.name}} on {{client.location}}{{ config.isCustomDomain ? '.' : '-' }}{{config.fqdn}}</h4>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="row">
<div class="col-xs-12">
You logged in <b>{{ client.tokenCount }}</b> times to this application.
<button class="btn btn-xs btn-danger pull-right" ng-click="removeAccessTokens(client)" ng-disabled="!client.tokenCount || client.busy"><i class="fa fa-spinner fa-pulse" ng-show="client.busy"></i> Remove access</button>
<br/>
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse{{client.id}}">Advanced</a>
<div id="collapse{{client.id}}" class="panel-collapse collapse">
<div class="panel-body">
<h4 class="text-muted">Credentials</h4>
<p>Permissions: <b>{{ client.scope }}</b></p>
<p>Client ID: <b>{{ client.id }}</b></p>
<p ng-show="client.clientSecret">Client Secret: <b>{{ client.clientSecret }}</b></p>
</div>
</div>
</div>
</div>
<p>You are logged into {{ activeClients.length + 1 }} app(s), including this session.</p>
<span ng-show="activeTokenCount > 1">
<hr/>
<h4>Active Applications:</h4>
<p ng-repeat="client in activeClients"><b>{{ client.name }} - {{client.activeTokens.length}} time(s)</b></p>
<hr/>
</span>
<button class="btn btn-outline btn-xs btn-danger pull-right" ng-click="revokeTokens()">Logout From All</button>
</div>
</div>
</div>
</div>
<!-- Offset the footer -->
<br/><br/>
+57 -22
View File
@@ -1,11 +1,12 @@
'use strict';
angular.module('Application').controller('AccountController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
angular.module('Application').controller('AccountController', ['$scope', 'Client', function ($scope, Client) {
$scope.user = Client.getUserInfo();
$scope.config = Client.getConfig();
$scope.activeTokens = 0;
$scope.activeClients = [];
$scope.tokenInUse = null;
$scope.webadminClient = {};
$scope.passwordchange = {
busy: false,
@@ -156,23 +157,6 @@ angular.module('Application').controller('AccountController', ['$scope', '$locat
}
};
$scope.removeAccessTokens = function (client) {
client.busy = true;
Client.delTokensByClientId(client.id, function (error) {
if (error) console.error(error);
client.busy = false;
// update the list
Client.getOAuthClients(function (error, activeClients) {
if (error) return console.error(error);
$scope.activeClients = activeClients;
});
});
};
$scope.showTutorial = function () {
Client.setShowTutorial(true, function (error) {
if (error) return console.error(error);
@@ -180,13 +164,64 @@ angular.module('Application').controller('AccountController', ['$scope', '$locat
});
};
Client.onReady(function () {
$scope.tokenInUse = Client._token;
// poor man's async
function asyncForEach(items, handler, callback) {
var cur = 0;
if (items.length === 0) return callback();
(function iterator() {
handler(items[cur], function () {
if (cur >= items.length-1) return callback();
++cur;
iterator();
});
})();
}
function revokeTokensByClient(client, callback) {
Client.delTokensByClientId(client.id, function (error) {
if (error) console.error(error);
callback();
});
}
$scope.revokeTokens = function () {
asyncForEach($scope.activeClients, revokeTokensByClient, function () {
// now kill this session if exists
if (!$scope.webadminClient || !$scope.webadminClient.id) return;
revokeTokensByClient($scope.webadminClient, function () {
// we should be logged out by now
});
});
};
function refreshClientTokens(client, callback) {
Client.getTokensByClientId(client.id, function (error, result) {
if (error) console.error(error);
client.activeTokens = result || [];
callback();
});
}
Client.onReady(function () {
Client.getOAuthClients(function (error, activeClients) {
if (error) return console.error(error);
$scope.activeClients = activeClients;
asyncForEach(activeClients, refreshClientTokens, function () {
activeClients = activeClients.filter(function (c) { return c.activeTokens.length > 0; });
$scope.activeClients = activeClients.filter(function (c) { return c.id !== 'cid-sdk' && c.id !== 'cid-webadmin'; });
$scope.webadminClient = activeClients.filter(function (c) { return c.id === 'cid-webadmin'; })[0];
$scope.activeTokenCount = $scope.activeClients.reduce(function (prev, cur) { return prev + cur.activeTokens.length; }, 0);
$scope.activeTokenCount += $scope.webadminClient ? $scope.webadminClient.activeTokens.length : 0;
});
});
});
+3
View File
@@ -45,3 +45,6 @@
</div>
</div>
</div>
<!-- Offset the footer -->
<br/><br/>
+1 -1
View File
@@ -274,7 +274,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appRestore.busy = true;
$scope.appRestore.error.password = null;
Client.restoreApp($scope.appRestore.app.id, $scope.appRestore.password, function (error) {
Client.restoreApp($scope.appRestore.app.id, $scope.appRestore.app.lastBackupId, $scope.appRestore.password, function (error) {
if (error && error.statusCode === 403) {
$scope.appRestore.password = '';
$scope.appRestore.error.password = true;
+6 -3
View File
@@ -1,10 +1,10 @@
<div style="max-width: 600px; margin: 0 auto;">
<div class="section-header">
<div class="text-left">
<h1>DNS & Certs</h1>
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;">
<div class="section-header">
<div class="text-left">
<h3>DNS Credentials</h3>
</div>
@@ -53,7 +53,7 @@
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;">
<div class="section-header">
<div class="text-left">
<h3>SSL Certificates</h3>
</div>
@@ -125,3 +125,6 @@
</div>
</div>
</div>
<br/>
<br/>
+9 -9
View File
@@ -77,13 +77,13 @@
<br/>
<div style="max-width: 600px; margin: 0 auto;">
<div class="section-header">
<div class="text-left">
<h1>Settings</h1>
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="section-header" ng-show="user.admin">
<div class="text-left">
<h3>About</h3>
</div>
@@ -115,7 +115,7 @@
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="section-header" ng-show="user.admin">
<div class="text-left">
<h3>Backups</h3>
</div>
@@ -145,19 +145,19 @@
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="section-header" ng-show="user.admin">
<div class="text-left">
<h3>CLI</h3>
<h3>API</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin">
<div class="row">
<div class="col-xs-12">
Enabling this will allow the <a href="https://cloudron.io/references/cli.html" target="_blank">CLI tool</a> to control this Cloudron. The CLI tool can be used to install, configure, inspect and backup applications.
The Cloudron <a href="https://cloudron.io/references/api.html" target="_blank">REST API</a> can be used to manage all aspects of the Cloudron like adding users and installing apps.
<br/>
<br/>
If you are a developer, please see the <a href="{{ config.webServerOrigin }}/documentation.html" target="_blank">docs</a>.
You can develop apps for the Cloudron using the <a href="https://cloudron.io/references/cli.html" target="_blank">CLI tool</a>. See the <a href="{{ config.webServerOrigin }}/documentation.html" target="_blank">docs</a> for more information.
</div>
</div>
<br/>
@@ -172,5 +172,5 @@
</div>
</div>
<br/>
<br/>
<!-- Offset the footer -->
<br/><br/>
+167
View File
@@ -0,0 +1,167 @@
<!-- Modal add client -->
<div class="modal fade" id="clientAddModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Add API Client</h4>
</div>
<div class="modal-body">
<form name="clientAddForm" role="form" novalidate ng-submit="clientAdd.submit()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.name.$dirty && clientAddForm.name.$invalid) || (!clientAddForm.name.$dirty && clientAdd.error.name) }">
<label class="control-label">Name</label>
<div class="control-label" ng-show="(!clientAddForm.name.$dirty && clientAdd.error.name) || (clientAddForm.name.$dirty && clientAddForm.name.$invalid)">
<small ng-show="clientAddForm.name.$error.required">A name is required</small>
<small ng-show="!clientAddForm.name.$dirty && clientAdd.error.name">{{ clientAdd.error.name }}</small>
</div>
<input type="text" class="form-control" ng-model="clientAdd.name" name="name" required autofocus>
</div>
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.scope.$dirty && clientAddForm.scope.$invalid) || (!clientAddForm.scope.$dirty && clientAdd.error.scope) }">
<label class="control-label">Scope</label>
<div class="control-label" ng-show="(!clientAddForm.scope.$dirty && clientAdd.error.scope) || (clientAddForm.scope.$dirty && clientAddForm.scope.$invalid)">
<small ng-show="clientAddForm.scope.$error.required">A scope is required</small>
<small ng-show="!clientAddForm.scope.$dirty && clientAdd.error.scope">{{ clientAdd.error.scope }}</small>
</div>
<input type="text" class="form-control" ng-model="clientAdd.scope" name="scope" id="clientAddScope" placeholder="Specify any number of scope separated by a comma ','" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.redirectURI.$dirty && clientAddForm.redirectURI.$invalid) || (!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI) }">
<label class="control-label">Redirect URI</label>
<div class="control-label" ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">
<small ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">{{ clientAdd.error.redirectURI }}</small>
</div>
<input type="text" class="form-control" ng-model="clientAdd.redirectURI" name="redirectURI" id="clientAddRedirectURI" placeholder="Only required if OAuth logins are used">
</div>
<input class="hide" type="submit" ng-disabled="clientAddForm.$invalid || clientAdd.busy"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" ng-click="clientAdd.submit()" ng-disabled="clientAddForm.$invalid || clientAdd.busy"><i class="fa fa-spinner fa-pulse" ng-show="clientAdd.busy"></i> Add API Client</button>
</div>
</div>
</div>
</div>
<!-- Modal remove client -->
<div class="modal fade" id="clientRemoveModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Remove API Client</h4>
</div>
<div class="modal-body">
<p>
Removing client <b>{{ clientRemove.client.appId }}</b> will also remove all access from scripts and apps using those credentials.
You may want to consult the other Cloudron admins first.
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" ng-click="clientRemove.submit()" ng-disabled="clientRemove.busy"><i class="fa fa-spinner fa-pulse" ng-show="clientRemove.busy"></i> Remove API Client</button>
</div>
</div>
</div>
</div>
<!-- Modal add token -->
<div class="modal fade" id="tokenAddModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">New token created</h4>
</div>
<div class="modal-body">
<p><b ng-click-select>{{ tokenAdd.token.accessToken }}</b></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Done</button>
</div>
</div>
</div>
</div>
<br/>
<div class="section-header">
<div class="text-left">
<h3>Personal access tokens <button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="tokenAdd.show(apiClient)"><i class="fa fa-plus"></i> New Token</button> </h3>
</div>
</div>
<!-- we will always at least have the webadmin token here, so activeClients always will have one entry with at least one token -->
<div class="card">
<div class="grid-item-top">
<div class="row">
<div class="col-xs-12">
<p>These tokens can be used to access the <a href="https://cloudron.io/references/api.html" target="_blank">Cloudron API</a>. They have the <b>admin</b> <a href="https://cloudron.io/references/api.html#scopes" target="_blank">scope</a> and do not expire.</p>
<h4 class="text-muted">Active Tokens</h4>
<hr/>
<p ng-repeat="token in apiClient.activeTokens">
<b ng-click-select>{{ token.accessToken }}</b> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(apiClient, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
</p>
</div>
</div>
</div>
</div>
<br/>
<div class="section-header">
<div class="text-left">
<h3>Applications<button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="clientAdd.show()"><i class="fa fa-plus"></i> New API Client</button></h3>
</div>
</div>
<br/>
<!-- we will always at least have the webadmin token here, so activeClients always will have one entry with at least one token -->
<div class="card" ng-repeat="client in activeClients | activeOAuthClients:user">
<div class="grid-item-top">
<div class="row">
<div class="col-xs-12">
<h4 class="text-muted">
{{client.name}} <span ng-show="client.type !== 'external' && client.type !== 'built-in'">on {{client.location}}{{ config.isCustomDomain ? '.' : '-' }}{{config.fqdn}}</span>
</h4>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="row">
<div class="col-xs-12">
<b>{{ client.activeTokens.length }}</b> active token(s).
<br/>
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse{{client.id}}">Advanced</a>
<div id="collapse{{client.id}}" class="panel-collapse collapse">
<div class="panel-body">
<h4 class="text-muted">Credentials <button class="btn btn-xs btn-danger pull-right" ng-click="clientRemove.show(client)" title="Remove API Client" ng-show="client.type === 'external'">Remove API Client</button></h4>
<hr/>
<p>Scope: <b ng-click-select>{{ client.scope }}</b></p>
<p>RedirectURI: <b ng-click-select>{{ client.redirectURI }}</b></p>
<p>Client ID: <b ng-click-select>{{ client.id }}</b></p>
<p ng-show="client.clientSecret">Client Secret: <b ng-click-select>{{ client.clientSecret }}</b></p>
<br/>
<h4 class="text-muted">Tokens
<div class="pull-right">
<button class="btn btn-xs btn-default" ng-click="removeAccessTokens(client)" ng-disabled="!client.activeTokens.length || client.busy"><i class="fa fa-spinner fa-pulse" ng-show="client.busy"></i> Revoke All</button>
<button class="btn btn-xs btn-primary btn-outline" ng-click="tokenAdd.show(client)"><i class="fa fa-plus"></i> New Token</button>
</div>
</h4>
<hr/>
<p ng-repeat="token in client.activeTokens">
<b ng-click-select>{{ token.accessToken }}</b> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(client, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Offset the footer -->
<br/><br/>

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