cloulu 0.0.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (152) hide show
  1. data/bin/{cloulu → cl} +0 -0
  2. data/lib/cc_api_stub/applications.rb +53 -0
  3. data/lib/cc_api_stub/domains.rb +16 -0
  4. data/lib/cc_api_stub/frameworks.rb +22 -0
  5. data/lib/cc_api_stub/helper.rb +131 -0
  6. data/lib/cc_api_stub/login.rb +21 -0
  7. data/lib/cc_api_stub/organization_users.rb +21 -0
  8. data/lib/cc_api_stub/organizations.rb +70 -0
  9. data/lib/cc_api_stub/routes.rb +26 -0
  10. data/lib/cc_api_stub/runtimes.rb +22 -0
  11. data/lib/cc_api_stub/service_bindings.rb +22 -0
  12. data/lib/cc_api_stub/service_instances.rb +22 -0
  13. data/lib/cc_api_stub/services.rb +25 -0
  14. data/lib/cc_api_stub/spaces.rb +49 -0
  15. data/lib/cc_api_stub/users.rb +84 -0
  16. data/lib/cc_api_stub.rb +17 -0
  17. data/lib/cfoundry/auth_token.rb +63 -0
  18. data/lib/cfoundry/baseclient.rb +201 -0
  19. data/lib/cfoundry/chatty_hash.rb +46 -0
  20. data/lib/cfoundry/client.rb +46 -0
  21. data/lib/cfoundry/concerns/login_helpers.rb +13 -0
  22. data/lib/cfoundry/errors.rb +160 -0
  23. data/lib/cfoundry/rest_client.rb +299 -0
  24. data/lib/cfoundry/test_support.rb +3 -0
  25. data/lib/cfoundry/trace_helpers.rb +40 -0
  26. data/lib/cfoundry/uaaclient.rb +112 -0
  27. data/lib/cfoundry/upload_helpers.rb +187 -0
  28. data/lib/cfoundry/v1/app.rb +363 -0
  29. data/lib/cfoundry/v1/base.rb +72 -0
  30. data/lib/cfoundry/v1/client.rb +193 -0
  31. data/lib/cfoundry/v1/framework.rb +21 -0
  32. data/lib/cfoundry/v1/model.rb +178 -0
  33. data/lib/cfoundry/v1/model_magic.rb +129 -0
  34. data/lib/cfoundry/v1/runtime.rb +24 -0
  35. data/lib/cfoundry/v1/service.rb +39 -0
  36. data/lib/cfoundry/v1/service_instance.rb +32 -0
  37. data/lib/cfoundry/v1/service_plan.rb +19 -0
  38. data/lib/cfoundry/v1/user.rb +22 -0
  39. data/lib/cfoundry/v2/app.rb +392 -0
  40. data/lib/cfoundry/v2/base.rb +83 -0
  41. data/lib/cfoundry/v2/client.rb +138 -0
  42. data/lib/cfoundry/v2/domain.rb +11 -0
  43. data/lib/cfoundry/v2/framework.rb +14 -0
  44. data/lib/cfoundry/v2/model.rb +148 -0
  45. data/lib/cfoundry/v2/model_magic.rb +449 -0
  46. data/lib/cfoundry/v2/organization.rb +16 -0
  47. data/lib/cfoundry/v2/route.rb +15 -0
  48. data/lib/cfoundry/v2/runtime.rb +12 -0
  49. data/lib/cfoundry/v2/service.rb +19 -0
  50. data/lib/cfoundry/v2/service_auth_token.rb +9 -0
  51. data/lib/cfoundry/v2/service_binding.rb +10 -0
  52. data/lib/cfoundry/v2/service_instance.rb +14 -0
  53. data/lib/cfoundry/v2/service_plan.rb +12 -0
  54. data/lib/cfoundry/v2/space.rb +18 -0
  55. data/lib/cfoundry/v2/user.rb +64 -0
  56. data/lib/cfoundry/validator.rb +39 -0
  57. data/lib/cfoundry/version.rb +4 -0
  58. data/lib/cfoundry/zip.rb +56 -0
  59. data/lib/cfoundry.rb +2 -0
  60. data/lib/manifests-vmc-plugin/errors.rb +21 -0
  61. data/lib/manifests-vmc-plugin/loader/builder.rb +34 -0
  62. data/lib/manifests-vmc-plugin/loader/normalizer.rb +149 -0
  63. data/lib/manifests-vmc-plugin/loader/resolver.rb +79 -0
  64. data/lib/manifests-vmc-plugin/loader.rb +31 -0
  65. data/lib/manifests-vmc-plugin/plugin.rb +145 -0
  66. data/lib/manifests-vmc-plugin/version.rb +3 -0
  67. data/lib/manifests-vmc-plugin.rb +313 -0
  68. data/lib/mothership/base.rb +99 -0
  69. data/lib/mothership/callbacks.rb +85 -0
  70. data/lib/mothership/command.rb +146 -0
  71. data/lib/mothership/errors.rb +38 -0
  72. data/lib/mothership/help/commands.rb +53 -0
  73. data/lib/mothership/help/printer.rb +170 -0
  74. data/lib/mothership/help.rb +64 -0
  75. data/lib/mothership/inputs.rb +189 -0
  76. data/lib/mothership/parser.rb +182 -0
  77. data/lib/mothership/version.rb +3 -0
  78. data/lib/mothership.rb +64 -0
  79. data/lib/tunnel-vmc-plugin/plugin.rb +178 -0
  80. data/lib/tunnel-vmc-plugin/tunnel.rb +308 -0
  81. data/lib/tunnel-vmc-plugin/version.rb +3 -0
  82. data/lib/uaa/http.rb +168 -0
  83. data/lib/uaa/misc.rb +121 -0
  84. data/lib/uaa/scim.rb +292 -0
  85. data/lib/uaa/token_coder.rb +196 -0
  86. data/lib/uaa/token_issuer.rb +255 -0
  87. data/lib/uaa/util.rb +235 -0
  88. data/lib/uaa/version.rb +19 -0
  89. data/lib/uaa.rb +18 -0
  90. data/lib/vmc/cli/app/app.rb +45 -0
  91. data/lib/vmc/cli/app/apps.rb +99 -0
  92. data/lib/vmc/cli/app/base.rb +90 -0
  93. data/lib/vmc/cli/app/crashes.rb +42 -0
  94. data/lib/vmc/cli/app/delete.rb +95 -0
  95. data/lib/vmc/cli/app/deprecated.rb +11 -0
  96. data/lib/vmc/cli/app/env.rb +78 -0
  97. data/lib/vmc/cli/app/files.rb +137 -0
  98. data/lib/vmc/cli/app/health.rb +26 -0
  99. data/lib/vmc/cli/app/instances.rb +53 -0
  100. data/lib/vmc/cli/app/logs.rb +76 -0
  101. data/lib/vmc/cli/app/push/create.rb +165 -0
  102. data/lib/vmc/cli/app/push/interactions.rb +94 -0
  103. data/lib/vmc/cli/app/push/sync.rb +64 -0
  104. data/lib/vmc/cli/app/push.rb +109 -0
  105. data/lib/vmc/cli/app/rename.rb +35 -0
  106. data/lib/vmc/cli/app/restart.rb +20 -0
  107. data/lib/vmc/cli/app/scale.rb +71 -0
  108. data/lib/vmc/cli/app/start.rb +143 -0
  109. data/lib/vmc/cli/app/stats.rb +67 -0
  110. data/lib/vmc/cli/app/stop.rb +27 -0
  111. data/lib/vmc/cli/help.rb +11 -0
  112. data/lib/vmc/cli/interactive.rb +105 -0
  113. data/lib/vmc/cli/route/base.rb +12 -0
  114. data/lib/vmc/cli/route/map.rb +82 -0
  115. data/lib/vmc/cli/route/routes.rb +25 -0
  116. data/lib/vmc/cli/route/unmap.rb +94 -0
  117. data/lib/vmc/cli/service/base.rb +8 -0
  118. data/lib/vmc/cli/service/bind.rb +44 -0
  119. data/lib/vmc/cli/service/create.rb +126 -0
  120. data/lib/vmc/cli/service/delete.rb +86 -0
  121. data/lib/vmc/cli/service/rename.rb +35 -0
  122. data/lib/vmc/cli/service/service.rb +42 -0
  123. data/lib/vmc/cli/service/services.rb +114 -0
  124. data/lib/vmc/cli/service/unbind.rb +38 -0
  125. data/lib/vmc/cli/start/base.rb +94 -0
  126. data/lib/vmc/cli/start/colors.rb +13 -0
  127. data/lib/vmc/cli/start/info.rb +126 -0
  128. data/lib/vmc/cli/start/login.rb +97 -0
  129. data/lib/vmc/cli/start/logout.rb +17 -0
  130. data/lib/vmc/cli/start/target.rb +60 -0
  131. data/lib/vmc/cli/start/target_interactions.rb +37 -0
  132. data/lib/vmc/cli/start/targets.rb +16 -0
  133. data/lib/vmc/cli/user/base.rb +29 -0
  134. data/lib/vmc/cli/user/create.rb +39 -0
  135. data/lib/vmc/cli/user/delete.rb +27 -0
  136. data/lib/vmc/cli/user/passwd.rb +50 -0
  137. data/lib/vmc/cli/user/register.rb +42 -0
  138. data/lib/vmc/cli/user/users.rb +32 -0
  139. data/lib/vmc/cli/v2_check_cli.rb +16 -0
  140. data/lib/vmc/cli.rb +474 -0
  141. data/lib/vmc/constants.rb +13 -0
  142. data/lib/vmc/detect.rb +129 -0
  143. data/lib/vmc/errors.rb +19 -0
  144. data/lib/vmc/plugin.rb +56 -0
  145. data/lib/vmc/spacing.rb +89 -0
  146. data/lib/vmc/spec_helper.rb +1 -0
  147. data/lib/vmc/test_support.rb +6 -0
  148. data/lib/vmc/version.rb +3 -0
  149. data/lib/vmc.rb +8 -0
  150. data/vendor/errors/v1.yml +189 -0
  151. data/vendor/errors/v2.yml +360 -0
  152. 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
+