23 February 2019

scim-kit-introduction

by mo


For the last few months I have been quietly working on a gem to simplify building and consuming SCIM 2.0 API’s. It is called scim-kit. This gem does not cover the SCIM protocol and is currently focused on the SCIM 2.0 Core Schema.

In this post I would like to go through a few examples for how to use the current version.

Server

scim-kit provides the ability to configure your schemas, resource types and service provider config via it’s configuration interface.

Here’s an example:

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'scim-kit', '~> 0.3'
end

user_schema_id = ::Scim::Kit::V2::Schemas::USER
Scim::Kit::V2.configure do |config|
  config.service_provider_configuration(location: '/ServiceProviderConfig') do |x|
    x.add_authentication(:oauthbearertoken)
    x.change_password.supported = true
    x.documentation_uri = '/doc'
    x.etag.supported = true
    x.filter.max_results = 100
    x.filter.supported = true
  end
  config.resource_type(id: 'User', location: '/ResourceTypes/User') do |x|
    x.endpoint = '/Users'
    x.name = 'User'
    x.description = 'User'
    x.schema = user_schema_id
  end
  config.schema(id: user_schema_id, name: 'User', location: "/Schemas/User/#{user_schema_id}") do |x|
    x.add_attribute(name: :user_name) do |attribute|
      attribute.required = true
      attribute.uniqueness = :server
    end
    x.add_attribute(name: :name) do | attribute|
      attribute.add_attribute(name: 'formatted') do |x|
        x.mutability = :read_only
      end
      attribute.add_attribute(name: :family_name)
      attribute.add_attribute(name: :given_name)
    end
    x.add_attribute(name: :display_name) do |attribute|
      attribute.mutability = :read_only
    end
    x.add_attribute(name: :locale)
    x.add_attribute(name: :timezone)
    x.add_attribute(name: :active, type: :boolean) do |attribute|
      attribute.mutability = :read_only
    end
    x.add_attribute(name: :password) do |attribute|
      attribute.mutability = :write_only
      attribute.returned  = :never
    end
  end
end

configuration = ::Scim::Kit::V2.configuration
puts JSON.pretty_generate(configuration.schemas[user_schema_id].to_h)
puts JSON.pretty_generate(configuration.resource_types['User'].to_h)
puts JSON.pretty_generate(configuration.schemas[user_schema_id].to_h)

The server configuration above sets up a ServiceProviderConfig, a User resource type and a User schema.

Running the example above yields the following result.

