maitre_d 0.3.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +2 -1
  3. data/Gemfile +0 -5
  4. data/README.textile +28 -47
  5. data/addon-manifest.json +11 -4
  6. data/lib/maitre_d.rb +3 -7
  7. data/lib/maitre_d/api.rb +32 -0
  8. data/lib/maitre_d/api/authenticated.rb +30 -0
  9. data/lib/maitre_d/api/authentication_guard.rb +25 -0
  10. data/lib/maitre_d/api/change_plan.rb +15 -0
  11. data/lib/maitre_d/api/create.rb +7 -0
  12. data/lib/maitre_d/api/delete.rb +13 -0
  13. data/lib/maitre_d/api/sso.rb +45 -0
  14. data/lib/maitre_d/api/sso_guard.rb +31 -0
  15. data/lib/maitre_d/heroku.rb +4 -4
  16. data/maitre_d.gemspec +10 -9
  17. data/spec/api/heroku/provisioning_spec.rb +48 -30
  18. data/spec/api/heroku/single_sign_on_spec.rb +30 -20
  19. data/spec/internal/app/listeners/heroku_listener.rb +4 -4
  20. data/spec/internal/config/initializers/heroku.rb +4 -2
  21. data/spec/internal/config/routes.rb +3 -1
  22. data/spec/spec_helper.rb +1 -2
  23. metadata +59 -62
  24. data/config/routes.rb +0 -6
  25. data/lib/maitre_d/broadstack.rb +0 -40
  26. data/lib/maitre_d/broadstack/api.rb +0 -49
  27. data/lib/maitre_d/broadstack/api_helpers.rb +0 -44
  28. data/lib/maitre_d/cloud_control.rb +0 -40
  29. data/lib/maitre_d/cloud_control/api.rb +0 -47
  30. data/lib/maitre_d/cloud_control/api_helpers.rb +0 -26
  31. data/lib/maitre_d/engine.rb +0 -3
  32. data/lib/maitre_d/heroku/api.rb +0 -47
  33. data/lib/maitre_d/heroku/api_helpers.rb +0 -44
  34. data/lib/maitre_d/heroku/listener.rb +0 -17
  35. data/lib/maitre_d/opperator.rb +0 -25
  36. data/lib/maitre_d/opperator/api.rb +0 -25
  37. data/lib/maitre_d/opperator/api_helpers.rb +0 -11
  38. data/lib/maitre_d/opperator/listener.rb +0 -13
  39. data/spec/api/broadstack/provisioning_spec.rb +0 -94
  40. data/spec/api/broadstack/single_sign_on_spec.rb +0 -49
  41. data/spec/api/cloud_control/provisioning_spec.rb +0 -94
  42. data/spec/api/cloud_control/single_sign_on_spec.rb +0 -49
  43. data/spec/api/opperator/provisioning_spec.rb +0 -55
  44. data/spec/internal/app/listeners/broadstack_listener.rb +0 -30
  45. data/spec/internal/app/listeners/cloud_control_listener.rb +0 -29
  46. data/spec/internal/app/listeners/opperator_listener.rb +0 -16
  47. data/spec/internal/config/initializers/broadstack.rb +0 -6
  48. data/spec/internal/config/initializers/cloud_control.rb +0 -6
  49. data/spec/internal/config/initializers/opperator.rb +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 1e79119958343605dabd87304c21e2e9c770711b
4
- data.tar.gz: 4ed73d17abf94091dfb18e566984637699e8569a
2
+ SHA256:
3
+ metadata.gz: 764dc0a04e766ea678f0bde9321fd3328dd98a138094a129f36845c409dfad27
4
+ data.tar.gz: ae4e4c87d345644f054f6b8ff2c56ede552d77ebe98012612f665a72dc31e520
5
5
  SHA512:
