Module: Scimitar::Resources::Mixin

Extended by:
ActiveSupport::Concern
Defined in:
app/models/scimitar/resources/mixin.rb

Overview

The mixin included by any class in your application which is to be mapped to and exposed via a SCIM interface. Any one such class must have one corresponding ResourcesController subclass declaring its association to that model.

Your class becomes responsible for implementing various *class methods* as described below. YOU MUST DECLARE THESE BEFORE YOU INCLUDE THE MIXIN MODULE because Ruby parses classes top-down and the mixin checks to make sure that required methods exist, so these must be defined first.

scim_resource_type

Define this method to return the Scimitar resource class that corresponds to the mixing-in class.

For example, if you have an ActiveRecord “User” class that maps to a SCIM “User” resource type:

def self.scim_resource_type
  return Scimitar::Resources::User
end

This is used to render SCIM JSON data via #to_scim.

scim_attributes_map

Define this method to return a Hash that maps SCIM attributes to corresponding supported accessor methods in the mixing-in class.

Define read-only, write-only or read-write attributes here. Scimitar will check for an appropriate accessor depending on whether SCIM operations are read or write and acts accordingly. At each level of the Ruby Hash, the keys are case-sensitive attributes from the SCIM schema and values are either Symbols, giving a corresponding read/write accessor name in the mixing-in class, Hashes for nested SCIM schema data as shown below or for Array entries, special structures described later.

For example, for a User model <-> SCIM user:

def self.scim_attributes_map
  return {
    id:         :id,
    externalId: :scim_external_id,
    userName:   :username,
    name:       {
      givenName:  :given_name,
      familyName: :last_name
    },
    active: :is_active?
  }
end

Note that providing storage and filter (search) support for externalId is VERY STRONGLY recommended (bordering on mandatory) for your service to provide adequate support for typical clients to function smoothly. See “scim_queryable_attributes” below for filtering.

This omits things like “email” because in SCIM those are specified in an Array, where each entry has a “type” field - e.g. “home”, “work”. Within SCIM this is common but there are also just free lists of data, such as the list of Members in a Group. This makes the mapping description more complex. You can provide two kinds of mapping data:

  • One where a specific SCIM attribute is present in each array entry and can contain only a set of specific, discrete values; your mapping defines entries for each value of interest. E-mail is an example here, where “type” is the SCIM attribute and you might map “work” and “home”.

For discrete matches, you declare the Array containing Hashes with key “match”, where the value gives the name of the SCIM attribute to read or write for each array entry; “with”, where the value gives the thing to match at this attribute; then “using”, where the value is a Hash giving a mapping schema just as described herein (schema can nest as deeply as you like).

Given that e-mails in SCIM look something like this:

"emails": [
  {
    "value": "[email protected]",
    "type": "work",
    "primary": true
  },
  {
    "value": "[email protected]",
    "type": "home"
  }
]

…then we could extend the above attributes map example thus:

def self.scim_attributes_map
  # ...
  emails: [
    {
      match: 'type',
      with:  'work',
      using: {
        value:   :work_email_address,
        primary: true
      }
    },
    {
      match: 'type',
      with:  'home',
      using: { value: :home_email_address }
    }
  ],
  # ...
end

…where the including class would have a #work_email_address accessor and we’re hard-coding this as the primary (preferred) address (but could just as well map this to another accessor, e.g. :work_email_is_primary?).

  • One where a SCIM array contains just a list of arbitrary entries, each with a known schema, and these map attribute-by-attribute to same-index items in a corresponding array in the mixing-in model. Group members are the example use case here.

For things like a group’s list of members, again include an array in the attribute map as above but this time have a key “list” with a value that is the attribute accessor in your mixing in model that returns an Enumerable of values to map, then as above, “using” which provides the nested schema saying how each of those objects should be mapped.

Suppose you were mixing this module into a Team class and there was an association Team#users that provided an Enumerable of team member User objects:

def self.scim_attributes_map
  # ...
  groups: [
    {
      list:  :users,         # <-- i.e. Team.users,
      using: {
        value:   :id,        # <-- i.e. Team.users[n].id
        display: :full_name  # <-- i.e. Team.users[n].full_name
      },
      class: Team, # Optional; see below
      find_with: -> (scim_list_entry) {...} # See below
    }
  ],
  #...
end