モ ruby _includes/scim-kit-intro-server-configuration.rb
{
  "meta": {
    "location": "/Schemas/User/urn:ietf:params:scim:schemas:core:2.0:User",
    "resourceType": "Schema"
  },
  "id": "urn:ietf:params:scim:schemas:core:2.0:User",
  "name": "User",
  "description": "User",
  "attributes": [
    {
      "description": "userName",
      "multiValued": false,
      "mutability": "readWrite",
      "name": "userName",
      "required": true,
      "returned": "default",
      "type": "string",
      "uniqueness": "server",
      "caseExact": false
    },
    {
      "description": "name",
      "multiValued": false,
      "mutability": "readWrite",
      "name": "name",
      "required": false,
      "returned": "default",
      "type": "complex",
      "uniqueness": "none",
      "subAttributes": [
        {
          "description": "formatted",
          "multiValued": false,
          "mutability": "readOnly",
          "name": "formatted",
          "required": false,
          "returned": "default",
          "type": "string",
          "uniqueness": "none",
          "caseExact": false
        },
        {
          "description": "familyName",
          "multiValued": false,
          "mutability": "readWrite",
          "name": "familyName",
          "required": false,
          "returned": "default",
          "type": "string",
          "uniqueness": "none",
          "caseExact": false
        },
        {
          "description": "givenName",
          "multiValued": false,
          "mutability": "readWrite",
          "name": "givenName",
          "required": false,
          "returned": "default",
          "type": "string",
          "uniqueness": "none",
          "caseExact": false
        }
      ]
    },
    {
      "description": "displayName",
      "multiValued": false,
      "mutability": "readOnly",
      "name": "displayName",
      "required": false,
      "returned": "default",
      "type": "string",
      "uniqueness": "none",
      "caseExact": false
    },
    {
      "description": "locale",
      "multiValued": false,
      "mutability": "readWrite",
      "name": "locale",
      "required": false,
      "returned": "default",
      "type": "string",
      "uniqueness": "none",
      "caseExact": false
    },
    {
      "description": "timezone",
      "multiValued": false,
      "mutability": "readWrite",
      "name": "timezone",
      "required": false,
      "returned": "default",
      "type": "string",
      "uniqueness": "none",
      "caseExact": false
    },
    {
      "description": "active",
      "multiValued": false,
      "mutability": "readOnly",
      "name": "active",
      "required": false,
      "returned": "default",
      "type": "boolean",
      "uniqueness": "none"
    },
    {
      "description": "password",
      "multiValued": false,
      "mutability": "writeOnly",
      "name": "password",
      "required": false,
      "returned": "never",
      "type": "string",
      "uniqueness": "none",
      "caseExact": false
    }
  ]
}
{
  "meta": {
    "location": "/ResourceTypes/User",
    "resourceType": "ResourceType"
  },
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:ResourceType"
  ],
  "id": "User",
  "name": "User",
  "description": "User",
  "endpoint": "/Users",
  "schema": "urn:ietf:params:scim:schemas:core:2.0:User",
  "schemaExtensions": [

  ]
}
{
  "meta": {
    "location": "/Schemas/User/urn:ietf:params:scim:schemas:core:2.0:User",
    "resourceType": "Schema"
  },
  "id": "urn:ietf:params:scim:schemas:core:2.0:User",
  "name": "User",
  "description": "User",
  "attributes": [
    {
      "description": "userName",
      "multiValued": false,
      "mutability": "readWrite",
      "name": "userName",
      "required": true,
      "returned": "default",
      "type": "string",
      "uniqueness": "server",
      "caseExact": false
    },
    {
      "description": "name",
      "multiValued": false,
      "mutability": "readWrite",
      "name": "name",
      "required": false,
      "returned": "default",
      "type": "complex",
      "uniqueness": "none",
      "subAttributes": [
        {
          "description": "formatted",
          "multiValued": false,
          "mutability": "readOnly",
          "name": "formatted",
          "required": false,
          "returned": "default",
          "type": "string",
          "uniqueness": "none",
          "caseExact": false
        },
        {
          "description": "familyName",
          "multiValued": false,
          "mutability": "readWrite",
          "name": "familyName",
          "required": false,
          "returned": "default",
          "type": "string",
          "uniqueness": "none",
          "caseExact": false
        },
        {
          "description": "givenName",
          "multiValued": false,
          "mutability": "readWrite",
          "name": "givenName",
          "required": false,
          "returned": "default",
          "type": "string",
          "uniqueness": "none",
          "caseExact": false
        }
      ]
    },
    {
      "description": "displayName",
      "multiValued": false,
      "mutability": "readOnly",
      "name": "displayName",
      "required": false,
      "returned": "default",
      "type": "string",
      "uniqueness": "none",
      "caseExact": false
    },
    {
      "description": "locale",
      "multiValued": false,
      "mutability": "readWrite",
      "name": "locale",
      "required": false,
      "returned": "default",
      "type": "string",
      "uniqueness": "none",
      "caseExact": false
    },
    {
      "description": "timezone",
      "multiValued": false,
      "mutability": "readWrite",
      "name": "timezone",
      "required": false,
      "returned": "default",
      "type": "string",
      "uniqueness": "none",
      "caseExact": false
    },
    {
      "description": "active",
      "multiValued": false,
      "mutability": "readOnly",
      "name": "active",
      "required": false,
      "returned": "default",
      "type": "boolean",
      "uniqueness": "none"
    },
    {
      "description": "password",
      "multiValued": false,
      "mutability": "writeOnly",
      "name": "password",
      "required": false,
      "returned": "never",
      "type": "string",
      "uniqueness": "none",
      "caseExact": false
    }
  ]
}

I have considered building the SCIM 2.0 protocol as a gem but today you will need to build your own protocol endpoints to utilize scim-kit.

Here’s an example of the a rails /Schemas endpoint.

class SchemasController < ::Api::Scim::Controller
  def index
    render status: :ok, json: scim_configuration.schemas.values
  end

  def show
    current_schema = scim_configuration.schemas[params[:id]]
    raise ActiveRecord::RecordNotFound unless current_schema
    render json: current_schema, status: :ok
  end

  private

  def scim_configuration
    ::Scim::Kit::V2.configuration
  end