6
- metadata.gz: 36feafdb16cd476413a95b34da6dfc675c43fa47772b04d6ec34117aeb71c42646556700920582689dda9fa845667451c93f8812408d340e14cd76eb5fe6cb3a
7
- data.tar.gz: aa9b728329779092e8f1482e50db5dbfe45751cce6957135ec35f8d7f959b162cd55fbcd0e093db3534b7bf82249becbccdc3604981f217dc780b61b5d7c10bd
6
+ metadata.gz: c3a147e73fae54e2c366fd269aa1be216295e80dd909285b31e997501d592b2040591a8fc4e9204f4c034b7da36dc47cdcca89df2a07f0b089a0cb5e9102195a
7
+ data.tar.gz: 42b838f44359b580ef759979c7bf3fd67acfb477ffb6bc972b7ffe9585767ad09b1077619a01507bd358592020af2a01b5c17a784be9e3e3b29f37cb1e5c54e9
@@ -1,4 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
3
+ - 2.6.6
4
+ - 2.7.2
4
5
  script: bundle exec rspec spec
data/Gemfile CHANGED
@@ -1,8 +1,3 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  gemspec
4
-
5
- # Until a new Grape release is out:
6
- gem 'grape',
7
- :git => 'git://github.com/intridea/grape.git',
8
- :ref => '212c96cdfb253a59bc93790808c568e559d04468'
@@ -1,71 +1,60 @@
1
1
  h1. Maître d'
2
2
 
3
- "!https://secure.travis-ci.org/flying-sphinx/maitre_d.png!":http://travis-ci.org/flying-sphinx/maitre_d
3
+ "!https://secure.travis-ci.org/flying-sphinx/maitre_d.svg!":http://travis-ci.org/flying-sphinx/maitre_d
4
4
 
5
- Rack APIs powered by Grape for managing Heroku, CloudControl and Opperator add-ons. If used within a Rails application, it'll automatically be mounted as a Rails engine at the appropriate paths.
5
+ Rack APIs powered by Sliver for managing Heroku add-ons.
6
6
 
7
7
  Maître d' manages all the authorisation checking for API requests and provides simple hooks for you to write just the code you need to handle provisioning, plan changes, deprovisioning and single-sign-on (SSO) requests.
8
8
 
9
9
  h2. Installing
10
10
 
11
- Add the following to your Gemfile - and take note that we need the git repository reference for Grape, as it includes some needed commits that aren't in the latest gem release.
11
+ Add the following to your Gemfile:
12
12
 
13
- <pre><code>gem 'grape',
14
- :git => 'git://github.com/intridea/grape.git',
15
- :ref => '212c96cdfb253a59bc93790808c568e559d04468'
16
- gem 'maitre_d', '~> 0.1.2'</code></pre>
13
+ <pre><code>gem 'maitre_d', '~> 0.7.0'</code></pre>
17
14
 
18
15
  h3. With Rails
19
16
 
20
- Because there's a Rails engine as part of Maître d', the API routing is set up automatically. Jump forward to the configuration and listener setup below.
17
+ Add the Rack app to your routes file:
18
+
19
+ <pre><code>mount MaitreD::API.new(MaitreD::Heroku) => '/heroku'</code></pre>
21
20
 
22
21
  h3. Without Rails
23
22
 
24
- This library provides two APIs as mountable Rack applications for you - so you'll want to mount them at the appropriate paths: @MaitreD::Heroku::API@ at @/heroku@, @MaitreD::CloudControl::API@ at @/cloudcontrol@, and/or @MaitreD::Opperator::API@ at @/opperator@.
23
+ As shown above, you can use Maître d' Rack API, mounting them to wherever you see fit. Once upon a time Heroku expected its endpoints to be at /heroku, but now things are a little more flexible.
25
24
 
26
25
  h2. Configuration
27
26
 
28
27
  You'll need to provide Maître d' with the appropriate provider credentials - in a Rails app, this would go in an initializer, but for Rack/Sinatra apps just get the values set before the routing is defined.
29
28
 
30
- <pre><code>MaitreD::Heroku.configure do |config|
31
- config.id = 'addon-id'
32
- config.password = 'random'
33
- config.sso_salt = 'gibberish'
34
- config.listener = HerokuListener
35
- end
29
+ <pre><code>require 'maitre_d/heroku'
36
30
 
37
- MaitreD::CloudControl.configure do |config|
31
+ MaitreD::Heroku.configure do |config|
38
32
  config.id = 'addon-id'
39
33
  config.password = 'random'
40
34
  config.sso_salt = 'gibberish'
