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