Class: CF::UAA::Scim

Inherits:
Object
  • Object
show all
Includes:
Http
Defined in:
lib/uaa/scim.rb

Overview

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.

The types of objects and links to their schema are as follows:

Naming attributes by type of object:

  • :user is “username”

  • :group is “displayname”

  • :client is “client_id”

Constant Summary

Constants included from Http

Http::FORM_UTF8, Http::JSON_UTF8

Instance Method Summary collapse

Methods included from Http

basic_auth, #initialize_http_options, #logger, #logger=, #set_request_handler, #trace?

Constructor Details

#initialize(target, auth_header, options = {}) ⇒ Scim

Returns a new instance of Scim.

Parameters:

  • auth_header (String)

    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.

  • options (Hash) (defaults to: {})

    can be

    • :symbolize_keys, if true, returned hash keys are symbols.



149
150
151
152
153
154
# File 'lib/uaa/scim.rb', line 149

def initialize(target, auth_header, options = {})
  @target, @auth_header = target, auth_header
  @key_style = options[:symbolize_keys] ? :downsym : :down
  @zone = options[:zone]
  initialize_http_options(options)
end

Instance Method Details

#add(type, info) ⇒ Hash

Creates a SCIM resource.

Parameters:

  • type (Symbol)

    can be :user, :group, :client, :user_id.

  • info (Hash)

    converted to json and sent to the scim endpoint. For schema of each type of object see CF::UAA::Scim.

Returns:

  • (Hash)

    contents of the object, including its id and meta-data.



167
168
169
170
171
172
173
# File 'lib/uaa/scim.rb', line 167

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

#all_pages(type, query = {}) ⇒ Array

Collects all pages of entries from a query

Parameters:

  • query (Hash) (defaults to: {})

    may contain the following keys:

  • type (Symbol)

    can be :user, :group, :client, :user_id.

Returns:

  • (Array)

    results



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/uaa/scim.rb', line 285

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_clientjwt(client_id, jwks_uri = nil, jwks = nil, kid = nil, changeMode = nil) ⇒ Hash

Change client jwt trust configuration.

  • For a client to change its jwt client trust, the token in @auth_header must contain “client.trust” scope.

  • For an admin to set a client secret, the token in @auth_header must contain “uaa.admin” scope.

Parameters:

  • client_id (String)

    the CF::UAA::Scim id attribute of the client

  • jwks_uri (String) (defaults to: nil)

    the URI to token endpoint

  • jwks (String) (defaults to: nil)

    the JSON Web Key Set

  • kid (String) (defaults to: nil)

    If changeMode is DELETE provide the id of key

  • changeMode (String) (defaults to: nil)

    Change mode, possible is ADD, UPDATE, DELETE

Returns:

  • (Hash)

    success message from server

See Also:



384
385
386
387
388
389
390
391
392
# File 'lib/uaa/scim.rb', line 384

def change_clientjwt(client_id, jwks_uri = nil, jwks = nil, kid = nil, changeMode = nil)
  req = {"client_id" => client_id }
  req["jwks_uri"] = jwks_uri if jwks_uri
  req["jwks"] = jwks if jwks
  req["kid"] = kid if kid
  req["changeMode"] = changeMode if changeMode
  json_parse_reply(@key_style, *json_put(@target,
                                         "#{type_info(:client, :path)}/#{Addressable::URI.encode(client_id)}/clientjwt", req, headers))
end

#change_password(user_id, new_password, old_password = nil) ⇒ Hash

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.



349
350
351
352
353
354
# File 'lib/uaa/scim.rb', line 349

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)}/#{Addressable::URI.encode(user_id)}/password", req, headers))
end

#change_secret(client_id, new_secret, old_secret = nil) ⇒ Hash

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.



365
366
367
368
369
370
# File 'lib/uaa/scim.rb', line 365

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)}/#{Addressable::URI.encode(client_id)}/secret", req, headers))
end

#delete(type, id) ⇒ nil

Deletes a SCIM resource

Parameters:

  • id (String)

    the id attribute of the SCIM object

  • type (Symbol)

    can be :user, :group, :client, :user_id.

Returns:

  • (nil)


179
180
181
# File 'lib/uaa/scim.rb', line 179

def delete(type, id)
  http_delete @target, "#{type_info(type, :path)}/#{Addressable::URI.encode(id)}", @auth_header, @zone
end

#get(type, id) ⇒ Hash

Get information about a specific object.

Parameters:

  • id (String)

    the id attribute of the SCIM object

  • type (Symbol)

    can be :user, :group, :client, :user_id.

Returns:

  • (Hash)

    contents of the object, including its id and meta-data.



260
261
262
263
264
265
266
# File 'lib/uaa/scim.rb', line 260