41
- config.listener = CloudControlListener
42
- end
43
-
44
- MaitreD::Opperator.configure do |config|
45
- config.shared_secret = 'something-special'
46
- config.listener = OpperatorListener
35
+ config.listener = HerokuListener
47
36
  end</code></pre>
48
37
 
49
- The listeners that are mentioned in the code above are classes, which will handle valid API requests. Read on for more details on how to set them up.
38
+ The listener that is mentioned in the code above is a class, which will handle valid API requests. Read on for more details on how to set them up.
50
39
 
51
- h2. Heroku Listener
40
+ h2. Listeners
52
41
 
53
- Your Heroku listener class should handle the following four methods:
42
+ Your listener class should handle the following four methods:
54
43
 
55
- h3. @provision(heroku_id, plan, region, callback_url, logplex_token, options)@
44
+ h3. @provision(params)@
56
45
 
57
- This gets called when Heroku's requesting an app be provisioned within your service, and expects a hash to be returned with the following keys:
46
+ This gets called when the provider is requesting an app be provisioned within your service, and expects a hash to be returned with the following keys:
58
47
 
59
48
  <dl>
60
49
  <dt><code>id</code></dt>
61
- <dd>Your local resource id, which Heroku will use in related requests (to change plans, deprovision or initialise single-sign-on).</dd>
50
+ <dd>Your local resource id, which the provider will use in related requests (to change plans, deprovision or initialise single-sign-on).</dd>
62
51
  <dt><code>config</code></dt>
63
52
  <dd>A hash of the newly provisioned resource's configuration values (that are provided as environment variables to the app in question).</dd>
64
53
  <dt><code>message</code></dt>
65
54
  <dd>An optional message that will be displayed when your add-on is added via the command-line.</dd>
66
55
  </dl>
67
56
 
68
- h3. @plan_change(resource_id, heroku_id, plan)@
57
+ h3. @plan_change(resource_id, plan)@
69
58
 
70
59
  This gets called when an app is upgrading or downgrading from their current plan. You need to return a hash with the following keys:
71
60
 
@@ -94,13 +83,13 @@ Maître d' will check the token and timestamp provided, and sets up the nav-data
94
83
  Here's a very basic example:
95
84
 
96
85
  <pre><code>class HerokuListener
97
- def provision(heroku_id, plan, region, callback_url, logplex_token, options)
98
- plan = Plan.find_by_name plan
86
+ def provision(params)
87
+ plan = Plan.find_by_name params["plan"]
99
88
  widget = Widget.create(
100
- :heroku_id => heroku_id,
101
- :callback_url => callback_url,
89
+ :heroku_id => params["uuid"],
90
+ :callback_url => params["callback_url"],
102
91
  :plan => plan,
103
- :region => region
92
+ :region => params["region"]
104
93
  )
105
94
 
106
95
  {
@@ -110,9 +99,9 @@ Here's a very basic example:
110
99
  }
111
100
  end
112
101
 
113
- def plan_change(resource_id, heroku_id, plan)
102
+ def plan_change(resource_id, plan)
114
103
  plan = Plan.find_by_name plan
115
- widget = Widget.find resource_id
104
+ widget = Widget.find_by! heroku_id: resource_id
116
105
  widget.plan = plan
117
106
  widget.save
118
107
 
@@ -120,12 +109,12 @@ Here's a very basic example:
120
109
  end
121
110
 
122
111
  def deprovision(resource_id)
123
- widget = Widget.find resource_id
112
+ widget = Widget.find_by! heroku_id: resource_id
124
113
  widget.destroy
125
114
  end
126
115
 
127
116
  def single_sign_on(resource_id)
128
- widget = Widget.find resource_id
117
+ widget = Widget.find_by! heroku_id: resource_id
129
118
 
