cloulu 0.0.0 → 0.1.1
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/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
data/lib/uaa/http.rb
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
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 'base64'
|
|
15
|
+
require 'net/http'
|
|
16
|
+
require 'uaa/util'
|
|
17
|
+
|
|
18
|
+
module CF::UAA
|
|
19
|
+
|
|
20
|
+
# Indicates URL for the target is bad or not accessible.
|
|
21
|
+
class BadTarget < UAAError; end
|
|
22
|
+
|
|
23
|
+
# Indicates the resource within the target server was not found.
|
|
24
|
+
class NotFound < UAAError; end
|
|
25
|
+
|
|
26
|
+
# Indicates a syntax error in a response from the UAA, e.g. missing required response field.
|
|
27
|
+
class BadResponse < UAAError; end
|
|
28
|
+
|
|
29
|
+
# Indicates an error from the http client stack.
|
|
30
|
+
class HTTPException < UAAError; end
|
|
31
|
+
|
|
32
|
+
# An application level error from the UAA which includes error info in the reply.
|
|
33
|
+
class TargetError < UAAError
|
|
34
|
+
attr_reader :info
|
|
35
|
+
def initialize(error_info = {})
|
|
36
|
+
@info = error_info
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Indicates a token is malformed or expired.
|
|
41
|
+
class InvalidToken < TargetError; end
|
|
42
|
+
|
|
43
|
+
# Utility accessors and methods for objects that want to access JSON web APIs.
|
|
44
|
+
module Http
|
|
45
|
+
|
|
46
|
+
# Sets the current logger instance to recieve error messages.
|
|
47
|
+
# @param [Logger] logr
|
|
48
|
+
# @return [Logger]
|
|
49
|
+
def logger=(logr); @logger = logr end
|
|
50
|
+
|
|
51
|
+
# The current logger or {Util.default_logger} if none has been set.
|
|
52
|
+
# @return [Logger]
|
|
53
|
+
def logger ; @logger || Util.default_logger end
|
|
54
|
+
|
|
55
|
+
# Indicates if the current logger is set to +:trace+ level.
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def trace? ; (lgr = logger).respond_to?(:trace?) && lgr.trace? end
|
|
58
|
+
|
|
59
|
+
# Sets a handler for outgoing http requests. If no handler is set, an
|
|
60
|
+
# internal cache of net/http connections is used. Arguments to the handler
|
|
61
|
+
# are url, method, body, headers.
|
|
62
|
+
# @param [Proc] blk handler block
|
|
63
|
+
# @return [nil]
|
|
64
|
+
def set_request_handler(&blk) @req_handler = blk; nil end
|
|
65
|
+
|
|
66
|
+
# Constructs an http basic authentication header.
|
|
67
|
+
# @return [String]
|
|
68
|
+
def self.basic_auth(name, password)
|
|
69
|
+
str = "#{name}:#{password}"
|
|
70
|
+
"Basic " + (Base64.respond_to?(:strict_encode64)?
|
|
71
|
+
Base64.strict_encode64(str): [str].pack("m").gsub(/\n/, ''))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
JSON_UTF8 = "application/json;charset=utf-8"
|
|
75
|
+
FORM_UTF8 = "application/x-www-form-urlencoded;charset=utf-8"
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def json_get(target, path = nil, style = nil, headers = {})
|
|
80
|
+
raise ArgumentError unless style.nil? || style.is_a?(Symbol)
|
|
81
|
+
json_parse_reply(style, *http_get(target, path, headers.merge("accept" => JSON_UTF8)))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def json_post(target, path, body, headers = {})
|
|
85
|
+
http_post(target, path, Util.json(body), headers.merge("content-type" => JSON_UTF8))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def json_put(target, path, body, headers = {})
|
|
89
|
+
http_put(target, path, Util.json(body), headers.merge("content-type" => JSON_UTF8))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def json_parse_reply(style, status, body, headers)
|
|
93
|
+
raise ArgumentError unless style.nil? || style.is_a?(Symbol)
|
|
94
|
+
unless [200, 201, 204, 400, 401, 403, 409].include? status
|
|
95
|
+
raise (status == 404 ? NotFound : BadResponse), "invalid status response: #{status}"
|
|
96
|
+
end
|
|
97
|
+
if body && !body.empty? && (status == 204 || headers.nil? ||
|
|
98
|
+
headers["content-type"] !~ /application\/json/i)
|
|
99
|
+
raise BadResponse, "received invalid response content or type"
|
|
100
|
+
end
|
|
101
|
+
parsed_reply = Util.json_parse(body, style)
|
|
102
|
+
if status >= 400
|
|
103
|
+
raise parsed_reply && parsed_reply["error"] == "invalid_token" ?
|
|
104
|
+
InvalidToken.new(parsed_reply) : TargetError.new(parsed_reply), "error response"
|
|
105
|
+
end
|
|
106
|
+
parsed_reply
|
|
107
|
+
rescue DecodeError
|
|
108
|
+
raise BadResponse, "invalid JSON response"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def http_get(target, path = nil, headers = {}) request(target, :get, path, nil, headers) end
|
|
112
|
+
def http_post(target, path, body, headers = {}) request(target, :post, path, body, headers) end
|
|
113
|
+
def http_put(target, path, body, headers = {}) request(target, :put, path, body, headers) end
|
|
114
|
+
|
|
115
|
+
def http_delete(target, path, authorization)
|
|
116
|
+
status = request(target, :delete, path, nil, "authorization" => authorization)[0]
|
|
117
|
+
unless [200, 204].include?(status)
|
|
118
|
+
raise (status == 404 ? NotFound : BadResponse), "invalid response from #{path}: #{status}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def request(target, method, path, body = nil, headers = {})
|
|
123
|
+
headers["accept"] = headers["content-type"] if headers["content-type"] && !headers["accept"]
|
|
124
|
+
url = "#{target}#{path}"
|
|
125
|
+
|
|
126
|
+
logger.debug { "--->\nrequest: #{method} #{url}\n" +
|
|
127
|
+
"headers: #{headers}\n#{'body: ' + Util.truncate(body.to_s, trace? ? 50000 : 50) if body}" }
|
|
128
|
+
status, body, headers = @req_handler ? @req_handler.call(url, method, body, headers) :
|
|
129
|
+
net_http_request(url, method, body, headers)
|
|
130
|
+
logger.debug { "<---\nresponse: #{status}\nheaders: #{headers}\n" +
|
|
131
|
+
"#{'body: ' + Util.truncate(body.to_s, trace? ? 50000: 50) if body}" }
|
|
132
|
+
|
|
133
|
+
[status, body, headers]
|
|
134
|
+
|
|
135
|
+
rescue Exception => e
|
|
136
|
+
logger.debug { "<---- no response due to exception: #{e.inspect}" }
|
|
137
|
+
raise e
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def net_http_request(url, method, body, headers)
|
|
141
|
+
raise ArgumentError unless reqtype = {:delete => Net::HTTP::Delete,
|
|
142
|
+
:get => Net::HTTP::Get, :post => Net::HTTP::Post, :put => Net::HTTP::Put}[method]
|
|
143
|
+
headers["content-length"] = body.length if body
|
|
144
|
+
uri = URI.parse(url)
|
|
145
|
+
req = reqtype.new(uri.request_uri)
|
|
146
|
+
headers.each { |k, v| req[k] = v }
|
|
147
|
+
http_key = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
|
148
|
+
@http_cache ||= {}
|
|
149
|
+
unless http = @http_cache[http_key]
|
|
150
|
+
@http_cache[http_key] = http = Net::HTTP.new(uri.host, uri.port)
|
|
151
|
+
if uri.is_a?(URI::HTTPS)
|
|
152
|
+
http.use_ssl = true
|
|
153
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
reply, outhdrs = http.request(req, body), {}
|
|
157
|
+
reply.each_header { |k, v| outhdrs[k] = v }
|
|
158
|
+
[reply.code.to_i, reply.body, outhdrs]
|
|
159
|
+
|
|
160
|
+
rescue URI::Error, SocketError, SystemCallError => e
|
|
161
|
+
raise BadTarget, "error: #{e.message}"
|
|
162
|
+
rescue Net::HTTPBadResponse => e
|
|
163
|
+
raise HTTPException, "HTTP exception: #{e.class}: #{e}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
end
|
data/lib/uaa/misc.rb
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
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/http'
|
|
15
|
+
|
|
16
|
+
module CF::UAA
|
|
17
|
+
|
|
18
|
+
# Provides interfaces to various UAA endpoints that are not in the context
|
|
19
|
+
# of an overall class of operations like SCIM resources or OAuth2 tokens.
|
|
20
|
+
class Misc
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
include Http
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# sets whether the keys in returned hashes should be symbols.
|
|
27
|
+
# @return [Boolean] the new state
|
|
28
|
+
def self.symbolize_keys=(bool) !!(@key_style = bool ? :sym : nil) end
|
|
29
|
+
|
|
30
|
+
# Gets information about the user authenticated by the token in the
|
|
31
|
+
# +auth_header+. It GETs from the +target+'s +/userinfo+ endpoint and
|
|
32
|
+
# returns user information as specified by OpenID Connect.
|
|
33
|
+
# @see http://openid.net/connect/
|
|
34
|
+
# @see http://openid.net/specs/openid-connect-standard-1_0.html#userinfo_ep
|
|
35
|
+
# @see http://openid.net/specs/openid-connect-messages-1_0.html#anchor9
|
|
36
|
+
# @param (see Misc.server)
|
|
37
|
+
# @param [String] auth_header see {TokenInfo#auth_header}
|
|
38
|
+
# @return [Hash]
|
|
39
|
+
def self.whoami(target, auth_header)
|
|
40
|
+
json_get(target, "/userinfo?schema=openid", @key_style, "authorization" => auth_header)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Gets various monitoring and status variables from the server.
|
|
44
|
+
# Authenticates using +name+ and +pwd+ for basic authentication.
|
|
45
|
+
# @param (see Misc.server)
|
|
46
|
+
# @return [Hash]
|
|
47
|
+
def self.varz(target, name, pwd)
|
|
48
|
+
json_get(target, "/varz", @key_style, "authorization" => Http.basic_auth(name, pwd))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Gets basic information about the target server, including version number,
|
|
52
|
+
# commit ID, and links to API endpoints.
|
|
53
|
+
# @param [String] target The base URL of the server. For example the target could
|
|
54
|
+
# be {https://login.cloudfoundry.com}, {https://uaa.cloudfoundry.com}, or
|
|
55
|
+
# {http://localhost:8080/uaa}.
|
|
56
|
+
# @return [Hash]
|
|
57
|
+
def self.server(target)
|
|
58
|
+
reply = json_get(target, '/login', @key_style)
|
|
59
|
+
return reply if reply && (reply[:prompts] || reply['prompts'])
|
|
60
|
+
raise BadResponse, "Invalid response from target #{target}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Gets a base url for the associated UAA from the target server by inspecting the
|
|
64
|
+
# links returned from its info endpoint.
|
|
65
|
+
# @param [String] target The base URL of the server. For example the target could
|
|
66
|
+
# be {https://login.cloudfoundry.com}, {https://uaa.cloudfoundry.com}, or
|
|
67
|
+
# {http://localhost:8080/uaa}.
|
|
68
|
+
# @return [String] url of UAA (or the target itself if it didn't provide a response)
|
|
69
|
+
def self.discover_uaa(target)
|
|
70
|
+
info = server(target)
|
|
71
|
+
links = info['links'] || info[:links]
|
|
72
|
+
uaa = links && (links['uaa'] || links[:uaa])
|
|
73
|
+
|
|
74
|
+
uaa || target
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Gets the key from the server that is used to validate token signatures. If
|
|
78
|
+
# the server is configured to use a symetric key, the caller must authenticate
|
|
79
|
+
# by providing a a +client_id+ and +client_secret+. If the server
|
|
80
|
+
# is configured to sign with a private key, this call will retrieve the
|
|
81
|
+
# public key and +client_id+ must be nil.
|
|
82
|
+
# @param (see Misc.server)
|
|
83
|
+
# @return [Hash]
|
|
84
|
+
def self.validation_key(target, client_id = nil, client_secret = nil)
|
|
85
|
+
hdrs = client_id && client_secret ?
|
|
86
|
+
{ "authorization" => Http.basic_auth(client_id, client_secret)} : {}
|
|
87
|
+
json_get(target, "/token_key", @key_style, hdrs)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Sends +token+ to the server to validate and decode. Authenticates with
|
|
91
|
+
# +client_id+ and +client_secret+. If +audience_ids+ are specified and the
|
|
92
|
+
# token's "aud" attribute does not contain one or more of the audience_ids,
|
|
93
|
+
# raises AuthError -- meaning the token is not for this audience.
|
|
94
|
+
# @param (see Misc.server)
|
|
95
|
+
# @param [String] token an access token as retrieved by {TokenIssuer}. See
|
|
96
|
+
# also {TokenInfo}.
|
|
97
|
+
# @param [String] token_type as retrieved by {TokenIssuer}. See {TokenInfo}.
|
|
98
|
+
# @return [Hash] contents of the token
|
|
99
|
+
def self.decode_token(target, client_id, client_secret, token, token_type = "bearer", audience_ids = nil)
|
|
100
|
+
reply = json_get(target, "/check_token?token_type=#{token_type}&token=#{token}",
|
|
101
|
+
@key_style, "authorization" => Http.basic_auth(client_id, client_secret))
|
|
102
|
+
auds = Util.arglist(reply[:aud] || reply['aud'])
|
|
103
|
+
if audience_ids && (!auds || (auds & audience_ids).empty?)
|
|
104
|
+
raise AuthError, "invalid audience: #{auds.join(' ')}"
|
|
105
|
+
end
|
|
106
|
+
reply
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Gets information about the given password, including a strength score and
|
|
110
|
+
# an indication of what strength is required.
|
|
111
|
+
# @param (see Misc.server)
|
|
112
|
+
# @return [Hash]
|
|
113
|
+
def self.password_strength(target, password)
|
|
114
|
+
json_parse_reply(@key_style, *request(target, :post, '/password/score',
|
|
115
|
+
Util.encode_form(:password => password), "content-type" => Http::FORM_UTF8,
|
|
116
|
+
"accept" => Http::JSON_UTF8))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
end
|
data/lib/uaa/scim.rb
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
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/http'
|
|
15
|
+
|
|
16
|
+
module CF::UAA
|
|
17
|
+
|
|
18
|
+
# This class is for apps that need to manage User Accounts, Groups, or OAuth
|
|
19
|
+
# Client Registrations. It provides access to the SCIM endpoints on the UAA.
|
|
20
|
+
# For more information about SCIM -- the IETF's System for Cross-domain
|
|
21
|
+
# Identity Management (formerly known as Simple Cloud Identity Management) --
|
|
22
|
+
# see {http://www.simplecloud.info}.
|
|
23
|
+
#
|
|
24
|
+
# The types of objects and links to their schema are as follows:
|
|
25
|
+
# * +:user+ -- {http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#user-resource}
|
|
26
|
+
# or {http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#anchor8}
|
|
27
|
+
# * +:group+ -- {http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#group-resource}
|
|
28
|
+
# or {http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#anchor10}
|
|
29
|
+
# * +:client+
|
|
30
|
+
# * +:user_id+ -- {https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#converting-userids-to-names}
|
|
31
|
+
#
|
|
32
|
+
# Naming attributes by type of object:
|
|
33
|
+
# * +:user+ is "username"
|
|
34
|
+
# * +:group+ is "displayname"
|
|
35
|
+
# * +:client+ is "client_id"
|
|
36
|
+
class Scim
|
|
37
|
+
|
|
38
|
+
include Http
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def force_attr(k)
|
|
43
|
+
kd = k.to_s.downcase
|
|
44
|
+
kc = {"username" => "userName", "familyname" => "familyName",
|
|
45
|
+
"givenname" => "givenName", "middlename" => "middleName",
|
|
46
|
+
"honorificprefix" => "honorificPrefix",
|
|
47
|
+
"honorificsuffix" => "honorificSuffix", "displayname" => "displayName",
|
|
48
|
+
"nickname" => "nickName", "profileurl" => "profileUrl",
|
|
49
|
+
"streetaddress" => "streetAddress", "postalcode" => "postalCode",
|
|
50
|
+
"usertype" => "userType", "preferredlanguage" => "preferredLanguage",
|
|
51
|
+
"x509certificates" => "x509Certificates", "lastmodified" => "lastModified",
|
|
52
|
+
"externalid" => "externalId", "phonenumbers" => "phoneNumbers",
|
|
53
|
+
"startindex" => "startIndex"}[kd]
|
|
54
|
+
kc || kd
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# This is very inefficient and should be unnecessary. SCIM (1.1 and early
|
|
58
|
+
# 2.0 drafts) specify that attribute names are case insensitive. However
|
|
59
|
+
# in the UAA attribute names are currently case sensitive. This hack takes
|
|
60
|
+
# a hash with keys as symbols or strings and with any case, and forces
|
|
61
|
+
# the attribute name to the case that the uaa expects.
|
|
62
|
+
def force_case(obj)
|
|
63
|
+
return obj.collect {|o| force_case(o)} if obj.is_a? Array
|
|
64
|
+
return obj unless obj.is_a? Hash
|
|
65
|
+
new_obj = {}
|
|
66
|
+
obj.each {|(k, v)| new_obj[force_attr(k)] = force_case(v) }
|
|
67
|
+
new_obj
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# an attempt to hide some scim and uaa oddities
|
|
71
|
+
def type_info(type, elem)
|
|
72
|
+
scimfo = {:user => ["/Users", "userName"], :group => ["/Groups", "displayName"],
|
|
73
|
+
:client => ["/oauth/clients", 'client_id'], :user_id => ["/ids/Users", 'userName']}
|
|
74
|
+
unless elem == :path || elem == :name_attr
|
|
75
|
+
raise ArgumentError, "scim schema element must be :path or :name_attr"
|
|
76
|
+
end
|
|
77
|
+
unless ary = scimfo[type]
|
|
78
|
+
raise ArgumentError, "scim resource type must be one of #{scimfo.keys.inspect}"
|
|
79
|
+
end
|
|
80
|
+
ary[elem == :path ? 0 : 1]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def jkey(k) @key_style == :down ? k.to_s : k end
|
|
84
|
+
|
|
85
|
+
def fake_client_id(info)
|
|
86
|
+
idk, ck = jkey(:id), jkey(:client_id)
|
|
87
|
+
info[idk] = info[ck] if info[ck] && !info[idk]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
public
|
|
91
|
+
|
|
92
|
+
# @param (see Misc.server)
|
|
93
|
+
# @param [String] auth_header a string that can be used in an
|
|
94
|
+
# authorization header. For OAuth2 with JWT tokens this would be something
|
|
95
|
+
# like "bearer xxxx.xxxx.xxxx". The {TokenInfo} class provides
|
|
96
|
+
# {TokenInfo#auth_header} for this purpose.
|
|
97
|
+
# @param [Hash] options can be
|
|
98
|
+
# * +:symbolize_keys+, if true, returned hash keys are symbols.
|
|
99
|
+
def initialize(target, auth_header, options = {})
|
|
100
|
+
@target, @auth_header = target, auth_header
|
|
101
|
+
@key_style = options[:symbolize_keys] ? :downsym : :down
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Convenience method to get the naming attribute, e.g. userName for user,
|
|
105
|
+
# displayName for group, client_id for client.
|
|
106
|
+
# @param type (see #add)
|
|
107
|
+
# @return [String] naming attribute
|
|
108
|
+
def name_attr(type) type_info(type, :name_attr) end
|
|
109
|
+
|
|
110
|
+
# Creates a SCIM resource.
|
|
111
|
+
# @param [Symbol] type can be :user, :group, :client, :user_id.
|
|
112
|
+
# @param [Hash] info converted to json and sent to the scim endpoint. For schema of
|
|
113
|
+
# each type of object see {Scim}.
|
|
114
|
+
# @return [Hash] contents of the object, including its +id+ and meta-data.
|
|
115
|
+
def add(type, info)
|
|
116
|
+
path, info = type_info(type, :path), force_case(info)
|
|
117
|
+
reply = json_parse_reply(@key_style, *json_post(@target, path, info,
|
|
118
|
+
"authorization" => @auth_header))
|
|
119
|
+
fake_client_id(reply) if type == :client # hide client reply, not quite scim
|
|
120
|
+
reply
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Deletes a SCIM resource
|
|
124
|
+
# @param type (see #add)
|
|
125
|
+
# @param [String] id the id attribute of the SCIM object
|
|
126
|
+
# @return [nil]
|
|
127
|
+
def delete(type, id)
|
|
128
|
+
http_delete @target, "#{type_info(type, :path)}/#{URI.encode(id)}", @auth_header
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Replaces the contents of a SCIM object.
|
|
132
|
+
# @param (see #add)
|
|
133
|
+
# @return (see #add)
|
|
134
|
+
def put(type, info)
|
|
135
|
+
path, info = type_info(type, :path), force_case(info)
|
|
136
|
+
ida = type == :client ? 'client_id' : 'id'
|
|
137
|
+
raise ArgumentError, "info must include #{ida}" unless id = info[ida]
|
|
138
|
+
hdrs = {'authorization' => @auth_header}
|
|
139
|
+
if info && info['meta'] && (etag = info['meta']['version'])
|
|
140
|
+
hdrs.merge!('if-match' => etag)
|
|
141
|
+
end
|
|
142
|
+
reply = json_parse_reply(@key_style,
|
|
143
|
+
*json_put(@target, "#{path}/#{URI.encode(id)}", info, hdrs))
|
|
144
|
+
|
|
145
|
+
# hide client endpoints that are not quite scim compatible
|
|
146
|
+
type == :client && !reply ? get(type, info['client_id']): reply
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Gets a set of attributes for each object that matches a given filter.
|
|
150
|
+
# @param (see #add)
|
|
151
|
+
# @param [Hash] query may contain the following keys:
|
|
152
|
+
# * +attributes+: a comma or space separated list of attribute names to be
|
|
153
|
+
# returned for each object that matches the filter. If no attribute
|
|
154
|
+
# list is given, all attributes are returned.
|
|
155
|
+
# * +filter+: a filter to select which objects are returned. See
|
|
156
|
+
# {http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources}
|
|
157
|
+
# * +startIndex+: for paged output, start index of requested result set.
|
|
158
|
+
# * +count+: maximum number of results per reply
|
|
159
|
+
# @return [Hash] including a +resources+ array of results and
|
|
160
|
+
# pagination data.
|
|
161
|
+
def query(type, query = {})
|
|
162
|
+
query = force_case(query).reject {|k, v| v.nil? }
|
|
163
|
+
if attrs = query['attributes']
|
|
164
|
+
attrs = Util.arglist(attrs).map {|a| force_attr(a)}
|
|
165
|
+
query['attributes'] = Util.strlist(attrs, ",")
|
|
166
|
+
end
|
|
167
|
+
qstr = query.empty?? '': "?#{Util.encode_form(query)}"
|
|
168
|
+
info = json_get(@target, "#{type_info(type, :path)}#{qstr}",
|
|
169
|
+
@key_style, 'authorization' => @auth_header)
|
|
170
|
+
unless info.is_a?(Hash) && info[rk = jkey(:resources)].is_a?(Array)
|
|
171
|
+
|
|
172
|
+
# hide client endpoints that are not yet scim compatible
|
|
173
|
+
if type == :client && info.is_a?(Hash)
|
|
174
|
+
info = info.each{ |k, v| fake_client_id(v) }.values
|
|
175
|
+
if m = /^client_id\s+eq\s+"([^"]+)"$/i.match(query['filter'])
|
|
176
|
+
idk = jkey(:client_id)
|
|
177
|
+
info = info.select { |c| c[idk].casecmp(m[1]) == 0 }
|
|
178
|
+
end
|
|
179
|
+
return {rk => info}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
raise BadResponse, "invalid reply to #{type} query of #{@target}"
|
|
183
|
+
end
|
|
184
|
+
info
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Get information about a specific object.
|
|
188
|
+
# @param (see #delete)
|
|
189
|
+
# @return (see #add)
|
|
190
|
+
def get(type, id)
|
|
191
|
+
info = json_get(@target, "#{type_info(type, :path)}/#{URI.encode(id)}",
|
|
192
|
+
@key_style, 'authorization' => @auth_header)
|
|
193
|
+
|
|
194
|
+
fake_client_id(info) if type == :client # hide client reply, not quite scim
|
|
195
|
+
info
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Collects all pages of entries from a query
|
|
199
|
+
# @param type (see #query)
|
|
200
|
+
# @param [Hash] query may contain the following keys:
|
|
201
|
+
# * +attributes+: a comma or space separated list of attribute names to be
|
|
202
|
+
# returned for each object that matches the filter. If no attribute
|
|
203
|
+
# list is given, all attributes are returned.
|
|
204
|
+
# * +filter+: a filter to select which objects are returned. See
|
|
205
|
+
# {http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources}
|
|
206
|
+
# @return [Array] results
|
|
207
|
+
def all_pages(type, query = {})
|
|
208
|
+
query = force_case(query).reject {|k, v| v.nil? }
|
|
209
|
+
query["startindex"], info, rk = 1, [], jkey(:resources)
|
|
210
|
+
while true
|
|
211
|
+
qinfo = query(type, query)
|
|
212
|
+
raise BadResponse unless qinfo[rk]
|
|
213
|
+
return info if qinfo[rk].empty?
|
|
214
|
+
info.concat(qinfo[rk])
|
|
215
|
+
total = qinfo[jkey :totalresults]
|
|
216
|
+
return info unless total && total > info.length
|
|
217
|
+
unless qinfo[jkey :startindex] && qinfo[jkey :itemsperpage]
|
|
218
|
+
raise BadResponse, "incomplete #{type} pagination data from #{@target}"
|
|
219
|
+
end
|
|
220
|
+
query["startindex"] = info.length + 1
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Gets id/name pairs for given names. For naming attribute of each object type see {Scim}
|
|
225
|
+
# @param type (see #add)
|
|
226
|
+
# @return [Array] array of name/id hashes for each object found
|
|
227
|
+
def ids(type, *names)
|
|
228
|
+
na = type_info(type, :name_attr)
|
|
229
|
+
filter = names.map { |n| "#{na} eq \"#{n}\""}
|
|
230
|
+
all_pages(type, :attributes => "id,#{na}", :filter => filter.join(" or "))
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Convenience method to query for single object by name.
|
|
234
|
+
# @param type (see #add)
|
|
235
|
+
# @param [String] name Value of the Scim object's name attribue. For naming
|
|
236
|
+
# attribute of each type of object see {Scim}.
|
|
237
|
+
# @return [String] the +id+ attribute of the object
|
|
238
|
+
def id(type, name)
|
|
239
|
+
res = ids(type, name)
|
|
240
|
+
|
|
241
|
+
# hide client endpoints that are not scim compatible
|
|
242
|
+
ik, ck = jkey(:id), jkey(:client_id)
|
|
243
|
+
if type == :client && res && res.length > 0 && (res.length > 1 || res[0][ik].nil?)
|
|
244
|
+
cr = res.find { |o| o[ck] && name.casecmp(o[ck]) == 0 }
|
|
245
|
+
return cr[ik] || cr[ck] if cr
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
unless res && res.is_a?(Array) && res.length == 1 &&
|
|
249
|
+
res[0].is_a?(Hash) && (id = res[0][jkey :id])
|
|
250
|
+
raise NotFound, "#{name} not found in #{@target}#{type_info(type, :path)}"
|
|
251
|
+
end
|
|
252
|
+
id
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Change password.
|
|
256
|
+
# * For a user to change their own password, the token in @auth_header must
|
|
257
|
+
# contain "password.write" scope and the correct +old_password+ must be given.
|
|
258
|
+
# * For an admin to set a user's password, the token in @auth_header must
|
|
259
|
+
# contain "uaa.admin" scope.
|
|
260
|
+
# @see https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-password-put-useridpassword
|
|
261
|
+
# @see https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-Security.md#password-change
|
|
262
|
+
# @param [String] user_id the {Scim} +id+ attribute of the user
|
|
263
|
+
# @return [Hash] success message from server
|
|
264
|
+
def change_password(user_id, new_password, old_password = nil)
|
|
265
|
+
req = {"password" => new_password}
|
|
266
|
+
req["oldPassword"] = old_password if old_password
|
|
267
|
+
json_parse_reply(@key_style, *json_put(@target,
|
|
268
|
+
"#{type_info(:user, :path)}/#{URI.encode(user_id)}/password", req,
|
|
269
|
+
'authorization' => @auth_header))
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Change client secret.
|
|
273
|
+
# * For a client to change its own secret, the token in @auth_header must contain
|
|
274
|
+
# "client.secret" scope and the correct +old_secret+ must be given.
|
|
275
|
+
# * For an admin to set a client secret, the token in @auth_header must contain
|
|
276
|
+
# "uaa.admin" scope.
|
|
277
|
+
# @see https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-client-secret-put-oauthclientsclient_idsecret
|
|
278
|
+
# @see https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-Security.md#client-secret-mangagement
|
|
279
|
+
# @param [String] client_id the {Scim} +id+ attribute of the client
|
|
280
|
+
# @return [Hash] success message from server
|
|
281
|
+
def change_secret(client_id, new_secret, old_secret = nil)
|
|
282
|
+
req = {"secret" => new_secret }
|
|
283
|
+
req["oldSecret"] = old_secret if old_secret
|
|
284
|
+
json_parse_reply(@key_style, *json_put(@target,
|
|
285
|
+
"#{type_info(:client, :path)}/#{URI.encode(client_id)}/secret", req,
|
|
286
|
+
'authorization' => @auth_header))
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
end
|
|
292
|
+
|