maestrano 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +34 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +43 -0
  5. data/LICENSE +21 -0
  6. data/README.md +4 -0
  7. data/Rakefile +32 -0
  8. data/bin/maestrano-console +9 -0
  9. data/lib/maestrano.rb +114 -0
  10. data/lib/maestrano/account/bill.rb +14 -0
  11. data/lib/maestrano/api/error/authentication_error.rb +8 -0
  12. data/lib/maestrano/api/error/base_error.rb +24 -0
  13. data/lib/maestrano/api/error/connection_error.rb +8 -0
  14. data/lib/maestrano/api/error/invalid_request_error.rb +14 -0
  15. data/lib/maestrano/api/list_object.rb +37 -0
  16. data/lib/maestrano/api/object.rb +187 -0
  17. data/lib/maestrano/api/operation/base.rb +216 -0
  18. data/lib/maestrano/api/operation/create.rb +18 -0
  19. data/lib/maestrano/api/operation/delete.rb +13 -0
  20. data/lib/maestrano/api/operation/list.rb +18 -0
  21. data/lib/maestrano/api/operation/update.rb +59 -0
  22. data/lib/maestrano/api/resource.rb +39 -0
  23. data/lib/maestrano/api/util.rb +121 -0
  24. data/lib/maestrano/saml/attribute_value.rb +15 -0
  25. data/lib/maestrano/saml/metadata.rb +64 -0
  26. data/lib/maestrano/saml/request.rb +93 -0
  27. data/lib/maestrano/saml/response.rb +201 -0
  28. data/lib/maestrano/saml/schemas/saml20assertion_schema.xsd +283 -0
  29. data/lib/maestrano/saml/schemas/saml20protocol_schema.xsd +302 -0
  30. data/lib/maestrano/saml/schemas/xenc_schema.xsd +146 -0
  31. data/lib/maestrano/saml/schemas/xmldsig_schema.xsd +318 -0
  32. data/lib/maestrano/saml/settings.rb +37 -0
  33. data/lib/maestrano/saml/validation_error.rb +7 -0
  34. data/lib/maestrano/sso.rb +81 -0
  35. data/lib/maestrano/sso/base_group.rb +31 -0
  36. data/lib/maestrano/sso/base_user.rb +75 -0
  37. data/lib/maestrano/sso/group.rb +24 -0
  38. data/lib/maestrano/sso/session.rb +63 -0
  39. data/lib/maestrano/sso/user.rb +34 -0
  40. data/lib/maestrano/version.rb +3 -0
  41. data/lib/maestrano/xml_security/signed_document.rb +170 -0
  42. data/maestrano.gemspec +32 -0
  43. data/test/helpers/api_helpers.rb +82 -0
  44. data/test/helpers/saml_helpers.rb +62 -0
  45. data/test/maestrano/account/bill_test.rb +48 -0
  46. data/test/maestrano/api/list_object_test.rb +20 -0
  47. data/test/maestrano/api/object_test.rb +28 -0
  48. data/test/maestrano/api/resource_test.rb +343 -0
  49. data/test/maestrano/api/util_test.rb +31 -0
  50. data/test/maestrano/maestrano_test.rb +49 -0
  51. data/test/maestrano/saml/request_test.rb +168 -0
  52. data/test/maestrano/saml/response_test.rb +290 -0
  53. data/test/maestrano/saml/settings_test.rb +51 -0
  54. data/test/maestrano/sso/base_group_test.rb +54 -0
  55. data/test/maestrano/sso/base_user_test.rb +114 -0
  56. data/test/maestrano/sso/group_test.rb +47 -0
  57. data/test/maestrano/sso/session_test.rb +108 -0
  58. data/test/maestrano/sso/user_test.rb +65 -0
  59. data/test/maestrano/sso_test.rb +81 -0
  60. data/test/maestrano/xml_security/signed_document.rb +163 -0
  61. data/test/support/saml/certificates/certificate1 +12 -0
  62. data/test/support/saml/certificates/r1_certificate2_base64 +1 -0
  63. data/test/support/saml/responses/adfs_response_sha1.xml +46 -0
  64. data/test/support/saml/responses/adfs_response_sha256.xml +46 -0
  65. data/test/support/saml/responses/adfs_response_sha384.xml +46 -0
  66. data/test/support/saml/responses/adfs_response_sha512.xml +46 -0
  67. data/test/support/saml/responses/no_signature_ns.xml +48 -0
  68. data/test/support/saml/responses/open_saml_response.xml +56 -0
  69. data/test/support/saml/responses/r1_response6.xml.base64 +1 -0
  70. data/test/support/saml/responses/response1.xml.base64 +1 -0
  71. data/test/support/saml/responses/response2.xml.base64 +79 -0
  72. data/test/support/saml/responses/response3.xml.base64 +66 -0
  73. data/test/support/saml/responses/response4.xml.base64 +93 -0
  74. data/test/support/saml/responses/response5.xml.base64 +102 -0
  75. data/test/support/saml/responses/response_with_ampersands.xml +139 -0
  76. data/test/support/saml/responses/response_with_ampersands.xml.base64 +93 -0
  77. data/test/support/saml/responses/response_with_multiple_attribute_values.xml +57 -0
  78. data/test/support/saml/responses/simple_saml_php.xml +71 -0
  79. data/test/support/saml/responses/starfield_response.xml.base64 +1 -0
  80. data/test/support/saml/responses/wrapped_response_2.xml.base64 +150 -0
  81. data/test/test_helper.rb +46 -0
  82. metadata +305 -0
