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