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
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
|
+
|