cf-uaac 1.3.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/.gitignore +8 -0
- data/Gemfile +16 -0
- data/README.md +48 -0
- data/Rakefile +50 -0
- data/bin/completion-helper +80 -0
- data/bin/uaac +5 -0
- data/bin/uaac-completion.sh +34 -0
- data/bin/uaas +7 -0
- data/cf-uaac.gemspec +48 -0
- data/lib/cli.rb +15 -0
- data/lib/cli/base.rb +277 -0
- data/lib/cli/client_reg.rb +103 -0
- data/lib/cli/common.rb +187 -0
- data/lib/cli/config.rb +163 -0
- data/lib/cli/favicon.ico +0 -0
- data/lib/cli/group.rb +85 -0
- data/lib/cli/info.rb +54 -0
- data/lib/cli/runner.rb +52 -0
- data/lib/cli/token.rb +217 -0
- data/lib/cli/user.rb +108 -0
- data/lib/cli/version.rb +18 -0
- data/lib/stub/scim.rb +387 -0
- data/lib/stub/server.rb +310 -0
- data/lib/stub/uaa.rb +485 -0
- data/spec/client_reg_spec.rb +104 -0
- data/spec/common_spec.rb +89 -0
- data/spec/group_spec.rb +93 -0
- data/spec/http_spec.rb +165 -0
- data/spec/info_spec.rb +74 -0
- data/spec/spec_helper.rb +87 -0
- data/spec/token_spec.rb +119 -0
- data/spec/user_spec.rb +61 -0
- metadata +292 -0
data/lib/stub/uaa.rb
ADDED
@@ -0,0 +1,485 @@
|
|
1
|
+
#--
|
2
|
+
# Cloud Foundry 2012.02.03 Beta
|
3
|
+
# Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
|
4
|
+
#
|
5
|
+
# This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
6
|
+
# You may not use this product except in compliance with the License.
|
7
|
+
#
|
8
|
+
# This product includes a number of subcomponents with
|
9
|
+
# separate copyright notices and license terms. Your use of these
|
10
|
+
# subcomponents is subject to the terms and conditions of the
|
11
|
+
# subcomponent's license, as noted in the LICENSE file.
|
12
|
+
#++
|
13
|
+
|
14
|
+
require 'uaa'
|
15
|
+
require 'stub/server'
|
16
|
+
require 'stub/scim'
|
17
|
+
require 'cli/version'
|
18
|
+
require 'pp'
|
19
|
+
|
20
|
+
module CF::UAA
|
21
|
+
|
22
|
+
class StubUAAConn < Stub::Base
|
23
|
+
|
24
|
+
def inject_error(input = nil)
|
25
|
+
case server.reply_badly
|
26
|
+
when :non_json then reply.text("non-json reply")
|
27
|
+
when :bad_json then reply.body = %<{"access_token":"good.access.token" "missed a comma":"there"}>
|
28
|
+
when :bad_state then input[:state] = "badstate"
|
29
|
+
when :no_token_type then input.delete(:token_type)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def valid_token(required_scope)
|
34
|
+
return nil unless (ah = request.headers["authorization"]) && (ah = ah.split(' '))[0] =~ /^bearer$/i
|
35
|
+
contents = TokenCoder.decode(ah[1])
|
36
|
+
contents["scope"], required_scope = Util.arglist(contents["scope"]), Util.arglist(required_scope)
|
37
|
+
return contents if required_scope.nil? || !(required_scope & contents["scope"]).empty?
|
38
|
+
reply_in_kind(403, error: "insufficient_scope",
|
39
|
+
error_description: "required scope #{Util.strlist(required_scope)}")
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def ids_to_names(ids); ids ? ids.map { |id| server.scim.name(id) } : [] end
|
44
|
+
def names_to_ids(names, rtype); names ? names.map { |name| server.scim.id(name, rtype) } : [] end
|
45
|
+
def bad_request(message = nil); reply_in_kind(400, error: "bad request#{message ? ',' : ''} #{message}") end
|
46
|
+
def not_found(name = nil); reply_in_kind(404, error: "#{name} not found") end
|
47
|
+
def encode_cookie(obj = {}) Util.json_encode64(obj) end
|
48
|
+
def decode_cookie(str) Util.json.decode64(str) end
|
49
|
+
|
50
|
+
def primary_email(emails)
|
51
|
+
return unless emails
|
52
|
+
emails.each {|e| return e[:value] if e[:type] && e[:type] == "primary"}
|
53
|
+
emails[0][:value]
|
54
|
+
end
|
55
|
+
|
56
|
+
def find_user(name, pwd = nil)
|
57
|
+
user = server.scim.get_by_name(name, :user, :password, :id, :emails, :username, :groups)
|
58
|
+
user if user && (!pwd || user[:password] == pwd)
|
59
|
+
end
|
60
|
+
|
61
|
+
#----------------------------------------------------------------------------
|
62
|
+
# miscellaneous endpoints
|
63
|
+
#
|
64
|
+
|
65
|
+
def default_route; reply_in_kind(404, error: "not found", error_description: "unknown path #{request.path}") end
|
66
|
+
|
67
|
+
route :get, '/favicon.ico' do
|
68
|
+
reply.headers[:content_type] = "image/vnd.microsoft.icon"
|
69
|
+
reply.body = File.read File.expand_path(File.join(__FILE__, '..', '..', 'lib', 'cli', 'favicon.ico'))
|
70
|
+
end
|
71
|
+
|
72
|
+
route :get, '/' do reply_in_kind "welcome to stub UAA, version #{VERSION}" end
|
73
|
+
route :get, '/varz' do reply_in_kind(mem: 0, type: 'UAA', app: { version: VERSION } ) end
|
74
|
+
route :get, '/token_key' do reply_in_kind(alg: "none", value: "none") end
|
75
|
+
|
76
|
+
route :post, '/password/score', "content-type" => %r{application/x-www-form-urlencoded} do
|
77
|
+
info = Util.decode_form_to_hash(request.body)
|
78
|
+
return bad_request "no password to score" unless pwd = info["password"]
|
79
|
+
score = pwd.length > 10 || pwd.length < 0 ? 10 : pwd.length
|
80
|
+
reply_in_kind(score: score, requiredScore: 0)
|
81
|
+
end
|
82
|
+
|
83
|
+
route :get, %r{^/userinfo(\?|$)(.*)} do
|
84
|
+
return not_found unless (tokn = valid_token("openid")) &&
|
85
|
+
(info = server.scim.get(tokn["user_id"], :user, :username, :id, :emails)) && info[:username]
|
86
|
+
reply_in_kind(user_id: info[:id], user_name: info[:username], email: primary_email(info[:emails]))
|
87
|
+
end
|
88
|
+
|
89
|
+
route :get, '/login' do
|
90
|
+
return reply_in_kind(server.info) unless request.headers["accept"] =~ /text\/html/
|
91
|
+
session = decode_cookie(request.cookies["stubsession"]) || {}
|
92
|
+
if session["username"]
|
93
|
+
page = <<-DATA.gsub(/^ +/, '')
|
94
|
+
you are logged in as #{session["username"]}
|
95
|
+
<form id='logout' action='login.do' method='get' accept-charset='UTF-8'>
|
96
|
+
<input type='submit' name='submit' value='Logout' /></form>
|
97
|
+
DATA
|
98
|
+
else
|
99
|
+
page = <<-DATA.gsub(/^ +/, '')
|
100
|
+
<form id='login' action='login.do' method='post' accept-charset='UTF-8'>
|
101
|
+
<fieldset><legend>Login</legend><label for='username'>User name:</label>
|
102
|
+
<input type='text' name='username' id='username' maxlength='50' />
|
103
|
+
<label for='password'>Password:</label>
|
104
|
+
<input type='password' name='password' id='password' maxlength='50' />
|
105
|
+
<input type='submit' name='submit' value='Login' /></fieldset></form>
|
106
|
+
DATA
|
107
|
+
end
|
108
|
+
reply.html page
|
109
|
+
#reply.set_cookie(:stubsession, encode_cookie(session), httponly: nil)
|
110
|
+
end
|
111
|
+
|
112
|
+
route :post, '/login.do', "content-type" => %r{application/x-www-form-urlencoded} do
|
113
|
+
creds = Util.decode_form_to_hash(request.body)
|
114
|
+
user = find_user(creds['username'], creds['password'])
|
115
|
+
reply.headers[:location] = "login"
|
116
|
+
reply.status = 302
|
117
|
+
reply.set_cookie(:stubsession, encode_cookie(username: user[:username], httponly: nil))
|
118
|
+
end
|
119
|
+
|
120
|
+
route :get, %r{^/logout.do(\?|$)(.*)} do
|
121
|
+
query = Util.decode_form_to_hash(match[2])
|
122
|
+
reply.headers[:location] = query['redirect_uri'] || "login"
|
123
|
+
reply.status = 302
|
124
|
+
reply.set_cookie(:stubsession, encode_cookie, max_age: -1)
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
#----------------------------------------------------------------------------
|
129
|
+
# oauth2 endpoints and helpers
|
130
|
+
#
|
131
|
+
|
132
|
+
# current uaa token contents: exp, user_name, scope, email, user_id,
|
133
|
+
# client_id, client_authorities, user_authorities
|
134
|
+
def token_reply_info(client, scope, user = nil, state = nil, refresh = false)
|
135
|
+
interval = client[:access_token_validity] || 3600
|
136
|
+
token_body = { jti: SecureRandom.uuid, aud: scope, scope: scope,
|
137
|
+
client_id: client[:client_id], exp: interval + Time.now.to_i }
|
138
|
+
if user
|
139
|
+
token_body[:user_id] = user[:id]
|
140
|
+
token_body[:email] = primary_email(user[:emails])
|
141
|
+
token_body[:user_name] = user[:username]
|
142
|
+
end
|
143
|
+
info = { access_token: TokenCoder.encode(token_body, nil, nil, 'none'),
|
144
|
+
token_type: "bearer", expires_in: interval, scope: scope}
|
145
|
+
info[:state] = state if state
|
146
|
+
info[:refresh_token] = "universal_refresh_token" if refresh
|
147
|
+
inject_error(info)
|
148
|
+
info
|
149
|
+
end
|
150
|
+
|
151
|
+
def auth_client(basic_auth_header)
|
152
|
+
ah = basic_auth_header.split(' ')
|
153
|
+
return unless ah[0] =~ /^basic$/i
|
154
|
+
ah = Base64::strict_decode64(ah[1]).split(':')
|
155
|
+
client = server.scim.get_by_name(ah[0], :client)
|
156
|
+
client if client && client[:client_secret] == ah[1]
|
157
|
+
end
|
158
|
+
|
159
|
+
def valid_redir_uri?(client, redir_uri)
|
160
|
+
t = URI.parse(redir_uri)
|
161
|
+
return true unless (ruris = client[:redirect_uris]) && !ruris.empty?
|
162
|
+
false unless ruris.each { |reg_uri|
|
163
|
+
r = URI.parse(reg_uri)
|
164
|
+
return true if r.scheme == t.scheme && r.host == t.host &&
|
165
|
+
(!r.port || r.port == t.port) && (!r.path || r.path == t.path)
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
def redir_with_fragment(cburi, params)
|
170
|
+
reply.status = 302
|
171
|
+
uri = URI.parse(cburi)
|
172
|
+
uri.fragment = URI.encode_www_form(params)
|
173
|
+
reply.headers[:location] = uri.to_s
|
174
|
+
end
|
175
|
+
|
176
|
+
def redir_with_query(cburi, params)
|
177
|
+
reply.status = 302
|
178
|
+
uri = URI.parse(cburi)
|
179
|
+
uri.query = URI.encode_www_form(params)
|
180
|
+
reply.headers[:location] = uri.to_s
|
181
|
+
end
|
182
|
+
|
183
|
+
def redir_err_f(cburi, state, msg); redir_with_fragment(cburi, error: msg, state: state) end
|
184
|
+
def redir_err_q(cburi, state, msg); redir_with_query(cburi, error: msg, state: state) end
|
185
|
+
|
186
|
+
# returns granted scopes
|
187
|
+
# TODO: doesn't handle actual user authorization yet
|
188
|
+
def calc_scope(client, user, requested_scope)
|
189
|
+
possible_scope = ids_to_names(client[user ? :scope : :authorities])
|
190
|
+
requested_scope = Util.arglist(requested_scope) || []
|
191
|
+
return unless (requested_scope - possible_scope).empty?
|
192
|
+
requested_scope = possible_scope if requested_scope.empty?
|
193
|
+
granted_scopes = user ? (ids_to_names(user[:groups]) & requested_scope) : requested_scope # handle auto-deny
|
194
|
+
Util.strlist(granted_scopes) unless granted_scopes.empty?
|
195
|
+
end
|
196
|
+
|
197
|
+
route [:post, :get], %r{^/oauth/authorize\?(.*)} do
|
198
|
+
query = Util.decode_form_to_hash(match[1])
|
199
|
+
client = server.scim.get_by_name(query["client_id"], :client)
|
200
|
+
cburi, state = query["redirect_uri"], query["state"]
|
201
|
+
|
202
|
+
# if invalid client_id or redir_uri: inform resource owner, do not redirect
|
203
|
+
unless client && valid_redir_uri?(client, cburi)
|
204
|
+
return bad_request "invalid client_id or redirect_uri"
|
205
|
+
end
|
206
|
+
if query["response_type"] == 'token'
|
207
|
+
unless client[:authorized_grant_types].include?("implicit")
|
208
|
+
return redir_err_f(cburi, state, "unauthorized_client")
|
209
|
+
end
|
210
|
+
if request.method == "post"
|
211
|
+
unless request.headers["content-type"] =~ %r{application/x-www-form-urlencoded} &&
|
212
|
+
(creds = Util.decode_form_to_hash(request.body)) &&
|
213
|
+
creds["source"] && creds["source"] == "credentials"
|
214
|
+
return redir_err_f(cburi, state, "invalid_request")
|
215
|
+
end
|
216
|
+
unless user = find_user(creds["username"], creds["password"])
|
217
|
+
return redir_err_f(cburi, state, "access_denied")
|
218
|
+
end
|
219
|
+
else
|
220
|
+
return reply.status = 501 # TODO: how to authN user and ask for authorizations?
|
221
|
+
end
|
222
|
+
unless (granted_scope = calc_scope(client, user, query["scope"]))
|
223
|
+
return redir_err_f(cburi, state, "invalid_scope")
|
224
|
+
end
|
225
|
+
# TODO: how to stub any remaining scopes that are not auto-approve?
|
226
|
+
return redir_with_fragment(cburi, token_reply_info(client, granted_scope, user, query["state"]))
|
227
|
+
end
|
228
|
+
return redir_err_q(cburi, state, "invalid_request") unless request.method == "get"
|
229
|
+
return redir_err_q(cburi, state, "unsupported_response_type") unless query["response_type"] == 'code'
|
230
|
+
unless client[:authorized_grant_types].include?("authorization_code")
|
231
|
+
return redir_err_f(cburi, state, "unauthorized_client")
|
232
|
+
end
|
233
|
+
return reply.status = 501 unless query["emphatic_user"] # TODO: how to authN user and ask for authorizations?
|
234
|
+
return redir_err_f(cburi, state, "access_denied") unless user = find_user(query["emphatic_user"])
|
235
|
+
scope = calc_scope(client, user, query["scope"])
|
236
|
+
redir_with_query(cburi, state: state, code: assign_auth_code(client[:id], user[:id], scope, cburi))
|
237
|
+
end
|
238
|
+
|
239
|
+
# if required and optional arrays are given, extra params are an error
|
240
|
+
def bad_params?(params, required, optional = nil)
|
241
|
+
required.each {|r|
|
242
|
+
next if params[r]
|
243
|
+
reply.json(400, error: "invalid_request", error_description: "no #{r} in request")
|
244
|
+
return true
|
245
|
+
}
|
246
|
+
return false unless optional
|
247
|
+
params.each {|k, v|
|
248
|
+
next if required.include?(k) || optional.include?(k)
|
249
|
+
reply.json(400, error: "invalid_request", error_description: "#{k} not allowed")
|
250
|
+
return true
|
251
|
+
}
|
252
|
+
false
|
253
|
+
end
|
254
|
+
|
255
|
+
# TODO: need to save scope, timeout, client, redir_url, user_id, etc
|
256
|
+
# when redeeming an authcode, code and redir_url must match
|
257
|
+
@authcode_store = {}
|
258
|
+
class << self; attr_accessor :authcode_store end
|
259
|
+
def assign_auth_code(client_id, user_id, scope, redir_uri)
|
260
|
+
code = SecureRandom.base64(8)
|
261
|
+
raise "authcode collision" if self.class.authcode_store[code]
|
262
|
+
self.class.authcode_store[code] = {client_id: client_id, user_id: user_id,
|
263
|
+
scope: scope, redir_uri: redir_uri}
|
264
|
+
code
|
265
|
+
end
|
266
|
+
def redeem_auth_code(client_id, redir_uri, code)
|
267
|
+
return unless info = self.class.authcode_store.delete(code)
|
268
|
+
return unless info[:client_id] == client_id && info[:redir_uri] == redir_uri
|
269
|
+
[info[:user_id], info[:scope]]
|
270
|
+
end
|
271
|
+
|
272
|
+
route :post, "/oauth/token", "content-type" => %r{application/x-www-form-urlencoded},
|
273
|
+
"accept" => %r{application/json} do
|
274
|
+
unless client = auth_client(request.headers["authorization"])
|
275
|
+
reply.headers[:www_authenticate] = "basic"
|
276
|
+
return reply.json(401, error: "invalid_client")
|
277
|
+
end
|
278
|
+
return if bad_params?(params = Util.decode_form_to_hash(request.body), ['grant_type'])
|
279
|
+
unless client[:authorized_grant_types].include?(params['grant_type'])
|
280
|
+
return reply.json(400, error: "unauthorized_client")
|
281
|
+
end
|
282
|
+
case params.delete('grant_type')
|
283
|
+
when "authorization_code"
|
284
|
+
# TODO: need authcode store with requested scope, redir_uri must match
|
285
|
+
return if bad_params?(params, ['code', 'redirect_uri'], [])
|
286
|
+
user_id, scope = redeem_auth_code(client[:id], params['redirect_uri'], params['code'])
|
287
|
+
return reply.json(400, error: "invalid_grant") unless user_id && scope
|
288
|
+
user = server.scim.get(user, :user, :id, :emails, :username)
|
289
|
+
reply.json(token_reply_info(client, scope, user, nil, true))
|
290
|
+
when "password"
|
291
|
+
return if bad_params?(params, ['username', 'password'], ['scope'])
|
292
|
+
user = find_user(params['username'], params['password'])
|
293
|
+
return reply.json(400, error: "invalid_grant") unless user
|
294
|
+
scope = calc_scope(client, user, params['scope'])
|
295
|
+
return reply.json(400, error: "invalid_scope") unless scope
|
296
|
+
reply.json(token_reply_info(client, scope, user))
|
297
|
+
when "client_credentials"
|
298
|
+
return if bad_params?(params, [], ['scope'])
|
299
|
+
scope = calc_scope(client, nil, params['scope'])
|
300
|
+
return reply.json(400, error: "invalid_scope") unless scope
|
301
|
+
reply.json(token_reply_info(client, scope))
|
302
|
+
when "refresh_token"
|
303
|
+
return if bad_params?(params, ['refresh_token'], ['scope'])
|
304
|
+
return reply.json(400, error: "invalid_grant") unless params['refresh_token'] == "universal_refresh_token"
|
305
|
+
# TODO: max scope should come from refresh token, or user from refresh token
|
306
|
+
# this should use calc_scope when we know the user
|
307
|
+
scope = ids_to_names(client[:scope])
|
308
|
+
scope = Util.strlist(Util.arglist(params['scope'], scope) & scope)
|
309
|
+
return reply.json(400, error: "invalid_scope") if scope.empty?
|
310
|
+
reply.json(token_reply_info(client, scope))
|
311
|
+
else
|
312
|
+
reply.json(400, error: "unsupported_grant_type")
|
313
|
+
end
|
314
|
+
inject_error
|
315
|
+
end
|
316
|
+
|
317
|
+
route :post, "/alternate/oauth/token", "content-type" => %r{application/x-www-form-urlencoded},
|
318
|
+
"accept" => %r{application/json} do
|
319
|
+
request.path.replace("/oauth/token")
|
320
|
+
server.info.delete(:token_endpoint) # this indicates this was executed for a unit test
|
321
|
+
process
|
322
|
+
end
|
323
|
+
|
324
|
+
#----------------------------------------------------------------------------
|
325
|
+
# client endpoints
|
326
|
+
#
|
327
|
+
def client_to_scim(info)
|
328
|
+
['authorities', 'scope', 'auto_approve_scope'].each { |a| info[a] = names_to_ids(info[a], :group) if info.key?(a) }
|
329
|
+
info
|
330
|
+
end
|
331
|
+
|
332
|
+
def scim_to_client(info)
|
333
|
+
[:authorities, :scope, :auto_approve_scope].each { |a| info[a] = ids_to_names(info[a]) if info.key?(a) }
|
334
|
+
info.delete(:id)
|
335
|
+
info
|
336
|
+
end
|
337
|
+
|
338
|
+
route :get, %r{^/oauth/clients(\?|$)(.*)} do
|
339
|
+
return unless valid_token("clients.read")
|
340
|
+
info, _ = server.scim.find(:client)
|
341
|
+
reply_in_kind(info.each_with_object({}) {|c, o| o[c[:client_id]] = scim_to_client(c)})
|
342
|
+
end
|
343
|
+
|
344
|
+
route :post, '/oauth/clients', "content-type" => %r{application/json} do
|
345
|
+
return unless valid_token("clients.write")
|
346
|
+
id = server.scim.add(:client, client_to_scim(Util.json_parse(request.body, :down)))
|
347
|
+
reply_in_kind scim_to_client(server.scim.get(id, :client, *StubScim::VISIBLE_ATTRS[:client]))
|
348
|
+
end
|
349
|
+
|
350
|
+
route :put, %r{^/oauth/clients/([^/]+)$}, "content-type" => %r{application/json} do
|
351
|
+
return unless valid_token("clients.write")
|
352
|
+
info = client_to_scim(Util.json_parse(request.body, :down))
|
353
|
+
server.scim.update(server.scim.id(match[1], :client), info)
|
354
|
+
reply.json(scim_to_client(info))
|
355
|
+
end
|
356
|
+
|
357
|
+
route :get, %r{^/oauth/clients/([^/]+)$} do
|
358
|
+
return unless valid_token("clients.read")
|
359
|
+
return not_found(match[1]) unless client = server.scim.get_by_name(match[1], :client, *StubScim::VISIBLE_ATTRS[:client])
|
360
|
+
reply_in_kind(scim_to_client(client))
|
361
|
+
end
|
362
|
+
|
363
|
+
route :delete, %r{^/oauth/clients/([^/]+)$} do
|
364
|
+
return unless valid_token("clients.write")
|
365
|
+
return not_found(match[1]) unless server.scim.remove(server.scim.id(match[1], :client))
|
366
|
+
end
|
367
|
+
|
368
|
+
route :put, %r{^/oauth/clients/([^/]+)/secret$}, "content-type" => %r{application/json} do
|
369
|
+
info = Util.json_parse(request.body, :down)
|
370
|
+
if oldsecret = info['oldsecret']
|
371
|
+
return unless valid_token("clients.secret")
|
372
|
+
return not_found(match[1]) unless client = server.scim.get(match[1], :client, :client_secret)
|
373
|
+
return bad_request("old secret does not match") unless oldsecret == client[:client_secret]
|
374
|
+
else
|
375
|
+
return unless valid_token("uaa.admin")
|
376
|
+
end
|
377
|
+
return bad_request("no new secret given") unless info['secret']
|
378
|
+
server.scim.set_hidden_attr(match[1], :client_secret, info['secret'])
|
379
|
+
reply.json(status: "ok", message: "secret updated")
|
380
|
+
end
|
381
|
+
|
382
|
+
#----------------------------------------------------------------------------
|
383
|
+
# users and groups endpoints
|
384
|
+
#
|
385
|
+
route :post, %r{^/(Users|Groups)$}, "content-type" => %r{application/json} do
|
386
|
+
return unless valid_token("scim.write")
|
387
|
+
rtype = match[1] == "Users"? :user : :group
|
388
|
+
id = server.scim.add(rtype, Util.json_parse(request.body, :down))
|
389
|
+
server.auto_groups.each {|g| server.scim.add_member(g, id)} if rtype == :user && server.auto_groups
|
390
|
+
reply_in_kind server.scim.get(id, rtype, *StubScim::VISIBLE_ATTRS[rtype])
|
391
|
+
end
|
392
|
+
|
393
|
+
route :put, %r{^/(Users|Groups)/([^/]+)$}, "content-type" => %r{application/json} do
|
394
|
+
return unless valid_token("scim.write")
|
395
|
+
rtype = match[1] == "Users"? :user : :group
|
396
|
+
id = server.scim.update(match[2], Util.json_parse(request.body, :down), request.headers[:match_if], rtype)
|
397
|
+
reply_in_kind server.scim.get(id, rtype, *StubScim::VISIBLE_ATTRS[rtype])
|
398
|
+
end
|
399
|
+
|
400
|
+
def sanitize_int(arg, default, min, max = nil)
|
401
|
+
return default if arg.nil?
|
402
|
+
return unless arg.to_i.to_s == arg && (i = arg.to_i) >= min
|
403
|
+
max && i > max ? max : i
|
404
|
+
end
|
405
|
+
|
406
|
+
def page_query(rtype, query, attrs)
|
407
|
+
if query['attributes']
|
408
|
+
attrs = attrs & Util.arglist(query['attributes']).each_with_object([]) {|a, o|
|
409
|
+
o << a.to_sym if StubScim::ATTR_NAMES.include?(a = a.downcase)
|
410
|
+
}
|
411
|
+
end
|
412
|
+
start = sanitize_int(query['startindex'], 1, 1)
|
413
|
+
count = sanitize_int(query['count'], 15, 1, 3000)
|
414
|
+
return bad_request("invalid startIndex or count") unless start && count
|
415
|
+
info, total = server.scim.find(rtype, start - 1, count, query['filter'], attrs)
|
416
|
+
reply_in_kind(resources: info, itemsPerPage: info.length, startIndex: start, totalResults: total)
|
417
|
+
end
|
418
|
+
|
419
|
+
route :get, %r{^/(Users|Groups)(\?|$)(.*)} do
|
420
|
+
return unless valid_token("scim.read")
|
421
|
+
rtype = match[1] == "Users"? :user : :group
|
422
|
+
page_query(rtype, Util.decode_form_to_hash(match[3], :down), StubScim::VISIBLE_ATTRS[rtype])
|
423
|
+
end
|
424
|
+
|
425
|
+
route :get, %r{^/(Users|Groups)/([^/]+)$} do
|
426
|
+
return unless valid_token("scim.read")
|
427
|
+
rtype = match[1] == "Users"? :user : :group
|
428
|
+
return not_found(match[2]) unless obj = server.scim.get(match[2], rtype, *StubScim::VISIBLE_ATTRS[rtype])
|
429
|
+
reply_in_kind(obj)
|
430
|
+
end
|
431
|
+
|
432
|
+
route :delete, %r{^/(Users|Groups)/([^/]+)$} do
|
433
|
+
return unless valid_token("scim.write")
|
434
|
+
not_found(match[2]) unless server.scim.remove(match[2], match[1] == "Users"? :user : :group)
|
435
|
+
end
|
436
|
+
|
437
|
+
route :put, %r{^/Users/([^/]+)/password$}, "content-type" => %r{application/json} do
|
438
|
+
info = Util.json_parse(request.body, :down)
|
439
|
+
if oldpwd = info['oldpassword']
|
440
|
+
return unless valid_token("password.write")
|
441
|
+
return not_found(match[1]) unless user = server.scim.get(match[1], :user, :password)
|
442
|
+
return bad_request("old password does not match") unless oldpwd == user[:password]
|
443
|
+
else
|
444
|
+
return unless valid_token("scim.write")
|
445
|
+
end
|
446
|
+
return bad_request("no new password given") unless newpwd = info['password']
|
447
|
+
server.scim.set_hidden_attr(match[1], :password, newpwd)
|
448
|
+
reply.json(status: "ok", message: "password updated")
|
449
|
+
end
|
450
|
+
|
451
|
+
route :get, %r{^/ids/Users(\?|$)(.*)} do
|
452
|
+
page_query(:user, Util.decode_form_to_hash(match[2], :down), [:username, :id])
|
453
|
+
end
|
454
|
+
|
455
|
+
end
|
456
|
+
|
457
|
+
class StubUAA < Stub::Server
|
458
|
+
|
459
|
+
attr_accessor :reply_badly
|
460
|
+
attr_reader :scim, :auto_groups
|
461
|
+
|
462
|
+
def initialize(boot_client = "admin", boot_secret = "adminsecret", logger = Util.default_logger)
|
463
|
+
@scim = StubScim.new
|
464
|
+
@auto_groups = ["password.write", "openid"]
|
465
|
+
.each_with_object([]) { |g, o| o << @scim.add(:group, 'displayname' => g) }
|
466
|
+
["scim.read", "scim.write", "uaa.resource"]
|
467
|
+
.each { |g| @scim.add(:group, 'displayname' => g) }
|
468
|
+
gids = ["clients.write", "clients.read", "clients.secret", "uaa.admin"]
|
469
|
+
.each_with_object([]) { |s, o| o << @scim.add(:group, 'displayname' => s) }
|
470
|
+
@scim.add(:client, 'client_id' => boot_client, 'client_secret' => boot_secret,
|
471
|
+
'authorized_grant_types' => ["client_credentials"], 'authorities' => gids,
|
472
|
+
'access_token_validity' => 60 * 60 * 24 * 7)
|
473
|
+
@scim.add(:client, 'client_id' => "vmc", 'authorized_grant_types' => ["implicit"],
|
474
|
+
'scope' => [@scim.id("openid", :group), @scim.id("password.write", :group)],
|
475
|
+
'access_token_validity' => 5 * 60 )
|
476
|
+
info = { commit_id: "not implemented",
|
477
|
+
app: {name: "Stub UAA", version: CLI_VERSION, description: "User Account and Authentication Service, test server"},
|
478
|
+
prompts: {username: ["text", "Username"], password: ["password","Password"]} }
|
479
|
+
super(StubUAAConn, logger, info)
|
480
|
+
end
|
481
|
+
|
482
|
+
end
|
483
|
+
|
484
|
+
end
|
485
|
+
|