130
119
  {
131
120
  :uri => '/my/dashboard',
@@ -136,14 +125,6 @@ end</code></pre>
136
125
 
137
126
  You can have the listener class wherever you like - as long as it's available within the context of your Rails/Rack site, it'll work as expected.
138
127
 
139
- h2. CloudControl Listener
140
-
141
- CloudControl is pretty much the same as Heroku - it has the same expectations for a listener, so perhaps you can use the same class if you like. Anything that Heroku sends through as the heroku_id becomes cloudcontrol_id - but that's just a matter of treating the given parameter appropriately.
142
-
143
- h2. Opperator Listener
144
-
145
- This listener is currently in progress as the Opperator API is established.
146
-
147
128
  h2. Contributing
148
129
 
149
130
  Contributions are very much welcome - but keep in mind the following:
@@ -154,4 +135,4 @@ Contributions are very much welcome - but keep in mind the following:
154
135
 
155
136
  h2. Credits
156
137
 
157
- Copyright (c) 2011-2012, Maître d' is developed and maintained by Pat Allan, and is released under the open MIT Licence.
138
+ Copyright (c) 2011-2020, Maître d' is developed and maintained by Pat Allan, and is released under the open MIT Licence.
@@ -2,9 +2,16 @@
2
2
  "id": "foo",
3
3
  "api": {
4
4
  "config_vars": [ "FOO_PROVISIONED" ],
5
- "password": "bar",
6
- "sso_salt": "sea salt",
7
- "production": "https://yourapp.com/",
8
- "test": "http://localhost:9292/"
5
+ "regions": [ "us" ],
6
+ "password": "056e8ff3d55ea6e1a7cc4aa7afeaf3ac",
7
+ "sso_salt": "db0ce73d4bd2dfa1d66c713a9aaf8f76",
8
+ "production": {
9
+ "base_url": "https://yourapp.com/heroku/resources",
10
+ "sso_url": "https://yourapp.com/sso/login"
11
+ },
12
+ "test": {
13
+ "base_url": "http://localhost:9292/heroku/resources",
14
+ "sso_url": "http://localhost:9292/heroku/resources/sso"
15
+ }
9
16
  }
10
17
  }
@@ -1,11 +1,7 @@
1
- require 'grape'
1
+ require 'sliver'
2
+ require 'multi_json'
2
3
 
3
4
  module MaitreD
4
5
  end
5
6
 