end

Client

A SCIM API client can also take advantage of the scim-kit configuration described above. Instead of building on the configuration the client can download the JSON representation of the configuration and load that into the scim-kit configuration.

E.g.

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'scim-kit', '~> 0.3'
end

base_url = 'https://saml-kit-proof.herokuapp.com/scim/v2/'
Scim::Kit::V2.configuration.load_from(base_url)

configuration = ::Scim::Kit::V2.configuration
puts configuration.schemas.keys.inspect
puts configuration.resource_types.keys.inspect

Running the above code will produce the following output:

モ ruby _includes/scim-kit-intro-client-configuration.rb
opening connection to saml-kit-proof.herokuapp.com:443...
opened
starting SSL for saml-kit-proof.herokuapp.com:443...
SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256
<- "GET https://saml-kit-proof.herokuapp.com/scim/v2/ServiceProviderConfig HTTP/1.1\r\nAccept: application/json\r\nContent-Type: application/json\r\nUser-Agent: net/hippie 0.2.5\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nConnection: close\r\nHost: saml-kit-proof.herokuapp.com\r\n\r\n"
-> "HTTP/1.1 200 OK\r\n"
-> "Server: Cowboy\r\n"
-> "Date: Fri, 22 Feb 2019 01:46:39 GMT\r\n"
-> "Connection: close\r\n"
-> "Content-Type: application/scim+json\r\n"
-> "Etag: W/\"79de235fcdfb0187d087fea00a868478\"\r\n"
-> "Cache-Control: max-age=0, private, must-revalidate\r\n"
-> "X-Request-Id: f743da05-d551-4a89-9b80-bbfef8f4db87\r\n"
-> "Transfer-Encoding: chunked\r\n"
-> "Via: 1.1 vegur\r\n"
-> "\r\n"
-> "360\r\n"
reading 864 bytes...
-> "{\"schemas\":[\"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig\"],\"documentationUri\":\"https://saml-kit-proof.herokuapp.com/doc\",\"patch\":{\"supported\":false},\"bulk\":{\"supported\":false,\"maxOperations\":null,\"maxPayloadSize\":null},\"filter\":{\"supported\":false,\"maxResults\":null},\"changePassword\":{\"supported\":false},\"sort\":{\"supported\":false},\"etag\":{\"supported\":false},\"authenticationSchemes\":[{\"name\":\"OAuth Bearer Token\",\"description\":\"Authentication scheme using the OAuth Bearer Token Standard\",\"specUri\":\"http://www.rfc-editor.org/info/rfc6750\",\"documentationUri\":\"http://example.com/help/oauth.html\",\"type\":\"oauthbearertoken\",\"primary\":true}],\"meta\":{\"resourceType\":\"ServiceProviderConfig\",\"created\":\"2019-02-22T01:46:39Z\",\"lastModified\":\"2019-02-22T01:46:39Z\",\"location\":\"https://saml-kit-proof.herokuapp.com/scim/v2/ServiceProviderConfig\",\"version\":1}}"
read 864 bytes
reading 2 bytes...
-> "\r\n"
read 2 bytes
-> "0\r\n"
-> "\r\n"
Conn close
opening connection to saml-kit-proof.herokuapp.com:443...
opened
starting SSL for saml-kit-proof.herokuapp.com:443...
SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256
<- "GET https://saml-kit-proof.herokuapp.com/scim/v2/Schemas HTTP/1.1\r\nAccept: application/scim+json\r\nContent-Type: application/scim+json\r\nUser-Agent: net/hippie 0.2.5\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nConnection: close\r\nHost: saml-kit-proof.herokuapp.com\r\n\r\n"
-> "HTTP/1.1 200 OK\r\n"
-> "Server: Cowboy\r\n"
-> "Date: Fri, 22 Feb 2019 01:46:38 GMT\r\n"
-> "Connection: close\r\n"
-> "Content-Type: application/scim+json\r\n"
-> "Etag: W/\"23c5170f723c4ad86a8102a99bcb956a\"\r\n"
-> "Cache-Control: max-age=0, private, must-revalidate\r\n"
-> "X-Request-Id: 326607c4-86ba-4b2e-9769-286e6215c18f\r\n"
-> "Transfer-Encoding: chunked\r\n"
-> "Via: 1.1 vegur\r\n"
-> "\r\n"
-> "d6b\r\n"
reading 3435 bytes...
-> "[{\"id\":\"urn:ietf:params:scim:schemas:core:2.0:User\",\"meta\":{\"resourceType\":\"Schema\",\"location\":\"https://saml-kit-proof.herokuapp.com/scim/v2/schemas/urn:ietf:params:scim:schemas:core:2.0:User\"},\"name\":\"User\",\"description\":\"User Account\",\"attributes\":[{\"name\":\"userName\",\"type\":\"string\",\"multiValued\":false,\"description\":\"Unique identifier for the User\",\"required\":true,\"caseExact\":false,\"mutability\":\"readWrite\",\"returned\":\"default\",\"uniqueness\":\"server\"},{\"name\":\"password\",\"type\":\"string\",\"multiValued\":false,\"description\":\"The User's cleartext password.\",\"required\":false,\"caseExact\":false,\"mutability\":\"writeOnly\",\"returned\":\"never\",\"uniqueness\":\"none\"},{\"name\":\"emails\",\"type\":\"complex\",\"multiValued\":true,\"description\":\"Email addresses for the user.\",\"required\":false,\"subAttributes\":[{\"name\":\"value\",\"type\":\"string\",\"multiValued\":false,\"description\":\"Email addresses for the user.\",\"required\":false,\"caseExact\":false,\"mutability\":\"readWrite\",\"returned\":\"default\",\"uniqueness\":\"none\"},{\"name\":\"primary\",\"type\":\"boolean\",\"multiValued\":false,\"description\":\"A Boolean value indicating the preferred email\",\"required\":false,\"mutability\":\"readWrite\",\"returned\":\"default\"}],\"mutability\":\"readWrite\",\"returned\":\"default\",\"uniqueness\":\"none\"},{\"name\":\"groups\",\"type\":\"complex\",\"multiValued\":true,\"description\":\"A list of groups to which the user belongs.\",\"required\":false,\"subAttributes\":[{\"name\":\"value\",\"type\":\"string\",\"multiValued\":false,\"description\":\"The identifier of the User's group.\",\"required\":false,\"caseExact\":false,\"mutability\":\"readOnly\",\"returned\":\"default\",\"uniqueness\":\"none\"},{\"name\":\"$ref\",\"type\":\"reference\",\"referenceTypes\":[\"User\",\"Group\"],\"multiValued\":false,\"description\":\"The URI of the corresponding 'Group' resource.\",\"required\":false,\"caseExact\":false,\"mutability\":\"readOnly\",\"returned\":\"default\",\"uniqueness\":\"none\"},{\"name\":\"display\",\"type\":\"string\",\"multiValued\":false,\"description\":\"A human-readable name.\",\"required\":false,\"caseExact\":false,\"mutability\":\"readOnly\",\"returned\":\"default\",\"uniqueness\":\"none\"}],\"mutability\":\"readOnly\",\"returned\":\"default\"}]},{\"id\":\"urn:ietf:params:scim:schemas:core:2.0:Group\",\"meta\":{\"resourceType\":\"Schema\",\"location\":\"https://saml-kit-proof.herokuapp.com/scim/v2/schemas/urn:ietf:params:scim:schemas:core:2.0:Group\"},\"name\":\"Group\",\"description\":\"Group\",\"attributes\":[{\"name\":\"displayName\",\"type\":\"string\",\"multiValued\":false,\"description\":\"A human-readable name for the Group.\",\"required\":false,\"caseExact\":false,\"mutability\":\"readWrite\",\"returned\":\"default\",\"uniqueness\":\"none\"},{\"name\":\"members\",\"type\":\"complex\",\"multiValued\":true,\"description\":\"A list of members of the Group.\",\"required\":false,\"subAttributes\":[{\"name\":\"value\",\"type\":\"string\",\"multiValued\":false,\"description\":\"Identifier of the member of this Group.\",\"required\":false,\"caseExact\":false,\"mutability\":\"immutable\",\"returned\":\"default\",\"uniqueness\":\"none\"},{\"name\":\"$ref\",\"type\":\"reference\",\"referenceTypes\":[\"User\",\"Group\"],\"multiValued\":false,\"description\":\"The URI corresponding to a SCIM resource.\",\"required\":false,\"caseExact\":false,\"mutability\":\"immutable\",\"returned\":\"default\",\"uniqueness\":\"none\"},{\"name\":\"type\",\"type\":\"string\",\"multiValued\":false,\"description\":\"A label indicating the type of resource\",\"required\":false,\"caseExact\":false,\"canonicalValues\":[\"User\",\"Group\"],\"mutability\":\"immutable\",\"returned\":\"default\",\"uniqueness\":\"none\"}],\"mutability\":\"readWrite\",\"returned\":\"default\"}]}]"
read 3435 bytes
reading 2 bytes...
-> "\r\n"
read 2 bytes
-> "0\r\n"
-> "\r\n"
Conn close
opening connection to saml-kit-proof.herokuapp.com:443...
opened
starting SSL for saml-kit-proof.herokuapp.com:443...
SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256
<- "GET https://saml-kit-proof.herokuapp.com/scim/v2/ResourceTypes HTTP/1.1\r\nAccept: application/scim+json\r\nContent-Type: application/scim+json\r\nUser-Agent: net/hippie 0.2.5\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nConnection: close\r\nHost: saml-kit-proof.herokuapp.com\r\n\r\n"
-> "HTTP/1.1 200 OK\r\n"
-> "Server: Cowboy\r\n"
-> "Date: Fri, 22 Feb 2019 01:46:40 GMT\r\n"
-> "Connection: close\r\n"
-> "Content-Type: application/scim+json\r\n"
-> "Etag: W/\"2d80d9ba57a067a7735b98796e1494f8\"\r\n"
-> "Cache-Control: max-age=0, private, must-revalidate\r\n"
-> "X-Request-Id: 59f2ea94-f6c6-4d66-b38f-da1468128bcf\r\n"
-> "Transfer-Encoding: chunked\r\n"
-> "Via: 1.1 vegur\r\n"
-> "\r\n"
-> "2f5\r\n"
reading 757 bytes...
-> "[{\"schemas\":[\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\"],\"id\":\"User\",\"meta\":{\"location\":\"https://saml-kit-proof.herokuapp.com/scim/v2/resource_types/User\",\"resourceType\":\"ResourceType\"},\"description\":\"User Account\",\"endpoint\":\"https://saml-kit-proof.herokuapp.com/scim/v2/users\",\"name\":\"User\",\"schema\":\"urn:ietf:params:scim:schemas:core:2.0:User\",\"schemaExtensions\":[]},{\"schemas\":[\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\"],\"id\":\"Group\",\"meta\":{\"location\":\"https://saml-kit-proof.herokuapp.com/scim/v2/resource_types/Group\",\"resourceType\":\"ResourceType\"},\"description\":\"Group\",\"endpoint\":\"https://saml-kit-proof.herokuapp.com/scim/v2/groups\",\"name\":\"Group\",\"schema\":\"urn:ietf:params:scim:schemas:core:2.0:Group\",\"schemaExtensions\":[]}]"
read 757 bytes
reading 2 bytes...
-> "\r\n"
read 2 bytes
-> "0\r\n"
-> "\r\n"
Conn close
["urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:core:2.0:Group"]
["User", "Group"]

scim-kit was able to download the remote configuration and build up the internal schema and resource types from that configuration. With the schema configured we can use it to build a Resource to submit to one of the scim protocol endpoints.

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'scim-kit', '~> 0.3'
end
Net::Hippie.logger = Logger.new('/dev/null')

base_url = 'https://saml-kit-proof.herokuapp.com/scim/v2/'
Scim::Kit::V2.configuration.load_from(base_url)

configuration = ::Scim::Kit::V2.configuration

resource = Scim::Kit::V2::Resource.new(schemas: [configuration.schemas.values.first])
resource.user_name = 'tsuyoshi'
resource.emails << 'tsuyoshi@example.org'
puts resource.to_json

The above code produces the following output:

モ ruby _includes/scim-kit-intro-client-resource.rb
{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"tsuyoshi","emails":["tsuyoshi@example.org"]}

I hope this brief introduction helps you the next time you need to work with SCIM in ruby.

💎