def get(type, id)
  info = json_get(@target, "#{type_info(type, :path)}/#{Addressable::URI.encode(id)}",
      @key_style, headers)

  fake_client_id(info) if type == :client # hide client reply, not quite scim
  info
end

#get_client_meta(client_id) ⇒ client meta

Get meta information about client

Parameters:

  • client_id

Returns:

  • (client meta)


271
272
273
274
# File 'lib/uaa/scim.rb', line 271

def get_client_meta(client_id)
  path = type_info(:client, :path)
  json_get(@target, "#{path}/#{Addressable::URI.encode(client_id)}/meta", @key_style, headers)
end

#id(type, name) ⇒ String

Convenience method to query for single object by name.

Parameters:

  • name (String)

    Value of the Scim object’s name attribue. For naming attribute of each type of object see CF::UAA::Scim.

  • type (Symbol)

    can be :user, :group, :client, :user_id.

Returns:

  • (String)

    the id attribute of the object



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/uaa/scim.rb', line 323

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

#ids(type, *names) ⇒ Array

Gets id/name pairs for given names. For naming attribute of each object type see CF::UAA::Scim

Parameters:

  • type (Symbol)

    can be :user, :group, :client, :user_id.

Returns:

  • (Array)

    array of name/id hashes for each object found



305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/uaa/scim.rb', line 305

def ids(type, *names)
  name_attr = type_info(type, :name_attr)
  origin_attr = type_info(type, :origin_attr)

  filter = names.map do |n|
    "#{name_attr} eq \"#{n}\""
  end

  attributes = ['id', name_attr, origin_attr]

  all_pages(type, attributes: attributes.join(','), filter: filter.join(' or '))
end

#list_group_mappings(start = nil, count = nil) ⇒ Object



414
415
416
# File 'lib/uaa/scim.rb', line 414

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

#map_group(group, is_id, external_group, origin = "ldap") ⇒ Object



400
401
402
403
404
405
406
407
# File 'lib/uaa/scim.rb', line 400

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

#name_attr(type) ⇒ String

Convenience method to get the naming attribute, e.g. userName for user, displayName for group, client_id for client.

Parameters:

  • type (Symbol)

    can be :user, :group, :client, :user_id.

Returns:

  • (String)

    naming attribute



160
# File 'lib/uaa/scim.rb', line 160

def name_attr(type) type_info(type, :name_attr) end

#patch(type, info) ⇒ Hash

Modifies the contents of a SCIM object.

Parameters:

  • type (Symbol)

    can be :user, :group, :client, :user_id.

  • info (Hash)

    converted to json and sent to the scim endpoint. For schema of each type of object see CF::UAA::Scim.

Returns:

  • (Hash)

    contents of the object, including its id and meta-data.

Raises:

  • (ArgumentError)


204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/uaa/scim.rb', line 204

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}/#{Addressable::URI.encode(id)}", info, hdrs))

  # hide client endpoints that are not quite scim compatible
  type == :client && !reply ? get(type, info['client_id']): reply
end

#put(type, info) ⇒ Hash

Replaces the contents of a SCIM object.

Parameters:

  • type (Symbol)

    can be :user, :group, :client, :user_id.

  • info (Hash)

    converted to json and sent to the scim endpoint. For schema of each type of object see CF::UAA::Scim.

Returns:

  • (Hash)

    contents of the object, including its id and meta-data.

Raises:

  • (ArgumentError)


186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/uaa/scim.rb', line 186

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}/#{Addressable::URI.encode(id)}", info, hdrs))

  # hide client endpoints that are not quite scim compatible
  type == :client && !reply ? get(type, info['client_id']): reply
end

#query(type, query = {}) ⇒ Hash

Gets a set of attributes for each object that matches a given filter.

Parameters:

  • query (Hash) (defaults to: {})

    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

  • type (Symbol)

    can be :user, :group, :client, :user_id.

  • info (Hash)

    converted to json and sent to the scim endpoint. For schema of each type of object see CF::UAA::Scim.

Returns:

  • (Hash)

    including a resources array of results and pagination data.



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/uaa/scim.rb', line 231

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

#unlock_user(user_id) ⇒ Object



394
395
396
397
398
# File 'lib/uaa/scim.rb', line 394

def unlock_user(user_id)
  req = {"locked" => false}
  json_parse_reply(@key_style, *json_patch(@target,
      "#{type_info(:user, :path)}/#{Addressable::URI.encode(user_id)}/status", req, headers))
end

#unmap_group(group_id, external_group, origin = "ldap") ⇒ Object



409
410
411
412
# File 'lib/uaa/scim.rb', line 409

def unmap_group(group_id, external_group, origin = "ldap")
  http_delete(@target, "#{type_info(:group_mapping, :path)}/groupId/#{group_id}/externalGroup/#{Addressable::URI.encode(external_group)}/origin/#{origin}",
                        @auth_header, @zone)
end