cloulu 0.0.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/{cloulu → cl} +0 -0
- data/lib/cc_api_stub/applications.rb +53 -0
- data/lib/cc_api_stub/domains.rb +16 -0
- data/lib/cc_api_stub/frameworks.rb +22 -0
- data/lib/cc_api_stub/helper.rb +131 -0
- data/lib/cc_api_stub/login.rb +21 -0
- data/lib/cc_api_stub/organization_users.rb +21 -0
- data/lib/cc_api_stub/organizations.rb +70 -0
- data/lib/cc_api_stub/routes.rb +26 -0
- data/lib/cc_api_stub/runtimes.rb +22 -0
- data/lib/cc_api_stub/service_bindings.rb +22 -0
- data/lib/cc_api_stub/service_instances.rb +22 -0
- data/lib/cc_api_stub/services.rb +25 -0
- data/lib/cc_api_stub/spaces.rb +49 -0
- data/lib/cc_api_stub/users.rb +84 -0
- data/lib/cc_api_stub.rb +17 -0
- data/lib/cfoundry/auth_token.rb +63 -0
- data/lib/cfoundry/baseclient.rb +201 -0
- data/lib/cfoundry/chatty_hash.rb +46 -0
- data/lib/cfoundry/client.rb +46 -0
- data/lib/cfoundry/concerns/login_helpers.rb +13 -0
- data/lib/cfoundry/errors.rb +160 -0
- data/lib/cfoundry/rest_client.rb +299 -0
- data/lib/cfoundry/test_support.rb +3 -0
- data/lib/cfoundry/trace_helpers.rb +40 -0
- data/lib/cfoundry/uaaclient.rb +112 -0
- data/lib/cfoundry/upload_helpers.rb +187 -0
- data/lib/cfoundry/v1/app.rb +363 -0
- data/lib/cfoundry/v1/base.rb +72 -0
- data/lib/cfoundry/v1/client.rb +193 -0
- data/lib/cfoundry/v1/framework.rb +21 -0
- data/lib/cfoundry/v1/model.rb +178 -0
- data/lib/cfoundry/v1/model_magic.rb +129 -0
- data/lib/cfoundry/v1/runtime.rb +24 -0
- data/lib/cfoundry/v1/service.rb +39 -0
- data/lib/cfoundry/v1/service_instance.rb +32 -0
- data/lib/cfoundry/v1/service_plan.rb +19 -0
- data/lib/cfoundry/v1/user.rb +22 -0
- data/lib/cfoundry/v2/app.rb +392 -0
- data/lib/cfoundry/v2/base.rb +83 -0
- data/lib/cfoundry/v2/client.rb +138 -0
- data/lib/cfoundry/v2/domain.rb +11 -0
- data/lib/cfoundry/v2/framework.rb +14 -0
- data/lib/cfoundry/v2/model.rb +148 -0
- data/lib/cfoundry/v2/model_magic.rb +449 -0
- data/lib/cfoundry/v2/organization.rb +16 -0
- data/lib/cfoundry/v2/route.rb +15 -0
- data/lib/cfoundry/v2/runtime.rb +12 -0
- data/lib/cfoundry/v2/service.rb +19 -0
- data/lib/cfoundry/v2/service_auth_token.rb +9 -0
- data/lib/cfoundry/v2/service_binding.rb +10 -0
- data/lib/cfoundry/v2/service_instance.rb +14 -0
- data/lib/cfoundry/v2/service_plan.rb +12 -0
- data/lib/cfoundry/v2/space.rb +18 -0
- data/lib/cfoundry/v2/user.rb +64 -0
- data/lib/cfoundry/validator.rb +39 -0
- data/lib/cfoundry/version.rb +4 -0
- data/lib/cfoundry/zip.rb +56 -0
- data/lib/cfoundry.rb +2 -0
- data/lib/manifests-vmc-plugin/errors.rb +21 -0
- data/lib/manifests-vmc-plugin/loader/builder.rb +34 -0
- data/lib/manifests-vmc-plugin/loader/normalizer.rb +149 -0
- data/lib/manifests-vmc-plugin/loader/resolver.rb +79 -0
- data/lib/manifests-vmc-plugin/loader.rb +31 -0
- data/lib/manifests-vmc-plugin/plugin.rb +145 -0
- data/lib/manifests-vmc-plugin/version.rb +3 -0
- data/lib/manifests-vmc-plugin.rb +313 -0
- data/lib/mothership/base.rb +99 -0
- data/lib/mothership/callbacks.rb +85 -0
- data/lib/mothership/command.rb +146 -0
- data/lib/mothership/errors.rb +38 -0
- data/lib/mothership/help/commands.rb +53 -0
- data/lib/mothership/help/printer.rb +170 -0
- data/lib/mothership/help.rb +64 -0
- data/lib/mothership/inputs.rb +189 -0
- data/lib/mothership/parser.rb +182 -0
- data/lib/mothership/version.rb +3 -0
- data/lib/mothership.rb +64 -0
- data/lib/tunnel-vmc-plugin/plugin.rb +178 -0
- data/lib/tunnel-vmc-plugin/tunnel.rb +308 -0
- data/lib/tunnel-vmc-plugin/version.rb +3 -0
- data/lib/uaa/http.rb +168 -0
- data/lib/uaa/misc.rb +121 -0
- data/lib/uaa/scim.rb +292 -0
- data/lib/uaa/token_coder.rb +196 -0
- data/lib/uaa/token_issuer.rb +255 -0
- data/lib/uaa/util.rb +235 -0
- data/lib/uaa/version.rb +19 -0
- data/lib/uaa.rb +18 -0
- data/lib/vmc/cli/app/app.rb +45 -0
- data/lib/vmc/cli/app/apps.rb +99 -0
- data/lib/vmc/cli/app/base.rb +90 -0
- data/lib/vmc/cli/app/crashes.rb +42 -0
- data/lib/vmc/cli/app/delete.rb +95 -0
- data/lib/vmc/cli/app/deprecated.rb +11 -0
- data/lib/vmc/cli/app/env.rb +78 -0
- data/lib/vmc/cli/app/files.rb +137 -0
- data/lib/vmc/cli/app/health.rb +26 -0
- data/lib/vmc/cli/app/instances.rb +53 -0
- data/lib/vmc/cli/app/logs.rb +76 -0
- data/lib/vmc/cli/app/push/create.rb +165 -0
- data/lib/vmc/cli/app/push/interactions.rb +94 -0
- data/lib/vmc/cli/app/push/sync.rb +64 -0
- data/lib/vmc/cli/app/push.rb +109 -0
- data/lib/vmc/cli/app/rename.rb +35 -0
- data/lib/vmc/cli/app/restart.rb +20 -0
- data/lib/vmc/cli/app/scale.rb +71 -0
- data/lib/vmc/cli/app/start.rb +143 -0
- data/lib/vmc/cli/app/stats.rb +67 -0
- data/lib/vmc/cli/app/stop.rb +27 -0
- data/lib/vmc/cli/help.rb +11 -0
- data/lib/vmc/cli/interactive.rb +105 -0
- data/lib/vmc/cli/route/base.rb +12 -0
- data/lib/vmc/cli/route/map.rb +82 -0
- data/lib/vmc/cli/route/routes.rb +25 -0
- data/lib/vmc/cli/route/unmap.rb +94 -0
- data/lib/vmc/cli/service/base.rb +8 -0
- data/lib/vmc/cli/service/bind.rb +44 -0
- data/lib/vmc/cli/service/create.rb +126 -0
- data/lib/vmc/cli/service/delete.rb +86 -0
- data/lib/vmc/cli/service/rename.rb +35 -0
- data/lib/vmc/cli/service/service.rb +42 -0
- data/lib/vmc/cli/service/services.rb +114 -0
- data/lib/vmc/cli/service/unbind.rb +38 -0
- data/lib/vmc/cli/start/base.rb +94 -0
- data/lib/vmc/cli/start/colors.rb +13 -0
- data/lib/vmc/cli/start/info.rb +126 -0
- data/lib/vmc/cli/start/login.rb +97 -0
- data/lib/vmc/cli/start/logout.rb +17 -0
- data/lib/vmc/cli/start/target.rb +60 -0
- data/lib/vmc/cli/start/target_interactions.rb +37 -0
- data/lib/vmc/cli/start/targets.rb +16 -0
- data/lib/vmc/cli/user/base.rb +29 -0
- data/lib/vmc/cli/user/create.rb +39 -0
- data/lib/vmc/cli/user/delete.rb +27 -0
- data/lib/vmc/cli/user/passwd.rb +50 -0
- data/lib/vmc/cli/user/register.rb +42 -0
- data/lib/vmc/cli/user/users.rb +32 -0
- data/lib/vmc/cli/v2_check_cli.rb +16 -0
- data/lib/vmc/cli.rb +474 -0
- data/lib/vmc/constants.rb +13 -0
- data/lib/vmc/detect.rb +129 -0
- data/lib/vmc/errors.rb +19 -0
- data/lib/vmc/plugin.rb +56 -0
- data/lib/vmc/spacing.rb +89 -0
- data/lib/vmc/spec_helper.rb +1 -0
- data/lib/vmc/test_support.rb +6 -0
- data/lib/vmc/version.rb +3 -0
- data/lib/vmc.rb +8 -0
- data/vendor/errors/v1.yml +189 -0
- data/vendor/errors/v2.yml +360 -0
- metadata +303 -190
@@ -0,0 +1,196 @@
|
|
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 "openssl"
|
15
|
+
require "uaa/util"
|
16
|
+
|
17
|
+
module CF::UAA
|
18
|
+
|
19
|
+
# this code does not support the given token signature algorithim
|
20
|
+
class SignatureNotSupported < DecodeError; end
|
21
|
+
|
22
|
+
# this instance policy does not accept the given token signature algorithim
|
23
|
+
class SignatureNotAccepted < DecodeError; end
|
24
|
+
|
25
|
+
class InvalidSignature < DecodeError; end
|
26
|
+
class InvalidTokenFormat < DecodeError; end
|
27
|
+
class TokenExpired < AuthError; end
|
28
|
+
class InvalidAudience < AuthError; end
|
29
|
+
|
30
|
+
# This class is for OAuth Resource Servers.
|
31
|
+
# Resource Servers get tokens and need to validate and decode them,
|
32
|
+
# but they do not obtain them from the Authorization Server. This
|
33
|
+
# class is for resource servers which accept bearer JWT tokens.
|
34
|
+
#
|
35
|
+
# For more on JWT, see the JSON Web Token RFC here:
|
36
|
+
# {http://tools.ietf.org/id/draft-ietf-oauth-json-web-token-05.html}
|
37
|
+
#
|
38
|
+
# An instance of this class can be used to decode and verify the contents
|
39
|
+
# of a bearer token. Methods of this class can validate token signatures
|
40
|
+
# with a secret or public key, and they can also enforce that the token
|
41
|
+
# is for a particular audience.
|
42
|
+
class TokenCoder
|
43
|
+
|
44
|
+
def self.init_digest(algo) # @private
|
45
|
+
OpenSSL::Digest::Digest.new(algo.sub('HS', 'sha').sub('RS', 'sha'))
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.normalize_options(opts) # @private
|
49
|
+
opts = opts.dup
|
50
|
+
pk = opts[:pkey]
|
51
|
+
opts[:pkey] = OpenSSL::PKey::RSA.new(pk) if pk && !pk.is_a?(OpenSSL::PKey::PKey)
|
52
|
+
opts[:audience_ids] = Util.arglist(opts[:audience_ids])
|
53
|
+
opts[:algorithm] = 'HS256' unless opts[:algorithm]
|
54
|
+
opts[:verify] = true unless opts.key?(:verify)
|
55
|
+
opts[:accept_algorithms] = Util.arglist(opts[:accept_algorithms],
|
56
|
+
["HS256", "HS384", "HS512", "RS256", "RS384", "RS512"])
|
57
|
+
opts
|
58
|
+
end
|
59
|
+
|
60
|
+
# Constructs a signed JWT.
|
61
|
+
# @param token_body Contents of the token in any object that can be converted to JSON.
|
62
|
+
# @param options (see #initialize)
|
63
|
+
# @return [String] a signed JWT token string in the form "xxxx.xxxxx.xxxx".
|
64
|
+
def self.encode(token_body, options = {}, obsolete1 = nil, obsolete2 = nil)
|
65
|
+
unless options.is_a?(Hash) && obsolete1.nil? && obsolete2.nil?
|
66
|
+
# deprecated: def self.encode(token_body, skey, pkey = nil, algo = 'HS256')
|
67
|
+
warn "#{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
|
68
|
+
options = {:skey => options }
|
69
|
+
options[:pkey], options[:algorithm] = obsolete1, obsolete2
|
70
|
+
end
|
71
|
+
options = normalize_options(options)
|
72
|
+
algo = options[:algorithm]
|
73
|
+
segments = [Util.json_encode64("typ" => "JWT", "alg" => algo)]
|
74
|
+
segments << Util.json_encode64(token_body)
|
75
|
+
if ["HS256", "HS384", "HS512"].include?(algo)
|
76
|
+
sig = OpenSSL::HMAC.digest(init_digest(algo), options[:skey], segments.join('.'))
|
77
|
+
elsif ["RS256", "RS384", "RS512"].include?(algo)
|
78
|
+
sig = options[:pkey].sign(init_digest(algo), segments.join('.'))
|
79
|
+
elsif algo == "none"
|
80
|
+
sig = ""
|
81
|
+
else
|
82
|
+
raise SignatureNotSupported, "unsupported signing method"
|
83
|
+
end
|
84
|
+
segments << Util.encode64(sig)
|
85
|
+
segments.join('.')
|
86
|
+
end
|
87
|
+
|
88
|
+
# Decodes a JWT token and optionally verifies the signature. Both a
|
89
|
+
# symmetrical key and a public key can be provided for signature verification.
|
90
|
+
# The JWT header indicates what signature algorithm was used and the
|
91
|
+
# corresponding key is used to verify the signature (if +verify+ is true).
|
92
|
+
# @param [String] token A JWT token as returned by {TokenCoder.encode}
|
93
|
+
# @param options (see #initialize)
|
94
|
+
# @return [Hash] the token contents
|
95
|
+
def self.decode(token, options = {}, obsolete1 = nil, obsolete2 = nil)
|
96
|
+
unless options.is_a?(Hash) && obsolete1.nil? && obsolete2.nil?
|
97
|
+
# deprecated: def self.decode(token, skey = nil, pkey = nil, verify = true)
|
98
|
+
warn "#{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
|
99
|
+
options = {:skey => options }
|
100
|
+
options[:pkey], options[:verify] = obsolete1, obsolete2
|
101
|
+
end
|
102
|
+
options = normalize_options(options)
|
103
|
+
segments = token.split('.')
|
104
|
+
raise InvalidTokenFormat, "Not enough or too many segments" unless [2,3].include? segments.length
|
105
|
+
header_segment, payload_segment, crypto_segment = segments
|
106
|
+
signing_input = [header_segment, payload_segment].join('.')
|
107
|
+
header = Util.json_decode64(header_segment)
|
108
|
+
payload = Util.json_decode64(payload_segment, (:sym if options[:symbolize_keys]))
|
109
|
+
return payload unless options[:verify]
|
110
|
+
raise SignatureNotAccepted, "Signature algorithm not accepted" unless
|
111
|
+
options[:accept_algorithms].include?(algo = header["alg"])
|
112
|
+
return payload if algo == 'none'
|
113
|
+
signature = Util.decode64(crypto_segment)
|
114
|
+
if ["HS256", "HS384", "HS512"].include?(algo)
|
115
|
+
raise InvalidSignature, "Signature verification failed" unless
|
116
|
+
options[:skey] && signature == OpenSSL::HMAC.digest(init_digest(algo), options[:skey], signing_input)
|
117
|
+
elsif ["RS256", "RS384", "RS512"].include?(algo)
|
118
|
+
raise InvalidSignature, "Signature verification failed" unless
|
119
|
+
options[:pkey] && options[:pkey].verify(init_digest(algo), signature, signing_input)
|
120
|
+
else
|
121
|
+
raise SignatureNotSupported, "Algorithm not supported"
|
122
|
+
end
|
123
|
+
payload
|
124
|
+
end
|
125
|
+
|
126
|
+
# Creates a new token en/decoder for a service that is associated with
|
127
|
+
# the the audience_ids, the symmetrical token validation key, and the
|
128
|
+
# public and/or private keys.
|
129
|
+
# @param [Hash] options Supported options:
|
130
|
+
# * :audience_ids [Array<String>, String] -- An array or space separated
|
131
|
+
# string of values which indicate the token is intended for this service
|
132
|
+
# instance. It will be compared with tokens as they are decoded to ensure
|
133
|
+
# that the token was intended for this audience.
|
134
|
+
# * :skey [String] -- used to sign and validate tokens using symmetrical
|
135
|
+
# key algoruthms
|
136
|
+
# * :pkey [String, File, OpenSSL::PKey::PKey] -- may be a String or File in
|
137
|
+
# PEM or DER formats. May include public and/or private key data. The
|
138
|
+
# private key is used to sign tokens and the public key is used to
|
139
|
+
# validate tokens.
|
140
|
+
# * :algorithm [String] -- Sets default used for encoding. May be HS256,
|
141
|
+
# HS384, HS512, RS256, RS384, RS512, or none.
|
142
|
+
# * :verify [String] -- Verifies signatures when decoding tokens. Defaults
|
143
|
+
# to +true+.
|
144
|
+
# * :accept_algorithms [String, Array<String>] -- An Array or space separated
|
145
|
+
# string of values which list what algorthms are accepted for token
|
146
|
+
# signatures. Defaults to all possible values of :algorithm except 'none'.
|
147
|
+
# @note the TokenCoder instance must be configured with the appropriate
|
148
|
+
# key material to support particular algorithm families and operations
|
149
|
+
# -- i.e. :pkey must include a private key in order to sign tokens with
|
150
|
+
# the RS algorithms.
|
151
|
+
def initialize(options = {}, obsolete1 = nil, obsolete2 = nil)
|
152
|
+
unless options.is_a?(Hash) && obsolete1.nil? && obsolete2.nil?
|
153
|
+
# deprecated: def initialize(audience_ids, skey, pkey = nil)
|
154
|
+
warn "#{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
|
155
|
+
options = {:audience_ids => options }
|
156
|
+
options[:skey], options[:pkey] = obsolete1, obsolete2
|
157
|
+
end
|
158
|
+
@options = self.class.normalize_options(options)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Encode a JWT token. Takes a hash of values to use as the token body.
|
162
|
+
# Returns a signed token in JWT format (header, body, signature).
|
163
|
+
# @param token_body (see TokenCoder.encode)
|
164
|
+
# @param [String] algorithm -- overrides default. See {#initialize} for possible values.
|
165
|
+
# @return (see TokenCoder.encode)
|
166
|
+
def encode(token_body = {}, algorithm = nil)
|
167
|
+
token_body[:aud] = @options[:audience_ids] if @options[:audience_ids] && !token_body[:aud] && !token_body['aud']
|
168
|
+
token_body[:exp] = Time.now.to_i + 7 * 24 * 60 * 60 unless token_body[:exp] || token_body['exp']
|
169
|
+
self.class.encode(token_body, algorithm ? @options.merge(:algorithm => algorithm) : @options)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns hash of values decoded from the token contents. If the
|
173
|
+
# audience_ids were specified in the options to this instance (see #initialize)
|
174
|
+
# and the token does not contain one or more of those audience_ids, an
|
175
|
+
# AuthError will be raised. AuthError is raised if the token has expired.
|
176
|
+
# @param [String] auth_header (see Scim.initialize#auth_header)
|
177
|
+
# @return (see TokenCoder.decode)
|
178
|
+
def decode(auth_header)
|
179
|
+
unless auth_header && (tkn = auth_header.split(' ')).length == 2 && tkn[0] =~ /^bearer$/i
|
180
|
+
raise InvalidTokenFormat, "invalid authentication header: #{auth_header}"
|
181
|
+
end
|
182
|
+
reply = self.class.decode(tkn[1], @options)
|
183
|
+
auds = Util.arglist(reply[:aud] || reply['aud'])
|
184
|
+
if @options[:audience_ids] && (!auds || (auds & @options[:audience_ids]).empty?)
|
185
|
+
raise InvalidAudience, "invalid audience: #{auds}"
|
186
|
+
end
|
187
|
+
exp = reply[:exp] || reply['exp']
|
188
|
+
unless exp.is_a?(Integer) && exp > Time.now.to_i
|
189
|
+
raise TokenExpired, "token expired"
|
190
|
+
end
|
191
|
+
reply
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
@@ -0,0 +1,255 @@
|
|
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 'securerandom'
|
15
|
+
require 'uaa/http'
|
16
|
+
|
17
|
+
module CF::UAA
|
18
|
+
|
19
|
+
# The TokenInfo class is returned by various TokenIssuer methods. It holds access
|
20
|
+
# and refresh tokens as well as token meta-data such as token type and
|
21
|
+
# expiration time. See {TokenInfo#info} for contents.
|
22
|
+
class TokenInfo
|
23
|
+
|
24
|
+
# Information about the current token. The info hash MUST include
|
25
|
+
# access_token, token_type and scope (if granted scope differs from requested
|
26
|
+
# scope). It should include expires_in. It may include refresh_token, scope,
|
27
|
+
# and other values from the auth server.
|
28
|
+
# @return [Hash]
|
29
|
+
attr_reader :info
|
30
|
+
|
31
|
+
# Normally instantiated by {TokenIssuer}.
|
32
|
+
def initialize(info) @info = info end
|
33
|
+
|
34
|
+
# Constructs a string for use in an authorization header from the contents of
|
35
|
+
# the TokenInfo.
|
36
|
+
# @return [String] Typically a string such as "bearer xxxx.xxxx.xxxx".
|
37
|
+
def auth_header
|
38
|
+
"#{@info[:token_type] || @info['token_type']} #{@info[:access_token] || @info['access_token']}"
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# Client Apps that want to get access to resource servers on behalf of their
|
44
|
+
# users need to get tokens via authcode and implicit flows,
|
45
|
+
# request scopes, etc., but they don't need to process tokens. This
|
46
|
+
# class is for these use cases.
|
47
|
+
#
|
48
|
+
# In general most of this class is an implementation of the client pieces of
|
49
|
+
# the OAuth2 protocol. See {http://tools.ietf.org/html/rfc6749}
|
50
|
+
class TokenIssuer
|
51
|
+
|
52
|
+
include Http
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def random_state; SecureRandom.hex end
|
57
|
+
|
58
|
+
def parse_implicit_params(encoded_params, state)
|
59
|
+
params = Util.decode_form(encoded_params)
|
60
|
+
raise BadResponse, "mismatched state" unless state && params.delete('state') == state
|
61
|
+
raise TargetError.new(params), "error response from #{@target}" if params['error']
|
62
|
+
raise BadResponse, "no type and token" unless params['token_type'] && params['access_token']
|
63
|
+
exp = params['expires_in'].to_i
|
64
|
+
params['expires_in'] = exp if exp.to_s == params['expires_in']
|
65
|
+
TokenInfo.new(Util.hash_keys!(params, @key_style))
|
66
|
+
rescue URI::InvalidURIError, ArgumentError
|
67
|
+
raise BadResponse, "received invalid response from target #{@target}"
|
68
|
+
end
|
69
|
+
|
70
|
+
# returns a CF::UAA::TokenInfo object which includes the access token and metadata.
|
71
|
+
def request_token(params)
|
72
|
+
if scope = Util.arglist(params.delete(:scope))
|
73
|
+
params[:scope] = Util.strlist(scope)
|
74
|
+
end
|
75
|
+
headers = {'content-type' => FORM_UTF8, 'accept' => JSON_UTF8,
|
76
|
+
'authorization' => Http.basic_auth(@client_id, @client_secret) }
|
77
|
+
reply = json_parse_reply(@key_style, *request(@token_target, :post,
|
78
|
+
'/oauth/token', Util.encode_form(params), headers))
|
79
|
+
raise BadResponse unless reply[jkey :token_type] && reply[jkey :access_token]
|
80
|
+
TokenInfo.new(reply)
|
81
|
+
end
|
82
|
+
|
83
|
+
def authorize_path_args(response_type, redirect_uri, scope, state = random_state, args = {})
|
84
|
+
params = args.merge(:client_id => @client_id, :response_type => response_type,
|
85
|
+
:redirect_uri => redirect_uri, :state => state)
|
86
|
+
params[:scope] = scope = Util.strlist(scope) if scope = Util.arglist(scope)
|
87
|
+
params[:nonce], params[:response_type] = state, "#{response_type} id_token" if scope && scope.include?('openid')
|
88
|
+
"/oauth/authorize?#{Util.encode_form(params)}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def jkey(k) @key_style ? k : k.to_s end
|
92
|
+
|
93
|
+
public
|
94
|
+
|
95
|
+
# @param [String] target The base URL of a UAA's oauth authorize endpoint.
|
96
|
+
# For example the target would be {https://login.cloudfoundry.com} if the
|
97
|
+
# endpoint is {https://login.cloudfoundry.com/oauth/authorize}.
|
98
|
+
# The target would be {http://localhost:8080/uaa} if the endpoint
|
99
|
+
# is {http://localhost:8080/uaa/oauth/authorize}.
|
100
|
+
# @param [String] client_id The oauth2 client id, see
|
101
|
+
# {http://tools.ietf.org/html/rfc6749#section-2.2}
|
102
|
+
# @param [String] client_secret Needed to authenticate the client for all
|
103
|
+
# grant types except implicit.
|
104
|
+
# @param [Hash] options can be
|
105
|
+
# * +:token_target+, the base URL of the oauth token endpoint -- if
|
106
|
+
# not specified, +target+ is used.
|
107
|
+
# * +:symbolize_keys+, if true, returned hash keys are symbols.
|
108
|
+
def initialize(target, client_id, client_secret = nil, options = {})
|
109
|
+
@target, @client_id, @client_secret = target, client_id, client_secret
|
110
|
+
@token_target = options[:token_target] || target
|
111
|
+
@key_style = options[:symbolize_keys] ? :sym : nil
|
112
|
+
end
|
113
|
+
|
114
|
+
# Allows an app to discover what credentials are required for
|
115
|
+
# {#implicit_grant_with_creds}.
|
116
|
+
# @return [Hash] of credential names with type and suggested prompt value,
|
117
|
+
# e.g. !{"username":["text","Email"],"password":["password","Password"]}
|
118
|
+
def prompts
|
119
|
+
reply = json_get(@target, '/login')
|
120
|
+
return reply[jkey :prompts] if reply && reply[jkey :prompts]
|
121
|
+
raise BadResponse, "No prompts in response from target #{@target}"
|
122
|
+
end
|
123
|
+
|
124
|
+
# Gets an access token in a single call to the UAA with the user
|
125
|
+
# credentials used for authentication.
|
126
|
+
# @param credentials should be an object such as a hash that can be converted
|
127
|
+
# to a json representation of the credential name/value pairs corresponding to
|
128
|
+
# the keys retrieved by {#prompts}.
|
129
|
+
# @return [TokenInfo]
|
130
|
+
def implicit_grant_with_creds(credentials, scope = nil)
|
131
|
+
# this manufactured redirect_uri is a convention here, not part of OAuth2
|
132
|
+
redir_uri = "https://uaa.cloudfoundry.com/redirect/#{@client_id}"
|
133
|
+
uri = authorize_path_args("token", redir_uri, scope, state = random_state)
|
134
|
+
|
135
|
+
# the accept header is only here so the uaa will issue error replies in json to aid debugging
|
136
|
+
headers = {'content-type' => FORM_UTF8, 'accept' => JSON_UTF8 }
|
137
|
+
body = Util.encode_form(credentials.merge(:source => 'credentials'))
|
138
|
+
status, body, headers = request(@target, :post, uri, body, headers)
|
139
|
+
raise BadResponse, "status #{status}" unless status == 302
|
140
|
+
req_uri, reply_uri = URI.parse(redir_uri), URI.parse(headers['location'])
|
141
|
+
fragment, reply_uri.fragment = reply_uri.fragment, nil
|
142
|
+
raise BadResponse, "bad location header" unless req_uri == reply_uri
|
143
|
+
parse_implicit_params(fragment, state)
|
144
|
+
rescue URI::Error => e
|
145
|
+
raise BadResponse, "bad location header in reply: #{e.message}"
|
146
|
+
end
|
147
|
+
|
148
|
+
# Constructs a uri that the client is to return to the browser to direct
|
149
|
+
# the user to the authorization server to get an authcode.
|
150
|
+
# @param [String] redirect_uri (see #authcode_uri)
|
151
|
+
# @return [String]
|
152
|
+
def implicit_uri(redirect_uri, scope = nil)
|
153
|
+
@target + authorize_path_args("token", redirect_uri, scope)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Gets a token via an implicit grant.
|
157
|
+
# @param [String] implicit_uri must be from a previous call to
|
158
|
+
# {#implicit_uri}, contains state used to validate the contents of the
|
159
|
+
# reply from the server.
|
160
|
+
# @param [String] callback_fragment must be the fragment portion of the URL
|
161
|
+
# received by the user's browser after the server redirects back to the
|
162
|
+
# +redirect_uri+ that was given to {#implicit_uri}. How the application
|
163
|
+
# gets the contents of the fragment is application specific -- usually
|
164
|
+
# some javascript in the page at the +redirect_uri+.
|
165
|
+
# @see http://tools.ietf.org/html/rfc6749#section-4.2
|
166
|
+
# @return [TokenInfo]
|
167
|
+
def implicit_grant(implicit_uri, callback_fragment)
|
168
|
+
in_params = Util.decode_form(URI.parse(implicit_uri).query)
|
169
|
+
unless in_params['state'] && in_params['redirect_uri']
|
170
|
+
raise ArgumentError, "redirect must happen before implicit grant"
|
171
|
+
end
|
172
|
+
parse_implicit_params(callback_fragment, in_params['state'])
|
173
|
+
end
|
174
|
+
|
175
|
+
# A UAA extension to OAuth2 that allows a client to pre-authenticate a
|
176
|
+
# user at the start of an authorization code flow. By passing in the
|
177
|
+
# user's credentials the server can establish a session with the user's
|
178
|
+
# browser without reprompting for authentication. This is useful for
|
179
|
+
# user account management apps so that they can create a user account,
|
180
|
+
# or reset a password for the user, without requiring the user to type
|
181
|
+
# in their credentials again.
|
182
|
+
# @param [String] credentials (see #implicit_grant_with_creds)
|
183
|
+
# @param [String] redirect_uri (see #authcode_uri)
|
184
|
+
# @return (see #authcode_uri)
|
185
|
+
def autologin_uri(redirect_uri, credentials, scope = nil)
|
186
|
+
headers = {'content-type' => FORM_UTF8, 'accept' => JSON_UTF8,
|
187
|
+
'authorization' => Http.basic_auth(@client_id, @client_secret) }
|
188
|
+
body = Util.encode_form(credentials)
|
189
|
+
reply = json_parse_reply(nil, *request(@target, :post, "/autologin", body, headers))
|
190
|
+
raise BadResponse, "no autologin code in reply" unless reply['code']
|
191
|
+
@target + authorize_path_args('code', redirect_uri, scope,
|
192
|
+
random_state, :code => reply['code'])
|
193
|
+
end
|
194
|
+
|
195
|
+
# Constructs a uri that the client is to return to the browser to direct
|
196
|
+
# the user to the authorization server to get an authcode.
|
197
|
+
# @param [String] redirect_uri is embedded in the returned uri so the server
|
198
|
+
# can redirect the user back to the caller's endpoint.
|
199
|
+
# @return [String] uri which
|
200
|
+
def authcode_uri(redirect_uri, scope = nil)
|
201
|
+
@target + authorize_path_args('code', redirect_uri, scope)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Uses the instance client credentials in addition to +callback_query+
|
205
|
+
# to get a token via the authorization code grant.
|
206
|
+
# @param [String] authcode_uri must be from a previous call to {#authcode_uri}
|
207
|
+
# and contains state used to validate the contents of the reply from the
|
208
|
+
# server.
|
209
|
+
# @param [String] callback_query must be the query portion of the URL
|
210
|
+
# received by the client after the user's browser is redirected back from
|
211
|
+
# the server. It contains the authorization code.
|
212
|
+
# @see http://tools.ietf.org/html/rfc6749#section-4.1
|
213
|
+
# @return [TokenInfo]
|
214
|
+
def authcode_grant(authcode_uri, callback_query)
|
215
|
+
ac_params = Util.decode_form(URI.parse(authcode_uri).query)
|
216
|
+
unless ac_params['state'] && ac_params['redirect_uri']
|
217
|
+
raise ArgumentError, "authcode redirect must happen before authcode grant"
|
218
|
+
end
|
219
|
+
begin
|
220
|
+
params = Util.decode_form(callback_query)
|
221
|
+
authcode = params['code']
|
222
|
+
raise BadResponse unless params['state'] == ac_params['state'] && authcode
|
223
|
+
rescue URI::InvalidURIError, ArgumentError, BadResponse
|
224
|
+
raise BadResponse, "received invalid response from target #{@target}"
|
225
|
+
end
|
226
|
+
request_token(:grant_type => 'authorization_code', :code => authcode,
|
227
|
+
:redirect_uri => ac_params['redirect_uri'])
|
228
|
+
end
|
229
|
+
|
230
|
+
# Uses the instance client credentials in addition to the +username+
|
231
|
+
# and +password+ to get a token via the owner password grant.
|
232
|
+
# See {http://tools.ietf.org/html/rfc6749#section-4.3}.
|
233
|
+
# @return [TokenInfo]
|
234
|
+
def owner_password_grant(username, password, scope = nil)
|
235
|
+
request_token(:grant_type => 'password', :username => username,
|
236
|
+
:password => password, :scope => scope)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Uses the instance client credentials to get a token with a client
|
240
|
+
# credentials grant. See http://tools.ietf.org/html/rfc6749#section-4.4
|
241
|
+
# @return [TokenInfo]
|
242
|
+
def client_credentials_grant(scope = nil)
|
243
|
+
request_token(:grant_type => 'client_credentials', :scope => scope)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Uses the instance client credentials and the given +refresh_token+ to get
|
247
|
+
# a new access token. See http://tools.ietf.org/html/rfc6749#section-6
|
248
|
+
# @return [TokenInfo] which may include a new refresh token as well as an access token.
|
249
|
+
def refresh_token_grant(refresh_token, scope = nil)
|
250
|
+
request_token(:grant_type => 'refresh_token', :refresh_token => refresh_token, :scope => scope)
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
254
|
+
|
255
|
+
end
|
data/lib/uaa/util.rb
ADDED
@@ -0,0 +1,235 @@
|
|
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 'multi_json'
|
15
|
+
require "base64"
|
16
|
+
require 'logger'
|
17
|
+
require 'uri'
|
18
|
+
|
19
|
+
# Cloud Foundry namespace
|
20
|
+
module CF
|
21
|
+
# Namespace for User Account and Authentication service
|
22
|
+
module UAA end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Logger # @private
|
26
|
+
Severity::TRACE = Severity::DEBUG - 1
|
27
|
+
def trace(progname, &blk); add(Logger::Severity::TRACE, nil, progname, &blk) end
|
28
|
+
def trace? ; @level <= Logger::Severity::TRACE end
|
29
|
+
end
|
30
|
+
|
31
|
+
module CF::UAA
|
32
|
+
|
33
|
+
# Useful parent class. All CF::UAA exceptions are derived from this.
|
34
|
+
class UAAError < RuntimeError; end
|
35
|
+
|
36
|
+
# Indicates an authentication error.
|
37
|
+
class AuthError < UAAError; end
|
38
|
+
|
39
|
+
# Indicates an error occurred decoding a token, base64 decoding, or JSON.
|
40
|
+
class DecodeError < UAAError; end
|
41
|
+
|
42
|
+
# Helper functions useful to the UAA client APIs
|
43
|
+
class Util
|
44
|
+
|
45
|
+
# General method to transform a hash key to a given style. Useful when
|
46
|
+
# dealing with HTTP headers and various protocol tags that tend to contain
|
47
|
+
# '-' characters and are case-insensitive and want to use them as keys in
|
48
|
+
# ruby hashes. Useful for dealing with {http://www.simplecloud.info/ SCIM}
|
49
|
+
# case-insensitive attribute names to normalize all attribute names (downcase).
|
50
|
+
#
|
51
|
+
# @param [String, Symbol] key current key
|
52
|
+
# @param [Symbol] style can be sym, downsym, down, str, [un]dash, [un]camel, nil, none
|
53
|
+
# @return [String, Symbol] new key
|
54
|
+
def self.hash_key(key, style)
|
55
|
+
case style
|
56
|
+
when nil, :none then key
|
57
|
+
when :downsym then key.to_s.downcase.to_sym
|
58
|
+
when :sym then key.to_sym
|
59
|
+
when :str then key.to_s
|
60
|
+
when :down then key.to_s.downcase
|
61
|
+
when :dash then key.to_s.downcase.tr('_', '-')
|
62
|
+
when :undash then key.to_s.downcase.tr('-', '_').to_sym
|
63
|
+
when :uncamel then key.to_s.gsub(/([A-Z])([^A-Z]*)/,'_\1\2').downcase.to_sym
|
64
|
+
when :camel then key.to_s.gsub(/(_[a-z])([^_]*)/) { $1[1].upcase + $2 }
|
65
|
+
else raise ArgumentError, "unknown hash key style: #{style}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Modifies obj in place changing any hash keys to style. Recursively modifies
|
70
|
+
# subordinate hashes.
|
71
|
+
# @param style (see Util.hash_key)
|
72
|
+
# @return modified obj
|
73
|
+
def self.hash_keys!(obj, style = nil)
|
74
|
+
return obj if style == :none
|
75
|
+
return obj.each {|o| hash_keys!(o, style)} if obj.is_a? Array
|
76
|
+
return obj unless obj.is_a? Hash
|
77
|
+
newkeys, nk = {}, nil
|
78
|
+
obj.delete_if { |k, v|
|
79
|
+
hash_keys!(v, style)
|
80
|
+
newkeys[nk] = v unless (nk = hash_key(k, style)) == k
|
81
|
+
nk != k
|
82
|
+
}
|
83
|
+
obj.merge!(newkeys)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Makes a new copy of obj with hash keys to style. Recursively modifies
|
87
|
+
# subordinate hashes.
|
88
|
+
# @param style (see Util.hash_key)
|
89
|
+
# @return obj or new object if hash keys were changed
|
90
|
+
def self.hash_keys(obj, style = nil)
|
91
|
+
return obj.collect {|o| hash_keys(o, style)} if obj.is_a? Array
|
92
|
+
return obj unless obj.is_a? Hash
|
93
|
+
obj.each_with_object({}) {|(k, v), h|
|
94
|
+
h[hash_key(k, style)] = hash_keys(v, style)
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
# handle ruby 1.8.7 compatibility for form encoding
|
99
|
+
if URI.respond_to?(:encode_www_form_component)
|
100
|
+
def self.encode_component(str) URI.encode_www_form_component(str) end #@private
|
101
|
+
def self.decode_component(str) URI.decode_www_form_component(str) end #@private
|
102
|
+
else
|
103
|
+
def self.encode_component(str) # @private
|
104
|
+
str.to_s.gsub(/([^ a-zA-Z0-9*_.-]+)/) {
|
105
|
+
'%' + $1.unpack('H2' * $1.size).join('%').upcase
|
106
|
+
}.tr(' ', '+')
|
107
|
+
end
|
108
|
+
def self.decode_component(str) # @private
|
109
|
+
str.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/) {[$1.delete('%')].pack('H*')}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Takes an x-www-form-urlencoded string and returns a hash of utf-8 key/value
|
114
|
+
# pairs. Useful for OAuth parameters. Raises ArgumentError if a key occurs
|
115
|
+
# more than once, which is a restriction of OAuth query strings.
|
116
|
+
# OAuth parameters are case sensitive, scim parameters are case-insensitive.
|
117
|
+
# @see http://tools.ietf.org/html/rfc6749#section-3.1
|
118
|
+
# @param [String] url_encoded_pairs in an x-www-form-urlencoded string
|
119
|
+
# @param style (see Util.hash_key)
|
120
|
+
# @return [Hash] of key value pairs
|
121
|
+
def self.decode_form(url_encoded_pairs, style = nil)
|
122
|
+
pairs = {}
|
123
|
+
url_encoded_pairs.split(/[&;]/).each do |pair|
|
124
|
+
k, v = pair.split('=', 2).collect { |v| decode_component(v) }
|
125
|
+
raise "duplicate keys in form parameters" if pairs.key?(k = hash_key(k, style))
|
126
|
+
pairs[k] = v
|
127
|
+
end
|
128
|
+
pairs
|
129
|
+
rescue Exception => e
|
130
|
+
raise ArgumentError, e.message
|
131
|
+
end
|
132
|
+
|
133
|
+
# Encode an object into x-www-form-urlencoded string suitable for oauth2.
|
134
|
+
# @note that ruby 1.9.3 converts form components to utf-8. Ruby 1.8.7
|
135
|
+
# users must ensure all data is in utf-8 format before passing to form encode.
|
136
|
+
# @param [Hash] obj a hash of key/value pairs to be encoded.
|
137
|
+
# @see http://tools.ietf.org/html/rfc6749#section-3.1
|
138
|
+
def self.encode_form(obj)
|
139
|
+
obj.map {|k, v| encode_component(k) << '=' << encode_component(v)}.join('&')
|
140
|
+
end
|
141
|
+
|
142
|
+
# Converts +obj+ to JSON
|
143
|
+
# @return [String] obj in JSON form.
|
144
|
+
def self.json(obj) MultiJson.dump(obj) end
|
145
|
+
|
146
|
+
# Converts +obj+ to nicely formatted JSON
|
147
|
+
# @return [String] obj in formatted json
|
148
|
+
def self.json_pretty(obj) MultiJson.dump(obj, :pretty => true) end
|
149
|
+
|
150
|
+
# Converts +obj+ to a URL-safe base 64 encoded string
|
151
|
+
# @return [String]
|
152
|
+
def self.json_encode64(obj = {}) encode64(json(obj)) end
|
153
|
+
|
154
|
+
# Decodes base64 encoding of JSON data.
|
155
|
+
# @param [String] str
|
156
|
+
# @param style (see Util.hash_key)
|
157
|
+
# @return [Hash]
|
158
|
+
def self.json_decode64(str, style = nil) json_parse(decode64(str), style) end
|
159
|
+
|
160
|
+
# Encodes +obj+ as a URL-safe base 64 encoded string, with trailing padding removed.
|
161
|
+
# @return [String]
|
162
|
+
def self.encode64(obj)
|
163
|
+
str = Base64.respond_to?(:urlsafe_encode64)? Base64.urlsafe_encode64(obj):
|
164
|
+
[obj].pack("m").tr("+/", "-_")
|
165
|
+
str.gsub!(/(\n|=*$)/, '')
|
166
|
+
str
|
167
|
+
end
|
168
|
+
|
169
|
+
# Decodes a URL-safe base 64 encoded string. Adds padding if necessary.
|
170
|
+
# @return [String] decoded string
|
171
|
+
def self.decode64(str)
|
172
|
+
return unless str
|
173
|
+
pad = str.length % 4
|
174
|
+
str = str + '=' * (4 - pad) if pad > 0
|
175
|
+
Base64.respond_to?(:urlsafe_decode64) ?
|
176
|
+
Base64.urlsafe_decode64(str) : Base64.decode64(str.tr('-_', '+/'))
|
177
|
+
rescue ArgumentError
|
178
|
+
raise DecodeError, "invalid base64 encoding"
|
179
|
+
end
|
180
|
+
|
181
|
+
# Parses a JSON string.
|
182
|
+
# @param style (see Util.hash_key)
|
183
|
+
# @return [Hash] parsed data
|
184
|
+
def self.json_parse(str, style = nil)
|
185
|
+
hash_keys!(MultiJson.load(str), style) if str && !str.empty?
|
186
|
+
rescue MultiJson::DecodeError
|
187
|
+
raise DecodeError, "json decoding error"
|
188
|
+
end
|
189
|
+
|
190
|
+
# Converts obj to a string and truncates if over limit.
|
191
|
+
# @return [String]
|
192
|
+
def self.truncate(obj, limit = 50)
|
193
|
+
return obj.to_s if limit == 0
|
194
|
+
limit = limit < 5 ? 1 : limit - 4
|
195
|
+
str = obj.to_s[0..limit]
|
196
|
+
str.length > limit ? str + '...': str
|
197
|
+
end
|
198
|
+
|
199
|
+
# Converts common input formats into array of strings.
|
200
|
+
# Many parameters in these classes can be given as arrays, or as a list of
|
201
|
+
# arguments separated by spaces or commas. This method handles the possible
|
202
|
+
# inputs and returns an array of strings.
|
203
|
+
# @return [Array<String>]
|
204
|
+
def self.arglist(arg, default_arg = nil)
|
205
|
+
arg = default_arg unless arg
|
206
|
+
return arg if arg.nil? || arg.respond_to?(:join)
|
207
|
+
raise ArgumentError, "arg must be Array or space|comma delimited strings" unless arg.respond_to?(:split)
|
208
|
+
arg.split(/[\s\,]+/).reject { |e| e.empty? }
|
209
|
+
end
|
210
|
+
|
211
|
+
# Joins arrays of strings into a single string. Reverse of {Util.arglist}.
|
212
|
+
# @param [Object, #join] arg
|
213
|
+
# @param [String] delim delimiter to put between strings.
|
214
|
+
# @return [String]
|
215
|
+
def self.strlist(arg, delim = ' ')
|
216
|
+
arg.respond_to?(:join) ? arg.join(delim) : arg.to_s if arg
|
217
|
+
end
|
218
|
+
|
219
|
+
# Set the default logger used by the higher level classes.
|
220
|
+
# @param [String, Symbol] level such as info, debug trace.
|
221
|
+
# @param [IO] sink output for log messages, defaults to $stdout
|
222
|
+
# @return [Logger]
|
223
|
+
def self.default_logger(level = nil, sink = nil)
|
224
|
+
if sink || !@default_logger
|
225
|
+
@default_logger = Logger.new(sink || $stdout)
|
226
|
+
level = :info unless level
|
227
|
+
@default_logger.formatter = Proc.new { |severity, time, pname, msg| puts msg }
|
228
|
+
end
|
229
|
+
@default_logger.level = Logger::Severity.const_get(level.to_s.upcase) if level
|
230
|
+
@default_logger
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
|
235
|
+
end
|