@@ -0,0 +1,216 @@
1
+ module Maestrano
2
+ module API
3
+ module Operation
4
+ module Base
5
+ # class << self
6
+ # attr_accessor :api_key, :api_base, :verify_ssl_certs, :api_version
7
+ # end
8
+
9
+ def self.api_url(url='')
10
+ Maestrano.param('api_host') + Maestrano.param('api_base') + url
11
+ end
12
+
13
+ # Perform remote request
14
+ def self.request(method, url, api_key, params={}, headers={})
15
+ unless api_key ||= Maestrano.param('api_key')
16
+ raise Maestrano::API::Error::AuthenticationError.new('No API key provided.')
17
+ end
18
+
19
+ request_opts = { :verify_ssl => false }
20
+
21
+ if self.ssl_preflight_passed?
22
+ request_opts.update(
23
+ verify_ssl: OpenSSL::SSL::VERIFY_PEER,
24
+ ssl_ca_file: Maestrano.param('ssl_bundle_path')
25
+ )
26
+ end
27
+
28
+ params = Util.objects_to_ids(params)
29
+ url = api_url(url)
30
+
31
+ case method.to_s.downcase.to_sym
32
+ when :get, :head, :delete
33
+ # Make params into GET parameters
34
+ url += "#{URI.parse(url).query ? '&' : '?'}#{uri_encode(params)}" if params && params.any?
35
+ payload = nil
36
+ else
37
+ payload = uri_encode(params)
38
+ end
39
+
40
+ request_opts.update(:headers => request_headers(api_key).update(headers),
41
+ :method => method, :open_timeout => 30,
42
+ :payload => payload, :url => url, :timeout => 80)
43
+
44
+ begin
45
+ response = execute_request(request_opts)
46
+ rescue SocketError => e
47
+ handle_restclient_error(e)
48
+ rescue NoMethodError => e
49
+ # Work around RestClient bug
50
+ if e.message =~ /\WRequestFailed\W/
51
+ e = APIConnectionError.new('Unexpected HTTP response code')
52
+ handle_restclient_error(e)
53
+ else
54
+ raise
55
+ end
56
+ rescue RestClient::ExceptionWithResponse => e
57
+ if rcode = e.http_code and rbody = e.http_body
58
+ handle_api_error(rcode, rbody)
59
+ else
60
+ handle_restclient_error(e)
61
+ end
62
+ rescue RestClient::Exception, Errno::ECONNREFUSED => e
63
+ handle_restclient_error(e)
64
+ end
65
+
66
+ [parse(response), api_key]
67
+ end
68
+
69
+ private
70
+
71
+ def self.ssl_preflight_passed?
72
+ if !Maestrano.param('verify_ssl_certs')
73
+ #$stderr.puts "WARNING: Running without SSL cert verification. " +
74
+ # "Execute 'Maestrano.configure { |config| config.verify_ssl_certs = true' } to enable verification."
75
+ return false
76
+ elsif !Util.file_readable(Maestrano.param('ssl_bundle_path'))
77
+ $stderr.puts "WARNING: Running without SSL cert verification " +
78
+ "because #{Maestrano.param('ssl_bundle_path')} isn't readable"
79
+
80
+ return false
81
+ end
82
+
83
+ return true
84
+ end
85
+
86
+ def self.user_agent
87
+ @uname ||= get_uname
88
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
89
+
90
+ {
91
+ :bindings_version => Maestrano::VERSION,
92
+ :lang => 'ruby',
93
+ :lang_version => lang_version,
94
+ :platform => RUBY_PLATFORM,
95
+ :publisher => 'maestrano',
96
+ :uname => @uname
97
+ }
98
+
99
+ end
100
+
101
+ def self.get_uname
102
+ `uname -a 2>/dev/null`.strip if RUBY_PLATFORM =~ /linux|darwin/i
103
+ rescue Errno::ENOMEM => ex # couldn't create subprocess
104
+ "uname lookup failed"
105
+ end
106
+
107
+ def self.uri_encode(params)
108
+ Util.flatten_params(params).
109
+ map { |k,v| "#{k}=#{Util.url_encode(v)}" }.join('&')
110
+ end
111
+
112
+ def self.request_headers(api_key)
113
+ headers = {
114
+ :user_agent => "Maestrano/v1 RubyBindings/#{Maestrano::VERSION}",
115
+ :authorization => "Basic #{Base64.encode64(api_key + ':')}",
116
+ :content_type => 'application/x-www-form-urlencoded'
117
+ }
118
+
119
+ api_version = Maestrano.param('api_version')
120
+ headers[:maestrano_version] = api_version if api_version
121
+
122
+ begin
123
+ headers.update(:x_maestrano_client_user_agent => JSON.generate(user_agent))
124
+ rescue => e
125
+ headers.update(:x_maestrano_client_raw_user_agent => user_agent.inspect,
126
+ :error => "#{e} (#{e.class})")
127
+ end
128
+ end
129
+
130
+ def self.execute_request(opts)
131
+ RestClient::Request.execute(opts)
132
+ end
133
+
134
+ def self.parse(response)
135
+ begin
136
+ # Would use :symbolize_names => true, but apparently there is
137
+ # some library out there that makes symbolize_names not work.
138
+ response = JSON.parse(response.body)
139
+ rescue JSON::ParserError
140
+ raise general_api_error(response.code, response.body)
141
+ end
142
+
143
+ response = Util.symbolize_names(response)
144
+ response[:data]
145
+ end
146
+
147
+ def self.general_api_error(rcode, rbody)
148
+ Maestrano::API::Error::BaseError.new("Invalid response object from API: #{rbody.inspect} " +
149
+ "(HTTP response code was #{rcode})", rcode, rbody)
150
+ end
151
+
152
+ def self.handle_api_error(rcode, rbody)
153
+ begin
154
+ error_obj = JSON.parse(rbody)
155
+ error_obj = Util.symbolize_names(error_obj)
156
+ errors = error_obj[:errors] or raise Maestrano::API::Error::BaseError.new # escape from parsing
157
+
158
+ rescue JSON::ParserError, Maestrano::API::Error::BaseError
159
+ raise general_api_error(rcode, rbody)
160
+ end
161
+
162
+ case rcode
163
+ when 400, 404
164
+ raise invalid_request_error(errors, rcode, rbody, error_obj)
165
+ when 401
166
+ raise authentication_error(errors, rcode, rbody, error_obj)
167
+ else
168
+ raise api_error(errors, rcode, rbody, error_obj)
169
+ end
170
+
171
+ end
172
+
173
+ def self.invalid_request_error(errors, rcode, rbody, error_obj)
174
+ Maestrano::API::Error::InvalidRequestError.new(errors.first.join(" "), errors.keys.first.to_s, rcode,
175
+ rbody, error_obj)
176
+ end
177
+
178
+ def self.authentication_error(errors, rcode, rbody, error_obj)
179
+ Maestrano::API::Error::AuthenticationError.new(errors.first.join(" "), rcode, rbody, error_obj)
180
+ end
181
+
182
+ def self.api_error(errors, rcode, rbody, error_obj)
183
+ Maestrano::API::Error::BaseError.new(errors[:message], rcode, rbody, error_obj)
184
+ end
185
+
186
+ def self.handle_restclient_error(e)
187
+ case e
188
+ when RestClient::ServerBrokeConnection, RestClient::RequestTimeout
189
+ message = "Could not connect to Maestrano. " +
190
+ "Please check your internet connection and try again. " +
191
+ "If this problem persists, you should check Maestrano service status at " +
192
+ "https://twitter.com/maestrano, or let us know at support@maestrano.com."
193
+
194
+ when RestClient::SSLCertificateNotVerified
195
+ message = "Could not verify Maestrano's SSL certificate. " +
196
+ "Please make sure that your network is not intercepting certificates. " +
197
+ "(Try going to https://maestrano.com/api/v1/ping in your browser.) " +
198
+ "If this problem persists, let us know at support@maestrano.com."
199
+
200
+ when SocketError
201
+ message = "Unexpected error communicating when trying to connect to Maestrano. " +
202
+ "You may be seeing this message because your DNS is not working. " +
203
+ "To check, try running 'host maestrano.com' from the command line."
204
+
205
+ else
206
+ message = "Unexpected error communicating with Maestrano. " +
207
+ "If this problem persists, let us know at support@maestrano.com."
208
+
209
+ end
210
+
211
+ raise APIConnectionError.new(message + "\n\n(Network error: #{e.message})")
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,18 @@
1
+ module Maestrano
2
+ module API
3
+ module Operation
4
+ module Create
5
+ module ClassMethods
6
+ def create(params={}, api_key=nil)
7
+ response, api_key = Maestrano::API::Operation::Base.request(:post, self.url, api_key, params)
8
+ Util.convert_to_maestrano_object(response, api_key)
9
+ end
10
+ end
11
+
12
+ def self.included(base)
13
+ base.extend(ClassMethods)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ module Maestrano
2
+ module API
3
+ module Operation
4
+ module Delete
5
+ def delete(params = {})
6
+ response, api_key = Maestrano::API::Operation::Base.request(:delete, url, @api_key, params)
7
+ refresh_from(response, api_key)
8
+ self
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ module Maestrano
2
+ module API
3
+ module Operation
4
+ module List
5
+ module ClassMethods
6
+ def all(filters={}, api_key=nil)
7
+ response, api_key = Maestrano::API::Operation::Base.request(:get, url, api_key, filters)
8
+ Util.convert_to_maestrano_object(response, api_key)
9
+ end
10
+ end
11
+
12
+ def self.included(base)
13
+ base.extend(ClassMethods)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,59 @@
1
+ module Maestrano
2
+ module API
3
+ module Operation
4
+ module Update
5
+ def save(opts={})
6
+ values = serialize_params(self).merge(opts)
7
+
8
+ if @values[:metadata]
9
+ values[:metadata] = serialize_metadata
10
+ end
11
+
12
+ if values.length > 0
13
+ values.delete(:id)
14
+
15
+ response, api_key = Maestrano::API::Operation::Base.request(:put, url, @api_key, values)
16
+ refresh_from(response, api_key)
17
+ end
18
+ self
19
+ end
20
+
21
+ def serialize_metadata
22
+ if @unsaved_values.include?(:metadata)
23
+ # the metadata object has been reassigned
24
+ # i.e. as object.metadata = {key => val}
25
+ metadata_update = @values[:metadata] # new hash
26
+ new_keys = metadata_update.keys.map(&:to_sym)
27
+ # remove keys at the server, but not known locally
28
+ keys_to_unset = @previous_metadata.keys - new_keys
29
+ keys_to_unset.each {|key| metadata_update[key] = ''}
30
+
31
+ metadata_update
32
+ else
33
+ # metadata is a Maestrano::API::Object, and can be serialized normally
34
+ serialize_params(@values[:metadata])
35
+ end
36
+ end
37
+
38
+ def serialize_params(obj)
39
+ case obj
40
+ when nil
41
+ ''
42
+ when Maestrano::API::Object
43
+ unsaved_keys = obj.instance_variable_get(:@unsaved_values)
44
+ obj_values = obj.instance_variable_get(:@values)
45
+ update_hash = {}
46
+
47
+ unsaved_keys.each do |k|
48
+ update_hash[k] = serialize_params(obj_values[k])
49
+ end
50
+
51
+ update_hash
52
+ else
53
+ obj
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,39 @@
1
+ module Maestrano
2
+ module API
3
+ class Resource < Maestrano::API::Object
4
+ def self.class_name
5
+ self.name.split('::').reject { |w| w.to_s == "Maestrano" }
6
+ end
7
+
8
+ def self.url
9
+ if self == Maestrano::API::Resource
10
+ raise NotImplementedError.new('Maestrano::API::Resource is an abstract class. You should perform actions on its subclasses (Bill, Customer, etc.)')
11
+ end
12
+ if class_name.is_a?(Array)
13
+ class_name.map { |w| CGI.escape(w.downcase) }.join("/") + 's'
14
+ else
15
+ "#{CGI.escape(class_name.downcase)}s"
16
+ end
17
+ end
18
+
19
+ def url
20
+ unless id = self['id']
21
+ raise Maestrano::API::Error::InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", 'id')
22
+ end
23
+ "#{self.class.url}/#{CGI.escape(id)}"
24
+ end
25
+
26
+ def refresh
27
+ response, api_key = Maestrano::API::Operation::Base.request(:get, url, @api_key, @retrieve_options)
28
+ refresh_from(response, api_key)
29
+ self
30
+ end
31
+
32
+ def self.retrieve(id, api_key=nil)
33
+ instance = self.new(id, api_key)
34
+ instance.refresh
35
+ instance
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,121 @@
1
+ module Maestrano
2
+ module API
3
+ module Util
4
+ def self.objects_to_ids(h)
5
+ case h
6
+ when Maestrano::API::Resource
7
+ h.id
8
+ when Hash
9
+ res = {}
10
+ h.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
11
+ res
12
+ when Array
13
+ h.map { |v| objects_to_ids(v) }
14
+ when Time
15
+ h.iso8601
16
+ else
17
+ h
18
+ end
19
+ end
20
+
21
+ def self.object_classes
22
+ @object_classes ||= {
23
+ 'account_bill' => Maestrano::Account::Bill,
24
+ 'internal_list_object' => Maestrano::API::ListObject
25
+ }
26
+ end
27
+
28
+ def self.convert_to_maestrano_object(resp, api_key)
29
+ case resp
30
+ when Array
31
+ if resp.empty? || !resp.first[:object]
32
+ resp
33
+ else
34
+ list = convert_to_maestrano_object({
35
+ object: 'internal_list_object',
36
+ data:[],
37
+ url: convert_to_maestrano_object(resp.first, api_key).class.url
38
+ },api_key)
39
+
40
+ resp.each do |i|
41
+ list.data.push(convert_to_maestrano_object(i, api_key))
42
+ end
43
+ list
44
+ end
45
+ when Hash
46
+ # Try converting to a known object class. If none available, fall back to generic Maestrano::API::Object
47
+ object_classes.fetch(resp[:object], Maestrano::API::Object).construct_from(resp, api_key)
48
+ else
49
+ # Automatically parse iso8601 dates
50
+ if resp =~ /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
51
+ Time.iso8601(resp).utc
52
+ else
53
+ resp
54
+ end
55
+ end
56
+ end
57
+
58
+ def self.file_readable(file)
59
+ # This is nominally equivalent to File.readable?, but that can
60
+ # report incorrect results on some more oddball filesystems
61
+ # (such as AFS)
62
+ begin
63
+ File.open(file) { |f| }
64
+ rescue
65
+ false
66
+ else
67
+ true
68
+ end
69
+ end
70
+
71
+ def self.symbolize_names(object)
72
+ case object
73
+ when Hash
74
+ new = {}
75
+ object.each do |key, value|
76
+ key = (key.to_sym rescue key) || key
77
+ new[key] = symbolize_names(value)
78
+ end
79
+ new
80
+ when Array
81
+ object.map { |value| symbolize_names(value) }
82
+ else
83
+ object
84
+ end
85
+ end
86
+
87
+ def self.url_encode(key)
88
+ URI.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
89
+ end
90
+
91
+ def self.flatten_params(params, parent_key=nil)
92
+ result = []
93
+ params.each do |key, value|
94
+ calculated_key = parent_key ? "#{parent_key}[#{url_encode(key)}]" : url_encode(key)
95
+ if value.is_a?(Hash)
96
+ result += flatten_params(value, calculated_key)
97
+ elsif value.is_a?(Array)
98
+ result += flatten_params_array(value, calculated_key)
99
+ else
100
+ result << [calculated_key, value]
101
+ end
102
+ end
103
+ result
104
+ end
105
+
106
+ def self.flatten_params_array(value, calculated_key)
107
+ result = []
108
+ value.each do |elem|
109
+ if elem.is_a?(Hash)
110
+ result += flatten_params(elem, calculated_key)
111
+ elsif elem.is_a?(Array)
112
+ result += flatten_params_array(elem, calculated_key)
113
+ else
114
+ result << ["#{calculated_key}[]", elem]
115
+ end
116
+ end
117
+ result
118
+ end
119
+ end
120
+ end
121
+ end