stormpath-sdk 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/Gemfile +4 -0
  2. data/README.md +24 -0
  3. data/Rakefile +16 -0
  4. data/lib/stormpath-sdk.rb +49 -0
  5. data/lib/stormpath-sdk/auth/authentication_result.rb +17 -0
  6. data/lib/stormpath-sdk/auth/basic_authenticator.rb +42 -0
  7. data/lib/stormpath-sdk/auth/basic_login_attempt.rb +30 -0
  8. data/lib/stormpath-sdk/auth/username_password_request.rb +43 -0
  9. data/lib/stormpath-sdk/client/api_key.rb +18 -0
  10. data/lib/stormpath-sdk/client/client.rb +23 -0
  11. data/lib/stormpath-sdk/client/client_builder.rb +291 -0
  12. data/lib/stormpath-sdk/ds/data_store.rb +150 -0
  13. data/lib/stormpath-sdk/ds/resource_factory.rb +22 -0
  14. data/lib/stormpath-sdk/http/authc/sauthc1_signer.rb +216 -0
  15. data/lib/stormpath-sdk/http/http_client_request_executor.rb +69 -0
  16. data/lib/stormpath-sdk/http/request.rb +83 -0
  17. data/lib/stormpath-sdk/http/response.rb +35 -0
  18. data/lib/stormpath-sdk/resource/account.rb +110 -0
  19. data/lib/stormpath-sdk/resource/account_list.rb +17 -0
  20. data/lib/stormpath-sdk/resource/application.rb +95 -0
  21. data/lib/stormpath-sdk/resource/application_list.rb +17 -0
  22. data/lib/stormpath-sdk/resource/collection_resource.rb +76 -0
  23. data/lib/stormpath-sdk/resource/directory.rb +76 -0
  24. data/lib/stormpath-sdk/resource/directory_list.rb +15 -0
  25. data/lib/stormpath-sdk/resource/email_verification_token.rb +11 -0
  26. data/lib/stormpath-sdk/resource/error.rb +42 -0
  27. data/lib/stormpath-sdk/resource/group.rb +73 -0
  28. data/lib/stormpath-sdk/resource/group_list.rb +18 -0
  29. data/lib/stormpath-sdk/resource/group_membership.rb +55 -0
  30. data/lib/stormpath-sdk/resource/group_membership_list.rb +17 -0
  31. data/lib/stormpath-sdk/resource/instance_resource.rb +13 -0
  32. data/lib/stormpath-sdk/resource/password_reset_token.rb +27 -0
  33. data/lib/stormpath-sdk/resource/resource.rb +173 -0
  34. data/lib/stormpath-sdk/resource/resource_error.rb +32 -0
  35. data/lib/stormpath-sdk/resource/status.rb +19 -0
  36. data/lib/stormpath-sdk/resource/tenant.rb +55 -0
  37. data/lib/stormpath-sdk/resource/utils.rb +16 -0
  38. data/lib/stormpath-sdk/util/assert.rb +26 -0
  39. data/lib/stormpath-sdk/util/hash.rb +17 -0
  40. data/lib/stormpath-sdk/util/request_utils.rb +57 -0
  41. data/lib/stormpath-sdk/version.rb +4 -0
  42. data/stormpath-sdk.gemspec +29 -0
  43. data/test/client/client.yml +16 -0
  44. data/test/client/clientbuilder_spec.rb +176 -0
  45. data/test/client/read_spec.rb +243 -0
  46. data/test/client/write_spec.rb +315 -0
  47. metadata +226 -0
