- Notifications
You must be signed in to change notification settings - Fork42
A SCIM v2 API endpoint implementation
License
pond/scimitar
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
A SCIM v2 API endpoint implementation for Ruby On Rails.
For a list of changes and information on major version upgrades, please seeCHANGELOG.md
.
System for Cross-domain Identity Management (SCIM) is a protocol that helps systems synchronise user data between different business systems. Aservice provider hosts a SCIM API endpoint implementation and the Scimitar gem is used to help quickly build this implementation. One or moreenterprise subscribers use these APIs to let that service know about changes in the enterprise's user (employee) list.
In the context of the names used by the SCIM standard, the service that is provided is some kind of software-as-a-service solution that the enterprise subscriber uses to assist with their day to day business. The enterprise maintains its user (employee) list via whatever means it wants, but includes SCIM support so that any third party services it uses can be kept up to date with adds, removals or changes to employee data.
- Overview at Wikipedia
- More detailed introduction at SimpleCloud
- SCIM v2 RFC7642: Concepts
- SCIM v2 RFC7643: Core schema
- SCIM v2 RFC7644: Protocol
Install using:
gem install scimitar
In your Gemfile:
gem'scimitar','~> 2.0'
Scimitar usessemantic versioning so you can be confident that patch and minor version updates for features, bug fixes and/or security patches will not break your application.
Scimitar borrows heavily - to the point of cut-and-paste - from:
- ScimEngine for the Rails controllers and resource-agnostic subclassing approach that makes supporting User and/or Group, along with custom resource types if you need them, quite easy.
- ScimRails for the bearer token support, 'index' actions and filter support.
- SCIM Query Filter Parser for advanced filter handling.
All three are provided under the MIT license. Scimitar is too.
Scimitar is best used with Rails and ActiveRecord, but it can be used with other persistence back-ends too - you just have to do more of the work in controllers using Scimitar's lower level controller subclasses, rather than relying on Scimitar's higher level ActiveRecord abstractions.
Noting theSecurity section later - to set up an authentication method, create aconfig/initializers/scimitar.rb
in your Rails application and define a token-based authenticator and/or a username-password authenticator in theengine configuration section documented in the sample file. For example:
Scimitar.engine_configuration=Scimitar::EngineConfiguration.new({token_authenticator:Proc.newdo |token,options |# This is where you'd write the code to validate `token` - the means by# which your application issues tokens to SCIM clients, or validates them,# is outside the scope of the gem; the required mechanisms vary by client.# More on this can be found in the 'Security' section later.#SomeLibraryModule.validate_access_token(token)end})
When it comes to token access, Scimitar neither enforces nor presumes any kind of encoding for bearer tokens. You can use anything you like, including encoding/encrypting JWTs if you so wish -https://rubygems.org/gems/jwt may be useful. The way in which a client might integrate with your SCIM service varies by client and you will have to check documentation to see how a token gets conveyed to that client in the first place (e.g. a full OAuth flow with your application, or just a static token generated in some UI which an administrator copies and pastes into their client's SCIM configuration UI).
Strongly recommended: You should wrap any Scimitar configuration withRails.application.config.to_prepare do...
so that any changes you make to configuration during local development are reflected via auto-reload, rather than requiring a server restart.
Rails.application.config.to_preparedoScimitar.engine_configuration=Scimitar::EngineConfiguration.new({# ...endend
In general, Scimitar's own development and tests assume this approach. If you choose to put the configuration directly into an initializer file without theto_prepare
wrapper, you will be at aslightly higher risk of tripping over unrecognised Scimitar bugs; please make sure that your own application test coverage is reasonably comprehensive.
For each resource you support, add these lines to yourroutes.rb
:
namespace:scim_v2domountScimitar::Engine,at:'/'get'Users',to:'users#index'get'Users/:id',to:'users#show'post'Users',to:'users#create'put'Users/:id',to:'users#replace'patch'Users/:id',to:'users#update'delete'Users/:id',to:'users#destroy'end
All routes then will be available athttps://.../scim_v2/...
via controllers you write inapp/controllers/scim_v2/...
, e.g.app/controllers/scim_v2/users_controller.rb
. More on controllers later.
Internally Scimitar always invokes URL helpers in the controller layer. I.e. any variable path parameters will be resolved by Rails automatically. If you need more control over the way URLs are generated you can override any URL helper by redefining it in the application controller mixin. See theapplication_controller_mixin
engine configuration option.
Note that Okta has somecurious documentation on its use ofPOST
vsPATCH
for Groups, which perthis Scimitar issue has caused at least one person some trouble. Defining the routes for both verbs as shown above (though in that issue's case, it was for the Group resource)does still seem to work, but take care if integrating with Okta to try and at least manually test, if not auto-test,PATCH
/PUT
operations initiated from Okta's side, just to make sure.
Scimitar assumes that each SCIM resource maps to a single corresponding class in your system. This might be an abstraction over more complex underpinings, but either way, a 1:1 relationship is expected. For example, a SCIM User might map to a User ActiveRecord model in your Rails application, while a SCIM Group might map to some custom class called Team which operates on a more complex set of data "under the hood".
Before writing any controllers, it's a good idea to examine the SCIM specification and figure out how you intend to map SCIM attributes in any resources of interest, to your local data. Amixin is provided which you can include in any plain old Ruby class (including, but not limited to ActiveRecord model classes) - a more readable form of the comments in this fileis in the RDoc output.
The functionality exposed by the mixin is relatively complicated because the range of operations that the SCIM API supports is quite extensive. Rather than duplicate all the information here, please see the extensive comments in the mixin linked above for more information. There are examples in thetest suite's Rails models, or for another example:
classUser <ActiveRecord::Base# The attributes in the SCIM section below include a reference to this# hypothesised 'groups' HABTM relationship. All of the other "leaf node"# Symbols - e.g. ":first_name", ":last_name" - are expected to be defined as# accessors e.g. via ActiveRecord and your related database table columns,# "attr_accessor" declarations, or bespoke "def foo"/"def foo=(value)". If a# write accessor is not present, the attribute will not be writable via SCIM.#has_and_belongs_to_many:groups# ===========================================================================# SCIM MIXIN AND REQUIRED METHODS# ===========================================================================## All class methods shown below are mandatory unless otherwise commented.defself.scim_resource_typereturnScimitar::Resources::Userenddefself.scim_attributes_mapreturn{id::id,externalId::scim_uid,userName::username,name:{givenName::first_name,familyName::last_name},emails:[{match:'type',with:'work',using:{value::work_email_address,primary:true}},{match:'type',with:'home',using:{value::home_email_address,primary:false}},],phoneNumbers:[{match:'type',with:'work',using:{value::work_phone_number,primary:false}},],# NB The 'groups' collection in a SCIM User resource is read-only, so# we provide no ":find_with" key for looking up records for writing# updates to the associated collection.#groups:[{list::groups,using:{value::id,display::display_name}}],active::is_active}enddefself.scim_mutable_attributesreturnnilend# The attributes in this example include a reference to the same hypothesised# 'Group' model as in the HABTM relationship above. In this case, in order to# filter by "groups" or "groups.value", the 'column' entry must reference the# Group model's ID column as an AREL attribute as shown below, and the SCIM# controller's #storage_scope implementation must also introduce a #join with# ':groups' - see the "Queries & Optimisations" section below.#defself.scim_queryable_attributesreturn{givenName:{column::first_name},familyName:{column::last_name},emails:{column::work_email_address},groups:{column:Group.arel_table[:id]},"groups.value"=>{column:Group.arel_table[:id]},}end# Optional but recommended.#defself.scim_timestamps_map{created::created_at,lastModified::updated_at}end# If you omit any mandatory declarations, you'll get an exception raised by# this inclusion which tells you which method(s) need(s) to be added.#includeScimitar::Resources::Mixinend
If you use ActiveRecord, your controllers can potentially be extremely simple by subclassingScimitar::ActiveRecordBackedResourcesController
- at a minimum:
moduleScimclassUsersController <Scimitar::ActiveRecordBackedResourcesControllerprotecteddefstorage_classUserenddefstorage_scopeUser.all# Or e.g. "User.where(is_deleted: false)" - whatever base scope you requireendendend
All data-layer actions are taken via#find
or#save!
, with exceptions such asActiveRecord::RecordNotFound
,ActiveRecord::RecordInvalid
or generalised SCIM exceptions handled by various superclasses. For a real Rails example of this, see thetest suite's controllers which are invoked via itsrouting declarations.
You can overwrite write-based controller methods#create
,#update
,#replace
and#destroy
in your controller subclass, should you wish, wherein a call tosuper
is passed a block. The block is invoked with the instance of a new unsaved record for#create
, the updated record that needs to have its changes saved for#update
and#replace
and the record that should be destroyed for#destroy
. This allows you to do things like applying business logic, default values, extra request-derived data and so-forth before then callingrecord.save!
, or using some different method other thanrecord.destroy!
to discard a record (e.g. you might be using soft-delete, or want to skip all callbacks for some reason viarecord.delete
).
The
#destroy
method just callsrecord.destroy!
unless a block is given, with nothing much else to say about it.The other methods all establish a database transaction and call through to thecontroller's protected
#save!
method, passing it the record; it isthis method which then either callsrecord.save!
or invokes a block. Using the exception-throwing versions of persistence methods is recommended, as there is exception handling within the controller's implementation which rescues things likeActiveRecord::RecordInvalid
and builds an appropriate SCIM error response when they occur. You can change the list of exceptions handled in this way by overriding protected method `#scimitar_rescuable_exceptions'.If you want to override saving behaviour for both new and modified records, overriding
#save!
in your controller subclass, rather than overriding all of#create
,#update
and#replace
, is likely to be the better choice.For more information, see theRDoc output for
Scimitar::ActiveRecordBackedResourcesController
.
Example:
moduleScimclassUsersController <Scimitar::ActiveRecordBackedResourcesController# Create all new records with some special internal field set to a value# determined by a bespoke-to-your-application mechanism.#defcreatesuperdo |user |user.some_special_on_creation_field=method_that_calculates_value()user.save!endend# Use #discard! rather than #destroy! as an example of soft-delete via the# 'discard' gem - https://rubygems.org/gems/discard.#defdestroysuperdo |user |user.discard!endendend
The scope can be optimised to eager load the data exposed by the SCIM interface, i.e.:
defstorage_scopeUser.eager_load(:groups)end
In cases where you have references to related columns in yourscim_queryable_attributes
, yourstorage_scope
must join the relation:
defstorage_scopeUser.left_join(:groups)end
If you donot use ActiveRecord to store data, or if you have very esoteric read-write requirements, you can subclassScimitar::ResourcesController
in a manner similar to this:
classUsersController <Scimitar::ResourcesController# SCIM clients don't use Rails CSRF tokens.#skip_before_action:verify_authenticity_token# If you have any filters you need to run BEFORE authentication done in# the superclass (typically set up in config/initializers/scimitar.rb),# then use "prepend_before_filter to declare these - else Scimitar's# own authorisation before-action filter would always run first.defindex# There's a degree of heavy lifting for arbitrary storage engines.query=ifparams[:filter].present?attribute_map=User.new.scim_queryable_attributes()# Note use of *instance* methodparser=Scimitar::Lists::QueryParser.new(attribute_map)parser.parse(params[:filter])# Then use 'parser' to read e.g. #tree or #rpn and turn this into a# query object for your storage engine. With ActiveRecord, you could# just do: parser.to_activerecord_query(base_scope)else# Return a query object for 'all results' (e.g. User.all).end# Assuming the 'query' object above had ActiveRecord-like semantics,# you'd create a Scimitar::Lists::Count object with total count filled in# via #scim_pagination_info and obtain a page of results with something# like the code shown below.pagination_info=scim_pagination_info(query.count())page_of_results=query.offset(pagination_info.offset).limit(pagination_info.limit).to_asuper(pagination_info,page_of_results)do |record |# Return each instance as a SCIM object, e.g. via Scimitar::Resources::Mixin#to_scimrecord.to_scim(location:url_for(action::show,id:record.id))endenddefshowsuperdo |user_id|user=find_user(user_id)# Evaluate to the record as a SCIM object, e.g. via Scimitar::Resources::Mixin#to_scimuser.to_scim(location:url_for(action::show,id:user_id))endenddefcreatesuperdo |scim_resource|# Create an instance based on the Scimitar::Resources::User in# "scim_resource" (or whatever your ::storage_class() defines via its# ::scim_resource_type class method).record=self.storage_class().newrecord.from_scim!(scim_hash:scim_resource.as_json())self.save!(record)# Evaluate to the record as a SCIM object (or do that via "self.save!")user.to_scim(location:url_for(action::show,id:record.id))endenddefreplacesuperdo |record_id,scim_resource|# Fully update an instance based on the Scimitar::Resources::User in# "scim_resource" (or whatever your ::storage_class() defines via its# ::scim_resource_type class method). For example:record=self.find_record(record_id)record.from_scim!(scim_hash:scim_resource.as_json())self.save!(record)# Evaluate to the record as a SCIM object (or do that via "self.save!")user.to_scim(location:url_for(action::show,id:record_id))endenddefupdatesuperdo |record_id,patch_hash|# Partially update an instance based on the PATCH payload *Hash* given# in "patch_hash" (note that unlike the "scim_resource" parameter given# to blocks in #create or #replace, this is *not* a high-level object).record=self.find_record(record_id)record.from_scim_patch!(patch_hash:patch_hash)self.save!(record)# Evaluate to the record as a SCIM object (or do that via "self.save!")user.to_scim(location:url_for(action::show,id:record_id))endenddefdestroysuperdo |user_id|user=find_user(user_id)user.deleteendendprotected# The class including Scimitar::Resources::Mixin which declares mappings# to the entity you return in #resource_type.#defstorage_classUserend# Find your user. The +id+ parameter is one of YOUR identifiers, which# are returned in "id" fields in JSON responses via SCIM schema. If the# remote caller (client) doesn't want to remember your IDs and hold a# mapping to their IDs, then they do an index with filter on their own# "externalId" value and retrieve your "id" from that response.#deffind_user(id)# Find records by your ID here.end# Persist 'user' - for example, if we *were* using ActiveRecord...#defsave!(user)user.save!rescueActiveRecord::RecordInvalid=>exceptionraiseScimitar::ResourceInvalidError.new(record.errors.full_messages.join('; '))endend
Note that theScimitar::ApplicationController
parent class ofScimitar::ResourcesController
has a few methods to help with handling exceptions and rendering them as SCIM responses; for example, if a resource were not found by ID, you might wish to useScimitar::ApplicationController#handle_resource_not_found
.
You can extend schema with custom data by defining an extension class and calling::extend_schema
on the SCIM resource class to which the extension applies. These extension classes:
- Must subclass
Scimitar::Schema::Base
- Must call
super
indef initialize
, providing data as shown in the example below - Must define class methods for
::id
and::scim_attributes
The::id
class method defines a unique schema ID that is used to namespace payloads or paths in JSON responses describing extended resources, JSON payloads creating them or PATCH paths modifying them. The RFCs require this to be a URN (see RFC 2141). Your extension's ID URN must be globally unique. Depending on your expected use case, you should review theIANA registration considerations that RFC 7643 describes and definitely review thesyntactic structure declaration therein (urn:ietf:params:scim:{type}:{name}{:other}
).
For example, we might choose to use theRFC-defined User extension schema to define a couple of extra fields our User model happens to support:
classUserEnterpriseExtension <Scimitar::Schema::Basedefinitialize(options={})super(name:'ExtendedUser',description:'Enterprise extension for a User',id:self.class.id,scim_attributes:self.class.scim_attributes)enddefself.id'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'enddefself.scim_attributes[Scimitar::Schema::Attribute.new(name:'organization',type:'string'),Scimitar::Schema::Attribute.new(name:'department',type:'string')]endend
...with thesuper
call providing your choice ofname
anddescription
, but also always providingid
andscim_attributes
as shown above. The class name chosen here is just an example and the class can be put inside any level of wrapping namespaces you choose - it'syour class that can be named however you like. The extension class is then applied to the SCIM User resourceglobally in your application by calling:
Scimitar::Resources::User.extend_schema(UserEnterpriseExtension)
This is often done inconfig/initializers/scimitar.rb
to help make it very clear that extensions are globally available and remove the risk of SCIM resources somehow being referenced before schema extensions have been applied.
Indef self.scim_attributes_map
in the underlying data model, add any new fields -organization
anddepartment
in this example - to map them to whatever the equivalent data model attributes are, just as you would do with any other resource fields. These are declared without any special nesting - for example:
defself.scim_attributes_mapreturn{id::id,externalId::scim_uid,userName::username,# ...etc...organization::company,department::team}end
Whatever you provide in the::id
method in your extension class will be used as a namespace in JSON data. This means that, for example, a SCIM representation of the above resource would look something like this:
{"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" ],"id":"2819c223-7f76-453a-413861904646","externalId":"701984","userName":"bjensen@example.com",// ..."urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {"organization":"Corporation Incorporated","department":"Marketing", },// ...}
...and likewise, creation viaPOST
would require the same nesting if a caller wanted to create a resource instance with those extended properties set (and RFC-compliant consumers of your SCIM API should already be doing this). ForPATCH
operations,thepath
uses acolon to separate the ID/URN part from the path rather than just using a dot as you might expect from the JSON nesting above:
{"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],"Operations": [ {"op":"replace","path":"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization","value":"Sales" } ]}
IMPORTANT: Attribute names must be unique across your entire combined schema, regardless of URNs used. This is because of a limitation in Scimitar's implementation.This GitHub issue explains more. If this is a problem for you, please comment on the GitHub issue to help the maintainers understand the level of demand for remediation.
Resource extensions can provide any fields you choose, under any ID/URN you choose, to either RFC-described resources or entirely custom SCIM resources. There are no hard-coded assumptions or other "magic" that might require you to only extend RFC-described resources with RFC-described extensions. Of course, if you use custom resources or custom extensions that are not described by the SCIM RFCs, then the SCIM API you provide may only work with custom-written API callers that are aware of your bespoke resources and/or extensions.
Extensions can also contain complex attributes such as groups. For instance, if you want the ability to write to groups from the User resource perspective (since 'groups' collection in a SCIM User resource is read-only), you can add one attribute to your extension like this:
Scimitar::Schema::Attribute.new(name:"userGroups",multiValued:true,complexType:Scimitar::ComplexTypes::ReferenceGroup,mutability:"writeOnly"),
Then map it in yourscim_attributes_map
:
userGroups:[{list::groups,find_with:->(value){Group.find(value["value"])},using:{value::id,display::name}}]
And write to it like this:
{"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],"Operations": [ {"op":"replace","path":"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:userGroups","value": [{"value":"1" }] } ]}
If you have an API consumer entity querying your Scimitar-based SCIM API provider endpoint and want to enable a degree of auto-discovery for that entity, then depending on your implementation, there may be customisations you wish to make.
By default, Scimitar advertises (via things likethe/Schemas
endpoint) support for both aUser
andGroup
resource, but if you (say) only support aUser
concept, you override the default using code such as this in yourconfig/initializers/scimitar.rb
file:
Rails.application.config.to_preparedoScimitar::Engine.set_default_resources([Scimitar::Resources::User])# ...other Scimitar configuration / initialisation code...end
One vital feature of SCIM is its authorisation and security model. The best resource I've found to describe this in any detail issection 2 of the protocol RFC, 7644.
Often, you'll find that bearer tokens are in use by SCIM API consumers, but the way in which this is used by that consumer in practice can vary a great deal. For example, suppose a corporation uses Microsoft Azure Active Directory to maintain a master database of employee details. Azure lets administratorsconnect to SCIM endpoints for services that this corporation might use. In all cases, bearer tokens are used.
When the third party integration builds an app that it gets hosted in the Azure Marketplace, the token is obtained via full OAuth flow of some kind - the enterprise corporation would sign into your app by some OAuth UI mechanism you provide, which leads to a Bearer token being issued. Thereafter, the Azure system would quote this back to you in API calls via the
Authorization
HTTP header.If you are providing SCIM services as part of some wider service offering it might not make sense to go to the trouble of adding all the extra features and requirements for Marketplace inclusion. Fortunately, Microsoft supportaddition of 'user-defined' enterprise "app" integrations in Azure, so the administrator can set up and 'provision' your SCIM API endpoint. Inthis case, the bearer token is just some string that you generate which they paste into the Azure AD UI. Clearly, then, this amounts to little more than a glorified password, but you can take steps to make sure that it's long, unguessable and potentially be some encrypted/encoded structure that allows you to make additional security checks on "your side" when you unpack the token as part of API request handling.
HTTPS is obviously a given here and localhost integration during development is difficult; perhaps search around for things like POSTman collections to assist with development testing. Scimitar has a reasonably comprehensive internal test suite but it's only as good as the accuracy and reliability of the subclass code you write to "bridge the gap" between SCIM schema and actions, and your User/Group equivalent records and the operations you perform upon them. Microsoft provideadditional information to help guide service provider implementors with best practice.
Several complex types for User contain the same set of
value
,display
,type
andprimary
fields, all used in synonymous ways.The
value
field - which is e.g. an e-mail address or phone number - is described as optional bythe RFC 7643 core schema, also using "SHOULD" rather than "MUST" in field descriptions elsewhere. Scimitar marks this as required by default, since there's not much point being sent (say) an e-mail section which has entries that don't provide the e-mail address. Some services might sendnull
values here regardless so, if you need to be able to accept such data, you can setengine configuration optionoptional_value_fields_required
tofalse
.The schemadescriptions for
display
declare that the field is something optionally sent by the service provider and state clearly that it is read-only - yet the formal schema declares itreadWrite
. Scimitar marks it as read-only.
The
displayName
of a Group is described inRFC 7643 section 4.2 and in the free-text schemadescription
field as required, but the schema nonetheless states"required" : false
in the formal definition. We consider this to be an error and mark the property as"required" : true
.In the
members
section of aGroup
in the RFC 7643 core schema, any member'svalue
is noted asnot required butthe RFC also says "Service providers MAY require clients to provide a non-empty value by setting the "required" attribute characteristic of a sub-attribute of the "members" attribute in the "Group" resource schema". Scimitar does this. Thevalue
field would contain theid
of a SCIM resource, which is the primary key on "our side" as a service provider. Just as we must storeexternalId
values to maintain a mapping on "our side", we in turndo require clients to provide our ID in group member lists via thevalue
field.While the gem attempts to support difficult/complex filter strings via incorporating code and ideas inSCIM Query Filter Parser, it is possible that ActiveRecord / Rails precedence on some query operations in complex cases might not exactly match the SCIM specification. Please do submit a bug report if you encounter this. You may also wish to view
query_parser_spec.rb
to get an idea of the tested examples - more interesting test cases are in the "context 'with complex cases' do
" section.Group resource examples show the
members
array including fielddisplay
, but this is not in theformal schema; Scimitar includes it in the Group definition.POST
actions with only a subset of attributes specified treat missing attributes "to be cleared" for anything that's mapped for the target model. If you have defaults established at instantiation rather than (say) before-validation, you'll need to overrideScimitar::ActiveRecordBackedResourcesController#create
(if using that controller as a base class) as normally the controller just instantiates a model, appliesall attributes (with any mapped attribute values without an inbound value set tonil
), then saves the record. This might cause default values to be overwritten. For consistency,PUT
operations apply the same behaviour. The decision on this optional specification aspect is in part constrained by the difficulties of implementingPATCH
.RFC 7644 indicates that a resource might only return its core schema in the
schemas
attribute if it was created without any extension fields used. Only if e.g. a subsequentPATCH
operation added data provided by extension schema, would that extension also appear inschemas
. This behaviour is extremely difficult to implement and Scimitar does not try - it will always return a resource's core schema and any/all defined extension schemas in theschemas
array at all times.As noted earlier, extension schema attribute names must be unique across your entire combined schema, regardless of schema IDs (URNs) used.
If you believe choices made in this section may be incorrect, pleasecreate a GitHub issue describing the problem.
Bulk operations are not supported.
List ("index") endpointfilters in SCIM areextremely complicated. There is a syntax for specifying equals, not-equals, precedence through parentheses and things like "and"/"or"/"not" along the lines of "attribute operator value", which Scimitar supports to a reasonably comprehensive degree but with some limitations discussed shortly. That aside, it isn't at all clear what some of theexamples in the RFC are even meant to mean. Consider:
filter=userType eq "Employee" and (emails co "example.com" or emails.value co "example.org")
It's very strange just specifying
emails co...
, since this is an Array which contains complex types. Is the filter there meant to try and match every attribute of the nested types in all array entries? I.e. iftype
happened to containexample.com
, is that meant to match? It's strongly implied, because the next part of the filter specifically saysemails.value
. Again, we have to reach a little and assume thatemails.value
means "inany of the objects in theemails
Array, match all things wherevalue
containsexample.org
. It seems likely that this is a specification error and both of the specifiers should beemails.value
.Currently filtering for lists is always matched case-insensitive regardless of schema declarations that might indicate otherwise, for
eq
,ne
,co
,sw
andew
operators; for greater/less-thank style filters, case is maintained with simple>
,<
etc. database operations in use. The standard Group and User schema havecaseExact
set tofalse
for just about anything readily queryable, so this hopefully would only ever potentially be an issue for custom schema.As an exception to the above, attributes
id
,externalId
andmeta.*
are matched case-sensitive. Filters that useeq
on such attributes will end up a comparison using=
rather than e.g.ILIKE
(arising from#36).The
PATCH
mechanism is supported, but where filters are included, only a single "attribute eq value" is permitted - no other operators or combinations. For example, a work e-mail address's value could be replaced by a PATCH patch ofemails[type eq "work"].value
. For in-path filters such as this, other operators such asne
are not supported; combinations with "and"/"or" are not supported; negation with "not" is not supported.
If you would like to see something listed in the session implemented, pleasecreate a GitHub issue asking for it to be implemented, or if possible, implement the feature and send a Pull Request.
Install Ruby dependencies first:
bundle install
For testing, two main options are available:
- The first option is running the project locally. This is also the recommended way, as running the tests on a variety of setups and platforms increases he chance of finding platform-specific or setup-specific bugs.
- The second option is utilising the existing Docker Compose setup provided in the project. You can use this if getting the project to work locally is hard or not feasible.
You will need to have PostgreSQL running. This database is chosen for tests to prove case-insensitive behaviour via detection of ILIKE in generated queries. Using SQLite would have resulted in a more conceptually self-contained test suite, but SQLite is case-insensitive by default and uses "LIKE" either way, making it hard to "see" if the query system is doing the right thing.
Afterbundle install
and with PostgreSQL up, set up the test database with:
pushd spec/apps/dummyRAILS_ENV=test bundleexec bin/rails db:drop db:create db:migratepopd
...and thereafter, run tests with:
bundle exec rspec
You can get an idea of arising test coverage by openingcoverage/index.html
in your preferred web browser.
In order to be able to utilise the Docker Compose setup, you will need to have Docker installed with the Compose plugin. For an easy installation of Docker (with a GUI and the Compose plugin preinstalled) please seeDocker Desktop.
In order to configure the Docker image, rundocker compose build
in a terminal of your choice, in the root of this project. This will download the required image and install the required libraries. After this is complete, running the tests is as easy as running the commanddocker compose up test
.
As mentioned in the previous section, test coverage may be analysed usingcoverage/index.html
after running the project.
You can also open a raw terminal in this test container by runningdocker run --rm test sh
. For more Compose commands, please refer tothe Docker Compose reference manual.
Locally generated RDoc HTML seems to contain a more comprehensive and inter-linked set of pages than those available fromrubydoc.info
. You can (re)generate the internalrdoc
documentation with:
bundleexec rake rerdoc
...yes, that'srerdoc
- Re-R-Doc - then opendocs/rdoc/index.html
.
About
A SCIM v2 API endpoint implementation