lib/uaa/scim.rb in cf-uaa-lib-1.3.1 vs lib/uaa/scim.rb in cf-uaa-lib-1.3.2
- old
+ new
@@ -17,11 +17,24 @@
# 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 http://www.simplecloud.info
+# see {http://www.simplecloud.info}.
+#
+# The types of objects and links to their schema are as follows:
+# * +:user+ -- {http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#user-resource}
+# or {http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#anchor8}
+# * +:group+ -- {http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#group-resource}
+# or {http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#anchor10}
+# * +:client+
+# * +:user_id+ -- {https://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"
class Scim
include Http
private
@@ -47,193 +60,220 @@
# a hash with keys as symbols or strings and with any case, and forces
# the attribute name to the case that the uaa expects.
def force_case(obj)
return obj.collect {|o| force_case(o)} if obj.is_a? Array
return obj unless obj.is_a? Hash
- obj.each_with_object({}) {|(k, v), h| h[force_attr(k)] = force_case(v) }
+ new_obj = {}
+ obj.each {|(k, v)| new_obj[force_attr(k)] = force_case(v) }
+ new_obj
end
# an attempt to hide some scim and uaa oddities
def type_info(type, elem)
- scimfo = {user: ["/Users", "userName"], group: ["/Groups", "displayName"],
- client: ["/oauth/clients", 'client_id'], user_id: ["/ids/Users", 'userName']}
+ scimfo = {:user => ["/Users", "userName"], :group => ["/Groups", "displayName"],
+ :client => ["/oauth/clients", 'client_id'], :user_id => ["/ids/Users", 'userName']}
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
- def prep_request(type, info = nil)
- [type_info(type, :path), force_case(info)]
+ def jkey(k) @key_style == :down ? k.to_s : k end
+
+ def fake_client_id(info)
+ idk, ck = jkey(:id), jkey(:client_id)
+ info[idk] = info[ck] if info[ck] && !info[idk]
end
public
- # The +auth_header+ parameter refers to 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 Token class provides
- # CF::UAA::Token#auth_header for this purpose.
- def initialize(target, auth_header) @target, @auth_header = target, auth_header end
+ # @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 style (see Util.hash_key)
+ def initialize(target, auth_header, options = {})
+ @target, @auth_header = target, auth_header
+ @key_style = options[:symbolize_keys] ? :downsym : :down
+ end
- # creates a SCIM resource. For possible values for the +type+ parameter, and links
- # to the schema of each type see #query
- # info is a hash structure converted to json and sent to the scim endpoint
- # A hash of the newly created object is returned, including its ID
- # and meta data.
+ # 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.
def add(type, info)
- path, info = prep_request(type, info)
- reply = json_parse_reply(*json_post(@target, path, info, @auth_header), :down)
-
- # hide client endpoints that are not scim compatible
- reply['id'] = reply['client_id'] if type == :client && reply['client_id'] && !reply['id']
-
- return reply if reply && reply["id"]
- raise BadResponse, "no id returned by add request to #{@target}#{path}"
+ path, info = type_info(type, :path), force_case(info)
+ reply = json_parse_reply(@key_style, *json_post(@target, path, info,
+ "authorization" => @auth_header))
+ fake_client_id(reply) if type == :client # hide client reply, not quite scim
+ reply
end
- # Deletes a SCIM resource identified by +id+. For possible values for type, see #query
+ # Deletes a SCIM resource
+ # @param type (see #add)
+ # @param [String] id the id attribute of the SCIM object
+ # @return [nil]
def delete(type, id)
- path, _ = prep_request(type)
- http_delete @target, "#{path}/#{URI.encode(id)}", @auth_header
+ http_delete @target, "#{type_info(type, :path)}/#{URI.encode(id)}", @auth_header
end
- # +info+ is a hash structure converted to json and sent to a scim endpoint
- # For possible types, see #query
+ # Replaces the contents of a SCIM object.
+ # @param (see #add)
+ # @return (see #add)
def put(type, info)
- path, info = prep_request(type, info)
+ path, info = type_info(type, :path), force_case(info)
ida = type == :client ? 'client_id' : 'id'
- raise ArgumentError, "scim info must include #{ida}" unless id = info[ida]
- hdrs = info && info["meta"] && info["meta"]["version"] ?
- {'if-match' => info["meta"]["version"]} : {}
- reply = json_parse_reply(*json_put(@target, "#{path}/#{URI.encode(id)}",
- info, @auth_header, hdrs), :down)
+ raise ArgumentError, "info must include #{ida}" unless id = info[ida]
+ hdrs = {'authorization' => @auth_header}
+ 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 scim compatible
- type == :client && !reply ? get(type, info["client_id"]): reply
+ # hide client endpoints that are not quite scim compatible
+ type == :client && !reply ? get(type, info['client_id']): reply
end
- # Queries for objects and returns a selected list of attributes for each
- # a given filter. Possible values for +type+ and links to the schema of
- # corresponding object type are:
- # +:user+:: http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#user-resource
- # :: http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#anchor8
- # +:group+:: http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#group-resource
- # :: http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#anchor10
- # +:client+::
- # +:user_id+:: https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#converting-userids-to-names
- #
- # The +query+ hash 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
+ # 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.
def query(type, query = {})
- path, query = prep_request(type, query)
- query = query.reject {|k, v| v.nil? }
+ 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?? '': "?#{URI.encode_www_form(query)}"
- info = json_get(@target, "#{path}#{qstr}", @auth_header, :down)
- unless info.is_a?(Hash) && info['resources'].is_a?(Array)
+ qstr = query.empty?? '': "?#{Util.encode_form(query)}"
+ info = json_get(@target, "#{type_info(type, :path)}#{qstr}", @key_style, 'authorization' => @auth_header)
+ unless info.is_a?(Hash) && info[rk = jkey(:resources)].is_a?(Array)
# hide client endpoints that are not scim compatible
- return {'resources' => info.values } if type == :client && info.is_a?(Hash)
+ if type == :client && info.is_a?(Hash)
+ info.each { |k, v| fake_client_id(v) }
+ return {rk => info.values }
+ end
- raise BadResponse, "invalid reply to query of #{@target}#{path}"
+ raise BadResponse, "invalid reply to #{type} query of #{@target}"
end
info
end
- # Returns a hash of information about a specific object.
- # [type] For possible values of type, see #add
- # [id] the id attribute of the object assigned by the UAA
+ # Get information about a specific object.
+ # @param (see #delete)
+ # @return (see #add)
def get(type, id)
- path, _ = prep_request(type)
- info = json_get(@target, "#{path}/#{URI.encode(id)}", @auth_header, :down)
+ info = json_get(@target, "#{type_info(type, :path)}/#{URI.encode(id)}",
+ @key_style, 'authorization' => @auth_header)
- # hide client endpoints that are not scim compatible
- info["id"] = info["client_id"] if type == :client && !info["id"]
+ fake_client_id(info) if type == :client # hide client reply, not quite scim
info
end
- # Collects all pages of entries from a query, returns array of results.
- # For descriptions of the +type+ and +query+ parameters, see #query.
+ # 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
def all_pages(type, query = {})
- query = query.reject {|k, v| v.nil? }
- query["startindex"], info = 1, []
+ 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["resources"]
- return info if qinfo["resources"].empty?
- info.concat(qinfo["resources"])
- return info unless qinfo["totalresults"] && qinfo["totalresults"] > info.length
- unless qinfo["startindex"] && qinfo["itemsperpage"]
- raise BadResponse, "incomplete pagination data from #{@target}#{path}"
+ 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
- # Queries for objects by name. Returns array of name/id hashes for each
- # name found. For possible values of +type+, see #query
+ # Gets id/name pairs for given names.
+ # @param type (see #add)
+ # @param [Array<String>] names. For naming attribute of each object type see {Scim}
+ # @return [Array] array of name/id hashes for each object found
def ids(type, *names)
na = type_info(type, :name_attr)
- filter = names.each_with_object([]) { |n, o| o << "#{na} eq \"#{n}\""}
- all_pages(type, attributes: "id,#{na}", filter: filter.join(" or "))
+ filter = names.map { |n| "#{na} eq \"#{n}\""}
+ all_pages(type, :attributes => "id,#{na}", :filter => filter.join(" or "))
end
- # Convenience method to query for single object by name. Returns its id.
- # Raises error if not found. For possible values of +type+, see #query
+ # 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
def id(type, name)
res = ids(type, name)
# hide client endpoints that are not scim compatible
- if type == :client && res && res.length > 0
- if res.length > 1 || res[0]["id"].nil?
- cr = res.find { |o| o['client_id'] && name.casecmp(o['client_id']) == 0 }
- return cr['id'] || cr['client_id'] if cr
- end
+ 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]["id"])
+ res[0].is_a?(Hash) && (id = res[0][jkey :id])
raise NotFound, "#{name} not found in #{@target}#{type_info(type, :path)}"
end
id
end
- # [For a user to change their own password] Token must contain "password.write" scope and the
- # correct +old_password+ must be given.
- # [For an admin to set a user's password] Token must contain "uaa.admin" scope.
- #
- # For more information see:
- # https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-password-put-useridpassword
- # or https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-Security.md#password-change
+ # 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 https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-password-put-useridpassword
+ # @see https://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
def change_password(user_id, new_password, old_password = nil)
- password_request = {"password" => new_password}
- password_request["oldPassword"] = old_password if old_password
- json_parse_reply(*json_put(@target, "/Users/#{URI.encode(user_id)}/password", password_request, @auth_header))
+ 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,
+ 'authorization' => @auth_header))
end
- # [For a client to change its own secret] Token must contain "uaa.admin,client.secret" scope and the
- # correct +old_secret+ must be given.
- # [For an admin to set a client secret] Token must contain "uaa.admin" scope.
- #
- # For more information see:
- # https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-client-secret-put-oauthclientsclient_idsecret
- # or https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-Security.md#client-secret-mangagement
+ # Change client secret.
+ # * For a client to change its own secret, the token in @auth_header must contain
+ # "uaa.admin,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 https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-client-secret-put-oauthclientsclient_idsecret
+ # @see https://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
def change_secret(client_id, new_secret, old_secret = nil)
req = {"secret" => new_secret }
req["oldSecret"] = old_secret if old_secret
- json_parse_reply(*json_put(@target, "/oauth/clients/#{URI.encode(client_id)}/secret", req, @auth_header))
+ json_parse_reply(@key_style, *json_put(@target,
+ "#{type_info(:client, :path)}/#{URI.encode(client_id)}/secret", req,
+ 'authorization' => @auth_header))
end
end
end