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