cf-uaa-lib 1.3.1 → 1.3.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|