6
- require 'maitre_d/broadstack'
7
- require 'maitre_d/heroku'
8
- require 'maitre_d/cloud_control'
9
- require 'maitre_d/opperator'
10
-
11
- require 'maitre_d/engine' if defined?(Rails)
7
+ require 'maitre_d/api'
@@ -0,0 +1,32 @@
1
+ class MaitreD::API
2
+ def initialize(configuration)
3
+ @configuration = configuration
4
+ end
5
+
6
+ def call(environment)
7
+ environment['maitre_d.configuration'] = configuration
8
+
9
+ endpoints.call environment
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :configuration
15
+
16
+ def endpoints
17
+ @endpoints ||= Sliver::API.new do |api|
18
+ api.connect :post, '/resources/sso', MaitreD::API::SSO
19
+ api.connect :post, '/resources', MaitreD::API::Create
20
+ api.connect :put, %r{/resources/\d+}, MaitreD::API::ChangePlan
21
+ api.connect :delete, %r{/resources/\d+}, MaitreD::API::Delete
22
+ end
23
+ end
24
+ end
25
+
26
+ require 'maitre_d/api/authentication_guard'
27
+ require 'maitre_d/api/authenticated'
28
+ require 'maitre_d/api/change_plan'
29
+ require 'maitre_d/api/create'
30
+ require 'maitre_d/api/delete'
31
+ require 'maitre_d/api/sso_guard'
32
+ require 'maitre_d/api/sso'
@@ -0,0 +1,30 @@
1
+ class MaitreD::API::Authenticated
2
+ include Sliver::Action
3
+
4
+ def self.guards
5
+ [MaitreD::API::AuthenticationGuard]
6
+ end
7
+
8
+ def call
9
+ response.body = [MultiJson.dump(response.body)]
10
+ response.status ||= 200
11
+ end
12
+
13
+ def configuration
14
+ environment['maitre_d.configuration']
15
+ end
16
+
17
+ private
18
+
19
+ def listener
20
+ configuration.listener.new
21
+ end
22
+
23
+ def params
24
+ @params ||= MultiJson.load request.body.read
25
+ end
26
+
27
+ def provider_id
28
+ configuration.provider_id_from params
29
+ end
30
+ end
@@ -0,0 +1,25 @@
1
+ class MaitreD::API::AuthenticationGuard < Sliver::Hook
2
+ def continue?
3
+ valid_authorization?
4
+ end
5
+
6
+ def respond
7
+ response.status = 401
8
+ response.body = ['401 Unauthorized']
9
+ response.headers['Content-Length'] = response.body.first.length.to_s
10
+ end
11
+
12
+ private
13
+
14
+ def expected_credentials
15
+ "#{action.configuration.id}:#{action.configuration.password}"
16
+ end
17
+
18
+ def provided_credentials
19
+ Base64.decode64 action.request.env['HTTP_AUTHORIZATION'].gsub(/^Basic /, '')
20
+ end
21
+
22
+ def valid_authorization?
23
+ provided_credentials == expected_credentials
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ class MaitreD::API::ChangePlan < MaitreD::API::Authenticated
2
+ def call
3
+ response.body = listener.plan_change(
4
+ resource_id, params['plan']
5
+ )
6
+
7
+ super
8
+ end
9
+
10
+ private
11
+
12
+ def resource_id
13
+ request.path[%r{resources/([\w-]+)}, 1]
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ class MaitreD::API::Create < MaitreD::API::Authenticated
2
+ def call
3
+ response.body = listener.provision(params)
4
+
5
+ super
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ class MaitreD::API::Delete < MaitreD::API::Authenticated
2
+ def call
3
+ response.body = listener.deprovision resource_id
4
+
5
+ super
6
+ end
7
+
8
+ private
9
+
10
+ def resource_id
11
+ request.path[%r{resources/([\w-]+)}, 1]
12
+ end
13
+ end
@@ -0,0 +1,45 @@
1
+ class MaitreD::API::SSO
2
+ include Sliver::Action
3
+
4
+ def self.guards
5
+ [MaitreD::API::SSOGuard]
6
+ end
7
+
8
+ def call
9
+ hash = listener.single_sign_on params['resource_id']
10
+
11
+ hash[:session] ||= {}
12
+ hash[:session].each { |key, value| session[key] = value }
13
+
14
+ if environment['action_dispatch.cookies']
15
+ environment['action_dispatch.cookies']['heroku-nav-data'] =
16
+ params['nav-data']
17
+ else
18
+ Rack::Utils.set_cookie_header! response.headers, 'heroku-nav-data',
19
+ :value => params['nav-data']
20
+ end
21
+
22
+ response.status = 302
23
+ response.body = ["Redirect to #{hash[:uri]}"]
24
+ response.headers ||= {}
25
+ response.headers['Location'] = hash[:uri]
26
+ end
27
+
28
+ def configuration
29
+ environment['maitre_d.configuration']
30
+ end
31
+
32
+ private
33
+
34
+ def listener
35
+ configuration.listener.new
36
+ end
37
+
38
+ def params
39
+ request.params
40
+ end
41
+
42
+ def session
43
+ environment['rack.session'] ||= {}
44
+ end
45
+ end
@@ -0,0 +1,31 @@
1
+ class MaitreD::API::SSOGuard < Sliver::Hook
2
+ def continue?
3
+ valid_token? && valid_timestamp?
4
+ end
5
+
6
+ def respond
7
+ response.status = 403
8
+ response.body = ['403 Forbidden']
9
+ response.headers['Content-Length'] = response.body.first.length.to_s
10
+ end
11
+
12
+ private
13
+
14
+ def expected_token
15
+ @expected_token ||= Digest::SHA1.hexdigest(
16
+ "#{params['resource_id']}:#{action.configuration.sso_salt}:#{params['timestamp']}"
17
+ ).to_s
18
+ end
19
+
20
+ def params
21
+ action.request.params
22
+ end
23
+
24
+ def valid_timestamp?
25
+ params['timestamp'].to_i >= (Time.now - 5*60).to_i
26
+ end
27
+
28
+ def valid_token?
29
+ expected_token == params['resource_token']
30
+ end
31
+ end
@@ -34,8 +34,8 @@ module MaitreD::Heroku
34
34
  def self.sso_salt=(salt)
35
35
  @sso_salt = salt
36
36
  end
37
- end
38
37
 
39
- require 'maitre_d/heroku/api_helpers'
40
- require 'maitre_d/heroku/api'
41
- require 'maitre_d/heroku/listener'
38
+ def self.provider_id_from(params)
39
+ params['heroku_id']
40
+ end
41
+ end