cf-uaac 1.3.0

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