cf-uaa-lib 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/
5
+ doc/
6
+ coverage/
7
+ spec_reports/
8
+ vendor/
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ # Cloud Foundry 2012.02.03 Beta
2
+ # Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
3
+ #
4
+ # This product is licensed to you under the Apache License, Version 2.0 (the "License").
5
+ # You may not use this product except in compliance with the License.
6
+ #
7
+ # This product includes a number of subcomponents with
8
+ # separate copyright notices and license terms. Your use of these
9
+ # subcomponents is subject to the terms and conditions of the
10
+ # subcomponent's license, as noted in the LICENSE file.
11
+ #
12
+
13
+ source "http://rubygems.org"
14
+
15
+ # Specify your gem's dependencies in uaa.gemspec
16
+ gemspec
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # CloudFoundry UAA Gem
2
+
3
+ Client gem for interacting with the CloudFoundry UAA server.
4
+
5
+ Set up a local ruby environment (so sudo not required):
6
+
7
+ `$ rvm use 1.9.2`
8
+
9
+ or
10
+
11
+ `$ rbenv global 1.9.2-p180`
12
+
13
+ see: https://rvm.io/ or http://rbenv.org/
14
+
15
+ Build the gem
16
+
17
+ `$ bundle install`
18
+ `$ gem build cf-uaa-lib.gemspec`
19
+
20
+ Install it
21
+
22
+ `$ gem install cf-uaa-lib<version>.gem`
23
+
24
+ Use the gem:
25
+
26
+ `#!/usr/bin/env ruby`
27
+ `require 'uaa'`
28
+ `token_issuer = CF::UAA::TokenIssuer.new("https://uaa.cloudfoundry.com", "vmc")`
29
+ `puts token\_issuer.prompts.inspect`
30
+ `token = token_issuer.implicit_grant_with_creds(username: "<your_username>", password: "<your_password>")`
31
+ `token_info = TokenCoder.decode(token.info["access_token"], nil, nil, false) #token signature not verified`
32
+ `puts token_info["user_name"]`
33
+
34
+ ## Tests
35
+
36
+ Run the tests with rake:
37
+
38
+ `$ bundle exec rake test`
39
+
40
+ Run the tests and see a fancy coverage report:
41
+
42
+ `$ bundle exec rake cov`
43
+
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ # Cloud Foundry 2012.02.03 Beta
2
+ # Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
3
+ #
4
+ # This product is licensed to you under the Apache License, Version 2.0 (the "License").
5
+ # You may not use this product except in compliance with the License.
6
+ #
7
+ # This product includes a number of subcomponents with
8
+ # separate copyright notices and license terms. Your use of these
9
+ # subcomponents is subject to the terms and conditions of the
10
+ # subcomponent's license, as noted in the LICENSE file.
11
+ #
12
+
13
+ require "rdoc/task"
14
+ require "rspec/core/rake_task"
15
+ require "bundler/gem_tasks" # only available in bundler >= 1.0.15
16
+ require "ci/reporter/rake/rspec"
17
+
18
+ ENV['CI_REPORTS'] = File.expand_path("spec_reports")
19
+ COV_REPORTS = File.expand_path("coverage")
20
+
21
+ task :default => [:test]
22
+ task :tests => [:test]
23
+ task :spec => [:test]
24
+
25
+ RSpec::Core::RakeTask.new("test") do |t|
26
+ t.rspec_opts = ["--format", "documentation", "--colour"]
27
+ t.pattern = "spec/**/*_spec.rb"
28
+ end
29
+
30
+ RDoc::Task.new do |rd|
31
+ rd.rdoc_files.include("lib/**/*.rb")
32
+ rd.rdoc_dir = "doc"
33
+ end
34
+
35
+ task :ci => [:pre_coverage, :rcov_reports, "ci:setup:rspec", :test]
36
+ task :cov => [:pre_coverage, :test, :view_coverage]
37
+ task :coverage => [:pre_coverage, :test]
38
+
39
+ task :pre_coverage do
40
+ rm_rf COV_REPORTS
41
+ ENV['COVERAGE'] = "exclude-spec exclude-vendor"
42
+ end
43
+
44
+ task :rcov_reports do
45
+ ENV['COVERAGE'] += " rcov"
46
+ end
47
+
48
+ task :view_coverage do
49
+ `firefox #{File.join(COV_REPORTS, 'index.html')}`
50
+ end
@@ -0,0 +1,45 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Cloud Foundry 2012.02.03 Beta
4
+ # Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
5
+ #
6
+ # This product is licensed to you under the Apache License, Version 2.0 (the "License").
7
+ # You may not use this product except in compliance with the License.
8
+ #
9
+ # This product includes a number of subcomponents with
10
+ # separate copyright notices and license terms. Your use of these
11
+ # subcomponents is subject to the terms and conditions of the
12
+ # subcomponent's license, as noted in the LICENSE file.
13
+ #
14
+
15
+ $:.push File.expand_path("../lib", __FILE__)
16
+ require "uaa/version"
17
+
18
+ Gem::Specification.new do |s|
19
+ s.name = "cf-uaa-lib"
20
+ s.version = CF::UAA::VERSION
21
+ s.authors = ["Dave Syer", "Dale Olds", "Joel D'sa", "Vidya Valmikinathan", "Luke Taylor"]
22
+ s.email = ["dsyer@vmware.com", "olds@vmware.com", "jdsa@vmware.com", "vidya@vmware.com", "ltaylor@vmware.com"]
23
+ s.homepage = "https://github.com/cloudfoundry/cf-uaa-lib"
24
+ s.summary = %q{Client library for CloudFoundry UAA}
25
+ s.description = %q{Client library for interacting with the CloudFoundry User Account and Authorization (UAA) server. The UAA is an OAuth2 Authorization Server so it can be used by webapps and command line apps to obtain access tokens to act on behalf of users. The tokens can then be used to access protected resources in a Resource Server. This library is for use by UAA client applications or resource servers.}
26
+
27
+ s.rubyforge_project = "cf-uaa-lib"
28
+
29
+ s.files = `git ls-files`.split("\n")
30
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
31
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
32
+ s.require_paths = ["lib"]
33
+
34
+ # dependencies
35
+ s.add_dependency "multi_json"
36
+
37
+ s.add_development_dependency "bundler"
38
+ s.add_development_dependency "rake"
39
+ s.add_development_dependency "rdoc"
40
+ s.add_development_dependency "rspec"
41
+ s.add_development_dependency "simplecov"
42
+ s.add_development_dependency "simplecov-rcov"
43
+ s.add_development_dependency "ci_reporter"
44
+
45
+ end
data/lib/uaa.rb ADDED
@@ -0,0 +1,18 @@
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/version"
15
+ require "uaa/misc"
16
+ require "uaa/token_issuer"
17
+ require "uaa/token_coder"
18
+ require "uaa/scim"
data/lib/uaa/http.rb ADDED
@@ -0,0 +1,147 @@
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
+ class BadTarget < UAAError; end
21
+ class NotFound < UAAError; end
22
+ class BadResponse < UAAError; end
23
+ class InvalidToken < UAAError; end
24
+ class HTTPException < UAAError; end
25
+ class TargetError < UAAError
26
+ attr_reader :info
27
+ def initialize(error_info = {})
28
+ @info = error_info
29
+ end
30
+ end
31
+
32
+ # Utility accessors and methods for objects that want to access JSON web APIs.
33
+ module Http
34
+
35
+ def logger=(logr); @logger = logr end
36
+ def logger ; @logger ||= Util.default_logger end
37
+ def trace? ; @logger && @logger.respond_to?(:trace?) && @logger.trace? end
38
+
39
+ # sets handler for outgoing http requests. If not set, net/http is used. :yields: url, method, body, headers
40
+ def set_request_handler(&blk) @req_handler = blk end
41
+
42
+ def self.basic_auth(name, password)
43
+ "Basic " + Base64::strict_encode64("#{name}:#{password}")
44
+ end
45
+
46
+ def add_auth_json(auth, headers, jsonhdr = "content-type") # :nodoc:
47
+ headers["authorization"] = auth if auth
48
+ headers.merge!(jsonhdr => "application/json")
49
+ end
50
+
51
+ def json_get(target, path = nil, authorization = nil, key_style = :none, headers = {})
52
+ json_parse_reply(*http_get(target, path,
53
+ add_auth_json(authorization, headers, "accept")), key_style)
54
+ end
55
+
56
+ def json_post(target, path, body, authorization, headers = {})
57
+ http_post(target, path, Util.json(body), add_auth_json(authorization, headers))
58
+ end
59
+
60
+ def json_put(target, path, body, authorization = nil, headers = {})
61
+ http_put(target, path, Util.json(body), add_auth_json(authorization, headers))
62
+ end
63
+
64
+ def json_patch(target, path, body, authorization = nil, headers = {})
65
+ http_patch(target, path, Util.json(body), add_auth_json(authorization, headers))
66
+ end
67
+
68
+ def json_parse_reply(status, body, headers, key_style = :none)
69
+ unless [200, 201, 204, 400, 401, 403].include? status
70
+ raise (status == 404 ? NotFound : BadResponse), "invalid status response: #{status}"
71
+ end
72
+ if body && !body.empty? && (status == 204 || headers.nil? ||
73
+ headers["content-type"] !~ /application\/json/i)
74
+ raise BadResponse, "received invalid response content or type"
75
+ end
76
+ parsed_reply = Util.json_parse(body, key_style)
77
+ if status >= 400
78
+ raise parsed_reply && parsed_reply["error"] == "invalid_token" ?
79
+ InvalidToken : TargetError.new(parsed_reply), "error response"
80
+ end
81
+ parsed_reply
82
+ rescue DecodeError
83
+ raise BadResponse, "invalid JSON response"
84
+ end
85
+
86
+ def http_get(target, path = nil, headers = {}) request(target, :get, path, nil, headers) end
87
+ def http_post(target, path, body, headers = {}) request(target, :post, path, body, headers) end
88
+ def http_put(target, path, body, headers = {}) request(target, :put, path, body, headers) end
89
+ def http_patch(target, path, body, headers = {}) request(target, :patch, path, body, headers) end
90
+
91
+ def http_delete(target, path, authorization)
92
+ status = request(target, :delete, path, nil, "authorization" => authorization)[0]
93
+ unless [200, 204].include?(status)
94
+ raise (status == 404 ? NotFound : BadResponse), "invalid response from #{path}: #{status}"
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def request(target, method, path, body = nil, headers = {})
101
+ headers["accept"] = headers["content-type"] if headers["content-type"] && !headers["accept"]
102
+ url = "#{target}#{path}"
103
+
104
+ logger.debug { "--->\nrequest: #{method} #{url}\n" +
105
+ "headers: #{headers}\n#{'body: ' + Util.truncate(body.to_s, trace? ? 50000 : 50) if body}" }
106
+ status, body, headers = @req_handler ? @req_handler.call(url, method, body, headers) :
107
+ net_http_request(url, method, body, headers)
108
+ logger.debug { "<---\nresponse: #{status}\nheaders: #{headers}\n" +
109
+ "#{'body: ' + Util.truncate(body.to_s, trace? ? 50000: 50) if body}" }
110
+
111
+ [status, body, headers]
112
+
113
+ rescue Exception => e
114
+ e.message.replace "Target #{target}, #{e.message}"
115
+ logger.debug { "<---- no response due to exception: #{e}" }
116
+ raise e
117
+ end
118
+
119
+ def net_http_request(url, method, body, headers)
120
+ raise ArgumentError unless reqtype = {delete: Net::HTTP::Delete,
121
+ get: Net::HTTP::Get, post: Net::HTTP::Post, put: Net::HTTP::Put}[method]
122
+ headers["content-length"] = body.length if body
123
+ uri = URI.parse(url)
124
+ req = reqtype.new(uri.request_uri)
125
+ headers.each { |k, v| req[k] = v }
126
+ http_key = "#{uri.scheme}://#{uri.host}:#{uri.port}"
127
+ @http_cache ||= {}
128
+ unless http = @http_cache[http_key]
129
+ @http_cache[http_key] = http = Net::HTTP.new(uri.host, uri.port)
130
+ if uri.is_a?(URI::HTTPS)
131
+ http.use_ssl = true
132
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
133
+ end
134
+ end
135
+ reply, outhdrs = http.request(req, body), {}
136
+ reply.each_header { |k, v| outhdrs[k] = v }
137
+ [reply.code.to_i, reply.body, outhdrs]
138
+
139
+ rescue URI::Error, SocketError, SystemCallError => e
140
+ raise BadTarget, "error: #{e.message}"
141
+ rescue Net::HTTPBadResponse => e
142
+ raise HTTPException, "HTTP exception: #{e.class}: #{e}"
143
+ end
144
+
145
+ end
146
+
147
+ end
data/lib/uaa/misc.rb ADDED
@@ -0,0 +1,67 @@
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
+ # This class is for Web Client Apps (in the OAuth2 sense) that want
15
+ # access to authenticated user information. Basically this class is
16
+ # an OpenID Connect client.
17
+
18
+ require 'uaa/http'
19
+
20
+ module CF::UAA
21
+
22
+ # everything is miscellaneous
23
+ #
24
+ # this class provides interfaces to UAA endpoints that are not in the context
25
+ # of an overall class of operations, like "user accounts" or "tokens". It's
26
+ # also for some apis like "change user password" or "change client secret" that
27
+ # use different forms of authentication than other operations on those types
28
+ # of resources.
29
+ class Misc
30
+
31
+ class << self
32
+ include Http
33
+ end
34
+
35
+ def self.whoami(target, auth_header) json_get(target, "/userinfo?schema=openid", auth_header) end
36
+ def self.varz(target, name, pwd) json_get(target, "/varz", Http.basic_auth(name, pwd)) end
37
+
38
+ def self.server(target)
39
+ reply = json_get(target, '/login')
40
+ return reply if reply && reply["prompts"]
41
+ raise BadResponse, "Invalid response from target #{target}"
42
+ end
43
+
44
+ def self.validation_key(target, client_id = nil, client_secret = nil)
45
+ json_get(target, "/token_key", (client_id && client_secret ? Http.basic_auth(client_id, client_secret) : nil))
46
+ end
47
+
48
+ # Returns hash of values from the Authorization Server that are associated
49
+ # with the opaque token.
50
+ def self.decode_token(target, client_id, client_secret, token, token_type = "bearer", audience_ids = nil)
51
+ reply = json_get(target, "/check_token?token_type=#{token_type}&token=#{token}",
52
+ Http.basic_auth(client_id, client_secret))
53
+ auds = Util.arglist(reply["aud"])
54
+ if audience_ids && (!auds || (auds & audience_ids).empty?)
55
+ raise AuthError, "invalid audience: #{auds.join(' ')}"
56
+ end
57
+ reply
58
+ end
59
+
60
+ def self.password_strength(target, password)
61
+ json_parse_reply(*request(target, :post, '/password/score', URI.encode_www_form("password" => password),
62
+ "content-type" => "application/x-www-form-urlencoded", "accept" => "application/json"))
63
+ end
64
+
65
+ end
66
+
67
+ end
data/lib/uaa/scim.rb ADDED
@@ -0,0 +1,206 @@
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 Client Registrations.
19
+ # It provides access to the SCIM endpoints on the UAA.
20
+ class Scim
21
+
22
+ include Http
23
+
24
+ private
25
+
26
+ def force_attr(k)
27
+ kd = k.to_s.downcase
28
+ kc = {"username" => "userName", "familyname" => "familyName",
29
+ "givenname" => "givenName", "middlename" => "middleName",
30
+ "honorificprefix" => "honorificPrefix",
31
+ "honorificsuffix" => "honorificSuffix", "displayname" => "displayName",
32
+ "nickname" => "nickName", "profileurl" => "profileUrl",
33
+ "streetaddress" => "streetAddress", "postalcode" => "postalCode",
34
+ "usertype" => "userType", "preferredlanguage" => "preferredLanguage",
35
+ "x509certificates" => "x509Certificates", "lastmodified" => "lastModified",
36
+ "externalid" => "externalId", "phonenumbers" => "phoneNumbers",
37
+ "startindex" => "startIndex"}[kd]
38
+ kc || kd
39
+ end
40
+
41
+ # This is very inefficient and should be unnecessary. SCIM (1.1 and early
42
+ # 2.0 drafts) specify that attribute names are case insensitive. However
43
+ # in the UAA attribute names are currently case sensitive. This hack takes
44
+ # a hash with keys as symbols or strings and with any case, and forces
45
+ # the attribute name to the case that the uaa expects.
46
+ def force_case(obj)
47
+ return obj.collect {|o| force_case(o)} if obj.is_a? Array
48
+ return obj unless obj.is_a? Hash
49
+ obj.each_with_object({}) {|(k, v), h| h[force_attr(k)] = force_case(v) }
50
+ end
51
+
52
+ # an attempt to hide some scim and uaa oddities
53
+ def type_info(type, elem)
54
+ scimfo = {user: ["/Users", "userName"], group: ["/Groups", "displayName"],
55
+ client: ["/oauth/clients", 'client_id'], user_id: ["/ids/Users", 'userName']}
56
+ unless elem == :path || elem == :name_attr
57
+ raise ArgumentError, "scim schema element must be :path or :name_attr"
58
+ end
59
+ unless ary = scimfo[type]
60
+ raise ArgumentError, "scim resource type must be one of #{scimfo.keys.inspect}"
61
+ end
62
+ ary[elem == :path ? 0 : 1]
63
+ end
64
+
65
+ def prep_request(type, info = nil)
66
+ [type_info(type, :path), force_case(info)]
67
+ end
68
+
69
+ public
70
+
71
+ # the auth_header parameter refers to a string that can be used in an
72
+ # authorization header. For oauth with jwt tokens this would be something
73
+ # like "bearer xxxx.xxxx.xxxx". The Token class returned by TokenIssuer
74
+ # provides an auth_header method for this purpose.
75
+ def initialize(target, auth_header) @target, @auth_header = target, auth_header end
76
+
77
+ # info is a hash structure converted to json and sent to the scim /Users endpoint
78
+ def add(type, info)
79
+ path, info = prep_request(type, info)
80
+ reply = json_parse_reply(*json_post(@target, path, info, @auth_header), :down)
81
+
82
+ # hide client endpoints that are not scim compatible
83
+ reply['id'] = reply['client_id'] if type == :client && reply['client_id'] && !reply['id']
84
+
85
+ return reply if reply && reply["id"]
86
+ raise BadResponse, "no id returned by add request to #{@target}#{path}"
87
+ end
88
+
89
+ def delete(type, id)
90
+ path, _ = prep_request(type)
91
+ http_delete @target, "#{path}/#{URI.encode(id)}", @auth_header
92
+ end
93
+
94
+ # info is a hash structure converted to json and sent to the scim /Users endpoint
95
+ def put(type, info)
96
+ path, info = prep_request(type, info)
97
+ ida = type == :client ? 'client_id' : 'id'
98
+ raise ArgumentError, "scim info must include #{ida}" unless id = info[ida]
99
+ hdrs = info && info["meta"] && info["meta"]["version"] ?
100
+ {'if-match' => info["meta"]["version"]} : {}
101
+ reply = json_parse_reply(*json_put(@target, "#{path}/#{URI.encode(id)}",
102
+ info, @auth_header, hdrs), :down)
103
+
104
+ # hide client endpoints that are not scim compatible
105
+ type == :client && !reply ? get(type, info["client_id"]): reply
106
+ end
107
+
108
+ # TODO: fix this when the UAA supports patch
109
+ # info is a hash structure converted to json and sent to the scim /Users endpoint
110
+ #def patch(path, id, info, attributes_to_delete = nil)
111
+ # info = info.merge(meta: { attributes: Util.arglist(attributes_to_delete) }) if attributes_to_delete
112
+ # json_parse_reply(*json_patch(@target, "#{path}/#{URI.encode(id)}", info, @auth_header))
113
+ #end
114
+
115
+ # supported query keys are: attributes, filter, startIndex, count
116
+ # output hash keys are: resources, totalResults, itemsPerPage
117
+ def query(type, query = {})
118
+ path, query = prep_request(type, query)
119
+ query = query.reject {|k, v| v.nil? }
120
+ if attrs = query['attributes']
121
+ attrs = Util.arglist(attrs).map {|a| force_attr(a)}
122
+ query['attributes'] = Util.strlist(attrs, ",")
123
+ end
124
+ qstr = query.empty?? '': "?#{URI.encode_www_form(query)}"
125
+ info = json_get(@target, "#{path}#{qstr}", @auth_header, :down)
126
+ unless info.is_a?(Hash) && info['resources'].is_a?(Array)
127
+
128
+ # hide client endpoints that are not scim compatible
129
+ return {'resources' => info.values } if type == :client && info.is_a?(Hash)
130
+
131
+ raise BadResponse, "invalid reply to query of #{@target}#{path}"
132
+ end
133
+ info
134
+ end
135
+
136
+ def get(type, id)
137
+ path, _ = prep_request(type)
138
+ info = json_get(@target, "#{path}/#{URI.encode(id)}", @auth_header, :down)
139
+
140
+ # hide client endpoints that are not scim compatible
141
+ info["id"] = info["client_id"] if type == :client && !info["id"]
142
+ info
143
+ end
144
+
145
+ # Collects all pages of entries from a query, returns array of results.
146
+ # Type can be any scim resource type
147
+ def all_pages(type, query = {})
148
+ query = query.reject {|k, v| v.nil? }
149
+ query["startindex"], info = 1, []
150
+ while true
151
+ qinfo = query(type, query)
152
+ raise BadResponse unless qinfo["resources"]
153
+ return info if qinfo["resources"].empty?
154
+ info.concat(qinfo["resources"])
155
+ return info unless qinfo["totalresults"] && qinfo["totalresults"] > info.length
156
+ unless qinfo["startindex"] && qinfo["itemsperpage"]
157
+ raise BadResponse, "incomplete pagination data from #{@target}#{path}"
158
+ end
159
+ query["startindex"] = info.length + 1
160
+ end
161
+ end
162
+
163
+ # Queries for objects by name. returns array of name/id hashes for each
164
+ # name found.
165
+ def ids(type, *names)
166
+ na = type_info(type, :name_attr)
167
+ filter = names.each_with_object([]) { |n, o| o << "#{na} eq \"#{n}\""}
168
+ all_pages(type, attributes: "id,#{na}", filter: filter.join(" or "))
169
+ end
170
+
171
+ # Convenience method to query for single object by name.
172
+ # Returns its id. Raises error if not found.
173
+ def id(type, name)
174
+ res = ids(type, name)
175
+
176
+ # hide client endpoints that are not scim compatible
177
+ if type == :client && res && res.length > 0
178
+ if res.length > 1 || res[0]["id"].nil?
179
+ cr = res.find { |o| o['client_id'] && name.casecmp(o['client_id']) == 0 }
180
+ return cr['id'] || cr['client_id'] if cr
181
+ end
182
+ end
183
+
184
+ unless res && res.is_a?(Array) && res.length == 1 &&
185
+ res[0].is_a?(Hash) && (id = res[0]["id"])
186
+ raise NotFound, "#{name} not found in #{@target}#{type_info(type, :path)}"
187
+ end
188
+ id
189
+ end
190
+
191
+ def change_password(user_id, new_password, old_password = nil)
192
+ password_request = {"password" => new_password}
193
+ password_request["oldPassword"] = old_password if old_password
194
+ json_parse_reply(*json_put(@target, "/Users/#{URI.encode(user_id)}/password", password_request, @auth_header))
195
+ end
196
+
197
+ def change_secret(client_id, new_secret, old_secret = nil)
198
+ req = {"secret" => new_secret }
199
+ req["oldSecret"] = old_secret if old_secret
200
+ json_parse_reply(*json_put(@target, "/oauth/clients/#{URI.encode(client_id)}/secret", req, @auth_header))
201
+ end
202
+
203
+ end
204
+
205
+ end
206
+