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, #logger, #logger=, #set_request_handler, #trace?

Constructor Details

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



99
100
101
102
# File 'lib/uaa/scim.rb', line 99

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

Instance Method Details

#add(type, info) ⇒ Hash

Creates a SCIM resource.



115
116
117
118
119
120
121
# File 'lib/uaa/scim.rb', line 115

def add(type, info)
  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

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

Collects all pages of entries from a query



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/uaa/scim.rb', line 207

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(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.



264
265
266
267
268
269
270
# File 'lib/uaa/scim.rb', line 264

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,
      'authorization' => @auth_header))
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.



281
282
283
284
285
286
287
# File 'lib/uaa/scim.rb', line 281

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,
      'authorization' => @auth_header))
end

#delete(type, id) ⇒ nil

Deletes a SCIM resource



127
128
129
# File 'lib/uaa/scim.rb', line 127

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

#get(type, id) ⇒ Hash

Get information about a specific object.



190
191
192
193
194
195
196
# File 'lib/uaa/scim.rb', line 190

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

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

#id(type, name) ⇒ String

Convenience method to query for single object by name.



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/uaa/scim.rb', line 238

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



227
228
229
230
231
# File 'lib/uaa/scim.rb', line 227

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

#name_attr(type) ⇒ String

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



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

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

#put(type, info) ⇒ Hash

Replaces the contents of a SCIM object.

Raises:

  • (ArgumentError)


134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/uaa/scim.rb', line 134

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 = {'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 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.



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/uaa/scim.rb', line 161

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,  'authorization' => @auth_header)
  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