class CF::UAA::Scim
This class is for apps that need to manage User Accounts, Groups, or OAuth Client Registrations. It provides access to the SCIM endpoints on the UAA. For more information about SCIM – the IETF's System for Cross-domain Identity Management (formerly known as Simple Cloud Identity Management) – see {www.simplecloud.info}.
The types of objects and links to their schema are as follows:
-
:user
– {www.simplecloud.info/specs/draft-scim-core-schema-01.html#user-resource} or {www.simplecloud.info/specs/draft-scim-core-schema-01.html#anchor8} -
:group
– {www.simplecloud.info/specs/draft-scim-core-schema-01.html#group-resource} or {www.simplecloud.info/specs/draft-scim-core-schema-01.html#anchor10} -
:client
-
:user_id
– {github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#converting-userids-to-names}
Naming attributes by type of object:
-
:user
is “username” -
:group
is “displayname” -
:client
is “client_id”
Public Class Methods
@param (see Misc.server) @param [String] auth_header a string that can be used in an
authorization header. For OAuth2 with JWT tokens this would be something like "bearer xxxx.xxxx.xxxx". The {TokenInfo} class provides {TokenInfo#auth_header} for this purpose.
@param [Hash] options can be
* +:symbolize_keys+, if true, returned hash keys are symbols.
# File lib/uaa/scim.rb, line 106 def initialize(target, auth_header, options = {}) @target, @auth_header = target, auth_header @key_style = options[:symbolize_keys] ? :downsym : :down self.skip_ssl_validation = options[:skip_ssl_validation] self.ssl_ca_file = options[:ssl_ca_file] self.ssl_cert_store = options[:ssl_cert_store] self.http_proxy = options[:http_proxy] self.https_proxy = options[:https_proxy] @zone = options[:zone] end
Public Instance Methods
Creates a SCIM resource. @param [Symbol] type can be :user, :group, :client, :user_id. @param [Hash] info converted to json and sent to the scim endpoint. For schema of
each type of object see {Scim}.
@return [Hash] contents of the object, including its id
and
meta-data.
# File lib/uaa/scim.rb, line 128 def add(type, info) path, info = type_info(type, :path), force_case(info) reply = json_parse_reply(@key_style, *json_post(@target, path, info, headers)) fake_client_id(reply) if type == :client # hide client reply, not quite scim reply end
Collects all pages of entries from a query @param type (see query) @param [Hash] query may contain the following keys:
* +attributes+: a comma or space separated list of attribute names to be returned for each object that matches the filter. If no attribute list is given, all attributes are returned. * +filter+: a filter to select which objects are returned. See {http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources}
@return [Array] results
# File lib/uaa/scim.rb, line 238 def all_pages(type, query = {}) query = force_case(query).reject {|k, v| v.nil? } query["startindex"], info, rk = 1, [], jkey(:resources) while true qinfo = query(type, query) raise BadResponse unless qinfo[rk] return info if qinfo[rk].empty? info.concat(qinfo[rk]) total = qinfo[jkey :totalresults] return info unless total && total > info.length unless qinfo[jkey :startindex] && qinfo[jkey :itemsperpage] raise BadResponse, "incomplete #{type} pagination data from #{@target}" end query["startindex"] = info.length + 1 end end
Change password.
-
For a user to change their own password, the token in @auth_header must contain “password.write” scope and the correct
old_password
must be given. -
For an admin to set a user's password, the token in @auth_header must contain “uaa.admin” scope.
@see github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-password-put-useridpassword
@see github.com/cloudfoundry/uaa/blob/master/docs/UAA-Security.md#password-change
@param [String] user_id the {Scim} id
attribute of the user
@return [Hash] success message from server
# File lib/uaa/scim.rb, line 295 def change_password(user_id, new_password, old_password = nil) req = {"password" => new_password} req["oldPassword"] = old_password if old_password json_parse_reply(@key_style, *json_put(@target, "#{type_info(:user, :path)}/#{URI.encode(user_id)}/password", req, headers)) end
Change client secret.
-
For a client to change its own secret, the token in @auth_header must contain “client.secret” scope and the correct
old_secret
must be given. -
For an admin to set a client secret, the token in @auth_header must contain “uaa.admin” scope.
@see github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-client-secret-put-oauthclientsclient_idsecret
@see github.com/cloudfoundry/uaa/blob/master/docs/UAA-Security.md#client-secret-mangagement
@param [String] client_id the {Scim} id
attribute of the
client @return [Hash] success message from server
# File lib/uaa/scim.rb, line 311 def change_secret(client_id, new_secret, old_secret = nil) req = {"secret" => new_secret } req["oldSecret"] = old_secret if old_secret json_parse_reply(@key_style, *json_put(@target, "#{type_info(:client, :path)}/#{URI.encode(client_id)}/secret", req, headers)) end
Deletes a SCIM resource @param type (see add) @param [String] id the id attribute of the SCIM object @return [nil]
# File lib/uaa/scim.rb, line 140 def delete(type, id) http_delete @target, "#{type_info(type, :path)}/#{URI.encode(id)}", @auth_header, @zone end
Get information about a specific object. @param (see delete) @return (see add)
# File lib/uaa/scim.rb, line 221 def get(type, id) info = json_get(@target, "#{type_info(type, :path)}/#{URI.encode(id)}", @key_style, headers) fake_client_id(info) if type == :client # hide client reply, not quite scim info end
Convenience method to query for single object by name. @param type (see add) @param [String] name Value of the Scim object's name attribue. For naming
attribute of each type of object see {Scim}.
@return [String] the id
attribute of the object
# File lib/uaa/scim.rb, line 269 def id(type, name) res = ids(type, name) # hide client endpoints that are not scim compatible ik, ck = jkey(:id), jkey(:client_id) if type == :client && res && res.length > 0 && (res.length > 1 || res[0][ik].nil?) cr = res.find { |o| o[ck] && name.casecmp(o[ck]) == 0 } return cr[ik] || cr[ck] if cr end unless res && res.is_a?(Array) && res.length == 1 && res[0].is_a?(Hash) && (id = res[0][jkey :id]) raise NotFound, "#{name} not found in #{@target}#{type_info(type, :path)}" end id end
Gets id/name pairs for given names. For naming attribute of each object type see {Scim} @param type (see add) @return [Array] array of name/id hashes for each object found
# File lib/uaa/scim.rb, line 258 def ids(type, *names) na = type_info(type, :name_attr) filter = names.map { |n| "#{na} eq \"#{n}\""} all_pages(type, :attributes => "id,#{na}", :filter => filter.join(" or ")) end
# File lib/uaa/scim.rb, line 332 def list_group_mappings(start = nil, count = nil) json_get(@target, "#{type_info(:group_mapping, :path)}/list?startIndex=#{start}&count=#{count}", @key_style, headers) end
# File lib/uaa/scim.rb, line 318 def map_group(group, is_id, external_group, origin = "ldap") key_name = is_id ? :groupId : :displayName request = {key_name => group, :externalGroup => external_group, :schemas => ["urn:scim:schemas:core:1.0"], :origin => origin } result = json_parse_reply(@key_style, *json_post(@target, "#{type_info(:group_mapping, :path)}", request, headers)) result end
Convenience method to get the naming attribute, e.g. userName for user, displayName for group, client_id for client. @param type (see add) @return [String] naming attribute
# File lib/uaa/scim.rb, line 121 def name_attr(type) type_info(type, :name_attr) end
Modifies the contents of a SCIM object. @param (see add) @return (see add)
# File lib/uaa/scim.rb, line 165 def patch(type, info) path, info = type_info(type, :path), force_case(info) ida = type == :client ? 'client_id' : 'id' raise ArgumentError, "info must include #{ida}" unless id = info[ida] hdrs = headers if info && info['meta'] && (etag = info['meta']['version']) hdrs.merge!('if-match' => etag) end reply = json_parse_reply(@key_style, *json_patch(@target, "#{path}/#{URI.encode(id)}", info, hdrs)) # hide client endpoints that are not quite scim compatible type == :client && !reply ? get(type, info['client_id']): reply end
Replaces the contents of a SCIM object. @param (see add) @return (see add)
# File lib/uaa/scim.rb, line 147 def put(type, info) path, info = type_info(type, :path), force_case(info) ida = type == :client ? 'client_id' : 'id' raise ArgumentError, "info must include #{ida}" unless id = info[ida] hdrs = headers if info && info['meta'] && (etag = info['meta']['version']) hdrs.merge!('if-match' => etag) end reply = json_parse_reply(@key_style, *json_put(@target, "#{path}/#{URI.encode(id)}", info, hdrs)) # hide client endpoints that are not quite scim compatible type == :client && !reply ? get(type, info['client_id']): reply end
Gets a set of attributes for each object that matches a given filter. @param (see add) @param [Hash] query may contain the following keys:
* +attributes+: a comma or space separated list of attribute names to be returned for each object that matches the filter. If no attribute list is given, all attributes are returned. * +filter+: a filter to select which objects are returned. See {http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources} * +startIndex+: for paged output, start index of requested result set. * +count+: maximum number of results per reply
@return [Hash] including a resources
array of results and
pagination data.
# File lib/uaa/scim.rb, line 192 def query(type, query = {}) query = force_case(query).reject {|k, v| v.nil? } if attrs = query['attributes'] attrs = Util.arglist(attrs).map {|a| force_attr(a)} query['attributes'] = Util.strlist(attrs, ",") end qstr = query.empty?? '': "?#{Util.encode_form(query)}" info = json_get(@target, "#{type_info(type, :path)}#{qstr}", @key_style, headers) unless info.is_a?(Hash) && info[rk = jkey(:resources)].is_a?(Array) # hide client endpoints that are not yet scim compatible if type == :client && info.is_a?(Hash) info = info.each{ |k, v| fake_client_id(v) }.values if m = /^client_id\s+eq\s+"([^"]+)"$/i.match(query['filter']) idk = jkey(:client_id) info = info.select { |c| c[idk].casecmp(m[1]) == 0 } end return {rk => info} end raise BadResponse, "invalid reply to #{type} query of #{@target}" end info end
# File lib/uaa/scim.rb, line 327 def unmap_group(group_id, external_group, origin = "ldap") http_delete(@target, "#{type_info(:group_mapping, :path)}/groupId/#{group_id}/externalGroup/#{URI.encode(external_group)}/origin/#{origin}", @auth_header, @zone) end
Private Instance Methods
# File lib/uaa/scim.rb, line 92 def fake_client_id(info) idk, ck = jkey(:id), jkey(:client_id) info[idk] = info[ck] if info[ck] && !info[idk] end
# File lib/uaa/scim.rb, line 43 def force_attr(k) kd = k.to_s.downcase kc = {"username" => "userName", "familyname" => "familyName", "givenname" => "givenName", "middlename" => "middleName", "honorificprefix" => "honorificPrefix", "honorificsuffix" => "honorificSuffix", "displayname" => "displayName", "nickname" => "nickName", "profileurl" => "profileUrl", "streetaddress" => "streetAddress", "postalcode" => "postalCode", "usertype" => "userType", "preferredlanguage" => "preferredLanguage", "x509certificates" => "x509Certificates", "lastmodified" => "lastModified", "externalid" => "externalId", "phonenumbers" => "phoneNumbers", "startindex" => "startIndex"}[kd] kc || kd end
This is very inefficient and should be unnecessary. SCIM (1.1 and early 2.0 drafts) specify that attribute names are case insensitive. However in the UAA attribute names are currently case sensitive. This hack takes a hash with keys as symbols or strings and with any case, and forces the attribute name to the case that the uaa expects.
# File lib/uaa/scim.rb, line 69 def force_case(obj) return obj.collect {|o| force_case(o)} if obj.is_a? Array return obj unless obj.is_a? Hash new_obj = {} obj.each {|(k, v)| new_obj[force_attr(k)] = force_case(v) } new_obj end
# File lib/uaa/scim.rb, line 58 def headers() hdrs = { 'authorization' => @auth_header } hdrs['X-Identity-Zone-Subdomain'] = @zone if @zone hdrs end
# File lib/uaa/scim.rb, line 90 def jkey(k) @key_style == :down ? k.to_s : k end
an attempt to hide some scim and uaa oddities
# File lib/uaa/scim.rb, line 78 def type_info(type, elem) scimfo = {:user => ["/Users", "userName"], :group => ["/Groups", "displayName"], :client => ["/oauth/clients", 'client_id'], :user_id => ["/ids/Users", 'userName'], :group_mapping => ["/Groups/External", "externalGroup"]} unless elem == :path || elem == :name_attr raise ArgumentError, "scim schema element must be :path or :name_attr" end unless ary = scimfo[type] raise ArgumentError, "scim resource type must be one of #{scimfo.keys.inspect}" end ary[elem == :path ? 0 : 1] end