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.
- data/Gemfile +4 -0
- data/README.md +24 -0
- data/Rakefile +16 -0
- data/lib/stormpath-sdk.rb +49 -0
- data/lib/stormpath-sdk/auth/authentication_result.rb +17 -0
- data/lib/stormpath-sdk/auth/basic_authenticator.rb +42 -0
- data/lib/stormpath-sdk/auth/basic_login_attempt.rb +30 -0
- data/lib/stormpath-sdk/auth/username_password_request.rb +43 -0
- data/lib/stormpath-sdk/client/api_key.rb +18 -0
- data/lib/stormpath-sdk/client/client.rb +23 -0
- data/lib/stormpath-sdk/client/client_builder.rb +291 -0
- data/lib/stormpath-sdk/ds/data_store.rb +150 -0
- data/lib/stormpath-sdk/ds/resource_factory.rb +22 -0
- data/lib/stormpath-sdk/http/authc/sauthc1_signer.rb +216 -0
- data/lib/stormpath-sdk/http/http_client_request_executor.rb +69 -0
- data/lib/stormpath-sdk/http/request.rb +83 -0
- data/lib/stormpath-sdk/http/response.rb +35 -0
- data/lib/stormpath-sdk/resource/account.rb +110 -0
- data/lib/stormpath-sdk/resource/account_list.rb +17 -0
- data/lib/stormpath-sdk/resource/application.rb +95 -0
- data/lib/stormpath-sdk/resource/application_list.rb +17 -0
- data/lib/stormpath-sdk/resource/collection_resource.rb +76 -0
- data/lib/stormpath-sdk/resource/directory.rb +76 -0
- data/lib/stormpath-sdk/resource/directory_list.rb +15 -0
- data/lib/stormpath-sdk/resource/email_verification_token.rb +11 -0
- data/lib/stormpath-sdk/resource/error.rb +42 -0
- data/lib/stormpath-sdk/resource/group.rb +73 -0
- data/lib/stormpath-sdk/resource/group_list.rb +18 -0
- data/lib/stormpath-sdk/resource/group_membership.rb +55 -0
- data/lib/stormpath-sdk/resource/group_membership_list.rb +17 -0
- data/lib/stormpath-sdk/resource/instance_resource.rb +13 -0
- data/lib/stormpath-sdk/resource/password_reset_token.rb +27 -0
- data/lib/stormpath-sdk/resource/resource.rb +173 -0
- data/lib/stormpath-sdk/resource/resource_error.rb +32 -0
- data/lib/stormpath-sdk/resource/status.rb +19 -0
- data/lib/stormpath-sdk/resource/tenant.rb +55 -0
- data/lib/stormpath-sdk/resource/utils.rb +16 -0
- data/lib/stormpath-sdk/util/assert.rb +26 -0
- data/lib/stormpath-sdk/util/hash.rb +17 -0
- data/lib/stormpath-sdk/util/request_utils.rb +57 -0
- data/lib/stormpath-sdk/version.rb +4 -0
- data/stormpath-sdk.gemspec +29 -0
- data/test/client/client.yml +16 -0
- data/test/client/clientbuilder_spec.rb +176 -0
- data/test/client/read_spec.rb +243 -0
- data/test/client/write_spec.rb +315 -0
- 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
|
+
|