The mixing-in class must implement the read accessor identified by the value of the “list” key, returning any indexed, Enumerable collection (e.g. an Array or ActiveRecord::Relation instance). The optional key “:find_with” is defined with a Proc that is passed the SCIM entry at each list position. It must use this to look up the equivalent entry for association via the write accessor described by the “:list” key. In the example above, “find_with”‘s Proc might look at a SCIM entry value which is expected to be a user ID and find that User. The mapped set of User data thus found would be written back with “#users=”, due to the “:list” key declaring the method name “:users”. The optional “class” key is recommended but not really needed unless the configuration option Scimitar::EngineConfiguration::schema_list_from_attribute_mappings is defined; see documentation of that option for more information.

Note that you can only use either:

  • One or more static maps where each matches some other piece of source SCIM data field value, so that specific SCIM array entries are matched

  • A single dynamic list entry which maps app SCIM array entries.

A mixture of static and dynamic data, or multiple dynamic entries in a single mapping array value will produce undefined behaviour.

scim_mutable_attributes

Define this method to return a Set (preferred) or Array of names of attributes which may be written in the mixing-in class. The names MUST be expressed as Symbols, not Strings.

If you return nil, it is assumed that any attribute mapped by ::scim_attributes_map which has a write accessor will be eligible for assignment during SCIM creation or update operations.

For example, if everything in ::scim_attributes_map with a write accessor is to be mutable over SCIM:

def self.scim_mutable_attributes
  return nil
end

Note that as a common special case, any mapped attribute of the Symbol value “:id” will be removed from the list, as it is assumed to be e.g. a primary key or similar. So, even though it’ll have a write accessor, it is not something that should be mutable over SCIM - it’s taken to be your internal record ID. If you do want :id included as mutable or if you have a different primary key attribute name, you’ll just need to return the mutable attribute list directly in your ::scim_mutable_attributes method rather than relying on the list extracted from ::scim_attributes_map.

scim_queryable_attributes

Define this method to return a Hash that maps field names you wish to support in SCIM filter queries to corresponding attributes in the in the mixing-in class. If nil then filtering is not supported in the ResourceController subclass which declares that it maps to the mixing-in class. If not nil but a SCIM filter enquiry is made for an unmapped attribute, an ‘invalid filter’ exception is raised.

If using ActiveRecord support in Scimitar::Lists::QueryParser, the mapped entities are columns and that’s expressed in the names of keys described below; if you have other approaches to searching, these might be virtual attributes or other such constructs rather than columns. That would be up to your non-ActiveRecord’s implementation to decide.

Each STRING field name(s) represents a flat attribute path that might be encountered in a filter - e.g. “name.familyName”, “emails.value” (and often it makes sense to define “emails” and “emails.value” identically to allow for different client searching “styles”, given ambiguities in RFC 7644 filter examples).

Each value is a hash of queryable SCIM attribute options, described below - for example:

def self.scim_queryable_attributes
  return {
    'name.givenName'  => { column: :first_name },
    'name.familyName' => { column: :last_name  },
    'emails'          => { columns: [ :work_email_address, :home_email_address ] },
    'emails.value'    => { columns: [ :work_email_address, :home_email_address ] },
    'emails.type'     => { ignore: true },
    'groups.value'    => { column: Group.arel_table[:id] }
  }
end

Column references can be either a Symbol representing a column within the resource model table, or an Arel::Attribute instance via e.g. MyModel.arel_table[:my_column].

Queryable SCIM attribute options

:column

Just one simple column for a mapping.

:columns

An Array of columns that you want to map using ‘OR’ for a single search of the corresponding entity.

:ignore

When set to true, the matching attribute is ignored rather than resulting in an “invalid filter” exception. Beware possibilities for surprised clients getting a broader result set than expected, since a constraint may have been ignored.

Filtering is currently limited and searching within e.g. arrays of data is not supported; only simple top-level keys can be mapped.

Optional methods

scim_timestamps_map

If you implement this class method, it should return a Hash with one or both of the keys ‘created’ and ‘lastModified’, as Symbols. The values should be methods that the including method supports which return a creation or most-recently-updated time, respectively. The returned object must support #iso8601 to convert to a String representation. Example for a typical ActiveRecord object with standard timestamps:

def self.scim_timestamps_map
  {
    created:      :created_at,
    lastModified: :updated_at
  }
end