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.