maestrano 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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