cf-uaa-lib 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE.TXT +12737 -0
- data/NOTICE.TXT +10 -0
- data/README.md +3 -1
- data/Rakefile +7 -6
- data/cf-uaa-lib.gemspec +3 -1
- data/lib/uaa/http.rb +37 -32
- data/lib/uaa/misc.rb +59 -30
- data/lib/uaa/scim.rb +150 -110
- data/lib/uaa/token_coder.rb +84 -42
- data/lib/uaa/token_issuer.rb +137 -120
- data/lib/uaa/util.rb +113 -62
- data/lib/uaa/version.rb +2 -1
- data/spec/http_spec.rb +1 -1
- data/spec/integration_spec.rb +149 -0
- data/spec/scim_spec.rb +12 -11
- data/spec/token_coder_spec.rb +6 -6
- data/spec/token_issuer_spec.rb +17 -14
- metadata +42 -6
data/NOTICE.TXT
ADDED
@@ -0,0 +1,10 @@
|
|
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.
|
data/README.md
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
Client gem for interacting with the [CloudFoundry UAA server](https://github.com/cloudfoundry/uaa)
|
4
4
|
|
5
|
+
For documentation see: https://rubygems.org/gems/cf-uaa-lib
|
6
|
+
|
5
7
|
## Install from rubygems
|
6
8
|
|
7
9
|
$ gem install cf-uaa-lib
|
@@ -17,7 +19,7 @@ Client gem for interacting with the [CloudFoundry UAA server](https://github.com
|
|
17
19
|
#!/usr/bin/env ruby
|
18
20
|
require 'uaa'
|
19
21
|
token_issuer = CF::UAA::TokenIssuer.new("https://uaa.cloudfoundry.com", "vmc")
|
20
|
-
puts
|
22
|
+
puts token_issuer.prompts.inspect
|
21
23
|
token = token_issuer.implicit_grant_with_creds(username: "<your_username>", password: "<your_password>")
|
22
24
|
token_info = TokenCoder.decode(token.info["access_token"], nil, nil, false) #token signature not verified
|
23
25
|
puts token_info["user_name"]
|
data/Rakefile
CHANGED
@@ -10,10 +10,10 @@
|
|
10
10
|
# subcomponent's license, as noted in the LICENSE file.
|
11
11
|
#
|
12
12
|
|
13
|
-
require "rdoc/task"
|
14
13
|
require "rspec/core/rake_task"
|
15
14
|
require "bundler/gem_tasks" # only available in bundler >= 1.0.15
|
16
15
|
require "ci/reporter/rake/rspec"
|
16
|
+
require "yard"
|
17
17
|
|
18
18
|
ENV['CI_REPORTS'] = File.expand_path("spec_reports")
|
19
19
|
COV_REPORTS = File.expand_path("coverage")
|
@@ -22,16 +22,17 @@ task :default => [:test]
|
|
22
22
|
task :tests => [:test]
|
23
23
|
task :spec => [:test]
|
24
24
|
|
25
|
+
YARD::Rake::YardocTask.new do |t|
|
26
|
+
t.files = ['lib/**/*.rb', '-', 'LICENSE.TXT', 'NOTICE.TXT']
|
27
|
+
t.options = ['--main', 'README.md', '--no-private',
|
28
|
+
'--title', 'Cloud Foundry UAA Client API']
|
29
|
+
end
|
30
|
+
|
25
31
|
RSpec::Core::RakeTask.new("test") do |t|
|
26
32
|
t.rspec_opts = ["--format", "documentation", "--colour"]
|
27
33
|
t.pattern = "spec/**/*_spec.rb"
|
28
34
|
end
|
29
35
|
|
30
|
-
RDoc::Task.new do |rd|
|
31
|
-
rd.rdoc_files.include("lib/**/*.rb")
|
32
|
-
rd.rdoc_dir = "doc"
|
33
|
-
end
|
34
|
-
|
35
36
|
task :ci => [:pre_coverage, :rcov_reports, "ci:setup:rspec", :test]
|
36
37
|
task :cov => [:pre_coverage, :test, :view_coverage]
|
37
38
|
task :coverage => [:pre_coverage, :test]
|
data/cf-uaa-lib.gemspec
CHANGED
@@ -36,10 +36,12 @@ Gem::Specification.new do |s|
|
|
36
36
|
|
37
37
|
s.add_development_dependency "bundler"
|
38
38
|
s.add_development_dependency "rake"
|
39
|
-
s.add_development_dependency "
|
39
|
+
s.add_development_dependency "yard"
|
40
|
+
s.add_development_dependency "redcarpet"
|
40
41
|
s.add_development_dependency "rspec"
|
41
42
|
s.add_development_dependency "simplecov"
|
42
43
|
s.add_development_dependency "simplecov-rcov"
|
43
44
|
s.add_development_dependency "ci_reporter"
|
45
|
+
s.add_development_dependency "json_pure"
|
44
46
|
|
45
47
|
end
|
data/lib/uaa/http.rb
CHANGED
@@ -17,19 +17,19 @@ require 'uaa/util'
|
|
17
17
|
|
18
18
|
module CF::UAA
|
19
19
|
|
20
|
-
# Indicates URL for the target is bad or not accessible
|
20
|
+
# Indicates URL for the target is bad or not accessible.
|
21
21
|
class BadTarget < UAAError; end
|
22
22
|
|
23
|
-
#
|
23
|
+
# Indicates the resource within the target server was not found.
|
24
24
|
class NotFound < UAAError; end
|
25
25
|
|
26
26
|
# Indicates a syntax error in a response from the UAA, e.g. missing required response field.
|
27
27
|
class BadResponse < UAAError; end
|
28
28
|
|
29
|
-
# Indicates a token is malformed or expired
|
29
|
+
# Indicates a token is malformed or expired.
|
30
30
|
class InvalidToken < UAAError; end
|
31
31
|
|
32
|
-
# Indicates an error from the http client stack
|
32
|
+
# Indicates an error from the http client stack.
|
33
33
|
class HTTPException < UAAError; end
|
34
34
|
|
35
35
|
# An application level error from the UAA which includes error info in the reply.
|
@@ -43,46 +43,51 @@ end
|
|
43
43
|
# Utility accessors and methods for objects that want to access JSON web APIs.
|
44
44
|
module Http
|
45
45
|
|
46
|
-
# Sets the current logger instance to recieve error messages
|
46
|
+
# Sets the current logger instance to recieve error messages.
|
47
|
+
# @param [Logger] logr
|
48
|
+
# @return [Logger]
|
47
49
|
def logger=(logr); @logger = logr end
|
48
50
|
|
49
|
-
#
|
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
|
51
54
|
|
52
|
-
#
|
53
|
-
|
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
|
54
58
|
|
55
|
-
# Sets handler for outgoing http requests. If
|
56
|
-
# net/http connections is used.
|
57
|
-
#
|
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
|
59
65
|
|
60
|
-
#
|
66
|
+
# Constructs an http basic authentication header.
|
67
|
+
# @return [String]
|
61
68
|
def self.basic_auth(name, password)
|
62
|
-
|
69
|
+
str = "#{name}:#{password}"
|
70
|
+
"Basic " + (Base64.respond_to?(:strict_encode64)?
|
71
|
+
Base64.strict_encode64(str): [str].pack("m").gsub(/\n/, ''))
|
63
72
|
end
|
64
73
|
|
65
74
|
private
|
66
75
|
|
67
|
-
def
|
68
|
-
|
69
|
-
headers.merge
|
76
|
+
def json_get(target, path = nil, style = nil, headers = {})
|
77
|
+
raise ArgumentError unless style.nil? || style.is_a?(Symbol)
|
78
|
+
json_parse_reply(style, *http_get(target, path, headers.merge("accept" => "application/json")))
|
70
79
|
end
|
71
80
|
|
72
|
-
def
|
73
|
-
|
74
|
-
add_auth_json(authorization, headers, "accept")), key_style)
|
81
|
+
def json_post(target, path, body, headers = {})
|
82
|
+
http_post(target, path, Util.json(body), headers.merge("content-type" => "application/json"))
|
75
83
|
end
|
76
84
|
|
77
|
-
def
|
78
|
-
|
85
|
+
def json_put(target, path, body, headers = {})
|
86
|
+
http_put(target, path, Util.json(body), headers.merge("content-type" => "application/json"))
|
79
87
|
end
|
80
88
|
|
81
|
-
def
|
82
|
-
|
83
|
-
end
|
84
|
-
|
85
|
-
def json_parse_reply(status, body, headers, key_style = :none)
|
89
|
+
def json_parse_reply(style, status, body, headers)
|
90
|
+
raise ArgumentError unless style.nil? || style.is_a?(Symbol)
|
86
91
|
unless [200, 201, 204, 400, 401, 403].include? status
|
87
92
|
raise (status == 404 ? NotFound : BadResponse), "invalid status response: #{status}"
|
88
93
|
end
|
@@ -90,8 +95,8 @@ module Http
|
|
90
95
|
headers["content-type"] !~ /application\/json/i)
|
91
96
|
raise BadResponse, "received invalid response content or type"
|
92
97
|
end
|
93
|
-
parsed_reply = Util.json_parse(body,
|
94
|
-
|
98
|
+
parsed_reply = Util.json_parse(body, style)
|
99
|
+
if status >= 400
|
95
100
|
raise parsed_reply && parsed_reply["error"] == "invalid_token" ?
|
96
101
|
InvalidToken : TargetError.new(parsed_reply), "error response"
|
97
102
|
end
|
@@ -131,8 +136,8 @@ module Http
|
|
131
136
|
end
|
132
137
|
|
133
138
|
def net_http_request(url, method, body, headers)
|
134
|
-
raise ArgumentError unless reqtype = {delete
|
135
|
-
get
|
139
|
+
raise ArgumentError unless reqtype = {:delete => Net::HTTP::Delete,
|
140
|
+
:get => Net::HTTP::Get, :post => Net::HTTP::Post, :put => Net::HTTP::Put}[method]
|
136
141
|
headers["content-length"] = body.length if body
|
137
142
|
uri = URI.parse(url)
|
138
143
|
req = reqtype.new(uri.request_uri)
|
data/lib/uaa/misc.rb
CHANGED
@@ -15,7 +15,7 @@ require 'uaa/http'
|
|
15
15
|
|
16
16
|
module CF::UAA
|
17
17
|
|
18
|
-
# interfaces to UAA endpoints that are not in the context
|
18
|
+
# Provides interfaces to various UAA endpoints that are not in the context
|
19
19
|
# of an overall class of operations like SCIM resources or OAuth2 tokens.
|
20
20
|
class Misc
|
21
21
|
|
@@ -23,55 +23,84 @@ class Misc
|
|
23
23
|
include Http
|
24
24
|
end
|
25
25
|
|
26
|
-
#
|
27
|
-
#
|
28
|
-
|
29
|
-
|
30
|
-
#
|
31
|
-
# and
|
32
|
-
|
33
|
-
|
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)
|
34
41
|
end
|
35
42
|
|
36
|
-
#
|
37
|
-
# Authenticates
|
38
|
-
#
|
39
|
-
|
40
|
-
|
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))
|
41
49
|
end
|
42
50
|
|
43
|
-
#
|
44
|
-
#
|
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]
|
45
57
|
def self.server(target)
|
46
|
-
reply = json_get(target, '/login')
|
47
|
-
return reply if reply && reply[
|
58
|
+
reply = json_get(target, '/login', @key_style)
|
59
|
+
return reply if reply && (reply[:prompts] || reply['prompts'])
|
48
60
|
raise BadResponse, "Invalid response from target #{target}"
|
49
61
|
end
|
50
62
|
|
63
|
+
# Gets the key from the server that is used to validate token signatures. If
|
64
|
+
# the server is configured to use a symetric key, the caller must authenticate
|
65
|
+
# by providing a a +client_id+ and +client_secret+. If the server
|
66
|
+
# is configured to sign with a private key, this call will retrieve the
|
67
|
+
# public key and +client_id+ must be nil.
|
68
|
+
# @param (see Misc.server)
|
69
|
+
# @return [Hash]
|
51
70
|
def self.validation_key(target, client_id = nil, client_secret = nil)
|
52
|
-
|
71
|
+
hdrs = client_id && client_secret ?
|
72
|
+
{ "authorization" => Http.basic_auth(client_id, client_secret)} : {}
|
73
|
+
json_get(target, "/token_key", @key_style, hdrs)
|
53
74
|
end
|
54
75
|
|
55
|
-
# Sends
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
76
|
+
# Sends +token+ to the server to validate and decode. Authenticates with
|
77
|
+
# +client_id+ and +client_secret+. If +audience_ids+ are specified and the
|
78
|
+
# token's "aud" attribute does not contain one or more of the audience_ids,
|
79
|
+
# raises AuthError -- meaning the token is not for this audience.
|
80
|
+
# @param (see Misc.server)
|
81
|
+
# @param [String] token an access token as retrieved by {TokenIssuer}. See
|
82
|
+
# also {TokenInfo}.
|
83
|
+
# @param [String] token_type as retrieved by {TokenIssuer}. See {TokenInfo}.
|
84
|
+
# @return [Hash] contents of the token
|
60
85
|
def self.decode_token(target, client_id, client_secret, token, token_type = "bearer", audience_ids = nil)
|
61
86
|
reply = json_get(target, "/check_token?token_type=#{token_type}&token=#{token}",
|
62
|
-
Http.basic_auth(client_id, client_secret))
|
63
|
-
auds = Util.arglist(reply[
|
87
|
+
@key_style, "authorization" => Http.basic_auth(client_id, client_secret))
|
88
|
+
auds = Util.arglist(reply[:aud] || reply['aud'])
|
64
89
|
if audience_ids && (!auds || (auds & audience_ids).empty?)
|
65
90
|
raise AuthError, "invalid audience: #{auds.join(' ')}"
|
66
91
|
end
|
67
92
|
reply
|
68
93
|
end
|
69
94
|
|
70
|
-
#
|
71
|
-
#
|
95
|
+
# Gets information about the given password, including a strength score and
|
96
|
+
# an indication of what strength is required.
|
97
|
+
# @param (see Misc.server)
|
98
|
+
# @return [Hash]
|
72
99
|
def self.password_strength(target, password)
|
73
|
-
json_parse_reply(*request(target, :post, '/password/score',
|
74
|
-
|
100
|
+
json_parse_reply(@key_style, *request(target, :post, '/password/score',
|
101
|
+
Util.encode_form(:password => password),
|
102
|
+
"content-type" => "application/x-www-form-urlencoded",
|
103
|
+
"accept" => "application/json"))
|
75
104
|
end
|
76
105
|
|
77
106
|
end
|
data/lib/uaa/scim.rb
CHANGED
@@ -19,7 +19,20 @@ module CF::UAA
|
|
19
19
|
# Client Registrations. It provides access to the SCIM endpoints on the UAA.
|
20
20
|
# For more information about SCIM -- the IETF's System for Cross-domain
|
21
21
|
# Identity Management (formerly known as Simple Cloud Identity Management) --
|
22
|
-
# see http://www.simplecloud.info
|
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"
|
23
36
|
class Scim
|
24
37
|
|
25
38
|
include Http
|
@@ -49,13 +62,15 @@ class Scim
|
|
49
62
|
def force_case(obj)
|
50
63
|
return obj.collect {|o| force_case(o)} if obj.is_a? Array
|
51
64
|
return obj unless obj.is_a? Hash
|
52
|
-
|
65
|
+
new_obj = {}
|
66
|
+
obj.each {|(k, v)| new_obj[force_attr(k)] = force_case(v) }
|
67
|
+
new_obj
|
53
68
|
end
|
54
69
|
|
55
70
|
# an attempt to hide some scim and uaa oddities
|
56
71
|
def type_info(type, elem)
|
57
|
-
scimfo = {user
|
58
|
-
client
|
72
|
+
scimfo = {:user => ["/Users", "userName"], :group => ["/Groups", "displayName"],
|
73
|
+
:client => ["/oauth/clients", 'client_id'], :user_id => ["/ids/Users", 'userName']}
|
59
74
|
unless elem == :path || elem == :name_attr
|
60
75
|
raise ArgumentError, "scim schema element must be :path or :name_attr"
|
61
76
|
end
|
@@ -65,174 +80,199 @@ class Scim
|
|
65
80
|
ary[elem == :path ? 0 : 1]
|
66
81
|
end
|
67
82
|
|
68
|
-
def
|
69
|
-
|
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]
|
70
88
|
end
|
71
89
|
|
72
90
|
public
|
73
91
|
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
# and meta data.
|
85
|
-
def add(type, info)
|
86
|
-
path, info = prep_request(type, info)
|
87
|
-
reply = json_parse_reply(*json_post(@target, path, info, @auth_header), :down)
|
88
|
-
|
89
|
-
# hide client endpoints that are not scim compatible
|
90
|
-
reply['id'] = reply['client_id'] if type == :client && reply['client_id'] && !reply['id']
|
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 style (see Util.hash_key)
|
98
|
+
def initialize(target, auth_header, options = {})
|
99
|
+
@target, @auth_header = target, auth_header
|
100
|
+
@key_style = options[:symbolize_keys] ? :downsym : :down
|
101
|
+
end
|
91
102
|
|
92
|
-
|
93
|
-
|
103
|
+
# Creates a SCIM resource.
|
104
|
+
# @param [Symbol] type can be :user, :group, :client, :user_id.
|
105
|
+
# @param [Hash] info converted to json and sent to the scim endpoint. For schema of
|
106
|
+
# each type of object see {Scim}.
|
107
|
+
# @return [Hash] contents of the object, including its +id+ and meta-data.
|
108
|
+
def add(type, info)
|
109
|
+
path, info = type_info(type, :path), force_case(info)
|
110
|
+
reply = json_parse_reply(@key_style, *json_post(@target, path, info,
|
111
|
+
"authorization" => @auth_header))
|
112
|
+
fake_client_id(reply) if type == :client # hide client reply, not quite scim
|
113
|
+
reply
|
94
114
|
end
|
95
115
|
|
96
|
-
# Deletes a SCIM resource
|
116
|
+
# Deletes a SCIM resource
|
117
|
+
# @param type (see #add)
|
118
|
+
# @param [String] id the id attribute of the SCIM object
|
119
|
+
# @return [nil]
|
97
120
|
def delete(type, id)
|
98
|
-
|
99
|
-
http_delete @target, "#{path}/#{URI.encode(id)}", @auth_header
|
121
|
+
http_delete @target, "#{type_info(type, :path)}/#{URI.encode(id)}", @auth_header
|
100
122
|
end
|
101
123
|
|
102
|
-
#
|
103
|
-
#
|
124
|
+
# Replaces the contents of a SCIM object.
|
125
|
+
# @param (see #add)
|
126
|
+
# @return (see #add)
|
104
127
|
def put(type, info)
|
105
|
-
path, info =
|
128
|
+
path, info = type_info(type, :path), force_case(info)
|
106
129
|
ida = type == :client ? 'client_id' : 'id'
|
107
|
-
raise ArgumentError, "
|
108
|
-
hdrs =
|
109
|
-
|
110
|
-
|
111
|
-
|
130
|
+
raise ArgumentError, "info must include #{ida}" unless id = info[ida]
|
131
|
+
hdrs = {'authorization' => @auth_header}
|
132
|
+
if info && info['meta'] && (etag = info['meta']['version'])
|
133
|
+
hdrs.merge!('if-match' => etag)
|
134
|
+
end
|
135
|
+
reply = json_parse_reply(@key_style,
|
136
|
+
*json_put(@target, "#{path}/#{URI.encode(id)}", info, hdrs))
|
112
137
|
|
113
|
-
# hide client endpoints that are not scim compatible
|
114
|
-
type == :client && !reply ? get(type, info[
|
138
|
+
# hide client endpoints that are not quite scim compatible
|
139
|
+
type == :client && !reply ? get(type, info['client_id']): reply
|
115
140
|
end
|
116
141
|
|
117
|
-
#
|
118
|
-
#
|
119
|
-
#
|
120
|
-
# +:
|
121
|
-
#
|
122
|
-
#
|
123
|
-
#
|
124
|
-
#
|
125
|
-
# +:
|
126
|
-
#
|
127
|
-
#
|
128
|
-
#
|
129
|
-
# returned for each object that matches the filter. If no attribute
|
130
|
-
# list is given, all attributes are returned.
|
131
|
-
# filter:: a filter to select which objects are returned. See
|
132
|
-
# http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources
|
133
|
-
# startIndex:: for paged output, start index of requested result set.
|
134
|
-
# count:: maximum number of results per reply
|
142
|
+
# Gets a set of attributes for each object that matches a given filter.
|
143
|
+
# @param (see #add)
|
144
|
+
# @param [Hash] query may contain the following keys:
|
145
|
+
# * +attributes+: a comma or space separated list of attribute names to be
|
146
|
+
# returned for each object that matches the filter. If no attribute
|
147
|
+
# list is given, all attributes are returned.
|
148
|
+
# * +filter+: a filter to select which objects are returned. See
|
149
|
+
# {http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources}
|
150
|
+
# * +startIndex+: for paged output, start index of requested result set.
|
151
|
+
# * +count+: maximum number of results per reply
|
152
|
+
# @return [Hash] including a +resources+ array of results and
|
153
|
+
# pagination data.
|
135
154
|
def query(type, query = {})
|
136
|
-
|
137
|
-
query = query.reject {|k, v| v.nil? }
|
155
|
+
query = force_case(query).reject {|k, v| v.nil? }
|
138
156
|
if attrs = query['attributes']
|
139
157
|
attrs = Util.arglist(attrs).map {|a| force_attr(a)}
|
140
158
|
query['attributes'] = Util.strlist(attrs, ",")
|
141
159
|
end
|
142
|
-
qstr = query.empty?? '': "?#{
|
143
|
-
info = json_get(@target, "#{path}#{qstr}", @
|
144
|
-
unless info.is_a?(Hash) && info[
|
160
|
+
qstr = query.empty?? '': "?#{Util.encode_form(query)}"
|
161
|
+
info = json_get(@target, "#{type_info(type, :path)}#{qstr}", @key_style, 'authorization' => @auth_header)
|
162
|
+
unless info.is_a?(Hash) && info[rk = jkey(:resources)].is_a?(Array)
|
145
163
|
|
146
164
|
# hide client endpoints that are not scim compatible
|
147
|
-
|
165
|
+
if type == :client && info.is_a?(Hash)
|
166
|
+
info.each { |k, v| fake_client_id(v) }
|
167
|
+
return {rk => info.values }
|
168
|
+
end
|
148
169
|
|
149
|
-
raise BadResponse, "invalid reply to query of #{@target}
|
170
|
+
raise BadResponse, "invalid reply to #{type} query of #{@target}"
|
150
171
|
end
|
151
172
|
info
|
152
173
|
end
|
153
174
|
|
154
|
-
#
|
155
|
-
#
|
156
|
-
#
|
175
|
+
# Get information about a specific object.
|
176
|
+
# @param (see #delete)
|
177
|
+
# @return (see #add)
|
157
178
|
def get(type, id)
|
158
|
-
|
159
|
-
|
179
|
+
info = json_get(@target, "#{type_info(type, :path)}/#{URI.encode(id)}",
|
180
|
+
@key_style, 'authorization' => @auth_header)
|
160
181
|
|
161
|
-
# hide client
|
162
|
-
info["id"] = info["client_id"] if type == :client && !info["id"]
|
182
|
+
fake_client_id(info) if type == :client # hide client reply, not quite scim
|
163
183
|
info
|
164
184
|
end
|
165
185
|
|
166
|
-
# Collects all pages of entries from a query
|
167
|
-
#
|
186
|
+
# Collects all pages of entries from a query
|
187
|
+
# @param type (see #query)
|
188
|
+
# @param [Hash] query may contain the following keys:
|
189
|
+
# * +attributes+: a comma or space separated list of attribute names to be
|
190
|
+
# returned for each object that matches the filter. If no attribute
|
191
|
+
# list is given, all attributes are returned.
|
192
|
+
# * +filter+: a filter to select which objects are returned. See
|
193
|
+
# {http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources}
|
194
|
+
# @return [Array] results
|
168
195
|
def all_pages(type, query = {})
|
169
|
-
query = query.reject {|k, v| v.nil? }
|
170
|
-
query["startindex"], info = 1, []
|
196
|
+
query = force_case(query).reject {|k, v| v.nil? }
|
197
|
+
query["startindex"], info, rk = 1, [], jkey(:resources)
|
171
198
|
while true
|
172
199
|
qinfo = query(type, query)
|
173
|
-
raise BadResponse unless qinfo[
|
174
|
-
return info if qinfo[
|
175
|
-
info.concat(qinfo[
|
176
|
-
|
177
|
-
unless
|
178
|
-
|
200
|
+
raise BadResponse unless qinfo[rk]
|
201
|
+
return info if qinfo[rk].empty?
|
202
|
+
info.concat(qinfo[rk])
|
203
|
+
total = qinfo[jkey :totalresults]
|
204
|
+
return info unless total && total > info.length
|
205
|
+
unless qinfo[jkey :startindex] && qinfo[jkey :itemsperpage]
|
206
|
+
raise BadResponse, "incomplete #{type} pagination data from #{@target}"
|
179
207
|
end
|
180
208
|
query["startindex"] = info.length + 1
|
181
209
|
end
|
182
210
|
end
|
183
211
|
|
184
|
-
#
|
185
|
-
#
|
212
|
+
# Gets id/name pairs for given names.
|
213
|
+
# @param type (see #add)
|
214
|
+
# @param [Array<String>] names. For naming attribute of each object type see {Scim}
|
215
|
+
# @return [Array] array of name/id hashes for each object found
|
186
216
|
def ids(type, *names)
|
187
217
|
na = type_info(type, :name_attr)
|
188
|
-
filter = names.
|
189
|
-
all_pages(type, attributes
|
218
|
+
filter = names.map { |n| "#{na} eq \"#{n}\""}
|
219
|
+
all_pages(type, :attributes => "id,#{na}", :filter => filter.join(" or "))
|
190
220
|
end
|
191
221
|
|
192
|
-
# Convenience method to query for single object by name.
|
193
|
-
#
|
222
|
+
# Convenience method to query for single object by name.
|
223
|
+
# @param type (see #add)
|
224
|
+
# @param [String] name Value of the Scim object's name attribue. For naming
|
225
|
+
# attribute of each type of object see {Scim}.
|
226
|
+
# @return [String] the +id+ attribute of the object
|
194
227
|
def id(type, name)
|
195
228
|
res = ids(type, name)
|
196
229
|
|
197
230
|
# hide client endpoints that are not scim compatible
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
end
|
231
|
+
ik, ck = jkey(:id), jkey(:client_id)
|
232
|
+
if type == :client && res && res.length > 0 && (res.length > 1 || res[0][ik].nil?)
|
233
|
+
cr = res.find { |o| o[ck] && name.casecmp(o[ck]) == 0 }
|
234
|
+
return cr[ik] || cr[ck] if cr
|
203
235
|
end
|
204
236
|
|
205
237
|
unless res && res.is_a?(Array) && res.length == 1 &&
|
206
|
-
res[0].is_a?(Hash) && (id = res[0][
|
238
|
+
res[0].is_a?(Hash) && (id = res[0][jkey :id])
|
207
239
|
raise NotFound, "#{name} not found in #{@target}#{type_info(type, :path)}"
|
208
240
|
end
|
209
241
|
id
|
210
242
|
end
|
211
243
|
|
212
|
-
#
|
213
|
-
#
|
214
|
-
#
|
215
|
-
#
|
216
|
-
#
|
217
|
-
# https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-password-put-useridpassword
|
218
|
-
#
|
244
|
+
# Change password.
|
245
|
+
# * For a user to change their own password, the token in @auth_header must
|
246
|
+
# contain "password.write" scope and the correct +old_password+ must be given.
|
247
|
+
# * For an admin to set a user's password, the token in @auth_header must
|
248
|
+
# contain "uaa.admin" scope.
|
249
|
+
# @see https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-password-put-useridpassword
|
250
|
+
# @see https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-Security.md#password-change
|
251
|
+
# @param [String] user_id the {Scim} +id+ attribute of the user
|
252
|
+
# @return [Hash] success message from server
|
219
253
|
def change_password(user_id, new_password, old_password = nil)
|
220
|
-
|
221
|
-
|
222
|
-
json_parse_reply(*json_put(@target,
|
254
|
+
req = {"password" => new_password}
|
255
|
+
req["oldPassword"] = old_password if old_password
|
256
|
+
json_parse_reply(@key_style, *json_put(@target,
|
257
|
+
"#{type_info(:user, :path)}/#{URI.encode(user_id)}/password", req,
|
258
|
+
'authorization' => @auth_header))
|
223
259
|
end
|
224
260
|
|
225
|
-
#
|
226
|
-
#
|
227
|
-
#
|
228
|
-
#
|
229
|
-
#
|
230
|
-
# https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-client-secret-put-oauthclientsclient_idsecret
|
231
|
-
#
|
261
|
+
# Change client secret.
|
262
|
+
# * For a client to change its own secret, the token in @auth_header must contain
|
263
|
+
# "uaa.admin,client.secret" scope and the correct +old_secret+ must be given.
|
264
|
+
# * For an admin to set a client secret, the token in @auth_header must contain
|
265
|
+
# "uaa.admin" scope.
|
266
|
+
# @see https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-client-secret-put-oauthclientsclient_idsecret
|
267
|
+
# @see https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-Security.md#client-secret-mangagement
|
268
|
+
# @param [String] client_id the {Scim} +id+ attribute of the client
|
269
|
+
# @return [Hash] success message from server
|
232
270
|
def change_secret(client_id, new_secret, old_secret = nil)
|
233
271
|
req = {"secret" => new_secret }
|
234
272
|
req["oldSecret"] = old_secret if old_secret
|
235
|
-
json_parse_reply(*json_put(@target,
|
273
|
+
json_parse_reply(@key_style, *json_put(@target,
|
274
|
+
"#{type_info(:client, :path)}/#{URI.encode(client_id)}/secret", req,
|
275
|
+
'authorization' => @auth_header))
|
236
276
|
end
|
237
277
|
|
238
278
|
end
|