├── .bundle └── config ├── .gitignore ├── .rspec ├── .ruby-gemset ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── NOTICE ├── README.md ├── Rakefile ├── app ├── assets │ ├── images │ │ ├── cf-mysql-logo.png │ │ └── favicon.ico │ ├── javascripts │ │ └── application.js │ └── stylesheets │ │ ├── application.css.scss │ │ ├── cf-mysql-broker.css │ │ └── pivotal-styles-full.css ├── controllers │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ ├── manage │ │ ├── auth_controller.rb │ │ └── instances_controller.rb │ ├── previews_controller.rb │ └── v2 │ │ ├── base_controller.rb │ │ ├── catalogs_controller.rb │ │ ├── service_bindings_controller.rb │ │ └── service_instances_controller.rb ├── models │ ├── base_model.rb │ ├── catalog.rb │ ├── cloud_controller_http_client.rb │ ├── concerns │ │ └── .keep │ ├── database.rb │ ├── plan.rb │ ├── read_only_user.rb │ ├── service.rb │ ├── service_binding.rb │ ├── service_instance.rb │ └── service_instance_access_verifier.rb ├── queries │ └── service_instance_usage_query.rb └── views │ ├── errors │ ├── approvals_error.html.erb │ └── not_authorized.html.erb │ ├── layouts │ └── application.html.erb │ └── manage │ └── instances │ └── show.html.erb ├── bin ├── bundle ├── rails └── rake ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml ├── environment.rb ├── environments │ ├── assets.rb │ ├── development.rb │ ├── production.rb │ ├── test.rb │ └── travis_test.rb ├── initializers │ ├── cookies_serializer.rb │ ├── omniauth.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── routes.rb ├── secrets.yml └── settings.yml ├── db ├── migrate │ ├── 20140529214059_create_service_instances.rb │ ├── 20140616163621_add_quota_to_service_instances.rb │ ├── 20140616212709_add_db_name_to_service_instances.rb │ ├── 20140918004518_set_db_name.rb │ └── 20180117002358_create_read_only_users.rb └── schema.rb ├── lib ├── configuration.rb ├── request_response_logger.rb ├── service_capacity.rb ├── service_instance_manager.rb ├── settings.rb ├── table_lock_manager.rb ├── tasks │ ├── brakeman.rake │ ├── broker.rake │ └── table_locks.rake └── uaa_session.rb ├── log └── .keep ├── manifest.yml └── spec ├── controllers ├── manage │ ├── auth_controller_spec.rb │ └── instances_controller_spec.rb ├── previews_controller_spec.rb └── v2 │ ├── catalogs_controller_spec.rb │ ├── service_bindings_controller_spec.rb │ └── service_instances_controller_spec.rb ├── features └── plan_update_spec.rb ├── lib ├── configuration_spec.rb ├── request_response_logger_spec.rb ├── service_capacity_spec.rb ├── service_instance_manager_spec.rb ├── table_lock_manager_spec.rb └── uaa_session_spec.rb ├── models ├── catalog_spec.rb ├── cloud_controller_http_client_spec.rb ├── database_spec.rb ├── plan_spec.rb ├── service_binding_spec.rb ├── service_instance_access_verifier_spec.rb ├── service_instance_spec.rb └── service_spec.rb ├── queries └── service_instance_usage_query_spec.rb ├── requests ├── catalog_spec.rb ├── lifecycle_spec.rb └── missing_things_spec.rb ├── spec_helper.rb └── support ├── controller_helpers.rb ├── feature_helpers.rb ├── model_helpers.rb ├── mysql_helper.rb └── request_helpers.rb /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_CACHE_ALL: "true" 3 | BUNDLE_PATH: "vendor/bundle" 4 | BUNDLE_DISABLE_SHARED_GEMS: "true" 5 | BUNDLE_WITHOUT: "development" 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore the default SQLite database. 8 | /db/*.sqlite3 9 | /db/*.sqlite3-journal 10 | 11 | # Ignore all logfiles and tempfiles. 12 | /log/*.log 13 | /tmp 14 | 15 | vendor/ 16 | 17 | .idea 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | cf-mysql-broker 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.8 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '~> 2.3' 4 | 5 | gem 'rails', '~> 4.2' 6 | gem 'rails-api' 7 | gem 'jquery-rails' 8 | gem 'settingslogic' 9 | gem 'mysql2', '< 0.5' 10 | gem 'omniauth-uaa-oauth2', git: 'https://github.com/cloudfoundry/omniauth-uaa-oauth2' 11 | gem 'nats' 12 | gem 'sass-rails' 13 | gem 'eventmachine', '~> 1.0.7' 14 | 15 | group :production do 16 | gem 'unicorn' 17 | end 18 | 19 | group :development, :test do 20 | gem 'test-unit' 21 | gem 'rspec-rails' 22 | gem 'database_cleaner' 23 | gem 'brakeman' 24 | gem 'pry' 25 | gem 'rb-readline' 26 | end 27 | 28 | group :test do 29 | gem 'codeclimate-test-reporter', '~> 0.6.0', require: nil 30 | gem 'webmock' 31 | end 32 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/cloudfoundry/omniauth-uaa-oauth2 3 | revision: 1eaf9b3f36f98a51a5453c94175d8b265d46bb56 4 | specs: 5 | omniauth-uaa-oauth2 (1.0.0) 6 | cf-uaa-lib (>= 3.2, < 4.0) 7 | omniauth (~> 1.0) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | actionmailer (4.2.11.1) 13 | actionpack (= 4.2.11.1) 14 | actionview (= 4.2.11.1) 15 | activejob (= 4.2.11.1) 16 | mail (~> 2.5, >= 2.5.4) 17 | rails-dom-testing (~> 1.0, >= 1.0.5) 18 | actionpack (4.2.11.1) 19 | actionview (= 4.2.11.1) 20 | activesupport (= 4.2.11.1) 21 | rack (~> 1.6) 22 | rack-test (~> 0.6.2) 23 | rails-dom-testing (~> 1.0, >= 1.0.5) 24 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 25 | actionview (4.2.11.1) 26 | activesupport (= 4.2.11.1) 27 | builder (~> 3.1) 28 | erubis (~> 2.7.0) 29 | rails-dom-testing (~> 1.0, >= 1.0.5) 30 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 31 | activejob (4.2.11.1) 32 | activesupport (= 4.2.11.1) 33 | globalid (>= 0.3.0) 34 | activemodel (4.2.11.1) 35 | activesupport (= 4.2.11.1) 36 | builder (~> 3.1) 37 | activerecord (4.2.11.1) 38 | activemodel (= 4.2.11.1) 39 | activesupport (= 4.2.11.1) 40 | arel (~> 6.0) 41 | activesupport (4.2.11.1) 42 | i18n (~> 0.7) 43 | minitest (~> 5.1) 44 | thread_safe (~> 0.3, >= 0.3.4) 45 | tzinfo (~> 1.1) 46 | addressable (2.6.0) 47 | public_suffix (>= 2.0.2, < 4.0) 48 | arel (6.0.4) 49 | brakeman (4.5.0) 50 | builder (3.2.3) 51 | cf-uaa-lib (3.14.3) 52 | httpclient (~> 2.8, >= 2.8.2.4) 53 | multi_json (~> 1.12.0, >= 1.12.1) 54 | codeclimate-test-reporter (0.6.0) 55 | simplecov (>= 0.7.1, < 1.0.0) 56 | coderay (1.1.2) 57 | concurrent-ruby (1.1.5) 58 | crack (0.4.3) 59 | safe_yaml (~> 1.0.0) 60 | crass (1.0.4) 61 | daemons (1.3.1) 62 | database_cleaner (1.7.0) 63 | diff-lcs (1.3) 64 | docile (1.3.1) 65 | erubis (2.7.0) 66 | eventmachine (1.0.7) 67 | ffi (1.10.0) 68 | globalid (0.4.2) 69 | activesupport (>= 4.2.0) 70 | hashdiff (0.3.8) 71 | hashie (3.6.0) 72 | httpclient (2.8.3) 73 | i18n (0.9.5) 74 | concurrent-ruby (~> 1.0) 75 | jquery-rails (4.3.3) 76 | rails-dom-testing (>= 1, < 3) 77 | railties (>= 4.2.0) 78 | thor (>= 0.14, < 2.0) 79 | json (2.2.0) 80 | json_pure (1.8.6) 81 | kgio (2.11.2) 82 | loofah (2.2.3) 83 | crass (~> 1.0.2) 84 | nokogiri (>= 1.5.9) 85 | mail (2.7.1) 86 | mini_mime (>= 0.1.1) 87 | method_source (0.9.2) 88 | mini_mime (1.0.1) 89 | mini_portile2 (2.4.0) 90 | minitest (5.11.3) 91 | multi_json (1.12.2) 92 | mysql2 (0.4.10) 93 | nats (0.6.0) 94 | daemons (~> 1.1, >= 1.2.2) 95 | eventmachine (~> 1.0, = 1.0.7) 96 | json_pure (~> 1.8, >= 1.8.1) 97 | thin (~> 1.6, >= 1.6.3) 98 | nokogiri (1.10.3) 99 | mini_portile2 (~> 2.4.0) 100 | omniauth (1.9.0) 101 | hashie (>= 3.4.6, < 3.7.0) 102 | rack (>= 1.6.2, < 3) 103 | power_assert (1.1.3) 104 | pry (0.12.2) 105 | coderay (~> 1.1.0) 106 | method_source (~> 0.9.0) 107 | public_suffix (3.0.3) 108 | rack (1.6.11) 109 | rack-test (0.6.3) 110 | rack (>= 1.0) 111 | rails (4.2.11.1) 112 | actionmailer (= 4.2.11.1) 113 | actionpack (= 4.2.11.1) 114 | actionview (= 4.2.11.1) 115 | activejob (= 4.2.11.1) 116 | activemodel (= 4.2.11.1) 117 | activerecord (= 4.2.11.1) 118 | activesupport (= 4.2.11.1) 119 | bundler (>= 1.3.0, < 2.0) 120 | railties (= 4.2.11.1) 121 | sprockets-rails 122 | rails-api (0.4.1) 123 | actionpack (>= 3.2.11) 124 | railties (>= 3.2.11) 125 | rails-deprecated_sanitizer (1.0.3) 126 | activesupport (>= 4.2.0.alpha) 127 | rails-dom-testing (1.0.9) 128 | activesupport (>= 4.2.0, < 5.0) 129 | nokogiri (~> 1.6) 130 | rails-deprecated_sanitizer (>= 1.0.1) 131 | rails-html-sanitizer (1.0.4) 132 | loofah (~> 2.2, >= 2.2.2) 133 | railties (4.2.11.1) 134 | actionpack (= 4.2.11.1) 135 | activesupport (= 4.2.11.1) 136 | rake (>= 0.8.7) 137 | thor (>= 0.18.1, < 2.0) 138 | raindrops (0.19.0) 139 | rake (12.3.2) 140 | rb-fsevent (0.10.3) 141 | rb-inotify (0.10.0) 142 | ffi (~> 1.0) 143 | rb-readline (0.5.5) 144 | rspec-core (3.8.0) 145 | rspec-support (~> 3.8.0) 146 | rspec-expectations (3.8.2) 147 | diff-lcs (>= 1.2.0, < 2.0) 148 | rspec-support (~> 3.8.0) 149 | rspec-mocks (3.8.0) 150 | diff-lcs (>= 1.2.0, < 2.0) 151 | rspec-support (~> 3.8.0) 152 | rspec-rails (3.8.2) 153 | actionpack (>= 3.0) 154 | activesupport (>= 3.0) 155 | railties (>= 3.0) 156 | rspec-core (~> 3.8.0) 157 | rspec-expectations (~> 3.8.0) 158 | rspec-mocks (~> 3.8.0) 159 | rspec-support (~> 3.8.0) 160 | rspec-support (3.8.0) 161 | safe_yaml (1.0.5) 162 | sass (3.7.3) 163 | sass-listen (~> 4.0.0) 164 | sass-listen (4.0.0) 165 | rb-fsevent (~> 0.9, >= 0.9.4) 166 | rb-inotify (~> 0.9, >= 0.9.7) 167 | sass-rails (5.0.7) 168 | railties (>= 4.0.0, < 6) 169 | sass (~> 3.1) 170 | sprockets (>= 2.8, < 4.0) 171 | sprockets-rails (>= 2.0, < 4.0) 172 | tilt (>= 1.1, < 3) 173 | settingslogic (2.0.9) 174 | simplecov (0.16.1) 175 | docile (~> 1.1) 176 | json (>= 1.8, < 3) 177 | simplecov-html (~> 0.10.0) 178 | simplecov-html (0.10.2) 179 | sprockets (3.7.2) 180 | concurrent-ruby (~> 1.0) 181 | rack (> 1, < 3) 182 | sprockets-rails (3.2.1) 183 | actionpack (>= 4.0) 184 | activesupport (>= 4.0) 185 | sprockets (>= 3.0.0) 186 | test-unit (3.3.0) 187 | power_assert 188 | thin (1.7.2) 189 | daemons (~> 1.0, >= 1.0.9) 190 | eventmachine (~> 1.0, >= 1.0.4) 191 | rack (>= 1, < 3) 192 | thor (0.20.3) 193 | thread_safe (0.3.6) 194 | tilt (2.0.9) 195 | tzinfo (1.2.5) 196 | thread_safe (~> 0.1) 197 | unicorn (5.5.0) 198 | kgio (~> 2.6) 199 | raindrops (~> 0.7) 200 | webmock (3.5.1) 201 | addressable (>= 2.3.6) 202 | crack (>= 0.3.2) 203 | hashdiff 204 | 205 | PLATFORMS 206 | ruby 207 | 208 | DEPENDENCIES 209 | brakeman 210 | codeclimate-test-reporter (~> 0.6.0) 211 | database_cleaner 212 | eventmachine (~> 1.0.7) 213 | jquery-rails 214 | mysql2 (< 0.5) 215 | nats 216 | omniauth-uaa-oauth2! 217 | pry 218 | rails (~> 4.2) 219 | rails-api 220 | rb-readline 221 | rspec-rails 222 | sass-rails 223 | settingslogic 224 | test-unit 225 | unicorn 226 | webmock 227 | 228 | RUBY VERSION 229 | ruby 2.3.1p112 230 | 231 | BUNDLED WITH 232 | 1.17.3 233 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2013-2015 Pivotal 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. 2 | 3 | This project contains software that is Copyright (c) 2013-2015 Pivotal Software, Inc. 4 | 5 | This project is licensed to you under the Apache License, Version 2.0 (the "License"). 6 | 7 | You may not use this project except in compliance with the License. 8 | 9 | This project may include a number of subcomponents with separate copyright notices 10 | and license terms. Your use of these subcomponents is subject to the terms and 11 | conditions of the subcomponent's license, as noted in the LICENSE file. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # CF MySQL Broker 3 | 4 | CF MySQL Broker provides MySQL databases as a Cloud Foundry service. This broker demonstrates the v2 services API between cloud controllers and service brokers. This API is not to be confused with the cloud controller API; for more info see [Service Broker API](http://docs.cloudfoundry.org/services/api.html). 5 | 6 | The broker does not include a MySQL server. Instead, it is meant to be deployed alongside a MySQL server, which it manages, as part of [cf-mysql-release](https://github.com/cloudfoundry/cf-mysql-release). Deploying this as a standalone application to cloud foundry (e.g. via `cf push`) is not supported. The MySQL management tasks that the broker performs are as follows: 7 | 8 | * Provisioning of database instances (create) 9 | * Creation of credentials (bind) 10 | * Removal of credentials (unbind) 11 | * Unprovisioning of database instances (delete) 12 | 13 | ## Running Tests 14 | 15 | The CF MySQL Broker integration specs will exercise the catalog fetch, create, bind, unbind, and delete functions against its locally installed database. 16 | 17 | 1. Run a local install MySQL server. 18 | * Specs have only been tested with MySQL 5.6 19 | * Specs assume MySQL root user without a password 20 | * The anonymous user (User='' and Host='localhost') must be deleted (see [spec/spec_helper]) 21 | - `delete from mysql.user where name='';` 22 | - `flush privileges` 23 | * Specific innodb settings are required (see [spec/spec_helper]) 24 | - innodb_stats_on_metadata = ON 25 | - `mysql -uroot -e "SET GLOBAL innodb_stats_on_metadata=ON"` 26 | - innodb_stats_persistent = OFF 27 | - `mysql -uroot -e "SET GLOBAL innodb_stats_persistent=OFF"` 28 | 2. Run the following commands 29 | 30 | ``` 31 | $ cd cf-mysql-broker 32 | $ bundle 33 | $ bundle exec rake db:setup 34 | $ bundle exec rake spec 35 | ``` 36 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | CfMysqlBroker::Application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/images/cf-mysql-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-mysql-broker/499b0398755e020c7cb71aca8be58b242bfd63c1/app/assets/images/cf-mysql-logo.png -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-mysql-broker/499b0398755e020c7cb71aca8be58b242bfd63c1/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require jquery_ujs 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css.scss: -------------------------------------------------------------------------------- 1 | @import "pivotal-styles-full"; 2 | @import "cf-mysql-broker"; 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/cf-mysql-broker.css: -------------------------------------------------------------------------------- 1 | .no-sidebar-main-wrapper { 2 | margin: 0 20px; 3 | left: 0; 4 | right: 0; 5 | width: auto; 6 | } 7 | 8 | .header { 9 | background-color: #243640; 10 | } 11 | 12 | .float-right { 13 | float: right; 14 | } 15 | 16 | .services-container { 17 | padding: 0 15px; 18 | } 19 | 20 | .header-container { 21 | color: #243640; 22 | font-size: 18px; 23 | margin-top: 10px; 24 | margin-bottom: 20px; 25 | } 26 | 27 | .teal-text { 28 | color: #00a79d; 29 | font-family: Monaco, Menlo, Consolas, "Courier New", monospace; 30 | font-size: 13px; 31 | } 32 | 33 | .panel-text { 34 | font-size: 16px; 35 | } 36 | 37 | .panel-message { 38 | max-width: 600px; 39 | background-color: #f6f6f6; 40 | border: none; 41 | border-bottom: 4px solid rgba(0, 0, 0, 0.07); 42 | min-height: 88px; 43 | -moz-background-clip: padding; 44 | /* Firefox 3.6 */ 45 | -webkit-background-clip: padding; 46 | /* Safari 4? Chrome 6? */ 47 | background-clip: padding-box; 48 | /* Firefox 4, Safari 5, Opera 10, IE 9 */ 49 | } 50 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-mysql-broker/499b0398755e020c7cb71aca8be58b242bfd63c1/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/manage/auth_controller.rb: -------------------------------------------------------------------------------- 1 | module Manage 2 | class AuthController < ApplicationController 3 | protect_from_forgery with: :exception, only: :destroy 4 | 5 | def create 6 | auth = request.env['omniauth.auth'].to_hash 7 | credentials = auth['credentials'] 8 | 9 | token = credentials['token'] 10 | if token.empty? 11 | return render 'errors/approvals_error' 12 | end 13 | 14 | raw_info = auth['extra']['raw_info'] 15 | unless raw_info 16 | return render 'errors/approvals_error' 17 | end 18 | 19 | session[:uaa_user_id] = auth['extra']['raw_info']['user_id'] 20 | session[:uaa_access_token] = credentials['token'] 21 | session[:uaa_refresh_token] = credentials['refresh_token'] 22 | session[:last_seen] = Time.now 23 | 24 | redirect_to manage_instance_path(session[:instance_id]) 25 | end 26 | 27 | def failure 28 | render text: message_param[:message], status: 403 29 | end 30 | 31 | def destroy 32 | session.clear 33 | redirect_to ::Configuration.auth_server_logout_url 34 | end 35 | 36 | private 37 | 38 | def message_param 39 | params.permit(:message) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/controllers/manage/instances_controller.rb: -------------------------------------------------------------------------------- 1 | module Manage 2 | class InstancesController < ApplicationController 3 | 4 | before_filter :redirect_ssl 5 | before_filter :require_login 6 | before_filter :build_uaa_session 7 | before_filter :ensure_all_necessary_scopes_are_approved 8 | before_filter :ensure_can_manage_instance 9 | 10 | def show 11 | instance = ServiceInstance.find_by_guid(params[:id]) 12 | 13 | @used_data = ServiceInstanceUsageQuery.new(instance).execute 14 | @quota = instance.max_storage_mb 15 | @over_quota = @used_data > @quota 16 | end 17 | 18 | private 19 | 20 | def redirect_ssl 21 | redirect_to :protocol => "https://" if Settings.ssl_enabled && request.protocol == 'http://' 22 | return true 23 | end 24 | 25 | def require_login 26 | session[:instance_id] = params[:id] 27 | unless logged_in? 28 | redirect_to '/manage/auth/cloudfoundry' 29 | return false 30 | end 31 | end 32 | 33 | def build_uaa_session 34 | @uaa_session = UaaSession.build(session[:uaa_access_token], session[:uaa_refresh_token]) 35 | session[:uaa_access_token] = @uaa_session.access_token 36 | session[:uaa_refresh_token] = @uaa_session.refresh_token 37 | end 38 | 39 | def ensure_all_necessary_scopes_are_approved 40 | token_hash = CF::UAA::TokenCoder.decode(@uaa_session.access_token, verify: false) 41 | return true if has_necessary_scopes?(token_hash) 42 | 43 | if need_to_retry? 44 | session[:has_retried] = 'true' 45 | redirect_to '/manage/auth/cloudfoundry' 46 | return false 47 | else 48 | session[:has_retried] = 'false' 49 | render 'errors/approvals_error' 50 | return false 51 | end 52 | end 53 | 54 | def ensure_can_manage_instance 55 | cc_client = CloudControllerHttpClient.new(@uaa_session.auth_header) 56 | unless ServiceInstanceAccessVerifier.can_manage_instance?(params[:id], cc_client) 57 | render 'errors/not_authorized' 58 | return false 59 | end 60 | end 61 | 62 | def logged_in? 63 | oldest_allowable_last_seen_time = Time.now - Settings.session_expiry 64 | 65 | if session[:uaa_user_id].present? && (session[:last_seen] > oldest_allowable_last_seen_time) 66 | session[:last_seen] = Time.now 67 | return true 68 | end 69 | 70 | return false 71 | end 72 | 73 | def has_necessary_scopes?(token_hash) 74 | %w(openid cloud_controller_service_permissions.read).all? { |scope| token_hash['scope'].include?(scope) } 75 | end 76 | 77 | def need_to_retry? 78 | session[:has_retried].nil? || session[:has_retried] == 'false' 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /app/controllers/previews_controller.rb: -------------------------------------------------------------------------------- 1 | class PreviewsController < ApplicationController 2 | before_filter :restrict_to_development 3 | 4 | def show 5 | @quota = 100 6 | @used_data = 101 7 | 8 | @over_quota = @used_data > @quota 9 | 10 | render 'manage/instances/show' 11 | end 12 | 13 | private 14 | 15 | def restrict_to_development 16 | head(:bad_request) unless Rails.env.development? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/v2/base_controller.rb: -------------------------------------------------------------------------------- 1 | class V2::BaseController < ActionController::API 2 | include ActionController::HttpAuthentication::Basic::ControllerMethods 3 | 4 | before_filter :authenticate 5 | 6 | protected 7 | 8 | def authenticate 9 | authenticate_or_request_with_http_basic do |username, password| 10 | username == Settings.auth_username && 11 | password == Settings.auth_password 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/v2/catalogs_controller.rb: -------------------------------------------------------------------------------- 1 | class V2::CatalogsController < V2::BaseController 2 | def show 3 | render json: { 4 | services: services.map {|service| service.to_hash } 5 | } 6 | end 7 | 8 | private 9 | 10 | def services 11 | (Settings['services'] || []).map {|attrs| Service.build(attrs)} 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/v2/service_bindings_controller.rb: -------------------------------------------------------------------------------- 1 | class V2::ServiceBindingsController < V2::BaseController 2 | ALLOWED_BINDING_PARAMETERS = ['read-only'] 3 | 4 | def update 5 | instance = ServiceInstance.find_by_guid(params.fetch(:service_instance_id)) 6 | if instance.nil? 7 | render status: 404, json: {} 8 | return 9 | end 10 | 11 | binding_parameters = params.fetch(:parameters, {}) 12 | binding_parameters_include_unknown_key = binding_parameters.keys.any? {|key| !ALLOWED_BINDING_PARAMETERS.include?(key)} 13 | 14 | read_only = binding_parameters.fetch('read-only', false) 15 | read_only_parameter_has_invalid_value = !read_only.in?([true, false]) 16 | 17 | if binding_parameters_include_unknown_key || read_only_parameter_has_invalid_value 18 | render status: 400, json: { 19 | "error" => "Error creating service binding", 20 | "description" => "Invalid arbitrary parameter syntax. Please check the documentation for supported arbitrary parameters.", 21 | } 22 | return 23 | end 24 | 25 | binding = ServiceBinding.new(id: params.fetch(:id), service_instance: instance, read_only: read_only) 26 | binding.save 27 | 28 | render status: 201, json: binding 29 | end 30 | 31 | def destroy 32 | binding = ServiceBinding.find_by_id(params.fetch(:id)) 33 | if binding 34 | binding.destroy 35 | status = 200 36 | else 37 | status = 410 38 | end 39 | 40 | render status: status, json: {} 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/controllers/v2/service_instances_controller.rb: -------------------------------------------------------------------------------- 1 | class V2::ServiceInstancesController < V2::BaseController 2 | 3 | # This is actually the create 4 | def update 5 | plan_guid = params.fetch(:plan_id) 6 | 7 | unless Catalog.has_plan?(plan_guid) 8 | return render status: 422, json: {'description' => "Cannot create a service instance. Plan #{plan_guid} was not found in the catalog."} 9 | end 10 | 11 | plan_max_storage_mb = Catalog.storage_quota_for_plan_guid(plan_guid) 12 | 13 | if ServiceCapacity.can_allocate?(plan_max_storage_mb) 14 | instance_guid = params.fetch(:id) 15 | instance = ServiceInstanceManager.create(guid: instance_guid, plan_guid: plan_guid) 16 | 17 | render status: 201, json: { dashboard_url: build_dashboard_url(instance) } 18 | else 19 | render status: 507, json: {'description' => 'Service capacity has been reached'} 20 | end 21 | end 22 | 23 | def set_plan 24 | instance_guid = params.fetch(:id) 25 | plan_guid = params.fetch(:plan_id) 26 | 27 | begin 28 | ServiceInstanceManager.set_plan(guid: instance_guid, plan_guid: plan_guid) 29 | status = 200 30 | body = {} 31 | rescue ServiceInstanceManager::ServiceInstanceNotFound 32 | status = 404 33 | body = { description: 'Service instance not found' } 34 | rescue ServiceInstanceManager::ServicePlanNotFound 35 | status = 400 36 | body = { description: 'Service plan not found' } 37 | rescue ServiceInstanceManager::InvalidServicePlanUpdate => e 38 | status = 422 39 | body = { description: e.message } 40 | end 41 | 42 | render status: status, json: body 43 | end 44 | 45 | def destroy 46 | instance_guid = params.fetch(:id) 47 | begin 48 | ServiceInstanceManager.destroy(guid: instance_guid) 49 | status = 200 50 | rescue ServiceInstanceManager::ServiceInstanceNotFound 51 | status = 410 52 | end 53 | 54 | render status: status, json: {} 55 | end 56 | 57 | private 58 | 59 | def build_dashboard_url(instance) 60 | domain = Settings.external_host 61 | path = manage_instance_path(instance.guid) 62 | 63 | "#{scheme}://#{domain}#{path}" 64 | end 65 | 66 | def scheme 67 | Settings['ssl_enabled'] == false ? 'http': 'https' 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /app/models/base_model.rb: -------------------------------------------------------------------------------- 1 | class BaseModel 2 | include ActiveModel::Model 3 | 4 | private 5 | 6 | def self.connection 7 | ActiveRecord::Base.connection 8 | end 9 | 10 | def connection 11 | self.class.connection 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/catalog.rb: -------------------------------------------------------------------------------- 1 | class Catalog 2 | def self.has_plan?(plan_guid) 3 | find_plan_by_guid(plan_guid).present? 4 | end 5 | 6 | def self.plans 7 | services.map do |service| 8 | service.plans 9 | end.flatten 10 | end 11 | 12 | def self.storage_quota_for_plan_guid(plan_guid) 13 | find_plan_by_guid(plan_guid).try(:max_storage_mb) 14 | end 15 | 16 | def self.connection_quota_for_plan_guid(plan_guid) 17 | find_plan_by_guid(plan_guid).try(:max_user_connections) 18 | end 19 | 20 | private 21 | 22 | def self.find_plan_by_guid(plan_guid) 23 | plans.detect do |plan| 24 | plan.id == plan_guid 25 | end 26 | end 27 | 28 | def self.services 29 | Settings['services'].map {|attrs| Service.build(attrs)} 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/models/cloud_controller_http_client.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | 3 | class CloudControllerHttpClient 4 | attr_reader :auth_header 5 | def initialize(auth_header=nil) 6 | @auth_header = auth_header 7 | end 8 | 9 | def get(path) 10 | uri = cc_uri(path) 11 | http = build_http(uri) 12 | 13 | request = Net::HTTP::Get.new(uri) 14 | request['Authorization'] = auth_header 15 | 16 | response = http.request(request) 17 | 18 | JSON.parse(response.body) 19 | end 20 | 21 | private 22 | 23 | def cc_uri(path) 24 | URI.parse("#{Settings.cc_api_uri.gsub(/\/$/, '')}/#{path.gsub(/^\//, '')}") 25 | end 26 | 27 | def build_http(uri) 28 | http = Net::HTTP.new(uri.hostname, uri.port) 29 | http.use_ssl = uri.scheme == 'https' 30 | http.verify_mode = Settings.skip_ssl_validation ? 31 | OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER 32 | 33 | http 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-mysql-broker/499b0398755e020c7cb71aca8be58b242bfd63c1/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/database.rb: -------------------------------------------------------------------------------- 1 | module Database 2 | extend self 3 | 4 | def create(database_name) 5 | connection.execute("CREATE DATABASE IF NOT EXISTS #{connection.quote_table_name(database_name)}") 6 | end 7 | 8 | def drop(database_name) 9 | connection.execute("DROP DATABASE IF EXISTS #{connection.quote_table_name(database_name)}") 10 | end 11 | 12 | def exists?(database_name) 13 | connection.select_values("SHOW DATABASES LIKE '#{database_name}'").count > 0 14 | end 15 | 16 | def usage(database_name) 17 | res = connection.select_value(<<-SQL) 18 | SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) 19 | FROM information_schema.tables AS tables 20 | WHERE tables.table_schema = '#{database_name}' 21 | SQL 22 | 23 | res.to_i 24 | end 25 | 26 | def with_reconnect 27 | yield 28 | rescue ActiveRecord::ActiveRecordError => e 29 | Rails.logger.warn(e) 30 | if ActiveRecord::Base.connection.active? 31 | raise e 32 | else 33 | reconnect_attempts = 0 34 | until ActiveRecord::Base.connection.active? 35 | reconnect_attempts += 1 36 | attempt_reconnect(reconnect_attempts) 37 | end 38 | end 39 | end 40 | 41 | private 42 | 43 | def attempt_reconnect(reconnect_attempts) 44 | Rails.logger.warn('No database connection, attempting to reconnect') 45 | ActiveRecord::Base.connection.reconnect! 46 | rescue Mysql2::Error => e 47 | Rails.logger.warn("Reconnect failed: #{e}") 48 | 49 | if reconnect_attempts < 20 50 | Kernel.sleep(3.seconds) 51 | else 52 | raise e 53 | end 54 | end 55 | 56 | def connection 57 | ActiveRecord::Base.connection 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /app/models/plan.rb: -------------------------------------------------------------------------------- 1 | class Plan 2 | attr_reader :id, :name, :description, :metadata, :max_storage_mb, :max_user_connections, :free 3 | 4 | def self.build(attrs) 5 | new(attrs) 6 | end 7 | 8 | def initialize(attrs) 9 | @id = attrs.fetch('id') 10 | @name = attrs.fetch('name') 11 | @description = attrs.fetch('description') 12 | @metadata = attrs.fetch('metadata', nil) 13 | @max_storage_mb = attrs.fetch('max_storage_mb', nil) 14 | @max_user_connections = attrs.fetch('max_user_connections', nil) 15 | @free = attrs.fetch('free',true) 16 | end 17 | 18 | def to_hash 19 | { 20 | 'id' => self.id, 21 | 'name' => self.name, 22 | 'description' => self.description, 23 | 'metadata' => self.metadata, 24 | 'max_storage_mb' => self.max_storage_mb, 25 | 'max_user_connections' => self.max_user_connections, 26 | 'free' => self.free, 27 | } 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/models/read_only_user.rb: -------------------------------------------------------------------------------- 1 | class ReadOnlyUser < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/models/service.rb: -------------------------------------------------------------------------------- 1 | class Service 2 | attr_reader :id, :name, :description, :tags, :metadata, :plans, :dashboard_client, :plan_updateable 3 | 4 | def self.build(attrs) 5 | plan_attrs = attrs['plans'] || [] 6 | plans = plan_attrs.map { |attr| Plan.build(attr) } 7 | new(attrs.merge('plans' => plans)) 8 | end 9 | 10 | def initialize(attrs) 11 | @id = attrs.fetch('id') 12 | @name = attrs.fetch('name') 13 | @description = attrs.fetch('description') 14 | @plan_updateable = attrs.fetch('plan_updateable', false) 15 | @tags = attrs.fetch('tags', []) 16 | @metadata = attrs.fetch('metadata', nil) 17 | @plans = attrs.fetch('plans', []) 18 | @dashboard_client = attrs.fetch('dashboard_client', {}) 19 | end 20 | 21 | def bindable? 22 | true 23 | end 24 | 25 | def to_hash 26 | { 27 | 'id' => id, 28 | 'name' => name, 29 | 'description' => description, 30 | 'tags' => tags, 31 | 'metadata' => metadata, 32 | 'plan_updateable' => plan_updateable, 33 | 'plans' => plans.map(&:to_hash), 34 | 'bindable' => bindable?, 35 | 'dashboard_client' => dashboard_client 36 | } 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/models/service_binding.rb: -------------------------------------------------------------------------------- 1 | require Rails.root.join('lib/service_instance_manager') 2 | 3 | class DatabaseNotFoundError < StandardError; end 4 | class ServiceBinding < BaseModel 5 | attr_accessor :id, :service_instance, :read_only 6 | 7 | def self.find_by_id(id) 8 | binding = new(id: id) 9 | 10 | begin 11 | connection.execute("SHOW GRANTS FOR '#{binding.username}'") 12 | binding 13 | rescue ActiveRecord::StatementInvalid => e 14 | raise unless e.message =~ /no such grant/ 15 | end 16 | end 17 | 18 | 19 | def self.find_by_id_and_service_instance_guid(id, instance_guid) 20 | binding = new(id: id) 21 | 22 | begin 23 | grants = connection.select_values("SHOW GRANTS FOR '#{binding.username}'") 24 | 25 | database_name = ServiceInstanceManager.database_name_from_service_instance_guid(instance_guid) 26 | # Can we do this more elegantly, i.e., without checking for a 27 | # particular raw GRANT statement? 28 | 29 | if grants.any? { |grant| grant.match(Regexp.new("GRANT .* ON `#{database_name}`\\.\\* TO '#{binding.username}'@'%'")) } 30 | binding 31 | end 32 | rescue ActiveRecord::StatementInvalid => e 33 | raise unless e.message =~ /no such grant/ 34 | end 35 | end 36 | 37 | def self.exists?(conditions) 38 | id = conditions.fetch(:id) 39 | instance_guid = conditions.fetch(:service_instance_guid) 40 | 41 | find_by_id_and_service_instance_guid(id, instance_guid).present? 42 | end 43 | 44 | def host 45 | connection_config.fetch('host') 46 | end 47 | 48 | def port 49 | connection_config.fetch('port') 50 | end 51 | 52 | def database_name 53 | ServiceInstanceManager.database_name_from_service_instance_guid(service_instance.guid) 54 | end 55 | 56 | def username 57 | Digest::MD5.base64digest(id).gsub(/[^a-zA-Z0-9]+/, '')[0...16] 58 | end 59 | 60 | def password 61 | @password ||= SecureRandom.base64(20).gsub(/[^a-zA-Z0-9]+/, '')[0...16] 62 | end 63 | 64 | def save 65 | unless Database.exists?(database_name) 66 | raise DatabaseNotFoundError.new("Service instance '#{service_instance.guid}' database does not exist") 67 | end 68 | 69 | begin 70 | connection.execute("CREATE USER '#{username}' IDENTIFIED BY '#{password}'") 71 | rescue => e 72 | raise e, e.message.gsub(password, 'redacted'), e.backtrace 73 | end 74 | 75 | update_connection_quota_for_user 76 | create_read_only_user if read_only 77 | end 78 | 79 | def destroy 80 | connection.execute("DROP USER '#{username}'") 81 | ReadOnlyUser.find_by_username(username).try(:destroy) 82 | rescue ActiveRecord::StatementInvalid => e 83 | raise unless e.message =~ /DROP USER failed/ 84 | end 85 | 86 | def to_json(*) 87 | obj = { 88 | 'credentials' => { 89 | 'hostname' => host, 90 | 'port' => port, 91 | 'name' => database_name, 92 | 'username' => username, 93 | 'password' => password, 94 | 'uri' => uri, 95 | 'jdbcUrl' => jdbc_url 96 | } 97 | } 98 | 99 | if Settings['tls_ca_certificate'] 100 | obj['credentials']['ca_certificate'] = Settings['tls_ca_certificate'] 101 | end 102 | obj.to_json 103 | end 104 | 105 | def self.update_all_max_user_connections 106 | # We would like to update these users in bulk by updating mysql.user 107 | # directly, but Galera does not replicate this table. DDL statments such 108 | # as GRANT USAGE must be used instead to ensure replication. 109 | Catalog.plans.each do |plan| 110 | users = connection.select_values(get_all_users_with_plan(plan)) 111 | users.each do |user| 112 | connection.execute(update_max_user_connection_for_user(user, plan)) 113 | end 114 | end 115 | end 116 | 117 | private 118 | 119 | def update_connection_quota_for_user 120 | max_user_connections = Catalog.connection_quota_for_plan_guid(service_instance.plan_guid) 121 | 122 | privileges = read_only ? "SELECT" : "ALL PRIVILEGES" 123 | grant_sql = "GRANT #{privileges} ON `#{service_instance.db_name}`.* TO '#{username}'@'%'" 124 | grant_sql = grant_sql + " WITH MAX_USER_CONNECTIONS #{max_user_connections}" if max_user_connections 125 | connection.execute(grant_sql) 126 | 127 | if !Settings.allow_table_locks 128 | revoke_sql = "REVOKE LOCK TABLES ON `#{service_instance.db_name}`.* FROM '#{username}'@'%'" 129 | connection.execute(revoke_sql) 130 | end 131 | end 132 | 133 | def create_read_only_user 134 | ReadOnlyUser.create(username: username, grantee: "'#{username}'@'%'") 135 | end 136 | 137 | def self.update_max_user_connection_for_user(user, plan) 138 | <<-SQL 139 | GRANT USAGE ON *.* TO '#{user}'@'%' 140 | WITH MAX_USER_CONNECTIONS #{plan.max_user_connections} 141 | SQL 142 | end 143 | 144 | def self.get_all_users_with_plan(plan) 145 | <<-SQL 146 | SELECT mysql.user.user 147 | FROM service_instances 148 | JOIN mysql.db ON service_instances.db_name=mysql.db.Db 149 | JOIN mysql.user ON mysql.user.User=mysql.db.User 150 | WHERE plan_guid='#{plan.id}' AND mysql.user.user NOT LIKE 'root' 151 | SQL 152 | end 153 | 154 | def connection_config 155 | Rails.configuration.database_configuration[Rails.env] 156 | end 157 | 158 | def uri 159 | "mysql://#{username}:#{password}@#{host}:#{port}/#{database_name}?reconnect=true" 160 | end 161 | 162 | def ssl_arguments 163 | return unless Settings['tls_ca_certificate'] 164 | mysql_connector_j_flag = 'enabledTLSProtocols=TLSv1.2' 165 | mariadb_connector_j_flag = 'enabledSslProtocolSuites=TLSv1.2' 166 | "&useSSL=true&#{mysql_connector_j_flag}&#{mariadb_connector_j_flag}" 167 | end 168 | 169 | def jdbc_url 170 | "jdbc:mysql://#{host}:#{port}/#{database_name}?user=#{username}&password=#{password}#{ssl_arguments}" 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /app/models/service_instance.rb: -------------------------------------------------------------------------------- 1 | class ServiceInstance < ActiveRecord::Base 2 | def self.reserved_space_in_mb 3 | self.sum(:max_storage_mb) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/service_instance_access_verifier.rb: -------------------------------------------------------------------------------- 1 | class ServiceInstanceAccessVerifier 2 | class << self 3 | def can_manage_instance?(instance_guid, cc_client) 4 | response_body = cc_client.get("/v2/service_instances/#{instance_guid}/permissions") 5 | !response_body.nil? && response_body['manage'] 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/queries/service_instance_usage_query.rb: -------------------------------------------------------------------------------- 1 | class ServiceInstanceUsageQuery 2 | attr_reader :instance 3 | 4 | def initialize(instance) 5 | @instance = instance 6 | end 7 | 8 | def execute 9 | db_name = instance.db_name 10 | escaped_database = ActiveRecord::Base.sanitize(db_name) 11 | query = <<-SQL 12 | SELECT SUM(ROUND(((data_length + index_length) / 1024 / 1024), 2)) 13 | FROM information_schema.TABLES 14 | WHERE table_schema = #{escaped_database} 15 | SQL 16 | 17 | result_set = ActiveRecord::Base.connection.execute(query).first 18 | result = result_set.first 19 | result.to_f 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /app/views/errors/approvals_error.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | This application requires the following permissions: 4 | 8 | <%= link_to "Manage third-party access", Configuration.manage_user_profile_url %> 9 |
10 |
11 | -------------------------------------------------------------------------------- /app/views/errors/not_authorized.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | Not Authorized: You do not have sufficient permissions for the space containing the requested service instance. 4 |
5 |
6 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MySQL Management Dashboard 5 | <%= favicon_link_tag %> 6 | <%= stylesheet_link_tag "application", media: "all" %> 7 | <%= javascript_include_tag :application %> 8 | <%= csrf_meta_tags %> 9 | 10 | 11 |
12 |
13 |
14 |
15 | Pivotal CF Services 16 |
17 |
18 | <% if Configuration.documentation_url %> 19 | <%= link_to "Docs", Configuration.documentation_url, class: 'mlxl' %> 20 | <% end %> 21 | <% if Configuration.support_url %> 22 | <%= link_to "Support", Configuration.support_url, class: 'mlxl' %> 23 | <% end %> 24 | <%= link_to "Logout", manage_auth_path, method: :delete, class: 'logout mlxl' %> 25 |
26 |
27 | 28 |
29 |
30 | <%= image_tag("cf-mysql-logo.png", height: '64', width: '64') %> 31 | 32 | MySQL for Pivotal Cloud Foundry 33 | 34 |
35 | 36 | <%= yield %> 37 |
38 |
39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /app/views/manage/instances/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= @used_data %> MB of <%= @quota %> MB used. 4 |
5 |
6 | 7 | <% if @over_quota %> 8 |
9 |
10 | Warning: Write permissions have been revoked due to storage utilization 11 | exceeding the plan quota. Read and delete permissions remain enabled. 12 | Write permissions will be restored when storage utilization has been 13 | reduced to below the plan quota. 14 |
15 |
16 | <% end %> 17 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require 'active_record/railtie' 5 | require 'action_controller/railtie' 6 | # require 'action_mailer/railtie' 7 | # require 'sprockets/railtie' 8 | # require 'rails/test_unit/railtie' 9 | 10 | # Require the gems listed in Gemfile, including any gems 11 | # you've limited to :test, :development, or :production. 12 | Bundler.require(:default, Rails.env) 13 | 14 | require File.expand_path('../../lib/settings', __FILE__) 15 | 16 | module CfMysqlBroker 17 | class Application < Rails::Application 18 | # Settings in config/environments/* take precedence over those specified here. 19 | # Application configuration should go into files in config/initializers 20 | # -- all .rb files in that directory are automatically loaded. 21 | 22 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 23 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 24 | # config.time_zone = 'Central Time (US & Canada)' 25 | 26 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 27 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 28 | # config.i18n.default_locale = :de 29 | 30 | config.api_only = false 31 | 32 | config.assets.enabled = true 33 | 34 | config.autoload_paths += %W(#{config.root}/lib) 35 | config.autoload_paths += Dir["#{config.root}/lib/**/"] 36 | 37 | config.paths.add 'config/database', with: Settings.database_config_path 38 | config.middleware.insert_after 'Rack::Runtime', Rack::MethodOverride 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 5 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # This file should not be used in deployed environments. Instead, the 2 | # application configuration file specified by SETTINGS_PATH should 3 | # include the path to a database configuration that includes these 4 | # settings. 5 | 6 | 7 | # This is included because our app connects to the database when Rails starts, 8 | # and Rails gets loaded when static assets are being precompiled in the 'assets' 9 | # environment. 10 | assets: 11 | adapter: mysql2 12 | encoding: utf8 13 | database: development 14 | pool: 5 15 | username: root 16 | password: 17 | host: localhost 18 | port: 3306 19 | 20 | development: 21 | adapter: mysql2 22 | encoding: utf8 23 | database: development 24 | pool: 5 25 | username: root 26 | password: 27 | host: localhost 28 | port: 3306 29 | 30 | # Warning: The database defined as "test" will be erased and 31 | # re-generated from your development database when you run "rake". 32 | # Do not set this db to the same as development or production. 33 | test: 34 | adapter: mysql2 35 | encoding: utf8 36 | database: test 37 | pool: 5 38 | username: root 39 | password: 40 | host: localhost 41 | port: 3306 42 | 43 | ci_test: 44 | adapter: mysql2 45 | encoding: utf8 46 | database: test 47 | pool: 5 48 | username: root 49 | password: 50 | host: localhost 51 | port: 3306 52 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | CfMysqlBroker::Application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/assets.rb: -------------------------------------------------------------------------------- 1 | CfMysqlBroker::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both thread web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | config.serve_static_files = true 23 | config.static_cache_control = 'public, max-age=3600' 24 | config.assets.compile = false 25 | config.assets.digest = true 26 | 27 | # Specifies the header that your server uses for sending files. 28 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for apache 29 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 30 | 31 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 32 | # config.force_ssl = true 33 | 34 | # Set to :debug to see everything in the log. 35 | config.log_level = :info 36 | 37 | # Prepend all log lines with the following tags. 38 | # config.log_tags = [ :subdomain, :uuid ] 39 | 40 | # Use a different logger for distributed setups. 41 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 42 | 43 | # Use a different cache store in production. 44 | # config.cache_store = :mem_cache_store 45 | 46 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 47 | # config.action_controller.asset_host = 'http://assets.example.com' 48 | 49 | # Ignore bad email addresses and do not raise email delivery errors. 50 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 51 | # config.action_mailer.raise_delivery_errors = false 52 | 53 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 54 | # the I18n.default_locale when a translation can not be found). 55 | config.i18n.fallbacks = true 56 | 57 | # Send deprecation notices to registered listeners. 58 | config.active_support.deprecation = :notify 59 | 60 | # Disable automatic flushing of the log to improve performance. 61 | # config.autoflush_log = false 62 | 63 | # Use default logging formatter so that PID and timestamp are not suppressed. 64 | config.log_formatter = ::Logger::Formatter.new 65 | end 66 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | CfMysqlBroker::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | # config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | end 22 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | CfMysqlBroker::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both thread web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | config.serve_static_files = true 23 | config.static_cache_control = 'public, max-age=3600' 24 | config.assets.compile = false 25 | config.assets.digest = true 26 | 27 | # Specifies the header that your server uses for sending files. 28 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for apache 29 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 30 | 31 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 32 | # config.force_ssl = true 33 | 34 | # Set to :debug to see everything in the log. 35 | config.log_level = :info 36 | 37 | # Prepend all log lines with the following tags. 38 | # config.log_tags = [ :subdomain, :uuid ] 39 | 40 | # Use a different logger for distributed setups. 41 | config.logger = Logger.new(STDOUT) 42 | 43 | # Use a different cache store in production. 44 | # config.cache_store = :mem_cache_store 45 | 46 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 47 | # config.action_controller.asset_host = 'http://assets.example.com' 48 | 49 | # Ignore bad email addresses and do not raise email delivery errors. 50 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 51 | # config.action_mailer.raise_delivery_errors = false 52 | 53 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 54 | # the I18n.default_locale when a translation can not be found). 55 | config.i18n.fallbacks = true 56 | 57 | # Send deprecation notices to registered listeners. 58 | config.active_support.deprecation = :notify 59 | 60 | # Disable automatic flushing of the log to improve performance. 61 | # config.autoflush_log = false 62 | 63 | # Use default logging formatter so that PID and timestamp are not suppressed. 64 | config.log_formatter = ::Logger::Formatter.new 65 | end 66 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | CfMysqlBroker::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | # config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | end 37 | -------------------------------------------------------------------------------- /config/environments/travis_test.rb: -------------------------------------------------------------------------------- 1 | CfMysqlBroker::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | # config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | end 37 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.action_dispatch.cookies_serializer = :json 2 | -------------------------------------------------------------------------------- /config/initializers/omniauth.rb: -------------------------------------------------------------------------------- 1 | unless Rails.env.assets? 2 | OmniAuth.config.failure_raise_out_environments = [] 3 | OmniAuth.config.path_prefix = '/manage/auth' 4 | 5 | client = Settings.services[0].dashboard_client 6 | 7 | Rails.application.config.middleware.use OmniAuth::Builder do 8 | unless (Rails.env.ci_test? || Rails.env.test? || Rails.env.development?) 9 | provider :cloudfoundry, client.id, client.secret, { 10 | auth_server_url: Configuration.auth_server_url, 11 | token_server_url: Configuration.token_server_url, 12 | scope: %w(cloud_controller_service_permissions.read openid), 13 | skip_ssl_validation: Settings.skip_ssl_validation 14 | } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.session_store :cookie_store, key: '_cf_mysql_broker_session' 2 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper 4 | 5 | # Enable parameter wrapping for JSON. 6 | # ActiveSupport.on_load(:action_controller) do 7 | # wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 8 | # end 9 | 10 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | CfMysqlBroker::Application.routes.draw do 2 | resource :preview, only: [:show] 3 | 4 | namespace :v2 do 5 | resource :catalog, only: [:show] 6 | patch 'service_instances/:id' => 'service_instances#set_plan' 7 | resources :service_instances, only: [:update, :destroy] do 8 | resources :service_bindings, only: [:update, :destroy] 9 | end 10 | end 11 | 12 | namespace :manage do 13 | get 'auth/cloudfoundry/callback' => 'auth#create' 14 | get 'auth/failure' => 'auth#failure' 15 | delete 'auth' => 'auth#destroy' 16 | resources :instances, only: [:show] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | development: 2 | secret_token: <%= Settings.cookie_secret %> 3 | 4 | test: 5 | secret_token: <%= Settings.cookie_secret %> 6 | 7 | production: 8 | secret_token: <`%= Settings.cookie_secret %> 9 | -------------------------------------------------------------------------------- /config/settings.yml: -------------------------------------------------------------------------------- 1 | # This file should not be used in deployed environments. Instead, set 2 | # the SETTINGS_PATH environment variable to point to a configuration 3 | # file that contains these settings. 4 | 5 | defaults: &defaults 6 | allow_table_locks: true 7 | database_config_path: 'config/database.yml' 8 | auth_username: cc 9 | cookie_secret: e7247dae-a252-4393-afa3-2219c1c02efd 10 | session_expiry: 86400 11 | message_bus_servers: 12 | - nats://127.0.0.1:4222 13 | external_host: p-mysql.bosh-lite.com 14 | external_ip: 127.0.0.1 15 | ssl_enabled: false 16 | skip_ssl_validation: true 17 | persistent_disk: 10000 18 | gcache_size: 50 19 | ib_log_file_size: 100 20 | 21 | services: 22 | - name: p-mysql-local 23 | id: 3101b971-1044-4816-a7ac-9ded2e028079 24 | description: MySQL service for application development and testing 25 | plan_updateable: true 26 | tags: 27 | - mysql 28 | - relational 29 | metadata: 30 | displayName: "Pivotal MySQL Dev" 31 | imageUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALIAAABMCAYAAADJEu0BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyNpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjBDOTlBMkE3NDQ3MDExRTNCN0I5RDc0MjE2NTBCMjgzIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjBDOTlBMkE4NDQ3MDExRTNCN0I5RDc0MjE2NTBCMjgzIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MEM5OUEyQTU0NDcwMTFFM0I3QjlENzQyMTY1MEIyODMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MEM5OUEyQTY0NDcwMTFFM0I3QjlENzQyMTY1MEIyODMiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7RmE0vAAAGn0lEQVR42uycTYgjRRTH38heFNTJiFdlO3rx2nMQQXbADAwePAg94ElByJxW0Eui4HmSo3OQ3RZkERFMHwTxIpODtznMNLuuKIim/dZZD7b4wXrZjZ3wauZtWZ2uTqfjJP3/QTGTTnem+tW/Xr33uiYr1L44pBIZ7u6tEAAlcxdMACBkACBkACBkACBksHycyzrh9ae2aO3ue05exzdv0sEP39L+118S3b4NC4LFEPKL64/Tw6tr/zn+6fHPtPH2m/T7n3/AiuDsC1mxdeUS3fj7L3ro/lV6+YkLtOE8Sm9sPUPPB+/CimBxYuTrvx7TtR+/pw8/v07Pvv/O+NiT5+uwIECyB8DcQ4sRq/feR4898CC9dqExfv1J9BUsCBZPyB889wJtnH9k/PthEma88vFHsCBYPCG/dXhAvc+u0rVffqKD774hGg5hQbB4Qn7v6iEsBpDsAQAhA1AktPjixvH4sfQ/t27BWmBxhfz0lUuwEkBoAQCEDACEDCBkACBkAObPyhCPmQE8MgAQMgAQMgAQMli+ZI/aF0c/vaS5SYuT1s1xvbpuRLtgX2pJayZt9O8njjgeJK3PbRINbp523OfPiDKub3Efit6H+hydmPsQzHIAh7t7ULEQck8IYNNCNIqBEF2Rb910uQ8OD3gsxK1EEXLfYsMEuCz6H2nv1YSg24br9Xsp+u2hA20imgTd575EEPJsQwtXHGtaXtsQwivqifeFNxx9iUad2xqLN+D3Y8ME2GcRB3xuXbt+mydBU/ydebBtaF2+h1F/j3LYGmQgd79FbOSG5bVqEPqG5TwPTSFiU1ijwopaiid2J1yrQpOAz1ViXp+DbYOUY23uR4f7VMsZzgGLZE95vpalR+7PYHlsTBh4fUnWY1GXRWAjhB3+G67l/ZWJL8KklsUq4RR0FpUUMll4ZWV8Uyzds4gTG3xOS4uH8yaGcc7krJszfCqTkPtTY+9sosN2GrBdh/zzxFYrr77US9ogaan2Tt5r8DmtqghZZdUNLW42CVGvcDhigLI8iMfnRHw+iWXWtlpSm6ICEPLkc3KEUGUiY2Z9oh6Jia5WnUDE17Up7V0JIZPwst6ECkPDICJXLJtxhtfzxKTp8mC4YvBslloSkyCvmCljos6TmO9X9kfG/uv8s80JY5vvv5XD3qPxioa7e0GVhOyzyLwpY9o4w+up5M4Xxzb5tcPL6W+clKWJ2k2Jm22Ipghl5pEUOuLePDHJ08Kj8fgk4jyxdxI6eIawosmf7VchRnYMxk1brppiiZ6UzKTFop5hIsScjNXZ44Q8CToiljZ55Gk94FnGs0h+I20i+hNWUdtkeuk8srxpz2AUx8IofSHGmiZAFZZEKQPU5eW0LgZIlaqoQEgxi0lQBk6KkGWip7c7auqJVz6xd+KBZSKonFGQnBNVUcghN08zdDPH7A5EdUEfJJsnhxF76XX+vSm8S1hAlM4MJkMZQg4ybCmbbwgVitp7KYWc5pXz1I59Q0aukrw88Zo8XxfyNLVVVXE5C0uty80kNPmEsm1oXYvx8jjJ86ssZF2IrZwlL5WEuFqbRkBRSuji5hRziz3gWfFQLYPHDLVJawWHDuMHPklIMW4F7L1UQtaFqDyZnyP2lElInrAk7bNjQ/besaxAqCd6eR+ilIXa6BRq3jWckCjbVkCalkljJYRMWrLVSDHKpCdzfRFrN8Rr3Su1LAXoa4PWZZHvU/YDAbkx6f9MfEYiOxLVn03t/a6wWZpdaiZ7c504otPtrP3kWFgFIZ/LKM/0hWHSzoksvERH86K6yFweWCX0WCyvnhCgXjpTx0af36PTDUaR8ORyr/WORXw+sKjI7FjYdpCxak3aVrrDE6/DfQ/ptOSmwql2yr34GfZeWiFnLcvKMFm14ywhpyV5m+x5vJTlNOSBTfMs6tHtZTrdXG8S3zbZ1ZCzKiG2satjWLkiUXWY5ABCrtZ02C5eSlVpkr3jKiR5CrWxvuzM/IgHb8di8F1twPKEATVNaMrT10Qsumgxo35Pfbqzhkwg5zfWF4gJiexKblHB+FUvq6k4usX96C2goGOqSMJWZrI3C7yMpXAeQlCbb9R+5B6vEgBCtiJv7blMIo6T62T3z6igwqGFJ+JaVT6Kzlj2rAQNIOSJ8XBDW9YhGrBwQvbptJjfZ0+MJRyUDr6NEyDZAwBCBgBCBgBCBhAyABAyABAyABAygJABgJABgJABgJABhAwAhAwAhAwAhAwgZAAgZAAgZAAgZAAgZLBc/CvAALNW7XVv+EUjAAAAAElFTkSuQmCC" 32 | longDescription: "A MySQL relational database service for development and testing. The MySQL server is multi-tenant and is not replicated." 33 | providerDisplayName: "Pivotal Software" 34 | documentationUrl: "http://docs.run.pivotal.io" 35 | supportUrl: "http://support.run.pivotal.io/home" 36 | dashboard_client: 37 | id: 'p-mysql' 38 | secret: 'p-mysql' 39 | redirect_uri: 'http://10.80.130.98:3000/manage/auth/cloudfoundry/callback' 40 | plans: 41 | - name: 5mb 42 | id: 2451fa22-df16-4c10-ba6e-1f682d3dcdc9 43 | description: Free Trial 44 | max_storage_mb: 5 45 | max_user_connections: 5 46 | displayName: 5MB 47 | metadata: 48 | costs: 49 | - amount: 50 | usd: 0.0 51 | unit: MONTHLY 52 | 53 | bullets: 54 | - Shared MySQL server 55 | - 5 MB storage 56 | - 40 concurrent connections 57 | - name: 10mb 58 | id: f488f238-f364-4712-808d-cacfc49db053 59 | description: Free Trial - 10MB 60 | max_storage_mb: 10 61 | max_user_connections: 10 62 | displayName: 10MB 63 | metadata: 64 | costs: 65 | - amount: 66 | usd: 0.0 67 | unit: MONTHLY 68 | 69 | bullets: 70 | - Shared MySQL server 71 | - 10 MB storage 72 | - 40 concurrent connections 73 | 74 | cc_api_uri: 'https://api.bosh-lite.com' 75 | 76 | assets: 77 | auth_password: 'secret' 78 | <<: *defaults 79 | 80 | development: 81 | auth_password: 'secret' 82 | <<: *defaults 83 | 84 | test: 85 | auth_password: 'secret' 86 | <<: *defaults 87 | 88 | ci_test: 89 | auth_password: 'secret' 90 | <<: *defaults 91 | -------------------------------------------------------------------------------- /db/migrate/20140529214059_create_service_instances.rb: -------------------------------------------------------------------------------- 1 | class CreateServiceInstances < ActiveRecord::Migration 2 | class ServiceInstance < ActiveRecord::Base 3 | end 4 | 5 | class DatabaseNameToServiceInstanceGuidConverter 6 | DATABASE_PREFIX = 'cf_'.freeze 7 | 8 | def self.guid_from_database_name(database_name) 9 | guid = database_name.sub(DATABASE_PREFIX, '').gsub('_', '-') 10 | 11 | # MySQL database names are limited to [0-9,a-z,A-Z$_] and 64 chars 12 | if guid =~ /[^0-9,a-z,A-Z$-]+/ 13 | raise 'Only ids matching [0-9,a-z,A-Z$-]+ are allowed' 14 | end 15 | 16 | guid 17 | end 18 | end 19 | 20 | def up 21 | services = Settings['services'] 22 | plans = services.first['plans'] 23 | plan_guid = plans.first['id'] 24 | 25 | raise "Migration cannot be performed: no service plans found" unless (1 == services.length && plans.length >= 1 ) 26 | puts "Migration will associate existing service instances with the first service plan from the catalog (id: #{plan_guid})" unless (1 == services.length && 1 == plans.length) 27 | 28 | create_table :service_instances do |t| 29 | t.string :guid 30 | t.string :plan_guid 31 | end 32 | 33 | schema_names = connection.select_values("SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE schema_name LIKE 'cf\\_%'") 34 | schema_names.each do |name| 35 | guid = DatabaseNameToServiceInstanceGuidConverter.guid_from_database_name(name) 36 | ServiceInstance.create(guid: guid, plan_guid: plan_guid) 37 | end 38 | end 39 | 40 | def down 41 | services = Settings['services'] 42 | plans = services.first['plans'] 43 | raise 'Migration can only be run if the catalog has a single service with one plan' unless (1 == services.length && 1 == plans.length) 44 | 45 | drop_table :service_instances 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /db/migrate/20140616163621_add_quota_to_service_instances.rb: -------------------------------------------------------------------------------- 1 | class AddQuotaToServiceInstances < ActiveRecord::Migration 2 | def change 3 | add_column :service_instances, :max_storage_mb, :integer, null: false, default: 0 4 | add_index :service_instances, :guid 5 | add_index :service_instances, :plan_guid 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20140616212709_add_db_name_to_service_instances.rb: -------------------------------------------------------------------------------- 1 | class AddDbNameToServiceInstances < ActiveRecord::Migration 2 | def change 3 | add_column :service_instances, :db_name, :string 4 | add_index :service_instances, :db_name 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20140918004518_set_db_name.rb: -------------------------------------------------------------------------------- 1 | class SetDbName < ActiveRecord::Migration 2 | def up 3 | ServiceInstance.reset_column_information 4 | ServiceInstance.find_each do |instance| 5 | instance.db_name = "cf_#{instance.guid.gsub('-', '_')}" 6 | instance.save! 7 | end 8 | end 9 | 10 | def down 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20180117002358_create_read_only_users.rb: -------------------------------------------------------------------------------- 1 | class CreateReadOnlyUsers < ActiveRecord::Migration 2 | def change 3 | create_table :read_only_users do |t| 4 | t.string :username 5 | t.string :grantee 6 | 7 | t.timestamps null: false 8 | end 9 | 10 | add_index :read_only_users, :username 11 | add_index :read_only_users, :grantee 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20180117002358) do 15 | 16 | create_table "read_only_users", force: :cascade do |t| 17 | t.string "username", limit: 255 18 | t.string "grantee", limit: 255 19 | t.datetime "created_at", null: false 20 | t.datetime "updated_at", null: false 21 | end 22 | 23 | add_index "read_only_users", ["grantee"], name: "index_read_only_users_on_grantee", using: :btree 24 | add_index "read_only_users", ["username"], name: "index_read_only_users_on_username", using: :btree 25 | 26 | create_table "service_instances", force: :cascade do |t| 27 | t.string "guid", limit: 255 28 | t.string "plan_guid", limit: 255 29 | t.integer "max_storage_mb", limit: 4, default: 0, null: false 30 | t.string "db_name", limit: 255 31 | end 32 | 33 | add_index "service_instances", ["db_name"], name: "index_service_instances_on_db_name", using: :btree 34 | add_index "service_instances", ["guid"], name: "index_service_instances_on_guid", using: :btree 35 | add_index "service_instances", ["plan_guid"], name: "index_service_instances_on_plan_guid", using: :btree 36 | 37 | end 38 | -------------------------------------------------------------------------------- /lib/configuration.rb: -------------------------------------------------------------------------------- 1 | module Configuration 2 | extend self 3 | 4 | def documentation_url 5 | Settings.services.first.metadata.documentationUrl rescue nil 6 | end 7 | 8 | def support_url 9 | Settings.services.first.metadata.supportUrl rescue nil 10 | end 11 | 12 | def manage_user_profile_url 13 | "#{auth_server_url}/profile" 14 | end 15 | 16 | def auth_server_url 17 | cc_api_info["authorization_endpoint"] 18 | end 19 | 20 | def auth_server_logout_url 21 | "#{cc_api_info["authorization_endpoint"]}/logout.do" 22 | end 23 | 24 | def token_server_url 25 | cc_api_info["token_endpoint"] 26 | end 27 | 28 | def cc_api_info 29 | return store[:cc_api_info] unless store[:cc_api_info].nil? 30 | 31 | cc_client = CloudControllerHttpClient.new 32 | response = cc_client.get('/info') 33 | 34 | store[:cc_api_info] = response 35 | end 36 | 37 | def store 38 | @store ||= {} 39 | end 40 | 41 | def clear 42 | store.clear 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/request_response_logger.rb: -------------------------------------------------------------------------------- 1 | class RequestResponseLogger 2 | attr_reader :logger 3 | attr_reader :message_type 4 | 5 | def initialize(message_type, logger) 6 | @message_type = message_type 7 | @logger = logger 8 | end 9 | 10 | def log_headers_and_body(headers, body, log_all_headers=false) 11 | headers_to_log = log_all_headers ? headers : remove_non_permitted_headers(headers) 12 | 13 | request_summary = { 14 | headers: filtered_headers(headers_to_log), 15 | body: body 16 | } 17 | 18 | logger.info " #{message_type} #{request_summary.to_json}" 19 | end 20 | 21 | private 22 | 23 | def filtered_headers(headers) 24 | headers.keys.each do |k| 25 | headers[k] = '[PRIVATE DATA HIDDEN]' if filtered_keys.include?(k) 26 | end 27 | 28 | headers 29 | end 30 | 31 | def remove_non_permitted_headers(headers) 32 | headers.select { |key, _| permitted_keys.include? key } 33 | end 34 | 35 | def permitted_keys 36 | %w(CONTENT_LENGTH 37 | CONTENT_TYPE 38 | GATEWAY_INTERFACE 39 | PATH_INFO 40 | QUERY_STRING 41 | REMOTE_ADDR 42 | REMOTE_HOST 43 | REQUEST_METHOD 44 | REQUEST_URI 45 | SCRIPT_NAME 46 | SERVER_NAME 47 | SERVER_PORT 48 | SERVER_PROTOCOL 49 | SERVER_SOFTWARE 50 | HTTP_ACCEPT 51 | HTTP_USER_AGENT 52 | HTTP_AUTHORIZATION 53 | HTTP_X_VCAP_REQUEST_ID 54 | HTTP_X_BROKER_API_VERSION 55 | HTTP_HOST 56 | HTTP_VERSION 57 | REQUEST_PATH) 58 | end 59 | 60 | def filtered_keys 61 | %w(HTTP_AUTHORIZATION) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/service_capacity.rb: -------------------------------------------------------------------------------- 1 | class ServiceCapacity 2 | def self.can_allocate?(new_db_storage_mb) 3 | estimated_overhead + ServiceInstance.reserved_space_in_mb + new_db_storage_mb <= Settings['persistent_disk'] 4 | end 5 | 6 | private 7 | 8 | def self.estimated_overhead 9 | #constant overhead on initialize of cluster values are in MB 10 | ibdata_file_size = 12 11 | broker_db_size = 0.26 12 | gcache_size = Settings['gcache_size'] 13 | mysql_db_size = 1.2 14 | performance_db_size = 0.22 15 | 16 | #despite innodb_log_per_table=true, there are only ever 2 ib_log files 17 | ib_log_file_size = 2 * Settings['ib_log_file_size'] 18 | 19 | current_db_overhead = 0.073 * ServiceInstance.all.count 20 | 21 | #can edit, for the sake of safety... undeleted GRA_*.logs & other logs 22 | buffer = 50 23 | 24 | ibdata_file_size + broker_db_size + gcache_size + 25 | mysql_db_size + performance_db_size + ib_log_file_size + 26 | current_db_overhead + buffer 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/service_instance_manager.rb: -------------------------------------------------------------------------------- 1 | class ServiceInstanceManager 2 | class ServiceInstanceNotFound < StandardError; end 3 | class ServicePlanNotFound < StandardError; end 4 | class InvalidServicePlanUpdate < StandardError; end 5 | 6 | DATABASE_PREFIX = 'cf_'.freeze 7 | 8 | def self.create(opts) 9 | guid = opts[:guid] 10 | plan_guid = opts[:plan_guid] 11 | 12 | unless Catalog.has_plan?(plan_guid) 13 | raise "Plan #{plan_guid} was not found in the catalog." 14 | end 15 | 16 | max_storage_mb = Catalog.storage_quota_for_plan_guid(plan_guid) 17 | 18 | if guid =~ /[^0-9,a-z,A-Z$-]+/ 19 | raise 'Only GUIDs matching [0-9,a-z,A-Z$-]+ are allowed' 20 | end 21 | 22 | db_name = database_name_from_service_instance_guid(guid) 23 | 24 | Database.create(db_name) 25 | ServiceInstance.create(guid: guid, plan_guid: plan_guid, max_storage_mb: max_storage_mb, db_name: db_name) 26 | end 27 | 28 | def self.set_plan(opts) 29 | guid = opts[:guid] 30 | plan_guid = opts[:plan_guid] 31 | 32 | unless Catalog.has_plan?(plan_guid) 33 | raise ServicePlanNotFound.new("Plan #{plan_guid} was not found in the catalog.") 34 | end 35 | 36 | instance = ServiceInstance.find_by_guid(guid) 37 | raise ServiceInstanceNotFound if instance.nil? 38 | 39 | if Database.usage(database_name_from_service_instance_guid(guid)) > Catalog.storage_quota_for_plan_guid(plan_guid) 40 | raise InvalidServicePlanUpdate.new('Downgrading this service instance will violate the quota of the new plan') 41 | end 42 | 43 | instance.plan_guid = plan_guid 44 | instance.max_storage_mb = Catalog.storage_quota_for_plan_guid(plan_guid) 45 | instance.save 46 | end 47 | 48 | def self.destroy(opts) 49 | guid = opts[:guid] 50 | instance = ServiceInstance.find_by_guid(guid) 51 | raise ServiceInstanceNotFound if instance.nil? 52 | instance.destroy 53 | Database.drop(database_name_from_service_instance_guid(guid)) 54 | end 55 | 56 | def self.database_name_from_service_instance_guid(guid) 57 | "#{DATABASE_PREFIX}#{guid.gsub('-', '_')}" 58 | end 59 | 60 | def self.sync_service_instances 61 | Catalog.plans.each do |plan| 62 | service_instances = ServiceInstance.where(plan_guid: plan.id) 63 | service_instances.update_all(max_storage_mb: plan.max_storage_mb) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/settings.rb: -------------------------------------------------------------------------------- 1 | ENV['SETTINGS_PATH'] ||= File.expand_path('../../config/settings.yml', __FILE__) 2 | 3 | class Settings < Settingslogic 4 | source ENV['SETTINGS_PATH'] 5 | namespace Rails.env 6 | end 7 | -------------------------------------------------------------------------------- /lib/table_lock_manager.rb: -------------------------------------------------------------------------------- 1 | class TableLockManager 2 | def self.update_table_lock_permissions 3 | connection = ActiveRecord::Base.connection 4 | if Settings.allow_table_locks 5 | connection.select_rows("SELECT User, Db, Host FROM mysql.db WHERE Lock_tables_priv='N'").each do |user, db, host| 6 | next unless user.present? 7 | connection.execute "GRANT LOCK TABLES ON `#{db}`.* TO '#{user}'@'#{host}'" 8 | end 9 | else 10 | connection.select_rows("SELECT User, Db, Host FROM mysql.db WHERE Lock_tables_priv='Y'").each do |user, db, host| 11 | next unless user.present? && db.present? 12 | connection.execute "REVOKE LOCK TABLES ON `#{db}`.* FROM '#{user}'@'#{host}'" 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/tasks/brakeman.rake: -------------------------------------------------------------------------------- 1 | namespace :brakeman do 2 | 3 | desc "Run Brakeman" 4 | task :run, :output_files do |t, args| 5 | require 'brakeman' 6 | 7 | files = args[:output_files].split(' ') if args[:output_files] 8 | Brakeman.run :app_path => ".", :output_files => files, :print_report => true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/tasks/broker.rake: -------------------------------------------------------------------------------- 1 | namespace :broker do 2 | 3 | desc "Update properties of existing service instances to match plans in Settings" 4 | task :sync_plans_in_db do 5 | require File.expand_path('../../../config/environment', __FILE__) 6 | ServiceInstanceManager.sync_service_instances 7 | ServiceBinding.update_all_max_user_connections 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/tasks/table_locks.rake: -------------------------------------------------------------------------------- 1 | namespace :table_locks do 2 | 3 | desc "Update Table Lock Permissions" 4 | task :update_table_lock_permissions => :environment do 5 | TableLockManager.update_table_lock_permissions 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /lib/uaa_session.rb: -------------------------------------------------------------------------------- 1 | class UaaSession 2 | class << self 3 | def build(access_token, refresh_token) 4 | token_info = existing_token_info(access_token, refresh_token) 5 | if token_expired?(token_info) 6 | token_info = refreshed_token_info(refresh_token) 7 | end 8 | 9 | new(token_info) 10 | end 11 | 12 | private 13 | 14 | def token_expired?(token_info) 15 | header = token_info.auth_header 16 | expiry = CF::UAA::TokenCoder.decode(header.split()[1], verify: false)['exp'] 17 | expiry.is_a?(Integer) && expiry <= Time.now.to_i 18 | end 19 | 20 | def existing_token_info(access_token, refresh_token) 21 | CF::UAA::TokenInfo.new(access_token: access_token, 22 | refresh_token: refresh_token, 23 | token_type: 'bearer') 24 | end 25 | 26 | def refreshed_token_info(refresh_token) 27 | dashboard_client = Settings.services[0].dashboard_client 28 | client = CF::UAA::TokenIssuer.new( 29 | Configuration.auth_server_url, 30 | dashboard_client.id, 31 | dashboard_client.secret, 32 | { token_target: Configuration.token_server_url } 33 | ) 34 | client.refresh_token_grant(refresh_token) 35 | end 36 | end 37 | 38 | def initialize(token_info) 39 | @token_info = token_info 40 | end 41 | 42 | def auth_header 43 | token_info.auth_header 44 | end 45 | 46 | def access_token 47 | token_info.info[:access_token] || token_info.info["access_token"] 48 | end 49 | 50 | def refresh_token 51 | token_info.info[:refresh_token] || token_info.info["refresh_token"] 52 | end 53 | 54 | private 55 | 56 | attr_reader :token_info 57 | end 58 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-mysql-broker/499b0398755e020c7cb71aca8be58b242bfd63c1/log/.keep -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: cfmysql 4 | memory: 256M 5 | instances: 1 6 | host: cfmysql 7 | path: . 8 | env: 9 | AUTH_TOKEN: secret 10 | -------------------------------------------------------------------------------- /spec/controllers/manage/auth_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Manage::AuthController do 4 | 5 | describe '#create' do 6 | let(:instance_id) { 'abc-123' } 7 | 8 | before do 9 | session[:instance_id] = instance_id 10 | request.env['omniauth.auth'] = { 11 | 'extra' => extra, 12 | 'credentials' => credentials 13 | } 14 | end 15 | 16 | context 'when access token, refresh token, and user_id are present' do 17 | let(:extra) { 18 | { 19 | 'raw_info' => { 20 | 'user_id' => 'mister_tee' 21 | } 22 | } 23 | } 24 | 25 | let(:credentials) { 26 | { 27 | 'token' => 'UAA access token', 28 | 'refresh_token' => 'UAA refresh token' 29 | } 30 | } 31 | 32 | it 'authenticates the user based on the permissions from UAA' do 33 | get :create, some: 'stuff' 34 | expect(response.status).to eql(302) 35 | expect(response).to redirect_to(manage_instance_path(instance_id)) 36 | 37 | expect(session[:uaa_user_id]).to eql('mister_tee') 38 | expect(session[:uaa_access_token]).to eql('UAA access token') 39 | expect(session[:uaa_refresh_token]).to eql('UAA refresh token') 40 | expect(session[:last_seen]).to be_a_kind_of(Time) 41 | end 42 | end 43 | 44 | context 'when omniauth does not yield an access token' do 45 | let(:extra) { 46 | { 47 | 'raw_info' => { 48 | 'user_id' => 'mister_tee' 49 | } 50 | } 51 | } 52 | 53 | let(:credentials) { 54 | { 55 | 'token' => '', 56 | 'refresh_token' => '', 57 | 'authorized_scopes' => '' 58 | } 59 | } 60 | 61 | it 'renders the approvals error page' do 62 | get :create, some: 'stuff' 63 | 64 | expect(response.status).to eql(200) 65 | expect(response).to render_template 'errors/approvals_error' 66 | end 67 | end 68 | 69 | context 'when omniauth does not yield user info (raw_info)' do 70 | let(:extra) { 71 | {} 72 | } 73 | 74 | let(:credentials) { 75 | { 76 | 'token' => 'access token', 77 | 'refresh_token' => 'refresh token', 78 | 'authorized_scopes' => 'scope.yay' 79 | } 80 | } 81 | 82 | it 'renders the approvals error page' do 83 | get :create, some: 'stuff' 84 | 85 | expect(response.status).to eql(200) 86 | expect(response).to render_template 'errors/approvals_error' 87 | end 88 | end 89 | end 90 | 91 | describe '#failure' do 92 | it 'returns a 403 status code' do 93 | get :failure 94 | expect(response.status).to eql(403) 95 | end 96 | 97 | it 'echos the passed message param as the failure message' do 98 | get :failure, message: 'something broke' 99 | expect(response.body).to eq 'something broke' 100 | end 101 | end 102 | 103 | describe '#destroy' do 104 | before do 105 | session[:foo] = 'bar' 106 | allow(Configuration).to receive(:auth_server_logout_url).and_return('auth_server_logout_url') 107 | end 108 | 109 | it 'destroys the session' do 110 | delete :destroy 111 | expect(session).to be_empty 112 | end 113 | 114 | it 'redirects to GET #{auth_server}/logout.do' do 115 | delete :destroy 116 | expect(response).to redirect_to 'auth_server_logout_url' 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/controllers/manage/instances_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Manage::InstancesController do 4 | 5 | describe 'show' do 6 | render_views 7 | 8 | before do 9 | allow(Settings).to receive(:ssl_enabled).and_return(false) 10 | allow(Settings).to receive(:cc_api_uri) { 'http://api.example.com' } 11 | allow(CF::UAA::TokenCoder).to receive(:decode).and_return('scope' => ['openid', 'cloud_controller_service_permissions.read']) 12 | end 13 | 14 | describe 'redirecting a user that is not logged in' do 15 | context 'when there is no session' do 16 | it 'redirects the user' do 17 | get :show, id: 'abc-123' 18 | expect(response).to redirect_to('/manage/auth/cloudfoundry') 19 | end 20 | end 21 | 22 | context 'when there is a session with a uaa_user_id' do 23 | let(:expiry) { 5 } #seconds 24 | before do 25 | allow(Settings).to receive(:session_expiry).and_return(expiry) 26 | 27 | session[:uaa_user_id] = 'some-user-id' 28 | session[:uaa_access_token] = '' 29 | session[:uaa_refresh_token] = '' 30 | end 31 | 32 | context 'when the last_seen is old' do 33 | before do 34 | session[:last_seen] = Time.now - (expiry+1) 35 | end 36 | 37 | it 'redirects to auth' do 38 | get :show, id: 'abc-123' 39 | expect(response).to redirect_to('/manage/auth/cloudfoundry') 40 | end 41 | end 42 | end 43 | end 44 | 45 | describe 'the dashboard redirects depending on ssl_enabled setting' do 46 | 47 | let(:instance) { ServiceInstance.new(id: 'abc-123') } 48 | let(:uaa_session) { double(UaaSession, auth_header: 'bearer ', refresh_token: 'new_refresh_token') } 49 | 50 | before do 51 | instance.save 52 | 53 | session[:uaa_user_id] = 'some-user-id' 54 | session[:uaa_access_token] = '' 55 | session[:uaa_refresh_token] = '' 56 | session[:last_seen] = Time.now 57 | 58 | allow(UaaSession).to receive(:build).with('', '').and_return(uaa_session) 59 | 60 | allow(uaa_session).to receive(:access_token).and_return('new_access_token') 61 | 62 | allow(ServiceInstanceAccessVerifier).to receive(:can_manage_instance?) 63 | end 64 | 65 | after { instance.destroy } 66 | 67 | context 'when ssl_enabled is false' do 68 | before do 69 | allow(Settings).to receive(:ssl_enabled).and_return(false) 70 | end 71 | 72 | it 'does not redirect http requests to https' do 73 | @request.env['HTTPS'] = nil 74 | get :show, id: 'abc-123' 75 | expect(response.status).to eq 200 76 | end 77 | 78 | it 'does not redirect https requests' do 79 | @request.env['HTTPS'] = 'on' 80 | get :show, id: 'abc-123' 81 | expect(response.status).to eq 200 82 | end 83 | end 84 | 85 | context 'when ssl_enabled is true' do 86 | before do 87 | allow(Settings).to receive(:ssl_enabled).and_return(true) 88 | end 89 | 90 | it 'redirects http requests to https' do 91 | @request.env['HTTPS'] = nil 92 | get :show, id: 'abc-123' 93 | expect(response).to redirect_to("https://#{request.host}#{request.path_info}") 94 | end 95 | 96 | it 'does not redirect https requests' do 97 | @request.env['HTTPS'] = 'on' 98 | get :show, id: 'abc-123', ssl: true 99 | expect(response.status).to eq 200 100 | end 101 | end 102 | end 103 | 104 | describe 'verifying that the user has approved the necessary scopes' do 105 | let(:uaa_session) { double(UaaSession, auth_header: 'bearer ', refresh_token: 'new_refresh_token') } 106 | let(:all_scopes) { ['openid', 'cloud_controller_service_permissions.read'] } 107 | let(:missing_scopes) { ['openid'] } 108 | 109 | before do 110 | session[:uaa_user_id] = 'some-user-id' 111 | session[:uaa_access_token] = '' 112 | session[:uaa_refresh_token] = '' 113 | session[:last_seen] = Time.now 114 | session[:has_retried] = has_retried 115 | 116 | allow(UaaSession).to receive(:build).with('', '').and_return(uaa_session) 117 | 118 | allow(uaa_session).to receive(:access_token).and_return('new_access_token') 119 | allow(CF::UAA::TokenCoder).to receive(:decode).with('new_access_token', verify: false).and_return({'scope' => scopes} ) 120 | 121 | allow(Configuration).to receive(:manage_user_profile_url).and_return('login.com/profile') 122 | end 123 | 124 | 125 | context 'when the user has not approved the necessary scopes' do 126 | let(:scopes) { missing_scopes } 127 | let(:has_retried) { 'true' } 128 | 129 | it 'renders the approval errors page' do 130 | get :show, id: 'abc-123' 131 | 132 | expect(response.status).to eq 200 133 | expect(response.body).to include('This application requires the following permissions') 134 | end 135 | end 136 | 137 | context 'when the user updates his approvals to include the necessary scopes' do 138 | context 'the first attempt that fails' do 139 | let(:scopes) { missing_scopes } 140 | let(:has_retried) { nil } 141 | 142 | it 'redirects to the auth endpoint' do 143 | get :show, id: 'abc-123' 144 | 145 | expect(response).to redirect_to '/manage/auth/cloudfoundry' 146 | end 147 | end 148 | end 149 | end 150 | 151 | context 'when the user is not authenticated' do 152 | it 'stores the instance id in the session and redirects to the auth endpoint' do 153 | get :show, id: 'abc-123' 154 | expect(session[:instance_id]).to eql('abc-123') 155 | expect(response.status).to eql(302) 156 | expect(response).to redirect_to('/manage/auth/cloudfoundry') 157 | end 158 | end 159 | 160 | context 'when the user is authenticated' do 161 | let(:query) { double(ServiceInstanceUsageQuery) } 162 | let(:instance) { ServiceInstance.new(guid: 'abc-123') } 163 | let(:uaa_session) { double(UaaSession, auth_header: 'bearer ') } 164 | 165 | before do 166 | instance.save 167 | allow(ServiceInstanceUsageQuery).to receive(:new).and_return(query) 168 | allow(query).to receive(:execute).and_return(10.3) 169 | 170 | session[:uaa_user_id] = 'some-user-id' 171 | session[:uaa_access_token] = '' 172 | session[:uaa_refresh_token] = '' 173 | session[:last_seen] = Time.now 174 | 175 | allow(UaaSession).to receive(:build).with('', '').and_return(uaa_session) 176 | 177 | allow(uaa_session).to receive(:access_token).and_return('new_access_token') 178 | allow(uaa_session).to receive(:refresh_token).and_return('new_refresh_token') 179 | 180 | allow(ServiceInstanceAccessVerifier).to receive(:can_manage_instance?) 181 | end 182 | 183 | after { instance.destroy } 184 | 185 | it 'updates the last_seen' do 186 | expect { 187 | get(:show, id: 'abc-123') 188 | }.to change { session[:last_seen] } 189 | end 190 | 191 | context 'when the user has permissions to manage the instance' do 192 | before do 193 | allow(Settings).to receive(:cc_api_uri) { 'http://api.example.com' } 194 | 195 | allow(ServiceInstanceAccessVerifier).to receive(:can_manage_instance?). 196 | with('abc-123', anything). 197 | and_return(true) 198 | end 199 | 200 | it 'updates the uaa access token' do 201 | get :show, id: 'abc-123' 202 | 203 | expect(session[:uaa_access_token]).to eql('new_access_token') 204 | end 205 | 206 | it 'updates the uaa refresh token' do 207 | get :show, id: 'abc-123' 208 | 209 | expect(session[:uaa_refresh_token]).to eql('new_refresh_token') 210 | end 211 | 212 | it 'displays the usage information for the given instance' do 213 | quota = instance.max_storage_mb 214 | 215 | get :show, id: 'abc-123' 216 | 217 | expect(response.status).to eql(200) 218 | expect(response.body).to match(/10\.3 MB of #{quota} MB used./) 219 | expect(query).to have_received(:execute).once 220 | end 221 | 222 | context 'when the user is over the quota' do 223 | before do 224 | allow(query).to receive(:execute).and_return(120) 225 | end 226 | it 'displays a warning' do 227 | get :show, id: 'abc-123' 228 | expect(response.body).to include("Warning:") 229 | end 230 | end 231 | end 232 | 233 | context 'when the user does not have permission to manage the instance' do 234 | before do 235 | allow(ServiceInstanceAccessVerifier).to receive(:can_manage_instance?). 236 | with('abc-123', anything). 237 | and_return(false) 238 | end 239 | 240 | it 'displays a "not authorized" message' do 241 | get :show, id: 'abc-123' 242 | expect(response.status).to eql(200) 243 | expect(response.body).to match(/Not\ Authorized/) 244 | expect(query).not_to have_received(:execute) 245 | end 246 | end 247 | end 248 | end 249 | end 250 | -------------------------------------------------------------------------------- /spec/controllers/previews_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe V2::PreviewsController do 4 | describe '#show' do 5 | it 'returns a 400' do 6 | get :show 7 | 8 | expect(response.code).to eq("400") 9 | end 10 | 11 | context 'when Rails.env is development' do 12 | before do 13 | allow(Rails).to receive_messages(env: ActiveSupport::StringInquirer.new("development")) 14 | end 15 | 16 | it 'renders a view that helps developers make CSS/HTML changes' do 17 | get :show 18 | 19 | expect(response).to render_template 'manage/instances/show' 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/controllers/v2/catalogs_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe V2::CatalogsController do 4 | describe '#show' do 5 | let(:make_request) { get :show } 6 | 7 | it_behaves_like 'a controller action that requires basic auth' 8 | 9 | context 'when the basic-auth credentials are correct' do 10 | before { authenticate } 11 | 12 | it_behaves_like 'a controller action that does not log its request and response headers and body' 13 | 14 | it 'builds services from the values in Settings' do 15 | service_setting_1_stub = double(:service_setting_1_stub) 16 | service_setting_2_stub = double(:service_setting_2_stub) 17 | service_1 = double(:service_1, to_hash: {'service1' => 'to_hash'}) 18 | service_2 = double(:service_1, to_hash: {'service2' => 'to_hash'}) 19 | allow(Settings).to receive(:[]).with('services'). 20 | and_return([service_setting_1_stub, service_setting_2_stub]) 21 | expect(Service).to receive(:build).with(service_setting_1_stub).and_return(service_1) 22 | expect(Service).to receive(:build).with(service_setting_2_stub).and_return(service_2) 23 | 24 | make_request 25 | 26 | expect(response.status).to eq(200) 27 | expect(JSON.parse(response.body)).to eq( 28 | {'services' => [ 29 | {'service1' => 'to_hash'}, 30 | {'service2' => 'to_hash'}, 31 | ]} 32 | ) 33 | end 34 | 35 | context 'with invalid catalog data' do 36 | before do 37 | allow(Settings).to receive(:[]).with('services').and_return(nil) 38 | end 39 | 40 | it_behaves_like 'a controller action that does not log its request and response headers and body' 41 | 42 | context 'when there are no services' do 43 | it 'produces an empty catalog' do 44 | make_request 45 | 46 | expect(response.status).to eq(200) 47 | catalog = JSON.parse(response.body) 48 | 49 | services = catalog.fetch('services') 50 | expect(services).to be_empty 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/controllers/v2/service_bindings_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe V2::ServiceBindingsController do 4 | let(:db_settings) { Rails.configuration.database_configuration[Rails.env] } 5 | let(:admin_user) { db_settings.fetch('username') } 6 | let(:admin_password) { db_settings.fetch('password') } 7 | let(:database_host) { db_settings.fetch('host') } 8 | let(:database_port) { db_settings.fetch('port') } 9 | 10 | let(:instance_guid) { 'instance-1' } 11 | let(:instance) { ServiceInstance.new(guid: instance_guid, db_name: database) } 12 | let(:database) { ServiceInstanceManager.database_name_from_service_instance_guid(instance_guid) } 13 | 14 | before do 15 | authenticate 16 | instance.save 17 | 18 | allow(Database).to receive(:exists?).with(database).and_return(true) 19 | allow(Settings).to receive(:allow_table_locks).and_return(true) 20 | end 21 | 22 | after { instance.destroy } 23 | 24 | describe '#update' do 25 | let(:binding_id) { '123' } 26 | let(:generated_dbname) { ServiceInstanceManager.database_name_from_service_instance_guid(instance_guid) } 27 | 28 | let(:generated_username) { ServiceBinding.new(id: binding_id).username } 29 | let(:generated_password) { 'generatedpw' } 30 | 31 | let(:make_request) { put :update, id: binding_id, service_instance_id: instance_guid } 32 | 33 | before { allow(SecureRandom).to receive(:base64).and_return(generated_password, 'notthepassword') } 34 | after { ServiceBinding.new(id: binding_id, service_instance: instance).destroy } 35 | 36 | it_behaves_like 'a controller action that requires basic auth' 37 | 38 | it_behaves_like 'a controller action that does not log its request and response headers and body' 39 | 40 | context 'when the service instance exists' do 41 | it 'grants permission to access the given database' do 42 | expect(ServiceBinding.exists?(id: binding_id, service_instance_guid: instance_guid)).to eq(false) 43 | 44 | make_request 45 | 46 | expect(ServiceBinding.exists?(id: binding_id, service_instance_guid: instance_guid)).to eq(true) 47 | end 48 | 49 | it 'returns a 201' do 50 | make_request 51 | 52 | expect(response.status).to eq(201) 53 | end 54 | 55 | it 'responds with generated credentials' do 56 | make_request 57 | 58 | binding = JSON.parse(response.body) 59 | expect(binding['credentials']).to eq( 60 | 'hostname' => database_host, 61 | 'name' => generated_dbname, 62 | 'username' => generated_username, 63 | 'password' => generated_password, 64 | 'port' => database_port, 65 | 'jdbcUrl' => "jdbc:mysql://#{database_host}:#{database_port}/#{generated_dbname}?user=#{generated_username}&password=#{generated_password}", 66 | 'uri' => "mysql://#{generated_username}:#{generated_password}@#{database_host}:#{database_port}/#{generated_dbname}?reconnect=true", 67 | ) 68 | end 69 | 70 | context 'when the read-only parameter is set to the boolean value true' do 71 | let(:make_request) { put :update, id: binding_id, service_instance_id: instance_guid, parameters: {'read-only' => true} } 72 | before { allow(ServiceBinding).to receive(:new).and_call_original } 73 | 74 | it 'creates a binding with read_only: true' do 75 | make_request 76 | 77 | expect(ServiceBinding).to have_received(:new).with(id: binding_id, service_instance: instance_of(ServiceInstance), read_only: true) 78 | end 79 | end 80 | 81 | context 'when the read-only parameter is not set' do 82 | let(:make_request) { put :update, id: binding_id, service_instance_id: instance_guid } 83 | before { allow(ServiceBinding).to receive(:new).and_call_original } 84 | 85 | it 'creates a binding with default read_only: false' do 86 | make_request 87 | 88 | expect(ServiceBinding).to have_received(:new).with(id: binding_id, service_instance: instance_of(ServiceInstance), read_only: false) 89 | end 90 | end 91 | 92 | context 'when the read-only parameter has a non-boolean value' do 93 | let(:make_request) { put :update, id: binding_id, service_instance_id: instance_guid, parameters: {'read-only' => 'true'} } 94 | 95 | it 'does not create a binding' do 96 | make_request 97 | expect(ServiceBinding.exists?(id: binding_id, service_instance_guid: instance_guid)).to eq(false) 98 | end 99 | 100 | it 'returns a 400 and an error message' do 101 | make_request 102 | 103 | expect(response.status).to eq(400) 104 | expect(JSON.parse(response.body)).to eq({ 105 | "error" => "Error creating service binding", 106 | "description" => "Invalid arbitrary parameter syntax. Please check the documentation for supported arbitrary parameters.", 107 | }) 108 | end 109 | end 110 | 111 | context 'when an invalid parameter is provided' do 112 | let(:make_request) { put :update, id: binding_id, service_instance_id: instance_guid, parameters: {'unexpected-parameter' => true} } 113 | 114 | it 'does not create a binding' do 115 | make_request 116 | expect(ServiceBinding.exists?(id: binding_id, service_instance_guid: instance_guid)).to eq(false) 117 | end 118 | 119 | it 'returns a 400 and an error message' do 120 | make_request 121 | 122 | expect(response.status).to eq(400) 123 | expect(JSON.parse(response.body)).to eq({ 124 | "error" => "Error creating service binding", 125 | "description" => "Invalid arbitrary parameter syntax. Please check the documentation for supported arbitrary parameters.", 126 | }) 127 | end 128 | end 129 | end 130 | 131 | context 'when the service instance does not exist' do 132 | let(:make_request) { put :update, id: binding_id, service_instance_id: 'non-existent-guid' } 133 | 134 | it 'returns a 404' do 135 | make_request 136 | 137 | expect(response.status).to eq(404) 138 | end 139 | end 140 | end 141 | 142 | describe '#destroy' do 143 | let(:binding_id) { 'BINDING-1' } 144 | let(:binding) { ServiceBinding.new(id: binding_id, service_instance: instance) } 145 | let(:username) { binding.username } 146 | 147 | let(:make_request) { delete :destroy, service_instance_id: instance.id, id: binding.id } 148 | 149 | it_behaves_like 'a controller action that requires basic auth' 150 | 151 | context 'when the binding exists' do 152 | before { binding.save } 153 | after { binding.destroy } 154 | 155 | it_behaves_like 'a controller action that does not log its request and response headers and body' 156 | 157 | it 'destroys the binding' do 158 | expect(ServiceBinding.exists?(id: binding.id, service_instance_guid: instance.guid)).to eq(true) 159 | 160 | make_request 161 | 162 | expect(ServiceBinding.exists?(id: binding.id, service_instance_guid: instance.guid)).to eq(false) 163 | end 164 | 165 | it 'returns a 200' do 166 | make_request 167 | 168 | expect(response.status).to eq(200) 169 | expect(response.body).to eq('{}') 170 | end 171 | end 172 | 173 | context 'when the binding does not exist' do 174 | it_behaves_like 'a controller action that does not log its request and response headers and body' 175 | 176 | it 'returns a 410' do 177 | make_request 178 | 179 | expect(response.status).to eq(410) 180 | end 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /spec/controllers/v2/service_instances_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe V2::ServiceInstancesController do 4 | let(:instance_id) { '88f6fa22-c8b7-4cdc-be3a-dc09ea7734db' } 5 | 6 | before { authenticate } 7 | 8 | # this is actually the create 9 | describe '#update' do 10 | let(:max_storage_mb) { 5 } 11 | let(:db_name) { ServiceInstanceManager.database_name_from_service_instance_guid(instance_id) } 12 | let(:services) do 13 | [ 14 | { 15 | 'id' => 'foo', 16 | 'name' => 'bar', 17 | 'description' => 'desc', 18 | 'bindable' => true, 19 | 'plans' => [ 20 | { 21 | 'id' => 'plan_id', 22 | 'name' => 'plan_name', 23 | 'description' => 'desc', 24 | 'max_storage_mb' => 5, 25 | }, 26 | { 27 | 'id' => 'new-plan-guid', 28 | 'name' => 'plan_name', 29 | 'description' => 'desc', 30 | 'max_storage_mb' => 12, 31 | } 32 | ] 33 | } 34 | ] 35 | end 36 | let(:plan_id) { 'plan_id' } 37 | let(:make_request) do 38 | put :update, { 39 | id: instance_id, 40 | plan_id: plan_id 41 | } 42 | end 43 | 44 | before do 45 | allow(Settings).to receive(:[]).with('services').and_return(services) 46 | allow(Settings).to receive(:[]).with('ssl_enabled').and_return(true) 47 | allow(ServiceCapacity).to receive(:can_allocate?).with(max_storage_mb).and_return(true) 48 | end 49 | 50 | it_behaves_like 'a controller action that requires basic auth' 51 | 52 | it_behaves_like 'a controller action that does not log its request and response headers and body' 53 | 54 | context 'when ssl is set to false' do 55 | before do 56 | allow(Settings).to receive(:[]).with('ssl_enabled').and_return(false) 57 | end 58 | 59 | it 'returns a dashboard URL without https' do 60 | make_request 61 | 62 | instance = JSON.parse(response.body) 63 | expect(instance).to eq({'dashboard_url' => "http://p-mysql.bosh-lite.com/manage/instances/#{instance_id}"}) 64 | end 65 | end 66 | 67 | context 'when the provided plan_id is not present in the catalog' do 68 | let(:plan_id) { "does-not-exist-in-catalog" } 69 | 70 | it 'does not attempt to create a service instance' do 71 | expect(ServiceInstanceManager).not_to receive(:create) 72 | make_request 73 | end 74 | 75 | it 'returns a 422 status code with a descriptive error message' do 76 | make_request 77 | 78 | expect(response.status).to eq(422) 79 | body = JSON.parse(response.body) 80 | expect(body['description']).to match /Cannot create a service instance. Plan does-not-exist-in-catalog was not found in the catalog./ 81 | end 82 | end 83 | 84 | context 'when creating additional instances is allowed' do 85 | before do 86 | allow(ServiceCapacity).to receive(:can_allocate?).with(max_storage_mb) { true } 87 | end 88 | 89 | it 'returns a 201' do 90 | make_request 91 | expect(response.status).to eq(201) 92 | end 93 | 94 | it 'tells the ServiceInstanceManager to create an instance with the correct attributes' do 95 | expect(ServiceInstanceManager).to receive(:create).with({ 96 | guid: instance_id, 97 | plan_guid: plan_id 98 | }).and_return(ServiceInstance.new(guid: instance_id, plan_guid: plan_id, max_storage_mb: max_storage_mb, db_name: db_name)) 99 | 100 | make_request 101 | end 102 | 103 | it 'returns the dashboard_url' do 104 | make_request 105 | 106 | instance = JSON.parse(response.body) 107 | expect(instance).to eq({'dashboard_url' => "https://p-mysql.bosh-lite.com/manage/instances/#{instance_id}"}) 108 | end 109 | end 110 | 111 | context 'when creating additional instances is not allowed' do 112 | before do 113 | allow(ServiceCapacity).to receive(:can_allocate?).with(max_storage_mb) { false } 114 | end 115 | 116 | it 'returns a 507' do 117 | make_request 118 | 119 | expect(response.status).to eq(507) 120 | response_json = JSON.parse(response.body) 121 | expect(response_json['description']).to eq('Service capacity has been reached') 122 | end 123 | 124 | it 'does not attempt to create a service instance' do 125 | expect(ServiceInstanceManager).not_to receive(:create) 126 | make_request 127 | end 128 | end 129 | end 130 | 131 | describe '#set_plan' do 132 | let(:plan_id) { 'new-plan-guid' } 133 | let(:make_request) { patch :set_plan, id: instance_id, plan_id: plan_id } 134 | 135 | before do 136 | allow(ServiceInstanceManager).to receive(:set_plan) 137 | 138 | request_body = {plan_id: 'new-plan-guid'}.to_json 139 | request.env['RAW_POST_DATA'] = request_body 140 | end 141 | 142 | it_behaves_like 'a controller action that requires basic auth' 143 | 144 | it_behaves_like 'a controller action that does not log its request and response headers and body' 145 | 146 | context 'when the service instance exists' do 147 | before do 148 | ServiceInstance.create(guid: instance_id, plan_guid: 'some-plan-guid') 149 | end 150 | 151 | it 'returns a 200' do 152 | make_request 153 | 154 | expect(response.status).to eq(200) 155 | body = JSON.parse(response.body) 156 | expect(body).to eq({}) 157 | end 158 | 159 | it 'tells the service instance manager to change the plan of the instance' do 160 | expect(ServiceInstanceManager).to receive(:set_plan).with({ 161 | guid: instance_id, 162 | plan_guid: 'new-plan-guid' 163 | }) 164 | 165 | make_request 166 | end 167 | end 168 | 169 | context 'when the service instance does not exist' do 170 | before do 171 | allow(ServiceInstanceManager).to receive(:set_plan).with({ 172 | guid: instance_id, 173 | plan_guid: 'new-plan-guid' 174 | }).and_raise(ServiceInstanceManager::ServiceInstanceNotFound) 175 | end 176 | 177 | it 'returns a 404' do 178 | make_request 179 | 180 | expect(response.status).to eq(404) 181 | expect(response.body).to eq '{"description":"Service instance not found"}' 182 | end 183 | end 184 | 185 | context 'when the service plan does not exist' do 186 | before do 187 | allow(ServiceInstanceManager).to receive(:set_plan).with({ 188 | guid: instance_id, 189 | plan_guid: 'new-plan-guid' 190 | }).and_raise(ServiceInstanceManager::ServicePlanNotFound) 191 | end 192 | 193 | it 'returns a 400' do 194 | make_request 195 | 196 | expect(response.status).to eq(400) 197 | expect(response.body).to eq '{"description":"Service plan not found"}' 198 | end 199 | end 200 | 201 | context 'when the service plan cannot be updated' do 202 | before do 203 | allow(ServiceInstanceManager).to receive(:set_plan).and_raise(ServiceInstanceManager::InvalidServicePlanUpdate.new('cannot downgrade')) 204 | end 205 | 206 | it 'returns a 422 and forwards the error message' do 207 | make_request 208 | 209 | expect(response.status).to eq 422 210 | expect(response.body).to eq '{"description":"cannot downgrade"}' 211 | end 212 | end 213 | end 214 | 215 | describe '#destroy' do 216 | let(:make_request) { delete :destroy, id: instance_id } 217 | 218 | it_behaves_like 'a controller action that requires basic auth' 219 | 220 | it_behaves_like 'a controller action that does not log its request and response headers and body' 221 | 222 | context 'when the service instance exists' do 223 | before do 224 | ServiceInstance.create(guid: instance_id, plan_guid: 'some-plan-guid') 225 | end 226 | 227 | it 'returns a 200' do 228 | make_request 229 | 230 | expect(response.status).to eq(200) 231 | body = JSON.parse(response.body) 232 | expect(body).to eq({}) 233 | end 234 | 235 | it 'tells the service instance manager to destroy the instance' do 236 | expect(ServiceInstanceManager).to receive(:destroy).with({ 237 | guid: instance_id 238 | }) 239 | 240 | make_request 241 | end 242 | end 243 | 244 | context 'when the service instance does not exist' do 245 | it 'returns a 410' do 246 | expect(ServiceInstanceManager).to receive(:destroy).with({ 247 | guid: instance_id 248 | }).and_raise(ServiceInstanceManager::ServiceInstanceNotFound) 249 | 250 | make_request 251 | 252 | expect(response.status).to eq(410) 253 | end 254 | end 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /spec/features/plan_update_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Plan Upgrade' do 4 | 5 | let(:instance_id_0) { SecureRandom.uuid } 6 | let(:binding_id_0) { SecureRandom.uuid } 7 | let(:plan_0) { Settings.services[0].plans[0] } 8 | let(:plan_1) { Settings.services[0].plans[1] } 9 | let(:max_storage_mb_0) { plan_0.max_storage_mb.to_i } 10 | let(:max_storage_mb_1) { plan_1.max_storage_mb.to_i } 11 | 12 | before do 13 | allow(Settings).to receive(:allow_table_locks).and_return(true) 14 | end 15 | 16 | after do 17 | delete "/v2/service_instances/#{instance_id_0}/service_bindings/#{binding_id_0}" 18 | delete "/v2/service_instances/#{instance_id_0}" 19 | end 20 | 21 | specify 'User updates to a plan with a larger quota' do 22 | put "/v2/service_instances/#{instance_id_0}", {plan_id: plan_0.id} 23 | put "/v2/service_instances/#{instance_id_0}/service_bindings/#{binding_id_0}" 24 | binding_0 = JSON.parse(response.body) 25 | credentials_0 = binding_0.fetch('credentials') 26 | 27 | # Fill db past the limit of plan 0 28 | client_0 = create_mysql_client(credentials_0) 29 | create_table_and_write_data(client_0, max_storage_mb_0) 30 | 31 | # Change instance to plan 1 32 | patch "/v2/service_instances/#{instance_id_0}", { plan_id: plan_1.id, previous_values: {} } 33 | expect(response.status).to eq 200 34 | 35 | # Verify that we can write 36 | client_0 = create_mysql_client(credentials_0) 37 | verify_client_can_write(client_0) 38 | end 39 | 40 | specify 'User tries to downgrade to a plan with a smaller quota than he is currently using' do 41 | # Create db with larger quota 42 | put "/v2/service_instances/#{instance_id_0}", {plan_id: plan_1.id} 43 | put "/v2/service_instances/#{instance_id_0}/service_bindings/#{binding_id_0}" 44 | binding_0 = JSON.parse(response.body) 45 | credentials_0 = binding_0.fetch('credentials') 46 | 47 | # Fill db past limit of plan 0 48 | client_0 = create_mysql_client(credentials_0) 49 | create_table_and_write_data(client_0, max_storage_mb_0 + 1) 50 | 51 | # Attempt to change instance to plan 0 52 | patch "/v2/service_instances/#{instance_id_0}", { plan_id: plan_0.id, previous_values: {} } 53 | expect(response.status).to eq 422 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/lib/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Configuration do 4 | 5 | before do 6 | Configuration.clear 7 | stub_request(:any, "#{Settings.cc_api_uri}/info"). 8 | to_return(body: JSON.generate({ 9 | name: 'vcap', 10 | build: '2222', 11 | support: 'http://support.cloudfoundry.com', 12 | version: 2, 13 | description: 'Cloud Foundry sponsored by Pivotal', 14 | authorization_endpoint: 'http://login.bosh-lite.com', 15 | token_endpoint: 'https://uaa.bosh-lite.com', 16 | allow_debug: true 17 | })) 18 | end 19 | 20 | describe '#auth_server_url' do 21 | it 'uses the cc_api_uri to get the url for the auth server' do 22 | expect(Configuration.auth_server_url).to eql('http://login.bosh-lite.com') 23 | expect(a_request(:get, "#{Settings.cc_api_uri}/info")).to have_been_made 24 | end 25 | end 26 | 27 | describe '#auth_server_logout_url' do 28 | it 'uses the cc_api_uri to return the auth server logout url' do 29 | expect(Configuration.auth_server_logout_url).to eql('http://login.bosh-lite.com/logout.do') 30 | end 31 | end 32 | 33 | describe '#token_server_url' do 34 | it 'uses the cc_api_uri to get the url for the token server' do 35 | expect(Configuration.token_server_url).to eql('https://uaa.bosh-lite.com') 36 | expect(a_request(:get, "#{Settings.cc_api_uri}/info")).to have_been_made 37 | end 38 | end 39 | 40 | describe '#documentation_url' do 41 | it 'uses the documentationUrl of the first service in the catalog' do 42 | expect(Configuration.documentation_url).to eql('http://docs.run.pivotal.io') 43 | end 44 | 45 | context 'when the catalog is empty' do 46 | before do 47 | allow(Settings).to receive(:services).and_return([]) 48 | end 49 | 50 | it 'is nil' do 51 | expect(Configuration.documentation_url).to be_nil 52 | end 53 | end 54 | end 55 | 56 | describe '#support_url' do 57 | it 'uses the supportUrl of the first service in the catalog' do 58 | expect(Configuration.support_url).to eql('http://support.run.pivotal.io/home') 59 | end 60 | 61 | context 'when the catalog is empty' do 62 | before do 63 | allow(Settings).to receive(:services).and_return([]) 64 | end 65 | 66 | it 'is nil' do 67 | expect(Configuration.support_url).to be_nil 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/lib/request_response_logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RequestResponseLogger do 4 | describe "#log_headers_and_body" do 5 | let(:fake_rails_logger) { double(:fake_rails_logger) } 6 | let(:request_response_logger) { described_class.new("Message:", fake_rails_logger) } 7 | 8 | let(:headers) { 9 | { 10 | "CONTENT_TYPE" => 'application/json', 11 | "HTTP_AUTHORIZATION" => 'basic: some-auth-token', 12 | "THIS_KEY_SHOULD_NOT_BE_LOGGED" => 'unknown' 13 | } 14 | } 15 | 16 | it 'logs the request headers and body' do 17 | expect(fake_rails_logger).to receive(:info) do |log_message| 18 | json_log_message = log_message.sub(/^\s+Message:\s+/, "") 19 | 20 | request_info = JSON.parse(json_log_message) 21 | expect(request_info['body']).to eq "body" 22 | expect(request_info['headers']["CONTENT_TYPE"]).to eq "application/json" 23 | end 24 | 25 | request_response_logger.log_headers_and_body(headers, "body") 26 | end 27 | 28 | it 'filters out sensitive data headers' do 29 | expect(fake_rails_logger).to receive(:info) do |log_message| 30 | json_log_message = log_message.sub(/^\s+Message:\s+/, "") 31 | 32 | request_info = JSON.parse(json_log_message) 33 | expect(request_info['headers']["HTTP_AUTHORIZATION"]).not_to match "some-auth-token" 34 | end 35 | 36 | request_response_logger.log_headers_and_body(headers, "body") 37 | end 38 | 39 | it 'does not log unknown headers' do 40 | expect(fake_rails_logger).to receive(:info) do |log_message| 41 | json_log_message = log_message.sub(/^\s+Message:\s+/, "") 42 | 43 | request_info = JSON.parse(json_log_message) 44 | expect(request_info['headers']).not_to have_key("THIS_KEY_SHOULD_NOT_BE_LOGGED") 45 | end 46 | 47 | request_response_logger.log_headers_and_body(headers, "body") 48 | end 49 | 50 | context 'when log_all_headers is true' do 51 | it 'filters out sensitive data headers' do 52 | expect(fake_rails_logger).to receive(:info) do |log_message| 53 | json_log_message = log_message.sub(/^\s+Message:\s+/, "") 54 | 55 | request_info = JSON.parse(json_log_message) 56 | expect(request_info['headers']["HTTP_AUTHORIZATION"]).not_to match "some-auth-token" 57 | end 58 | 59 | request_response_logger.log_headers_and_body(headers, "body", true) 60 | end 61 | 62 | it 'logs unknown headers' do 63 | expect(fake_rails_logger).to receive(:info) do |log_message| 64 | json_log_message = log_message.sub(/^\s+Message:\s+/, "") 65 | 66 | request_info = JSON.parse(json_log_message) 67 | expect(request_info['headers']).to have_key("THIS_KEY_SHOULD_NOT_BE_LOGGED") 68 | end 69 | 70 | request_response_logger.log_headers_and_body(headers, "body", true) 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/lib/service_capacity_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ServiceCapacity do 4 | describe '.can_allocate?' do 5 | before do 6 | allow(Settings).to receive(:[]).with('persistent_disk').and_return(1000) 7 | allow(Settings).to receive(:[]).with('gcache_size').and_return(10) 8 | allow(Settings).to receive(:[]).with('ib_log_file_size').and_return(5) 9 | allow(ServiceInstance).to receive(:reserved_space_in_mb).and_return(900) 10 | allow(ServiceInstance).to receive_message_chain(:all, :count).and_return(5) 11 | end 12 | 13 | it 'returns true when the allocated space + requested space is < the storage capacity' do 14 | expect(described_class.can_allocate?(15)).to eq true 15 | end 16 | 17 | it 'returns true when the allocated space + requested space is = the storage capacity' do 18 | expect(described_class.can_allocate?(15.947)).to eq true 19 | end 20 | 21 | it 'returns false when the allocated space + requested space is > the storage capacity' do 22 | expect(described_class.can_allocate?(16)).to eq false 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/lib/service_instance_manager_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ServiceInstanceManager do 4 | let(:instance_id) { '88f6fa22-c8b7-4cdc-be3a-dc09ea7734db' } 5 | let(:database_name) { 'cf_88f6fa22_c8b7_4cdc_be3a_dc09ea7734db' } 6 | let(:plan_id) { '8888-ffff' } 7 | let(:non_existent_plan_id) { 'non-existent-guid' } 8 | let(:max_storage_mb) { 300 } 9 | 10 | before do 11 | allow(Catalog).to receive(:has_plan?).with(plan_id).and_return(true) 12 | allow(Catalog).to receive(:has_plan?).with(non_existent_plan_id).and_return(false) 13 | allow(Catalog).to receive(:storage_quota_for_plan_guid).with(plan_id).and_return(max_storage_mb) 14 | end 15 | 16 | describe '.database_name_from_service_instance_guid' do 17 | it 'converts instance_id to database_name' do 18 | expect(ServiceInstanceManager.database_name_from_service_instance_guid(instance_id)).to eq(database_name) 19 | end 20 | end 21 | 22 | describe '.create' do 23 | after { 24 | Database.drop(database_name) 25 | } 26 | 27 | it 'saves a ServiceInstance in the broker database' do 28 | expect { described_class.create(guid: instance_id, plan_guid: plan_id) }. 29 | to change(ServiceInstance, :count).from(0).to(1) 30 | expect(ServiceInstance.last.guid).to eq(instance_id) 31 | expect(ServiceInstance.last.plan_guid).to eq(plan_id) 32 | expect(ServiceInstance.last.max_storage_mb).to eq (max_storage_mb) 33 | expect(ServiceInstance.last.db_name).to eq (database_name) 34 | end 35 | 36 | it 'creates a new MySQL database' do 37 | described_class.create(guid: instance_id, plan_guid: plan_id) 38 | expect(Database.exists?(database_name)).to eq true 39 | end 40 | 41 | context 'when creating the MySQL database fails' do 42 | before do 43 | expect(Database).to receive(:create).and_raise(ActiveRecord::ActiveRecordError) 44 | end 45 | 46 | it 'does not save a ServiceInstance in the broker database' do 47 | expect { 48 | begin 49 | described_class.create(guid: instance_id, plan_guid: plan_id) 50 | rescue ActiveRecord::ActiveRecordError 51 | end 52 | }.not_to change(ServiceInstance, :count) 53 | end 54 | end 55 | 56 | context 'when the plan guid is not in the catalog' do 57 | 58 | it 'raises an error' do 59 | expect { 60 | described_class.create(guid: instance_id, plan_guid: non_existent_plan_id) 61 | }. to raise_error(RuntimeError, "Plan #{non_existent_plan_id} was not found in the catalog.") 62 | end 63 | 64 | it 'does not save a ServiceInstance in the broker database' do 65 | expect { 66 | begin 67 | described_class.create(guid: instance_id, plan_guid: non_existent_plan_id) 68 | rescue RuntimeError 69 | end 70 | }.not_to change(ServiceInstance, :count) 71 | end 72 | 73 | it 'does not try to create a database' do 74 | expect(Database).not_to receive(:create) 75 | begin 76 | described_class.create(guid: instance_id, plan_guid: non_existent_plan_id) 77 | rescue RuntimeError 78 | end 79 | end 80 | end 81 | 82 | context 'when the instance guid is of the wrong format' do 83 | it 'raises an error' do 84 | expect { 85 | described_class.create(guid: 'Very$%$%#$BAD--__,,guid', plan_guid: plan_id) 86 | }.to raise_error(RuntimeError, 'Only GUIDs matching [0-9,a-z,A-Z$-]+ are allowed') 87 | end 88 | 89 | it 'does not save a ServiceInstance in the broker database' do 90 | expect { 91 | begin 92 | described_class.create(guid: 'Very$%$%#$BAD--__,,guid', plan_guid: plan_id) 93 | rescue RuntimeError 94 | end 95 | }.not_to change(ServiceInstance, :count) 96 | end 97 | 98 | it 'does not try to create a database' do 99 | expect(Database).not_to receive(:create) 100 | begin 101 | described_class.create(guid: 'Very$%$%#$BAD--__,,guid', plan_guid: plan_id) 102 | rescue RuntimeError 103 | end 104 | end 105 | end 106 | end 107 | 108 | describe '.set_plan' do 109 | let(:new_plan_id) { 'new-plan-id' } 110 | let!(:service_instance) { described_class.create(guid: instance_id, plan_guid: plan_id) } 111 | 112 | before do 113 | allow(Catalog).to receive(:has_plan?).with(new_plan_id).and_return(true) 114 | allow(Catalog).to receive(:storage_quota_for_plan_guid).with(new_plan_id).and_return(12) 115 | end 116 | 117 | it 'changes the plan_guid' do 118 | described_class.set_plan(guid: instance_id, plan_guid: new_plan_id) 119 | service_instance.reload 120 | expect(service_instance.plan_guid).to eq new_plan_id 121 | expect(service_instance.max_storage_mb).to eq 12 122 | end 123 | 124 | context 'when there is no plan with the given guid' do 125 | it 'raises a ServiceInstanceManager::ServicePlanNotFound error' do 126 | expect { described_class.set_plan(guid: instance_id, plan_guid: non_existent_plan_id) }.to raise_error(ServiceInstanceManager::ServicePlanNotFound) 127 | end 128 | end 129 | 130 | context 'when there is no instance with the given guid' do 131 | let!(:service_instance) { nil } 132 | it 'raises a ServiceInstanceManager::ServiceInstanceNotFound error' do 133 | expect { described_class.set_plan(guid: instance_id, plan_guid: new_plan_id) }.to raise_error(ServiceInstanceManager::ServiceInstanceNotFound) 134 | end 135 | end 136 | 137 | context 'when downgrading would put the databases over the quota limit of its new plan' do 138 | before do 139 | db_name = ServiceInstanceManager.database_name_from_service_instance_guid(instance_id) 140 | allow(Database).to receive(:usage).with(db_name).and_return 30 141 | end 142 | 143 | it 'raises an InvalidServicePlanUpdate error' do 144 | expect{ServiceInstanceManager.set_plan(guid: instance_id, plan_guid: new_plan_id)}.to raise_error(ServiceInstanceManager::InvalidServicePlanUpdate) 145 | end 146 | end 147 | end 148 | 149 | describe '.destroy' do 150 | context 'when there is an instance with the given guid' do 151 | before do 152 | described_class.create(guid: instance_id, plan_guid: plan_id) 153 | end 154 | 155 | it 'removes the ServiceInstance from the broker database' do 156 | expect { described_class.destroy(guid: instance_id) }. 157 | to change(ServiceInstance, :count).from(1).to(0) 158 | end 159 | 160 | it 'drops the MySQL database' do 161 | expect(Database.exists?(database_name)).to eq true 162 | described_class.destroy(guid: instance_id) 163 | expect(Database.exists?(database_name)).to eq false 164 | end 165 | end 166 | 167 | context 'when there is no instance with the given guid' do 168 | it 'raises an error' do 169 | expect { 170 | described_class.destroy(guid: instance_id) 171 | }.to raise_error(ServiceInstanceManager::ServiceInstanceNotFound) 172 | end 173 | 174 | it 'does not attempt to drop any databases' do 175 | expect(Database).not_to receive(:drop) 176 | begin 177 | described_class.destroy(guid: instance_id) 178 | rescue ServiceInstanceManager::ServiceInstanceNotFound 179 | end 180 | end 181 | end 182 | end 183 | 184 | describe '.sync_service_instances' do 185 | context 'when the plan db size has changed' do 186 | it 'updates service instance plan sizes' do 187 | # create an instance of default size 188 | instance = described_class.create(guid: instance_id, plan_guid: plan_id) 189 | expect(instance.max_storage_mb).to eq max_storage_mb 190 | 191 | # increase plan size in Catalog 192 | new_plan_size = max_storage_mb + 100 193 | expect(Catalog).to receive(:plans).and_return([ 194 | Plan.build('id' => plan_id, 195 | 'name' => 'plan_name', 196 | 'description' => 'plan description', 197 | 'max_storage_mb' => new_plan_size) 198 | ]) 199 | 200 | # call sync_service_instances 201 | described_class.sync_service_instances 202 | 203 | # expect instance to have same guid but new plan size 204 | updated_instance = ServiceInstance.find_by(id: instance.id) 205 | expect(updated_instance.plan_guid).to eq instance.plan_guid 206 | expect(updated_instance.max_storage_mb).to eq new_plan_size 207 | end 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /spec/lib/table_lock_manager_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | 4 | describe TableLockManager do 5 | let(:instance_guid) {'88f6fa22-c8b7-4cdc-be3a-dc09ea7734db'} 6 | let(:username) {binding.username} 7 | let(:database) {ServiceInstanceManager.database_name_from_service_instance_guid(instance_guid)} 8 | let(:connection) {ServiceInstance.connection} 9 | 10 | let(:instance) {ServiceInstance.new( 11 | guid: instance_guid, 12 | plan_guid: 'plan_guid', 13 | db_name: database) 14 | } 15 | 16 | let(:binding) {ServiceBinding.new(id: 'fa790aea-ab7f-41e8-b6f9-a2a1d60403f5', service_instance: instance)} 17 | 18 | before do 19 | allow(Database).to receive(:exists?).with(database).and_return(true) 20 | end 21 | 22 | after do 23 | begin 24 | connection.execute("DROP USER #{username}") 25 | rescue ActiveRecord::StatementInvalid => e 26 | raise unless e.message =~ /DROP USER failed/ 27 | end 28 | end 29 | 30 | def fetch_grants 31 | grants = connection.select_values("SHOW GRANTS FOR #{username}") 32 | 33 | matching_grants = grants.select {|grant| grant.match(/GRANT .* ON `#{database}`\.\* TO '#{username}'@'%'/)} 34 | end 35 | 36 | describe 'update_table_lock_permissions' do 37 | context 'when table locks are disabled' do 38 | before do 39 | allow(Settings).to receive(:allow_table_locks).and_return(true) 40 | binding.save 41 | grants = fetch_grants 42 | expect(grants.length).to eq(1) 43 | expect(grants[0]).to include("ALL PRIVILEGES") 44 | 45 | allow(Settings).to receive(:allow_table_locks).and_return(false) 46 | end 47 | 48 | it 'revokes lock table permissions on all users' do 49 | TableLockManager.update_table_lock_permissions 50 | 51 | grants = fetch_grants 52 | 53 | expect(grants.length).to eq(1) 54 | expect(grants[0]).not_to include("ALL PRIVILEGES") 55 | expect(grants[0]).not_to include("LOCK TABLES") 56 | end 57 | end 58 | 59 | context 'when table locks are enabled' do 60 | before do 61 | allow(Settings).to receive(:allow_table_locks).and_return(false) 62 | binding.save 63 | grants = fetch_grants 64 | expect(grants.length).to eq(1) 65 | expect(grants[0]).not_to include("ALL PRIVILEGES") 66 | 67 | allow(Settings).to receive(:allow_table_locks).and_return(true) 68 | end 69 | 70 | it 'grants lock table permissions on all users' do 71 | TableLockManager.update_table_lock_permissions 72 | 73 | grants = fetch_grants 74 | 75 | expect(grants.length).to eq(1) 76 | expect(grants[0]).to include("ALL PRIVILEGES") 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/lib/uaa_session_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe UaaSession do 4 | let(:access_token) { 'my_access_token' } 5 | let(:refresh_token) { 'my_refresh_token' } 6 | 7 | describe '.build' do 8 | let(:login_url) { 'http://login.example.com' } 9 | let(:uaa_url) { 'http://uaa.example.com' } 10 | let(:dashboard_client_id) { '' } 11 | let(:dashboard_client_secret) { '' } 12 | 13 | before do 14 | allow(Configuration).to receive(:auth_server_url).and_return(login_url) 15 | allow(Configuration).to receive(:token_server_url).and_return(uaa_url) 16 | allow(Settings).to receive(:services).and_return( 17 | [ 18 | double(dashboard_client: double(id: dashboard_client_id, secret: dashboard_client_secret)) 19 | ] 20 | ) 21 | end 22 | 23 | subject { UaaSession.build(access_token, refresh_token) } 24 | 25 | context 'when the access token is not expired' do 26 | before do 27 | allow(CF::UAA::TokenCoder).to receive(:decode).and_return('exp' => 1.minute.from_now.to_i) 28 | end 29 | 30 | it 'sets the access token member' do 31 | expect(subject.access_token).to eq(access_token) 32 | end 33 | 34 | it 'sets the refresh token member' do 35 | expect(subject.refresh_token).to eq(refresh_token) 36 | end 37 | 38 | it 'returns a token that is encoded and can be used in a header' do 39 | expect(subject.auth_header).to eql('bearer my_access_token') 40 | end 41 | end 42 | 43 | context 'when the access token is expired and refreshed token is symbol' do 44 | let(:token_issuer) { double(CF::UAA::TokenIssuer, refresh_token_grant: token_info) } 45 | let(:token_info) { CF::UAA::TokenInfo.new(access_token: 'new_access_token', refresh_token: 'new_refresh_token', token_type: 'bearer') } 46 | 47 | before do 48 | allow(CF::UAA::TokenCoder).to receive(:decode).and_return('exp' => 1.minute.ago.to_i) 49 | 50 | expect(CF::UAA::TokenIssuer).to receive(:new). 51 | with(login_url, dashboard_client_id, dashboard_client_secret, { token_target: uaa_url }). 52 | and_return(token_issuer) 53 | end 54 | 55 | it 'uses the refresh token to get a new access token' do 56 | expect(subject.auth_header).to eql('bearer new_access_token') 57 | end 58 | 59 | it 'updates the access token' do 60 | expect(subject.access_token).to eql('new_access_token') 61 | end 62 | 63 | it 'updates the refresh token' do 64 | expect(subject.refresh_token).to eql('new_refresh_token') 65 | end 66 | end 67 | 68 | context 'when the access token is expired and refreshed token is string' do 69 | let(:token_issuer) { double(CF::UAA::TokenIssuer, refresh_token_grant: token_info) } 70 | let(:token_info) { CF::UAA::TokenInfo.new("access_token" => 'new_access_token', "refresh_token" => 'new_refresh_token', "token_type" => 'bearer') } 71 | 72 | before do 73 | allow(CF::UAA::TokenCoder).to receive(:decode).and_return('exp' => 1.minute.ago.to_i) 74 | 75 | expect(CF::UAA::TokenIssuer).to receive(:new). 76 | with(login_url, dashboard_client_id, dashboard_client_secret, { token_target: uaa_url }). 77 | and_return(token_issuer) 78 | end 79 | 80 | it 'uses the refresh token to get a new access token' do 81 | expect(subject.auth_header).to eql('bearer new_access_token') 82 | end 83 | 84 | it 'updates the access token' do 85 | expect(subject.access_token).to eql('new_access_token') 86 | end 87 | end 88 | end 89 | 90 | end 91 | -------------------------------------------------------------------------------- /spec/models/catalog_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Catalog do 4 | let(:services) { 5 | [ 6 | service_1_attrib, 7 | service_2_attrib 8 | ] 9 | } 10 | let(:service_1_attrib) { 11 | { 12 | 'id' => 'foo', 13 | 'name' => 'bar', 14 | 'description' => 'desc', 15 | 'bindable' => true, 16 | 'plans' => [ 17 | plan_1_attrib, 18 | plan_2_attrib 19 | ] 20 | } 21 | } 22 | let(:service_2_attrib) { 23 | { 24 | 'id' => 'foo2', 25 | 'name' => 'bar2', 26 | 'description' => 'desc2', 27 | 'bindable' => true, 28 | 'plans' => [ 29 | plan_3_attrib 30 | ] 31 | } 32 | } 33 | let(:plan_1_attrib) { 34 | { 35 | 'id' => 'plan_id_1', 36 | 'name' => 'plan_name_1', 37 | 'description' => 'desc1', 38 | 'max_storage_mb' => 5, 39 | 'max_user_connections' => 7, 40 | } 41 | } 42 | let(:plan_2_attrib) { 43 | { 44 | 'id' => 'plan_id_2', 45 | 'name' => 'plan_name_2', 46 | 'description' => 'desc2', 47 | 'max_storage_mb' => 100, 48 | 'max_user_connections' => 40, 49 | } 50 | } 51 | let(:plan_3_attrib) { 52 | { 53 | 'id' => 'plan_id_3', 54 | 'name' => 'plan_name_3', 55 | 'description' => 'desc3', 56 | 'max_storage_mb' => 101, 57 | 'max_user_connections' => 41, 58 | } 59 | } 60 | 61 | before do 62 | allow(Settings).to receive(:[]).with('services').and_return(services) 63 | end 64 | 65 | describe '.plans' do 66 | it 'returns an array of plan objects representing the plans in the catalog' do 67 | catalog_plans = Catalog.plans.map(&:to_hash) 68 | expected_plans = [ 69 | Plan.build(plan_1_attrib), 70 | Plan.build(plan_2_attrib), 71 | Plan.build(plan_3_attrib) 72 | ].map(&:to_hash) 73 | 74 | expect(catalog_plans).to match_array expected_plans 75 | end 76 | end 77 | 78 | describe '.storage_quota_for_plan_guid' do 79 | it 'returns max_storage_mb for the plan with the specified guid' do 80 | expect(Catalog.storage_quota_for_plan_guid('plan_id_2')).to eq(100) 81 | end 82 | 83 | context 'when the plan with the guid is not found' do 84 | it 'returns nil' do 85 | expect(Catalog.storage_quota_for_plan_guid('non-existent-plan')).to be nil 86 | end 87 | end 88 | end 89 | 90 | describe '.connection_quota_for_plan_guid' do 91 | it 'returns max_user_connections for the plan with the specified guid' do 92 | expect(Catalog.connection_quota_for_plan_guid('plan_id_2')).to eq(40) 93 | end 94 | 95 | context 'when the plan with the guid is not found' do 96 | it 'returns nil' do 97 | expect(Catalog.connection_quota_for_plan_guid('non-existent-plan')).to be nil 98 | end 99 | end 100 | end 101 | 102 | describe '.has_plan?' do 103 | it 'returns true if plan_id exists in the catalog' do 104 | expect(Catalog.has_plan?('plan_id_2')).to be true 105 | end 106 | 107 | it 'returns false if plan_id does not exist in the catalog' do 108 | expect(Catalog.has_plan?('plan_id_banana')).to be false 109 | end 110 | 111 | context 'when there are no services' do 112 | let(:services) { [] } 113 | 114 | it 'returns false' do 115 | expect(Catalog.has_plan?('any-plan')).to be false 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/models/cloud_controller_http_client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CloudControllerHttpClient do 4 | describe '#get' do 5 | let(:client) { CloudControllerHttpClient.new(auth_header) } 6 | let(:cc_url) { 'http://api.example.com/cc' } 7 | let(:auth_header) { 'a-correct-header' } 8 | let(:response_body) { '{"something": true}' } 9 | 10 | before do 11 | allow(Settings).to receive(:cc_api_uri).and_return(cc_url) 12 | 13 | stub_request(:get, %r(#{cc_url}/.*)). 14 | to_return(body: response_body) 15 | end 16 | 17 | it 'makes a request to the correct endpoint' do 18 | client.get("/path/to/endpoint") 19 | expect( 20 | a_request(:get, "#{cc_url}/path/to/endpoint"). 21 | with(headers: { 'Authorization' => auth_header }) 22 | ).to have_been_made 23 | end 24 | 25 | it 'returns the parsed response body' do 26 | expect(client.get("/path/to/endpoint")).to eq(JSON.parse(response_body)) 27 | end 28 | 29 | context 'when the cc_url has a trailing slash' do 30 | let(:cc_url) { 'http://api.example.com/cc/' } 31 | 32 | before do 33 | stub_request(:get, %r(#{cc_url}.*)). 34 | to_return(body: response_body) 35 | end 36 | 37 | it 'constructs the request url appropriately' do 38 | client.get("/path/to/endpoint") 39 | expect( 40 | a_request(:get, "#{cc_url}path/to/endpoint") 41 | ).to have_been_made 42 | end 43 | end 44 | 45 | context 'when the CC uri uses https' do 46 | let(:cc_url) { 'https://api.example.com/cc' } 47 | 48 | it 'makes a request to the correct endpoint' do 49 | client.get("/path/to/endpoint") 50 | expect( 51 | a_request(:get, "#{cc_url}/path/to/endpoint"). 52 | with(headers: { 'Authorization' => auth_header }) 53 | ).to have_been_made 54 | end 55 | 56 | describe 'ssl cert verification' do 57 | let(:http) { double(:http) } 58 | let(:response) { double(:response, body: '{}') } 59 | 60 | before do 61 | allow(Net::HTTP).to receive(:new).and_return(http) 62 | allow(http).to receive(:use_ssl=) 63 | allow(http).to receive(:verify_mode=) 64 | allow(http).to receive(:request).and_return(response) 65 | end 66 | 67 | it 'sets use_ssl to true' do 68 | client.get('/a/path') 69 | expect(http).to have_received(:use_ssl=).with(true) 70 | end 71 | 72 | context 'when skip_ssl_validation is false' do 73 | before do 74 | allow(Settings).to receive(:skip_ssl_validation).and_return(false) 75 | end 76 | 77 | it 'verifies the ssl cert' do 78 | client.get('/a/path') 79 | expect(http).to have_received(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER) 80 | end 81 | end 82 | 83 | context 'when skip_ssl_validation is true' do 84 | before do 85 | allow(Settings).to receive(:skip_ssl_validation).and_return(true) 86 | end 87 | 88 | it 'does not verify the ssl cert' do 89 | client.get('/a/path') 90 | expect(http).to have_received(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) 91 | end 92 | end 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/models/database_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Database do 4 | let(:db_name) { 'database_name' } 5 | 6 | describe '.create' do 7 | after { connection.execute("DROP DATABASE `#{db_name}`") } 8 | 9 | it 'creates a new database' do 10 | Database.create(db_name) 11 | expect(connection.select_value("SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE schema_name='#{db_name}'")).to eq(1) 12 | end 13 | 14 | context 'when the database already exists' do 15 | before do 16 | Database.create(db_name) 17 | end 18 | 19 | it 'avoids collisions with existing databases' do 20 | expect { 21 | Database.create(db_name) 22 | }.to_not change { 23 | connection.select_values("SHOW DATABASES LIKE '#{db_name}'").count 24 | }.from(1) 25 | end 26 | end 27 | end 28 | 29 | describe '.drop' do 30 | context 'when the database exists' do 31 | before { connection.execute("CREATE DATABASE `#{db_name}`") } 32 | 33 | it 'drops the database' do 34 | expect { 35 | Database.drop(db_name) 36 | }.to change { 37 | connection.select_values("SHOW DATABASES LIKE '#{db_name}'").count 38 | }.from(1).to(0) 39 | end 40 | end 41 | 42 | context 'when the database does not exist' do 43 | it 'does not raise an error' do 44 | expect { 45 | Database.drop('unknown_database') 46 | }.to_not raise_error 47 | end 48 | end 49 | end 50 | 51 | describe '.exists?' do 52 | context 'when the database exists' do 53 | before { connection.execute("CREATE DATABASE `#{db_name}`") } 54 | after { connection.execute("DROP DATABASE `#{db_name}`") } 55 | 56 | it 'returns true' do 57 | expect(Database.exists?(db_name)).to eq(true) 58 | end 59 | end 60 | 61 | context 'when the database does not exist' do 62 | it 'returns false' do 63 | expect(Database.exists?(db_name)).to eq(false) 64 | end 65 | end 66 | end 67 | 68 | describe '.usage' do 69 | let(:mb_string) { 'a' * 1024 * 1024 } 70 | before { Database.create(db_name) } 71 | after { Database.drop(db_name) } 72 | 73 | it 'returns the data usage of the db in megabytes' do 74 | connection.execute("CREATE TABLE #{db_name}.mytable (id MEDIUMINT, data LONGTEXT)") 75 | connection.execute("INSERT INTO #{db_name}.mytable (id, data) VALUES (1, '#{mb_string}')") 76 | connection.execute("INSERT INTO #{db_name}.mytable (id, data) VALUES (2, '#{mb_string}')") 77 | connection.execute("INSERT INTO #{db_name}.mytable (id, data) VALUES (3, '#{mb_string}')") 78 | connection.execute("INSERT INTO #{db_name}.mytable (id, data) VALUES (4, '#{mb_string}')") 79 | 80 | expect(Database.usage(db_name)).to eq 4 81 | end 82 | end 83 | 84 | describe '.with_reconnect' do 85 | before do 86 | allow(Kernel).to receive(:sleep) 87 | 88 | reconnect_count = 0 89 | allow(ActiveRecord::Base.connection).to receive(:reconnect!) do 90 | reconnect_count += 1 91 | if reconnect_count == 1 92 | raise Mysql2::Error.new("fake") 93 | else 94 | allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true) 95 | end 96 | end 97 | 98 | @foo = double('bob') 99 | allow(@foo).to receive(:bar).and_raise(ActiveRecord::ActiveRecordError) 100 | end 101 | 102 | it 'attempts to reconnect every 3 seconds if the connection becomes inactive' do 103 | allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false) 104 | 105 | Database.with_reconnect do 106 | @foo.bar 107 | end 108 | 109 | expect(@foo).to have_received(:bar) 110 | expect(ActiveRecord::Base.connection).to have_received(:reconnect!).twice 111 | expect(Kernel).to have_received(:sleep).with(3.seconds) 112 | end 113 | 114 | it 'stops trying to reconnect eventually, in case there is an unrecoverable error' do 115 | allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false) 116 | allow(ActiveRecord::Base.connection).to receive(:reconnect!).and_raise(Mysql2::Error.new("fake")) 117 | 118 | expect { 119 | Database.with_reconnect do 120 | @foo.bar 121 | end 122 | }.to raise_error(Mysql2::Error) 123 | end 124 | 125 | it 'does not reconnect if there was an error but the connection is active' do 126 | allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true) 127 | 128 | expect { 129 | Database.with_reconnect do 130 | @foo.bar 131 | end 132 | }.to raise_error(ActiveRecord::ActiveRecordError) 133 | 134 | expect(@foo).to have_received(:bar) 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /spec/models/plan_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Plan do 4 | describe '.build' do 5 | it 'sets the attributes correctly' do 6 | plan = Plan.build( 7 | 'id' => 'plan_id', 8 | 'name' => 'plan_name', 9 | 'description' => 'plan_description', 10 | 'metadata' => { 'meta_key' => 'meta_value' }, 11 | 'max_storage_mb' => 50, 12 | 'max_user_connections' => 5, 13 | 'free' => false, 14 | ) 15 | 16 | expect(plan.id).to eq('plan_id') 17 | expect(plan.name).to eq('plan_name') 18 | expect(plan.description).to eq('plan_description') 19 | expect(plan.metadata).to eq({ 'meta_key' => 'meta_value' }) 20 | expect(plan.max_storage_mb).to eq(50) 21 | expect(plan.max_user_connections).to eq(5) 22 | expect(plan.free).to be false 23 | end 24 | 25 | context 'when the id key is missing' do 26 | it 'raises an error' do 27 | expect { 28 | Plan.build( 29 | 'name' => 'plan_name', 30 | 'description' => 'plan_description', 31 | 'metadata' => { 'meta_key' => 'meta_value' }, 32 | 'max_storage_mb' => 50, 33 | 'max_user_connections' => 5, 34 | 'free' => false, 35 | ) 36 | }.to raise_error(KeyError, 'key not found: "id"') 37 | end 38 | end 39 | 40 | context 'when the name key is missing' do 41 | it 'raises an error' do 42 | expect { 43 | Plan.build( 44 | 'id' => 'plan_id', 45 | 'description' => 'plan_description', 46 | 'metadata' => { 'meta_key' => 'meta_value' }, 47 | 'max_storage_mb' => 50, 48 | 'max_user_connections' => 5, 49 | 'free' => false, 50 | ) 51 | }.to raise_error(KeyError, 'key not found: "name"') 52 | end 53 | end 54 | 55 | context 'when the description key is missing' do 56 | it 'raises an error' do 57 | expect { 58 | Plan.build( 59 | 'id' => 'plan_id', 60 | 'name' => 'plan_name', 61 | 'metadata' => { 'meta_key' => 'meta_value' }, 62 | 'max_storage_mb' => 50, 63 | 'max_user_connections' => 5, 64 | 'free' => false, 65 | ) 66 | }.to raise_error(KeyError, 'key not found: "description"') 67 | end 68 | end 69 | 70 | context 'when the metadata key is missing' do 71 | let(:plan) do 72 | Plan.build( 73 | 'id' => 'plan_id', 74 | 'name' => 'plan_name', 75 | 'description' => 'plan_description', 76 | 'max_storage_mb' => 50, 77 | 'max_user_connections' => 5, 78 | 'free' => false, 79 | ) 80 | end 81 | 82 | it 'sets the field to nil' do 83 | expect(plan.metadata).to be_nil 84 | end 85 | end 86 | 87 | context 'when the max_storage_mb key is missing' do 88 | let(:plan) do 89 | Plan.build( 90 | 'id' => 'plan_id', 91 | 'name' => 'plan_name', 92 | 'description' => 'plan_description', 93 | 'metadata' => { 'meta_key' => 'meta_value' }, 94 | 'max_user_connections' => 5, 95 | 'free' => false, 96 | ) 97 | end 98 | 99 | it 'sets the field to nil' do 100 | expect(plan.max_storage_mb).to be_nil 101 | end 102 | end 103 | 104 | context 'when the max_user_connections key is missing' do 105 | let(:plan) do 106 | Plan.build( 107 | 'id' => 'plan_id', 108 | 'name' => 'plan_name', 109 | 'description' => 'plan_description', 110 | 'metadata' => { 'meta_key' => 'meta_value' }, 111 | 'max_storage_mb' => 50, 112 | 'free' => false, 113 | ) 114 | end 115 | 116 | it 'sets the field to nil' do 117 | expect(plan.max_user_connections).to be_nil 118 | end 119 | end 120 | 121 | context 'when the free key is missing' do 122 | let(:plan) do 123 | Plan.build( 124 | 'id' => 'plan_id', 125 | 'name' => 'plan_name', 126 | 'description' => 'plan_description', 127 | 'metadata' => { 'meta_key' => 'meta_value' }, 128 | 'max_storage_mb' => 50, 129 | 'max_user_connections' => 5, 130 | ) 131 | end 132 | 133 | it 'sets the field to true' do 134 | expect(plan.free).to be true 135 | end 136 | end 137 | end 138 | 139 | describe '#to_hash' do 140 | it 'contains the correct values' do 141 | plan = Plan.new( 142 | 'id' => 'plan_id', 143 | 'name' => 'plan_name', 144 | 'description' => 'plan_description', 145 | 'metadata' => { 'key1' => 'value1' }, 146 | 'max_storage_mb' => 50, 147 | 'max_user_connections' => 5, 148 | 'free' => false, 149 | ) 150 | 151 | expect(plan.to_hash.fetch('id')).to eq('plan_id') 152 | expect(plan.to_hash.fetch('name')).to eq('plan_name') 153 | expect(plan.to_hash.fetch('description')).to eq('plan_description') 154 | expect(plan.to_hash.fetch('metadata')).to eq({ 'key1' => 'value1' }) 155 | expect(plan.to_hash.fetch('max_storage_mb')).to eq(50) 156 | expect(plan.to_hash.fetch('max_user_connections')).to eq(5) 157 | expect(plan.to_hash.fetch('free')).to be false 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /spec/models/service_binding_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ServiceBinding do 4 | let(:id) { 'fa790aea-ab7f-41e8-b6f9-a2a1d60403f5' } 5 | let(:username) { Digest::MD5.base64digest(id)[0...16] } 6 | let(:password) { 'randompassword' } 7 | let(:binding) { ServiceBinding.new(id: id, service_instance: instance) } 8 | 9 | let(:instance_guid) { '88f6fa22-c8b7-4cdc-be3a-dc09ea7734db' } 10 | let(:plan_guid) { 'plan-guid' } 11 | let(:instance) { ServiceInstance.new( 12 | guid: instance_guid, 13 | plan_guid: plan_guid, 14 | db_name: database) 15 | } 16 | let(:database) { ServiceInstanceManager.database_name_from_service_instance_guid(instance_guid) } 17 | let(:connection_quota) { 12 } 18 | 19 | before do 20 | allow(SecureRandom).to receive(:base64).and_return(password, 'notthepassword') 21 | allow(Database).to receive(:exists?).with(database).and_return(true) 22 | allow(Catalog).to receive(:connection_quota_for_plan_guid).with(plan_guid).and_return(connection_quota) 23 | allow(Settings).to receive(:allow_table_locks).and_return(true) 24 | end 25 | 26 | after do 27 | begin 28 | allow(connection).to receive(:execute).and_call_original 29 | connection.execute("DROP USER #{username}") 30 | rescue ActiveRecord::StatementInvalid => e 31 | raise unless e.message =~ /DROP USER failed/ 32 | end 33 | end 34 | 35 | describe '.find_by_id' do 36 | context 'when the user exists' do 37 | before { connection.execute("CREATE USER '#{username}' IDENTIFIED BY '#{password}'") } 38 | 39 | it 'returns the binding' do 40 | binding = ServiceBinding.find_by_id(id) 41 | expect(binding).to be_a(ServiceBinding) 42 | expect(binding.id).to eq(id) 43 | end 44 | end 45 | 46 | context 'when the user does not exist' do 47 | it 'returns nil' do 48 | binding = ServiceBinding.find_by_id(id) 49 | expect(binding).to be_nil 50 | end 51 | end 52 | end 53 | 54 | describe '.find_by_id_and_service_instance_guid' do 55 | context 'when the user exists and has all privileges' do 56 | before { binding.save } 57 | 58 | it 'returns the binding' do 59 | binding = ServiceBinding.find_by_id_and_service_instance_guid(id, instance_guid) 60 | expect(binding).to be_a(ServiceBinding) 61 | expect(binding.id).to eq(id) 62 | end 63 | end 64 | 65 | context 'when the user exists but does not have all privileges' do 66 | before { connection.execute("CREATE USER '#{username}' IDENTIFIED BY '#{password}'") } 67 | 68 | it 'returns nil' do 69 | binding = ServiceBinding.find_by_id_and_service_instance_guid(id, instance_guid) 70 | expect(binding).to be_nil 71 | end 72 | end 73 | 74 | context 'when the user does not exist' do 75 | it 'returns nil' do 76 | binding = ServiceBinding.find_by_id_and_service_instance_guid(id, instance_guid) 77 | expect(binding).to be_nil 78 | end 79 | end 80 | end 81 | 82 | describe '.update_all_max_user_connections' do 83 | let(:users) { ["fake-user"] } 84 | let(:plan) do 85 | Plan.new( 86 | { 87 | 'id' => plan_guid, 88 | 'max_user_connections' => 45, 89 | 'name' => 'fake-plan-name', 90 | 'description' => 'some-silly-description', 91 | } 92 | ) 93 | end 94 | 95 | before do 96 | allow(Catalog).to receive(:plans).and_return([plan]) 97 | end 98 | 99 | it 'updates max user connections for all plans' do 100 | expect(Catalog).to receive(:plans) 101 | expect(connection).to receive(:select_values). 102 | with( 103 | <<-SQL 104 | SELECT mysql.user.user 105 | FROM service_instances 106 | JOIN mysql.db ON service_instances.db_name=mysql.db.Db 107 | JOIN mysql.user ON mysql.user.User=mysql.db.User 108 | WHERE plan_guid='#{plan.id}' AND mysql.user.user NOT LIKE 'root' 109 | SQL 110 | ).and_return(users) 111 | 112 | expect(connection).to receive(:execute). 113 | with( 114 | <<-SQL 115 | GRANT USAGE ON *.* TO '#{users[0]}'@'%' 116 | WITH MAX_USER_CONNECTIONS #{plan.max_user_connections} 117 | SQL 118 | ) 119 | ServiceBinding.update_all_max_user_connections 120 | end 121 | end 122 | 123 | describe '.exists?' do 124 | context 'when the user exists and has all privileges' do 125 | before { binding.save } 126 | 127 | it 'returns true' do 128 | expect(ServiceBinding.exists?(id: id, service_instance_guid: instance_guid)).to eq(true) 129 | end 130 | end 131 | 132 | context 'when the user exists but does not have all privileges' do 133 | before { connection.execute("CREATE USER '#{username}' IDENTIFIED BY '#{password}'") } 134 | 135 | it 'returns false' do 136 | expect(ServiceBinding.exists?(id: id, service_instance_guid: instance_guid)).to eq(false) 137 | end 138 | end 139 | 140 | context 'when the user does not exist' do 141 | it 'returns false' do 142 | expect(ServiceBinding.exists?(id: id, service_instance_guid: instance_guid)).to eq(false) 143 | end 144 | end 145 | end 146 | 147 | describe '#username' do 148 | it 'returns the same username for a given id' do 149 | binding1 = ServiceBinding.new(id: 'some_id') 150 | binding2 = ServiceBinding.new(id: 'some_id') 151 | expect(binding1.username).to eq (binding2.username) 152 | end 153 | 154 | it 'returns different usernames for different ids' do 155 | binding1 = ServiceBinding.new(id: 'some_id') 156 | binding2 = ServiceBinding.new(id: 'some_other_id') 157 | expect(binding2.username).to_not eq (binding1.username) 158 | end 159 | 160 | it 'returns only alphanumeric characters' do 161 | # MySQL doesn't explicitly require this, but we're doing it to be safe 162 | binding = ServiceBinding.new(id: '~!@#$%^&*()_+{}|:"<>?') 163 | expect(binding.username).to match /^[a-zA-Z0-9]+$/ 164 | end 165 | 166 | it 'returns no more than 16 characters' do 167 | # MySQL usernames cannot be greater than 16 characters 168 | binding = ServiceBinding.new(id: 'fa790aea-ab7f-41e8-b6f9-a2a1d60403f5') 169 | expect(binding.username.length).to be <= 16 170 | end 171 | end 172 | 173 | describe '#save' do 174 | it 'creates a user with a random password' do 175 | expect { 176 | binding.save 177 | }.to change { 178 | password_sql = "SELECT * FROM mysql.user WHERE user = '#{username}' AND password = PASSWORD('#{password}')" 179 | connection.select_values(password_sql).count 180 | }.from(0).to(1) 181 | end 182 | 183 | context 'when table locks are enabled' do 184 | it 'grants the user all privileges including for LOCK TABLES' do 185 | expect { 186 | connection.select_values("SHOW GRANTS FOR #{username}") 187 | }.to raise_error(ActiveRecord::StatementInvalid, /no such grant/) 188 | 189 | binding.save 190 | 191 | grants = connection.select_values("SHOW GRANTS FOR #{username}") 192 | 193 | matching_grants = grants.select { |grant| grant.match(/GRANT .* ON `#{database}`\.\* TO '#{username}'@'%'/) } 194 | 195 | expect(matching_grants.length).to eq(1) 196 | expect(matching_grants[0]).to include("ALL PRIVILEGES") 197 | end 198 | end 199 | 200 | context 'when table locks are disabled' do 201 | before do 202 | allow(Settings).to receive(:allow_table_locks).and_return(false) 203 | end 204 | 205 | it 'grants the user all privileges except for LOCK TABLES' do 206 | expect { 207 | connection.select_values("SHOW GRANTS FOR #{username}") 208 | }.to raise_error(ActiveRecord::StatementInvalid, /no such grant/) 209 | 210 | binding.save 211 | 212 | grants = connection.select_values("SHOW GRANTS FOR #{username}") 213 | 214 | matching_grants = grants.select { |grant| grant.match(/GRANT .* ON `#{database}`\.\* TO '#{username}'@'%'/) } 215 | 216 | expect(matching_grants.length).to eq(1) 217 | expect(matching_grants[0]).not_to include("ALL PRIVILEGES") 218 | expect(matching_grants[0]).not_to include("LOCK TABLES") 219 | end 220 | end 221 | 222 | it 'sets the max connections to the value specified by the plan' do 223 | binding.save 224 | 225 | max_user_connection_sql = "WITH MAX_USER_CONNECTIONS #{connection_quota}" 226 | expect(connection.select_values("SHOW GRANTS FOR #{username}")[0]).to include(max_user_connection_sql) 227 | end 228 | 229 | it 'raises an error when creating the same user twice' do 230 | binding.save 231 | 232 | expect { 233 | ServiceBinding.new(id: id, service_instance: instance).save 234 | }.to raise_error(ActiveRecord::StatementInvalid) 235 | 236 | password_sql = "SELECT * FROM mysql.user WHERE user = '#{username}' AND password = PASSWORD('#{password}')" 237 | expect(connection.select_values(password_sql).count).to eq(1) 238 | end 239 | 240 | context 'when the database does not exist' do 241 | before { allow(Database).to receive(:exists?).with(database).and_return(false) } 242 | 243 | it 'raises an error' do 244 | expect{binding.save}.to raise_error(DatabaseNotFoundError) 245 | end 246 | end 247 | 248 | context 'when an error occurs creating the user' do 249 | 250 | let(:db_error) do 251 | ActiveRecord::StatementInvalid.new( 252 | "Lost connection to MySQL server during query: CREATE USER '#{username}' IDENTIFIED BY '#{password}'") 253 | end 254 | 255 | before do 256 | expect(connection).to receive(:execute).with(/CREATE USER/).and_raise(db_error) 257 | end 258 | 259 | it 'redacts the password before re-raising the error' do 260 | expect{binding.save}.to raise_error { |error| 261 | expect(error.message).to_not include password 262 | } 263 | end 264 | 265 | it 'retains the original error message' do 266 | expect{binding.save}.to raise_error { |error| 267 | expect(error.message).to eq "Lost connection to MySQL server during query: CREATE USER '#{username}' IDENTIFIED BY 'redacted'" 268 | } 269 | end 270 | 271 | it 'retains the original error backtrace' do 272 | expect{binding.save}.to raise_error { |error| 273 | expect(error.backtrace).to eq db_error.backtrace 274 | } 275 | end 276 | end 277 | 278 | context 'when read_only is true' do 279 | before do 280 | binding.read_only = true 281 | end 282 | 283 | it 'creates an entry in the read-only table' do 284 | binding.save 285 | 286 | read_only_user = ReadOnlyUser.find_by_username(username) 287 | expect(read_only_user).to be_present 288 | expect(read_only_user.grantee).to eq("'#{username}'@'%'") 289 | end 290 | 291 | it 'grants the correct set of privileges' do 292 | binding.save 293 | 294 | grants = connection.select_values("SHOW GRANTS FOR #{username}") 295 | 296 | matching_grants = grants.select { |grant| grant.match(/GRANT .* ON `#{database}`\.\* TO '#{username}'@'%'/) } 297 | 298 | expect(matching_grants.length).to eq(1) 299 | expect(matching_grants[0]).to include("GRANT SELECT ON") 300 | end 301 | end 302 | end 303 | 304 | describe '#destroy' do 305 | context 'when the user exists' do 306 | before { binding.save } 307 | 308 | it 'deletes the user' do 309 | grant_sql_regex = /GRANT .* ON `#{database}`\.\* TO '#{username}'@'%'/ 310 | grants = connection.select_values("SHOW GRANTS FOR #{username}") 311 | expect(grants.any? { |grant| grant.match(grant_sql_regex) }).to be_truthy 312 | 313 | binding.destroy 314 | 315 | expect { 316 | connection.select_values("SHOW GRANTS FOR #{username}") 317 | }.to raise_error(ActiveRecord::StatementInvalid, /no such grant/) 318 | end 319 | 320 | context 'when the user is read-only' do 321 | let(:binding) { ServiceBinding.new(id: id, service_instance: instance, read_only: true) } 322 | 323 | it 'also deletes the ReadOnlyUser record' do 324 | expect(ReadOnlyUser.find_by_username(binding.username)).to be_present 325 | 326 | binding.destroy 327 | 328 | expect(ReadOnlyUser.find_by_username(binding.username)).not_to be_present 329 | end 330 | end 331 | end 332 | 333 | context 'when the user does not exist' do 334 | it 'does not raise an error' do 335 | expect { 336 | connection.select_values("SHOW GRANTS FOR #{username}") 337 | }.to raise_error(ActiveRecord::StatementInvalid, /no such grant/) 338 | 339 | expect { 340 | binding.destroy 341 | }.to_not raise_error 342 | 343 | expect { 344 | connection.select_values("SHOW GRANTS FOR #{username}") 345 | }.to raise_error(ActiveRecord::StatementInvalid, /no such grant/) 346 | end 347 | end 348 | end 349 | 350 | describe '#to_json' do 351 | let(:connection_config) { Rails.configuration.database_configuration[Rails.env] } 352 | let(:host) { connection_config.fetch('host') } 353 | let(:port) { connection_config.fetch('port') } 354 | let(:uri) { "mysql://#{username}:#{password}@#{host}:#{port}/#{database}?reconnect=true" } 355 | let(:jdbc_url) { "jdbc:mysql://#{host}:#{port}/#{database}?user=#{username}&password=#{password}" } 356 | let(:tls_ca_certificate) { nil } 357 | 358 | before do 359 | allow(Settings).to receive(:[]).with('tls_ca_certificate').and_return(tls_ca_certificate) 360 | binding.save 361 | end 362 | 363 | it 'includes the credentials' do 364 | hash = JSON.parse(binding.to_json) 365 | credentials = hash.fetch('credentials') 366 | expect(credentials.fetch('hostname')).to eq(host) 367 | expect(credentials.fetch('port')).to eq(port) 368 | expect(credentials.fetch('name')).to eq(database) 369 | expect(credentials.fetch('username')).to eq(username) 370 | expect(credentials.fetch('password')).to eq(password) 371 | expect(credentials.fetch('uri')).to eq(uri) 372 | expect(credentials.fetch('jdbcUrl')).to eq(jdbc_url) 373 | expect(credentials).to_not have_key('ca_certificate') 374 | end 375 | 376 | context 'when the broker starts with a ca cert' do 377 | let(:tls_ca_certificate) { 'this-is-a-ca-certificate' } 378 | 379 | it 'includes the ca_certificate' do 380 | hash = JSON.parse(binding.to_json) 381 | credentials = hash.fetch('credentials') 382 | expect(credentials.fetch('ca_certificate')).to eq(tls_ca_certificate) 383 | end 384 | 385 | it 'adds useSSL to the jdbc url' do 386 | hash = JSON.parse(binding.to_json) 387 | credentials = hash.fetch('credentials') 388 | expect(credentials.fetch('jdbcUrl')).to eq("#{jdbc_url}&useSSL=true&enabledTLSProtocols=TLSv1.2&enabledSslProtocolSuites=TLSv1.2") 389 | end 390 | end 391 | end 392 | end 393 | -------------------------------------------------------------------------------- /spec/models/service_instance_access_verifier_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ServiceInstanceAccessVerifier do 4 | let(:http_client) { double(CloudControllerHttpClient, get: {}) } 5 | let(:instance_guid) { 'an-instance-guid'} 6 | 7 | describe '#can_manage_instance?' do 8 | it 'makes a request to CC' do 9 | ServiceInstanceAccessVerifier.can_manage_instance?(instance_guid, http_client) 10 | expect(http_client).to have_received(:get).with("/v2/service_instances/#{instance_guid}/permissions") 11 | end 12 | 13 | context 'when the user does not approve cloud_controller_service_permissions.read' do 14 | before do 15 | allow(http_client).to receive(:get).and_return(nil) 16 | end 17 | 18 | it 'returns false' do 19 | expect(ServiceInstanceAccessVerifier.can_manage_instance?(instance_guid, http_client)).to eql(false) 20 | end 21 | end 22 | 23 | context 'when the user can manage the service instance' do 24 | before do 25 | allow(http_client).to receive(:get).and_return({'manage' => true}) 26 | end 27 | it 'returns true' do 28 | expect(ServiceInstanceAccessVerifier.can_manage_instance?(instance_guid, http_client)).to eql(true) 29 | end 30 | end 31 | 32 | context "when the user can't manage the service instance" do 33 | before do 34 | allow(http_client).to receive(:get).and_return({'manage' => false}) 35 | end 36 | it 'returns false' do 37 | expect(ServiceInstanceAccessVerifier.can_manage_instance?(instance_guid, http_client)).to eql(false) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/models/service_instance_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ServiceInstance do 4 | let(:plan_guid_1) { '8888-ffff' } 5 | let(:max_storage_mb_1) { 8 } 6 | let(:plan_guid_2) { '4444-ffff' } 7 | let(:max_storage_mb_2) { 4 } 8 | 9 | let(:instance_guid_1) { 'instance-guid-1' } 10 | let(:instance_guid_2) { 'instance-guid-2' } 11 | 12 | before do 13 | allow(Catalog).to receive(:has_plan?).with(plan_guid_1).and_return(true) 14 | allow(Catalog).to receive(:has_plan?).with(plan_guid_2).and_return(true) 15 | 16 | allow(Catalog).to receive(:storage_quota_for_plan_guid).with(plan_guid_1).and_return(max_storage_mb_1) 17 | allow(Catalog).to receive(:storage_quota_for_plan_guid).with(plan_guid_2).and_return(max_storage_mb_2) 18 | end 19 | 20 | describe '.reserved_space_in_mb' do 21 | it 'returns 0 when no instances have been created' do 22 | expect(ServiceInstance.reserved_space_in_mb).to eq 0 23 | end 24 | 25 | it 'returns the sum of max_storage_mb for all existing instances' do 26 | ServiceInstanceManager.create(guid: instance_guid_1, plan_guid: plan_guid_1) 27 | ServiceInstanceManager.create(guid: instance_guid_2, plan_guid: plan_guid_2) 28 | 29 | expect(ServiceInstance.reserved_space_in_mb).to eq 12 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/models/service_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Service do 4 | describe '.build' do 5 | before do 6 | allow(Plan).to receive(:build) 7 | end 8 | 9 | it 'sets fields correctly' do 10 | service = Service.build( 11 | 'id' => 'my-id', 12 | 'name' => 'my-name', 13 | 'description' => 'my description', 14 | 'tags' => ['tagA', 'tagB'], 15 | 'metadata' => { 'stuff' => 'goes here' }, 16 | 'plans' => [], 17 | 'dashboard_client' => { 18 | 'id' => 'sso-client', 19 | 'secret' => 'something-secret', 20 | 'redirect_uri' => 'example.com' 21 | }, 22 | 'plan_updateable' => true 23 | ) 24 | expect(service.id).to eq('my-id') 25 | expect(service.name).to eq('my-name') 26 | expect(service.description).to eq('my description') 27 | expect(service.tags).to eq(['tagA', 'tagB']) 28 | expect(service.metadata).to eq({ 'stuff' => 'goes here' }) 29 | expect(service.dashboard_client).to eql({ 30 | 'id' => 'sso-client', 31 | 'secret' => 'something-secret', 32 | 'redirect_uri' => 'example.com' 33 | }) 34 | expect(service.plan_updateable).to eq true 35 | end 36 | 37 | it 'is bindable' do 38 | service = Service.build( 39 | 'id' => 'my-id', 40 | 'name' => 'my-name', 41 | 'description' => 'my description', 42 | 'tags' => ['tagA', 'tagB'], 43 | 'metadata' => { 'stuff' => 'goes here' }, 44 | 'plans' => [] 45 | ) 46 | 47 | expect(service).to be_bindable 48 | end 49 | 50 | it 'builds plans and sets the plans field' do 51 | plan_attrs = [double(:plan_attr1), double(:plan_attr2)] 52 | plan1 = double(:plan1) 53 | plan2 = double(:plan2) 54 | 55 | allow(Plan).to receive(:build).with(plan_attrs[0]).and_return(plan1) 56 | allow(Plan).to receive(:build).with(plan_attrs[1]).and_return(plan2) 57 | 58 | service = Service.build( 59 | 'plans' => plan_attrs, 60 | 'id' => 'my-id', 61 | 'name' => 'my-name', 62 | 'description' => 'my description', 63 | 'tags' => ['tagA', 'tagB'], 64 | 'metadata' => { 'stuff' => 'goes here' }, 65 | ) 66 | 67 | expect(service.plans).to eq([plan1, plan2]) 68 | end 69 | 70 | context 'when the metadata attr is missing' do 71 | let(:service) do 72 | Service.build( 73 | 'id' => 'my-id', 74 | 'name' => 'my-name', 75 | 'description' => 'my description', 76 | 'tags' => ['tagA', 'tagB'], 77 | 'plans' => [] 78 | ) 79 | end 80 | 81 | it 'sets the field to nil' do 82 | expect(service.metadata).to be_nil 83 | end 84 | end 85 | 86 | context 'when the tags attr is missing' do 87 | let(:service) do 88 | Service.build( 89 | 'id' => 'my-id', 90 | 'name' => 'my-name', 91 | 'description' => 'my description', 92 | 'metadata' => { 'stuff' => 'goes here' }, 93 | 'plans' => [] 94 | ) 95 | end 96 | 97 | it 'sets the field to an empty array' do 98 | expect(service.tags).to eq([]) 99 | end 100 | end 101 | 102 | context 'when the dashboard_client attr is missing' do 103 | let(:service) do 104 | Service.build( 105 | 'id' => 'my-id', 106 | 'name' => 'my-name', 107 | 'description' => 'my description', 108 | 'metadata' => { 'stuff' => 'goes here' }, 109 | 'plans' => [] 110 | ) 111 | end 112 | 113 | it 'sets the field to an empty hash' do 114 | expect(service.dashboard_client).to eql({}) 115 | end 116 | end 117 | 118 | context 'when the plan_updateable attr is missing' do 119 | let(:service) do 120 | Service.build( 121 | 'id' => 'my-id', 122 | 'name' => 'my-name', 123 | 'description' => 'my description', 124 | 'metadata' => { 'stuff' => 'goes here' }, 125 | 'plans' => [], 126 | ) 127 | end 128 | 129 | it 'sets the field to false' do 130 | expect(service.plan_updateable).to eql(false) 131 | end 132 | end 133 | end 134 | 135 | describe '#to_hash' do 136 | it 'contains the right values' do 137 | service = Service.new( 138 | 'id' => 'my-id', 139 | 'name' => 'my-name', 140 | 'description' => 'my-description', 141 | 'tags' => ['tagA', 'tagB'], 142 | 'metadata' => { 'meta' => 'data' }, 143 | 'plans' => [], 144 | 'dashboard_client' => { 145 | 'id' => 'sso-client', 146 | 'secret' => 'something-secret', 147 | 'redirect_uri' => 'example.com' 148 | }, 149 | 'plan_updateable' => true 150 | ) 151 | 152 | expect(service.to_hash.fetch('id')).to eq('my-id') 153 | expect(service.to_hash.fetch('name')).to eq('my-name') 154 | expect(service.to_hash.fetch('bindable')).to eq(true) 155 | expect(service.to_hash.fetch('description')).to eq('my-description') 156 | expect(service.to_hash.fetch('tags')).to eq(['tagA', 'tagB']) 157 | expect(service.to_hash.fetch('metadata')).to eq({ 'meta' => 'data' }) 158 | expect(service.to_hash).to have_key('plans') 159 | expect(service.to_hash.fetch('dashboard_client')).to eq({ 160 | 'id' => 'sso-client', 161 | 'secret' => 'something-secret', 162 | 'redirect_uri' => 'example.com' 163 | }) 164 | expect(service.to_hash.fetch('plan_updateable')).to eq true 165 | end 166 | 167 | it 'includes the #to_hash for each plan' do 168 | plan_1 = double(:plan_1) 169 | plan_2 = double(:plan_2) 170 | plan_1_to_hash = double(:plan_1_to_hash) 171 | plan_2_to_hash = double(:plan_2_to_hash) 172 | 173 | expect(plan_1).to receive(:to_hash).and_return(plan_1_to_hash) 174 | expect(plan_2).to receive(:to_hash).and_return(plan_2_to_hash) 175 | 176 | service = Service.new( 177 | 'plans' => [plan_1, plan_2], 178 | 'id' => 'my-id', 179 | 'name' => 'my-name', 180 | 'description' => 'my-description', 181 | 'tags' => ['tagA', 'tagB'], 182 | 'metadata' => { 'meta' => 'data' }, 183 | ) 184 | 185 | expect(service.to_hash.fetch('plans')).to eq([plan_1_to_hash, plan_2_to_hash]) 186 | end 187 | 188 | context 'when there is no plans key' do 189 | let(:service) do 190 | Service.build( 191 | 'id' => 'my-id', 192 | 'name' => 'my-name', 193 | 'description' => 'my-description', 194 | 'tags' => ['tagA', 'tagB'], 195 | 'metadata' => { 'meta' => 'data' }, 196 | ) 197 | end 198 | 199 | it 'has an empty list of plans' do 200 | expect(service.to_hash.fetch('plans')).to eq([]) 201 | end 202 | end 203 | 204 | # There might be a dangling "plans:" in the yaml, which assigns a nil value 205 | context 'when the plans key is nil' do 206 | let(:service) do 207 | Service.build( 208 | 'id' => 'my-id', 209 | 'name' => 'my-name', 210 | 'description' => 'my-description', 211 | 'tags' => ['tagA', 'tagB'], 212 | 'metadata' => { 'meta' => 'data' }, 213 | 'plans' => nil, 214 | ) 215 | end 216 | 217 | it 'has an empty list of plans' do 218 | expect(service.to_hash.fetch('plans')).to eq([]) 219 | end 220 | end 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /spec/queries/service_instance_usage_query_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ServiceInstanceUsageQuery do 4 | describe 'getting MB used' do 5 | let(:instance_guid) { SecureRandom.uuid } 6 | let(:instance) { ServiceInstance.find_by_guid(instance_guid) } 7 | let(:binding_id) { SecureRandom.uuid } 8 | let(:binding) { ServiceBinding.new(id: binding_id, service_instance: instance) } 9 | let(:mbs_used) { 10 } 10 | let(:client) { create_mysql_client } 11 | let(:max_storage_mb) { 15 } 12 | let(:plan_guid) {'plan_guid'} 13 | 14 | before do 15 | allow(Catalog).to receive(:has_plan?).with(plan_guid).and_return(true) 16 | allow(Catalog).to receive(:storage_quota_for_plan_guid).with(plan_guid).and_return(max_storage_mb) 17 | ServiceInstanceManager.create(guid: instance_guid, plan_guid: plan_guid) 18 | allow(Settings).to receive(:allow_table_locks).and_return(true) 19 | binding.save 20 | fill_db 21 | end 22 | 23 | after do 24 | binding.destroy 25 | ServiceInstanceManager.destroy(guid: instance_guid) 26 | end 27 | 28 | it 'returns the correct MB used' do 29 | query = ServiceInstanceUsageQuery.new(instance) 30 | 31 | result = query.execute 32 | 33 | expect(result).to be_within(1).of(mbs_used) 34 | end 35 | end 36 | 37 | def fill_db 38 | data = '1' * (1024 * 1024) # 1 MB 39 | data = client.escape(data) 40 | 41 | client.query('CREATE TABLE stuff (id INT PRIMARY KEY, data LONGTEXT) ENGINE=InnoDB') 42 | mbs_used.times do |n| 43 | client.query("INSERT INTO stuff (id, data) VALUES (#{n}, '#{data}')") 44 | end 45 | end 46 | 47 | def create_mysql_client 48 | Mysql2::Client.new( 49 | :host => binding.host, 50 | :port => binding.port, 51 | :database => binding.database_name, 52 | :username => binding.username, 53 | :password => binding.password 54 | ) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/requests/catalog_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'GET /v2/catalog' do 4 | it 'returns the catalog of services' do 5 | get '/v2/catalog' 6 | 7 | expect(response.status).to eq(200) 8 | catalog = JSON.parse(response.body) 9 | service_settings = YAML.load_file(Rails.root + 'config/settings.yml').fetch('test').fetch('services').first 10 | 11 | services = catalog.fetch('services') 12 | expect(services.size).to eq 1 13 | 14 | service = services.first 15 | expect(service.fetch('name')).to eq(service_settings.fetch('name')) 16 | expect(service.fetch('description')).to eq(service_settings.fetch('description')) 17 | expect(service.fetch('bindable')).to be true 18 | expect(service.fetch('metadata')).to eq(service_settings.fetch('metadata')) 19 | 20 | plans = service.fetch('plans') 21 | expect(plans.size).to eq 2 22 | 23 | plan = plans.first 24 | plan_settings = service_settings.fetch('plans').first 25 | expect(plan.fetch('name')).to eq(plan_settings.fetch('name')) 26 | expect(plan.fetch('description')).to eq(plan_settings.fetch('description')) 27 | expect(plan.fetch('metadata')).to eq(plan_settings.fetch('metadata')) 28 | 29 | plan = plans[1] 30 | plan_settings = service_settings.fetch('plans')[1] 31 | expect(plan.fetch('name')).to eq(plan_settings.fetch('name')) 32 | expect(plan.fetch('description')).to eq(plan_settings.fetch('description')) 33 | expect(plan.fetch('metadata')).to eq(plan_settings.fetch('metadata')) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/requests/lifecycle_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Provisions, binds, unbinds, deprovisions a service 4 | 5 | def cleanup_mysql_user(username) 6 | ActiveRecord::Base.connection.execute("DROP USER #{username}") 7 | rescue 8 | end 9 | 10 | def cleanup_mysql_database(dbname) 11 | ActiveRecord::Base.connection.execute("DROP DATABASE #{dbname}") 12 | rescue 13 | end 14 | 15 | def create_mysql_client(username=Rails.configuration.database_configuration[Rails.env].fetch('username'), 16 | password=Rails.configuration.database_configuration[Rails.env].fetch('password'), 17 | database='mysql') 18 | Mysql2::Client.new( 19 | :host => Rails.configuration.database_configuration[Rails.env].fetch('host'), 20 | :port => Rails.configuration.database_configuration[Rails.env].fetch('port'), 21 | :database => database, 22 | :username => username, 23 | :password => password 24 | ) 25 | end 26 | 27 | describe 'the service lifecycle' do 28 | let(:instance_id) {'instance-1'} 29 | let(:dbname) {'cf_instance_1'} 30 | 31 | let(:binding_id) {'binding-1'} 32 | let(:password) {'somepassword'} 33 | let(:username) {ServiceBinding.new(id: binding_id).username} 34 | let(:headers) {{"CONTENT_TYPE" => "application/json"}} 35 | 36 | before do 37 | cleanup_mysql_user(username) 38 | cleanup_mysql_database(dbname) 39 | allow(Settings).to receive(:allow_table_locks).and_return(true) 40 | end 41 | 42 | after do 43 | cleanup_mysql_user(username) 44 | cleanup_mysql_database(dbname) 45 | end 46 | 47 | describe 'bindings' do 48 | before do 49 | allow(SecureRandom).to receive(:base64).and_return(password, 'notthepassword') 50 | 51 | ## 52 | ## Provision the instance 53 | ## 54 | put "/v2/service_instances/#{instance_id}", {plan_id: '2451fa22-df16-4c10-ba6e-1f682d3dcdc9'} 55 | 56 | expect(response.status).to eq(201) 57 | expect(response.body).to eq("{\"dashboard_url\":\"http://p-mysql.bosh-lite.com/manage/instances/#{instance_id}\"}") 58 | end 59 | 60 | after do 61 | ## 62 | ## Unbind 63 | ## 64 | delete "/v2/service_instances/#{instance_id}/service_bindings/#{binding_id}" 65 | expect(response.status).to eq(200) 66 | expect(response.body).to eq('{}') 67 | 68 | ## 69 | ## Test that the binding no longer works 70 | ## 71 | expect { 72 | create_mysql_client(username, password, dbname) 73 | }.to raise_error(Mysql2::Error) 74 | 75 | ## 76 | ## Test that we have purged any data associated with the user 77 | ## 78 | client = create_mysql_client() 79 | found = client.query("SELECT * FROM mysql.db WHERE User = '#{username}';") 80 | expect(found.count).to eq(0) 81 | found = client.query("SELECT * FROM mysql.user WHERE User = '#{username}';") 82 | expect(found.count).to eq(0) 83 | 84 | ## 85 | ## Deprovision 86 | ## 87 | delete "/v2/service_instances/#{instance_id}" 88 | expect(response.status).to eq(200) 89 | expect(response.body).to eq('{}') 90 | 91 | ## 92 | ## Test that the database no longer exists 93 | ## 94 | found = client.query("SHOW DATABASES LIKE '#{dbname}'") 95 | expect(found.count).to eq(0) 96 | end 97 | 98 | describe 'read/write binding' do 99 | it 'provisions, binds, unbinds, deprovisions' do 100 | ## 101 | ## Bind with incorrect arbitrary parameters 102 | ## 103 | put "/v2/service_instances/#{instance_id}/service_bindings/#{binding_id}", 104 | JSON.dump({"parameters" => {"read-oooonly" => true}}), 105 | headers.merge(env) 106 | 107 | expect(response.status).to eq(400) 108 | expect(JSON.parse(response.body)).to eq({ 109 | "error" => "Error creating service binding", 110 | "description" => "Invalid arbitrary parameter syntax. Please check the documentation for supported arbitrary parameters.", 111 | }) 112 | 113 | ## 114 | ## Bind with invalid value for read-only parameter 115 | ## 116 | put "/v2/service_instances/#{instance_id}/service_bindings/#{binding_id}", 117 | JSON.dump({"parameters" => {"read-only" => "tomato"}}), 118 | headers.merge(env) 119 | 120 | expect(response.status).to eq(400) 121 | expect(JSON.parse(response.body)).to eq({ 122 | "error" => "Error creating service binding", 123 | "description" => "Invalid arbitrary parameter syntax. Please check the documentation for supported arbitrary parameters.", 124 | }) 125 | 126 | ## 127 | ## Bind 128 | ## 129 | put "/v2/service_instances/#{instance_id}/service_bindings/#{binding_id}" 130 | 131 | expect(response.status).to eq(201) 132 | instance = JSON.parse(response.body) 133 | 134 | expect(instance.fetch('credentials')).to eq({ 135 | 'hostname' => 'localhost', 136 | 'name' => dbname, 137 | 'username' => username, 138 | 'password' => password, 139 | 'port' => 3306, 140 | 'jdbcUrl' => "jdbc:mysql://localhost:3306/#{dbname}?user=#{username}&password=#{password}", 141 | 'uri' => "mysql://#{username}:#{password}@localhost:3306/#{dbname}?reconnect=true", 142 | }) 143 | 144 | ## 145 | ## Test the binding 146 | ## 147 | client = create_mysql_client(username, password, dbname) 148 | 149 | client.query("CREATE TABLE IF NOT EXISTS data_values (id VARCHAR(20), data_value VARCHAR(20));") 150 | client.query("INSERT INTO data_values VALUES('123', '456');") 151 | found = client.query("SELECT id, data_value FROM data_values;").first 152 | expect(found.fetch('data_value')).to eq('456') 153 | end 154 | end 155 | 156 | describe 'read-only bindings' do 157 | it 'provisions, binds, unbinds, deprovisions' do 158 | ## 159 | ## Make a read-only binding 160 | ## 161 | put "/v2/service_instances/#{instance_id}/service_bindings/#{binding_id}", 162 | JSON.dump({"parameters" => {"read-only" => true}}), 163 | headers.merge(env) 164 | 165 | expect(response.status).to eq(201) 166 | instance = JSON.parse(response.body) 167 | 168 | expect(instance.fetch('credentials')).to eq({ 169 | 'hostname' => 'localhost', 170 | 'name' => dbname, 171 | 'username' => username, 172 | 'password' => password, 173 | 'port' => 3306, 174 | 'jdbcUrl' => "jdbc:mysql://localhost:3306/#{dbname}?user=#{username}&password=#{password}", 175 | 'uri' => "mysql://#{username}:#{password}@localhost:3306/#{dbname}?reconnect=true", 176 | }) 177 | 178 | ## 179 | ## Verify that the binding has read access 180 | ## 181 | client = create_mysql_client(username, password, dbname) 182 | found = client.query("SHOW DATABASES LIKE '#{dbname}'") 183 | expect(found.count).to eq(1) 184 | 185 | ## 186 | ## Verify that the binding does not have write access 187 | ## 188 | expect { 189 | client.query("CREATE TABLE IF NOT EXISTS data_values (id VARCHAR(20), data_value VARCHAR(20));") 190 | }.to raise_error(Mysql2::Error) 191 | end 192 | end 193 | end 194 | end 195 | -------------------------------------------------------------------------------- /spec/requests/missing_things_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Behavior when calling endpoints associated with things that do not exist. 4 | 5 | describe 'endpoints' do 6 | describe 'deleting an instance' do 7 | context 'when the service instance does not exist' do 8 | it 'returns 410' do 9 | delete '/v2/service_instances/DOESNOTEXIST' 10 | expect(response.status).to eq(410) 11 | end 12 | end 13 | end 14 | 15 | describe 'deleting a service binding' do 16 | context 'when the service binding does not exist' do 17 | it 'returns 410' do 18 | delete '/v2/service_instances/service_instance_id/service_bindings/DOESNOTEXIST' 19 | expect(response.status).to eq(410) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.raise_errors_for_deprecations! 3 | end 4 | 5 | require 'codeclimate-test-reporter' 6 | CodeClimate::TestReporter.start 7 | 8 | # This file is copied to spec/ when you run 'rails generate rspec:install' 9 | ENV['RAILS_ENV'] ||= 'test' 10 | require File.expand_path('../../config/environment', __FILE__) 11 | require 'rspec/rails' 12 | require 'webmock/rspec' 13 | require 'database_cleaner' 14 | 15 | DatabaseCleaner.strategy = :truncation 16 | WebMock.disable_net_connect!(allow: 'codeclimate.com') 17 | 18 | # Requires supporting ruby files with custom matchers and macros, etc, 19 | # in spec/support/ and its subdirectories. 20 | Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } 21 | 22 | # Checks for pending migrations before tests are run. 23 | # If you are not using ActiveRecord, you can remove this line. 24 | ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration) 25 | 26 | RSpec.configure do |config| 27 | # ## Mock Framework 28 | # 29 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: 30 | # 31 | # config.mock_with :mocha 32 | # config.mock_with :flexmock 33 | # config.mock_with :rr 34 | 35 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 36 | # config.fixture_path = "#{::Rails.root}/spec/fixtures" 37 | 38 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 39 | # examples within a transaction, remove the following line or assign false 40 | # instead of true. 41 | # config.use_transactional_fixtures = true 42 | 43 | # If true, the base class of anonymous controllers will be inferred 44 | # automatically. This will be the default behavior in future versions of 45 | # rspec-rails. 46 | config.infer_base_class_for_anonymous_controllers = true 47 | 48 | config.infer_spec_type_from_file_location! 49 | 50 | # Run specs in random order to surface order dependencies. If you find an 51 | # order dependency and want to debug it, you can fix the order by providing 52 | # the seed, which is printed after each run. 53 | # --seed 1234 54 | config.order = 'random' 55 | 56 | # 57 | # Our tests won't work with the default localhost wildcard user record in mysql 58 | # We also need a few innodb settings in order for stats to update automatically. 59 | # 60 | config.before :suite do 61 | count_of_bad_users = ActiveRecord::Base.connection.select_value("select count(*) from mysql.user where Host='localhost' and User=''") 62 | if count_of_bad_users > 0 63 | raise %Q{You must delete the Host='localhost' User='' record from the mysql.users table.\nRun this command:\nmysql -u root -e "DELETE FROM mysql.user WHERE Host='localhost' AND User=''"} 64 | end 65 | 66 | variable_records = ActiveRecord::Base.connection.select_rows("show variables like 'innodb_stats_%'") 67 | variables = Hash[ 68 | ActiveRecord::Base.connection.select_rows("show variables like 'innodb_stats_%'") 69 | ] 70 | 71 | raise <<-TEXT.strip_heredoc unless variables['innodb_stats_on_metadata'] == 'ON' 72 | innodb_stats_on_metadata must be ON 73 | Option 1 (permanent): set innodb_stats_on_metadata=ON in my.cnf 74 | Option 2 (temporary): `mysql -uroot -e "SET GLOBAL innodb_stats_on_metadata=ON"` 75 | TEXT 76 | raise <<-TEXT.strip_heredoc if variables['innodb_stats_persistent'] == 'ON' 77 | innodb_stats_persistent must be OFF 78 | Option 1 (permanent): set innodb_stats_persistent=OFF in my.cnf 79 | Option 2 (temporary): `mysql -uroot -e "SET GLOBAL innodb_stats_persistent=OFF"` 80 | TEXT 81 | end 82 | 83 | config.after do 84 | DatabaseCleaner.clean 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/support/controller_helpers.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'a controller action that does not log its request and response headers and body' do 2 | it 'does not log the request' do 3 | expect(Rails.logger).to_not receive(:info).with(/Request:/) 4 | make_request 5 | end 6 | 7 | it 'does not log the response' do 8 | expect(Rails.logger).to_not receive(:info).with(/Response:/) 9 | make_request 10 | end 11 | end 12 | 13 | shared_examples_for 'a controller action that requires basic auth' do 14 | context 'when the basic-auth username is incorrect' do 15 | before do 16 | set_basic_auth('wrong_username', Settings.auth_password) 17 | end 18 | 19 | it 'responds with a 401' do 20 | make_request 21 | 22 | expect(response.status).to eq(401) 23 | end 24 | end 25 | end 26 | 27 | module ControllerHelpers 28 | extend ActiveSupport::Concern 29 | 30 | def authenticate 31 | set_basic_auth(Settings.auth_username, Settings.auth_password) 32 | end 33 | 34 | def set_basic_auth(username, password) 35 | request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(username, password) 36 | end 37 | 38 | def db 39 | ActiveRecord::Base.connection 40 | end 41 | end 42 | 43 | RSpec.configure do |config| 44 | config.include ControllerHelpers, type: :controller 45 | config.infer_spec_type_from_file_location! 46 | end 47 | -------------------------------------------------------------------------------- /spec/support/feature_helpers.rb: -------------------------------------------------------------------------------- 1 | # Inspired by rspec-rails' request example group. 2 | module FeatureHelpers 3 | extend ActiveSupport::Concern 4 | include ActionController::TemplateAssertions 5 | include ActionDispatch::Integration::Runner 6 | 7 | included do 8 | metadata[:type] = :feature 9 | 10 | let(:default_env) do 11 | username = Settings.auth_username 12 | password = Settings.auth_password 13 | 14 | { 15 | 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(username, password) 16 | } 17 | end 18 | let(:env) { default_env } 19 | 20 | before do 21 | @routes = ::Rails.application.routes 22 | end 23 | end 24 | 25 | def app 26 | ::Rails.application 27 | end 28 | 29 | def get(*args) 30 | args[2] ||= env 31 | super(*args) 32 | end 33 | 34 | def post(*args) 35 | args[2] ||= env 36 | super(*args) 37 | end 38 | 39 | def put(*args) 40 | args[2] ||= env 41 | super(*args) 42 | end 43 | 44 | def patch(*args) 45 | args[2] ||= env 46 | super(*args) 47 | end 48 | 49 | def delete(*args) 50 | args[2] ||= env 51 | super(*args) 52 | end 53 | 54 | def create_mysql_client(config) 55 | Mysql2::Client.new( 56 | :host => config.fetch('hostname'), 57 | :port => config.fetch('port'), 58 | :database => config.fetch('name'), 59 | :username => config.fetch('username'), 60 | :password => config.fetch('password') 61 | ) 62 | end 63 | 64 | def create_table_and_write_data(client, max_storage_mb) 65 | client.query('DROP TABLE IF EXISTS stuff') 66 | client.query('CREATE TABLE stuff (id INT PRIMARY KEY, data LONGTEXT) ENGINE=InnoDB') 67 | 68 | data = '1' * (1024 * 1024) # 1 MB 69 | 70 | max_storage_mb.times do |n| 71 | client.query("INSERT INTO stuff (id, data) VALUES (#{n}, '#{data}')") 72 | end 73 | end 74 | 75 | def verify_client_can_write(client) 76 | client.query("INSERT INTO stuff (id, data) VALUES (99999, 'This should succeed.')") 77 | client.query("UPDATE stuff SET data = 'This should also succeed.' WHERE id = 99999") 78 | end 79 | end 80 | 81 | RSpec.configure do |config| 82 | config.include FeatureHelpers, :file_path => %r(spec/features) 83 | end 84 | -------------------------------------------------------------------------------- /spec/support/model_helpers.rb: -------------------------------------------------------------------------------- 1 | module ModelHelpers 2 | def connection 3 | ActiveRecord::Base.connection 4 | end 5 | end 6 | 7 | RSpec.configure do |config| 8 | config.include ModelHelpers, type: :model 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/mysql_helper.rb: -------------------------------------------------------------------------------- 1 | module MysqlHelpers 2 | def create_mysql_client(binding) 3 | Mysql2::Client.new( 4 | :host => binding.host, 5 | :port => binding.port, 6 | :database => binding.database_name, 7 | :username => binding.username, 8 | :password => binding.password 9 | ) 10 | end 11 | 12 | def create_root_mysql_client 13 | config = Rails.configuration.database_configuration[Rails.env] 14 | 15 | Mysql2::Client.new( 16 | :host => binding1.host, 17 | :port => binding1.port, 18 | :database => binding1.database_name, 19 | :username => config.fetch('username'), 20 | :password => config.fetch('password') 21 | ) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/support/request_helpers.rb: -------------------------------------------------------------------------------- 1 | module RequestHelpers 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | let(:default_env) do 6 | username = Settings.auth_username 7 | password = Settings.auth_password 8 | 9 | { 10 | 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(username, password) 11 | } 12 | end 13 | let(:env) { default_env } 14 | end 15 | 16 | def get(*args) 17 | args[2] ||= env 18 | super(*args) 19 | end 20 | 21 | def post(*args) 22 | args[2] ||= env 23 | super(*args) 24 | end 25 | 26 | def put(*args) 27 | args[2] ||= env 28 | super(*args) 29 | end 30 | 31 | def delete(*args) 32 | args[2] ||= env 33 | super(*args) 34 | end 35 | end 36 | 37 | RSpec.configure do |config| 38 | config.include RequestHelpers, type: :request 39 | end 40 | --------------------------------------------------------------------------------