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 +8 -0
- data/Gemfile +16 -0
- data/README.md +43 -0
- data/Rakefile +50 -0
- data/cf-uaa-lib.gemspec +45 -0
- data/lib/uaa.rb +18 -0
- data/lib/uaa/http.rb +147 -0
- data/lib/uaa/misc.rb +67 -0
- data/lib/uaa/scim.rb +206 -0
- data/lib/uaa/token_coder.rb +124 -0
- data/lib/uaa/token_issuer.rb +173 -0
- data/lib/uaa/util.rb +159 -0
- data/lib/uaa/version.rb +18 -0
- data/spec/http_spec.rb +37 -0
- data/spec/misc_spec.rb +42 -0
- data/spec/scim_spec.rb +117 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/token_coder_spec.rb +128 -0
- data/spec/token_issuer_spec.rb +190 -0
- metadata +210 -0
data/.gitignore
ADDED
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
|
data/cf-uaa-lib.gemspec
ADDED
@@ -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
|
+
|