@@ -0,0 +1,150 @@
1
+ module Stormpath
2
+
3
+ module DataStore
4
+
5
+ class DataStore
6
+
7
+ include Stormpath::Http
8
+ include Stormpath::Resource
9
+ include Stormpath::Resource::Utils
10
+ include Stormpath::Util::Assert
11
+
12
+ DEFAULT_SERVER_HOST = "api.stormpath.com"
13
+
14
+ DEFAULT_API_VERSION = 1
15
+
16
+ def initialize(request_executor, *base_url)
17
+
18
+ assert_not_nil request_executor, "RequestExecutor cannot be null."
19
+ @base_url = get_base_url *base_url
20
+ @request_executor = request_executor
21
+ @resource_factory = ResourceFactory.new(self)
22
+ end
23
+
24
+ def instantiate(clazz, properties)
25
+
26
+ @resource_factory.instantiate(clazz, properties)
27
+ end
28
+
29
+ def get_resource(href, clazz)
30
+
31
+ q_href = href
32
+
33
+ if needs_to_be_fully_qualified q_href
34
+ q_href = qualify q_href
35
+ end
36
+
37
+ data = execute_request('get', q_href, nil)
38
+ @resource_factory.instantiate(clazz, data.to_hash)
39
+
40
+ end
41
+
42
+ def create parent_href, resource, return_type
43
+ save_resource parent_href, resource, return_type
44
+ end
45
+
46
+ def save resource, *clazz
47
+ assert_not_nil resource, "resource argument cannot be null."
48
+ assert_kind_of Resource, resource, "resource argument must be instance of Resource"
49
+
50
+ href = resource.get_href
51
+ assert_true href.length > 0, "save may only be called on objects that have already been persisted (i.e. they have an existing href)."
52
+
53
+ if needs_to_be_fully_qualified(href)
54
+ href = qualify(href)
55
+ end
56
+
57
+ clazz = (clazz.nil? or clazz.length == 0) ? to_class_from_instance(resource) : clazz[0]
58
+
59
+ return_value = save_resource href, resource, clazz
60
+
61
+ #ensure the caller's argument is updated with what is returned from the server:
62
+ resource.set_properties return_value.properties
63
+
64
+ return_value
65
+
66
+ end
67
+
68
+ def delete resource
69
+
70
+ assert_not_nil resource, "resource argument cannot be null."
71
+ assert_kind_of Resource, resource, "resource argument must be instance of Resource"
72
+
73
+ execute_request('delete', resource.get_href, nil)
74
+
75
+ end
76
+
77
+ protected
78
+
79
+ def needs_to_be_fully_qualified href
80
+ !href.downcase.start_with? 'http'
81
+ end
82
+
83
+ def qualify href
84
+
85
+ slash_added = ''
86
+
87
+ if !href.start_with? '/'
88
+ slash_added = '/'
89
+ end
90
+
91
+ @base_url + slash_added + href
92
+ end
93
+
94
+ private
95
+
96
+ def execute_request(http_method, href, body)
97
+
98
+ request = Request.new(http_method, href, nil, Hash.new, body)
99
+ apply_default_request_headers request
100
+ response = @request_executor.execute_request request
101
+
102
+ result = response.body.length > 0 ? MultiJson.load(response.body) : ''
103
+
104
+ if response.error?
105
+ error = Error.new result
106
+ raise ResourceError.new error
107
+ end
108
+
109
+ result
110
+ end
111
+
112
+ def apply_default_request_headers request
113
+
114
+ request.http_headers.store 'Accept', 'application/json'
115
+ request.http_headers.store 'User-Agent', 'Stormpath-RubySDK/' + Stormpath::VERSION
116
+
117
+ if !request.body.nil? and request.body.length > 0
118
+ request.http_headers.store 'Content-Type', 'application/json'
119
+ end
120
+ end
121
+
122
+ def save_resource href, resource, return_type
123
+
124
+ assert_not_nil resource, "resource argument cannot be null."
125
+ assert_not_nil return_type, "returnType class cannot be null."
126
+ assert_kind_of Resource, resource, "resource argument must be instance of Resource"
127
+
128
+ q_href = href
129
+
130
+ if needs_to_be_fully_qualified q_href
131
+ q_href = qualify q_href
132
+ end
133
+
134
+ response = execute_request('post', q_href, MultiJson.dump(resource.properties))
135
+ @resource_factory.instantiate(return_type, response.to_hash)
136
+
137
+ end
138
+
139
+ def get_base_url *base_url
140
+ (!base_url.empty? and !base_url[0].nil?) ?
141
+ base_url[0] :
142
+ "https://" + DEFAULT_SERVER_HOST + "/v" + DEFAULT_API_VERSION.to_s
143
+ end
144
+
145
+ end
146
+
147
+ end
148
+
149
+ end
150
+
@@ -0,0 +1,22 @@
1
+ module Stormpath
2
+
3
+ module DataStore
4
+
5
+ class ResourceFactory
6
+
7
+ def initialize(data_store)
8
+
9
+ @data_store = data_store
10
+ end
11
+
12
+ def instantiate(clazz, constructor_args)
13
+
14
+ clazz.new @data_store, constructor_args
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+
@@ -0,0 +1,216 @@
1
+ module Stormpath
2
+
3
+ module Http
4
+
5
+ module Authc
6
+
7
+ class Sauthc1Signer
8
+
9
+ include OpenSSL
10
+ include UUIDTools
11
+ include Stormpath::Util
12
+
13
+ DEFAULT_ALGORITHM = "SHA256"
14
+ HOST_HEADER = "Host"
15
+ AUTHORIZATION_HEADER = "Authorization"
16
+ STORMPATH_DATE_HEADER = "X-Stormpath-Date"
17
+ ID_TERMINATOR = "sauthc1_request"
18
+ ALGORITHM = "HMAC-SHA-256"
19
+ AUTHENTICATION_SCHEME = "SAuthc1"
20
+ SAUTHC1_ID = "sauthc1Id"
21
+ SAUTHC1_SIGNED_HEADERS = "sauthc1SignedHeaders"
22
+ SAUTHC1_SIGNATURE = "sauthc1Signature"
23
+ DATE_FORMAT = "%Y%m%d"
24
+ TIMESTAMP_FORMAT = "%Y%m%dT%H%M%SZ"
25
+ NL = "\n"
26
+
27
+ def sign_request request, api_key
28
+
29
+ time = Time.now
30
+ time_stamp = time.utc.strftime TIMESTAMP_FORMAT
31
+ date_stamp = time.utc.strftime DATE_FORMAT
32
+
33
+ nonce = UUID.random_create.to_s
34
+
35
+ uri = request.resource_uri
36
+
37
+ # SAuthc1 requires that we sign the Host header so we
38
+ # have to have it in the request by the time we sign.
39
+ host_header = uri.host
40
+ if !RequestUtils.default_port?(uri)
41
+
42
+ host_header << ":" << uri.port.to_s
43
+ end
44
+
45
+ request.http_headers.store HOST_HEADER, host_header
46
+
47
+ request.http_headers.store STORMPATH_DATE_HEADER, time_stamp
48
+
49
+ method = request.http_method
50
+ canonical_resource_path = canonicalize_resource_path uri.path
51
+ canonical_query_string = canonicalize_query_string request
52
+ canonical_headers_string = canonicalize_headers request
53
+ signed_headers_string = get_signed_headers request
54
+ request_payload_hash_hex = to_hex(hash_text(get_request_payload(request)))
55
+
56
+ canonical_request = method + NL +
57
+ canonical_resource_path + NL +
58
+ canonical_query_string + NL +
59
+ canonical_headers_string + NL +
60
+ signed_headers_string + NL +
61
+ request_payload_hash_hex
62
+
63
+ id = api_key.id + "/" + date_stamp + "/" + nonce + "/" + ID_TERMINATOR
64
+
65
+ canonical_request_hash_hex = to_hex(hash_text(canonical_request))
66
+
67
+ string_to_sign = ALGORITHM + NL +
68
+ time_stamp + NL +
69
+ id + NL +
70
+ canonical_request_hash_hex
71
+
72
+ # SAuthc1 uses a series of derived keys, formed by hashing different pieces of data
73
+ k_secret = to_utf8 AUTHENTICATION_SCHEME + api_key.secret
74
+ k_date = sign date_stamp, k_secret, DEFAULT_ALGORITHM
75
+ k_nonce = sign nonce, k_date, DEFAULT_ALGORITHM
76
+ k_signing = sign ID_TERMINATOR, k_nonce, DEFAULT_ALGORITHM
77
+
78
+ signature = sign to_utf8(string_to_sign), k_signing, DEFAULT_ALGORITHM
79
+ signature_hex = to_hex signature
80
+
81
+ authorization_header = AUTHENTICATION_SCHEME + " " +
82
+ create_name_value_pair(SAUTHC1_ID, id) + ", " +
83
+ create_name_value_pair(SAUTHC1_SIGNED_HEADERS, signed_headers_string) + ", " +
84
+ create_name_value_pair(SAUTHC1_SIGNATURE, signature_hex)
85
+
86
+ request.http_headers.store AUTHORIZATION_HEADER, authorization_header
87
+
88
+ end
89
+
90
+
91
+ def to_hex data
92
+
93
+ result = ''
94
+ data.each_byte { |val|
95
+
96
+ hex = val.to_s(16)
97
+
98
+ if hex.length == 1
99
+
100
+ result << '0'
101
+
102
+ elsif hex.length == 8
103
+
104
+ hex = hex[0..6]
105
+ end
106
+
107
+ result << hex
108
+
109
+ }
110
+
111
+ result
112
+
113
+ end
114
+
115
+ protected
116
+
117
+ def canonicalize_query_string request
118
+ request.to_s_query_string true
119
+ end
120
+
121
+ def hash_text text
122
+ Digest.digest DEFAULT_ALGORITHM, to_utf8(text)
123
+ end
124
+
125
+ def sign data, key, algorithm
126
+
127
+ digest_data = to_utf8 data
128
+
129
+ digest = Digest::Digest.new(algorithm)
130
+
131
+ HMAC.digest(digest, key, digest_data)
132
+
133
+ end
134
+
135
+ def to_utf8 str
136
+ #we ask for multi line UTF-8 text
137
+ str.scan(/./mu).join
138
+ end
139
+
140
+ def get_request_payload request
141
+ get_request_payload_without_query_params request
142
+ end
143
+
144
+ def get_request_payload_without_query_params request
145
+
146
+ result = ''
147
+
148
+ if !request.body.nil?
149
+ result = request.body
150
+ end
151
+
152
+ result
153
+
154
+ end
155
+
156
+ private
157
+
158
+ def create_name_value_pair name, value
159
+ name + '=' + value
160
+ end
161
+
162
+ def canonicalize_resource_path resource_path
163
+
164
+ if resource_path.nil? or resource_path.empty?
165
+ '/'
166
+ else
167
+ RequestUtils.encode_url resource_path, true, true
168
+ end
169
+ end
170
+
171
+
172
+ def canonicalize_headers request
173
+
174
+ sorted_headers = request.http_headers.keys.sort!
175
+
176
+ result = ''
177
+
178
+ sorted_headers.each do |header|
179
+
180
+ result << header.downcase << ':' << request.http_headers[header].to_s
181
+
182
+ result << NL
183
+ end
184
+
185
+ result
186
+
187
+ end
188
+
189
+ def get_signed_headers request
190
+
191
+ sorted_headers = request.http_headers.keys.sort!
192
+
193
+ result = ''
194
+ sorted_headers.each do |header|
195
+
196
+ if !result.empty?
197
+ result << ';' << header
198
+ else
199
+ result << header
200
+ end
201
+
202
+
203
+ end
204
+
205
+ result.downcase
206
+
207
+ end
208
+
209
+
210
+ end
211
+
212
+ end
213
+
214
+ end
215
+
216
+ end
@@ -0,0 +1,69 @@
1
+ module Stormpath
2
+
3
+ module Http
4
+
5
+ class HttpClientRequestExecutor
6
+
7
+ include Stormpath::Http::Authc
8
+ include Stormpath::Util::Assert
9
+
10
+ def initialize(api_key)
11
+ @signer = Sauthc1Signer.new
12
+ @api_key = api_key
13
+ @http_client = HTTPClient.new
14
+
15
+ end
16
+
17
+ def execute_request(request)
18
+
19
+ assert_not_nil request, "Request argument cannot be null."
20
+
21
+ @signer.sign_request request, @api_key
22
+
23
+ domain = request.href
24
+
25
+ method = @http_client.method(request.http_method.downcase)
26
+
27
+ if request.body.nil?
28
+
29
+ response = method.call domain, request.query_string, request.http_headers
30
+
31
+ else
32
+
33
+ add_query_string domain, request.query_string
34
+
35
+ response = method.call domain, request.body, request.http_headers
36
+
37
+ end
38
+
39
+ Response.new response.http_header.status_code,
40
+ response.http_header.body_type,
41
+ response.content,
42
+ response.http_header.body_size
43
+
44
+ end
45
+
46
+ private
47
+
48
+ def add_query_string href, query_string
49
+
50
+ query_string.each do |key, value|
51
+
52
+ if href.include? '?'
53
+
54
+ href << '&' << key.to_s << '=' << value.to_s
55
+
56
+ else
57
+ href << '?' << key.to_s << '=' << value.to_s
58
+ end
59
+
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+