stormpath-sdk 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 (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
+