This quick-start shows what you need to know to create your own stores and validation for SCIM requests.
Using a Custom Store
To use your store for resources, you need to create a store that implements IScimStore<T>
where T
is a type derived from Resource
. To find more information about Resource
and its derived models please see the introduction. The interface IScimStore<T>
looks as follows:
Task<T> Add(T resource);
Task<ScimPageResults<TResource>> GetAll(IResourceQuery query);
Task<T> GetById(string id, ResourceAttributeSet attributes) ;
Task<ScimPageResults<T>> GetAll(IResourceQuery query);
Task<T> Update(T resource);
Task PartialUpdate(string resourceId, IEnumerable<PatchCommand> updates);
Task Delete(string id);
To register your store with the SCIM library, you must register it alongside a resource in the ConfigureServices
method.
public void ConfigureServices(IServiceCollection services)
{
services.AddScimServiceProvider("/SCIM", new ScimLicensingOptions("Demo", "eyJTb2xkRm9yIjowLjAsI .... "))
.AddResource<User, CustomUserStore>("urn:ietf:params:scim:schemas:core:2.0:User", "Users");
}
In this example, we provide a custom store implementation for the default User
model. This example will also utilize the built-in ScimValidator, as described in the installation quick-start.
Filtering
To provide a Full implementation of the GetAll
method requires compiling a SCIM filter expression to an expression that the store can execute against the underlying data store. The SCIM component ships with a type that compiles SCIM filter expressions to an IQueryable<T>
to assist in executing a SCIM filter. Use the extension method AddFilterPropertyExpressionCompiler
on the DI container to make the service available to the stores via IScimQueryBuilderFactory
.
Configure the factory using the extension method MapScimAttributes
. The extension method maps from SCIM attribute paths to the storage type's properties.
.MapScimAttributes<AppUser>(ScimSchemas.User, mapper =>
{
mapper
.Map("id", u => u.Id)
.Map("userName", u => u.Username)
.Map("name.familyName", u => u.LastName)
.Map("name.givenName", u => u.FirstName)
.Map("active", u => u.IsActive)
.Map("locale", u => u.Locale);
})
.MapScimAttributes<AppUser>(ScimSchemas.EnterpriseUser, mapper =>
{
mapper
.Map("department", u => u.Department);
})
.MapScimAttributes<AppRole>(ScimSchemas.Group, mapper =>
{
mapper
.Map("id", r => r.Id)
.Map("displayName", r => r.Name)
.MapCollection<Member>("members", r => r.Members, member =>
{
member
.Map("value", m => m.Value)
.Map("display", m => m.Display)
.Map("type", m => m.Type);
});
});
The above mapping woks for directly mapping attribute values onto store properties. When the storage format for the attribute in the store is different the comparison value used in the filter needs to be modified. Consider the following representation of a user.
public class AppUser
{
public string Id { get; }
public string? Username { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Department { get; set; }
public bool IsDisabled { get; set; }
public string? NormalizedUsername { get; set; }
}
IsDisabled
models the inverse of the active attribute.
NormalizedUsername
models the username capitalized, to enable case insensitive matching .
To create a filter map that supports querying username and active, in addition to the property mapping a converter is supplied
.Map("active", u => u.IsDisabled , ScimFilterAttributeConverters.Inverse)
.Map("userName", u => u.NormalizedUsername , ScimFilterAttributeConverters.ToUpper)
The third optional argument provides the conversion logic, its a function that takes the comparison value used in a filter expression, and transforms it into a compatible form for the stores schema.
When compiling a SCIM filter of active eq true and username sw "Andy"
, the compiled filter will compare IsDisabled
with false
and NormalizedUsername
starts with "ANDY"
.
An alternative to using the mapper is to provide an implementation of IScimAttributeToPropertyMapper
and register it with the DI container. You can register many instances with the DI container, and the compiler will try each one, in turn, to map a SCIM attribute into a .NET property.
public delegate object LiteralConverter(object toConvert);
/// <summary>
/// Used by the ScimQueryCompiler to map SCIM attributes into .NET properties
/// </summary>
public interface IScimAttributeToPropertyMapper
{
/// <summary>
/// Maps an attribute to a property expression, used to build a LINQ expression for filtering
/// resources
/// </summary>
/// <param name="schema">Schema of the attribute</param>
/// <param name="attribute">attribute path</param>
/// <param name="root">An expression describing how to get to the root .NET object for the attribute container</param>
/// <param name="expression">The expression that returns the value of the attriubte</param>
/// <returns>true if the map was sucessfull</returns>
bool TryMapAttribute(string schema,string attribute, Expression root , out MemberExpression expression);
/// <summary>
/// Returns a literal value converter for the attribute, used to modify a literal value used in a filter
/// prior to any comparison
/// </summary>
/// <param name="type">Return type of the .NET property</param>
/// <param name="schema">Schema of the attribute</param>
/// <param name="attribute">attribute path</param>
/// <param name="converter">conversion function</param>
/// <returns>true if a converter exists</returns>
bool TryMapAttributeValueConverter(Type type , string schema, string attribute, out LiteralConverter converter);
}
The store uses the factory to create a query builder for the store's storage type (the T in the IQueryable). The builder provides methods to define the filter along with paging and sorting.
public async Task<ScimPageResults<User>> GetAll(IResourceQuery query)
{
IQueryable<AppUser> databaseQuery =
queryBuilderFactory.CreateQueryBuilder<AppUser>(ctx.Users)
.Filter(query.Filter)
.Build();
IQueryable<AppUser> pageQuery = queryBuilderFactory.CreateQueryBuilder<AppUser>(databaseQuery)
.Page(query.StartIndex, query.Count)
.Sort(query.Sort.By, query.Sort.Direction)
.Build();
. . .
}
The store then uses the compiled expression against any data store that can compile an IQueryable
int totalCount = await databaseQuery.CountAsync();
var matchingUsers = await pageQuery
.AsAsyncEnumerable()
.Select(MapAppUserToScimUser)
.ToListAsync();
return new ScimPageResults<User>(matchingUsers, totalCount);
The alternative is to walk the filter abstraction syntax tree to determine the underlying query. All items in the tree are a kind of a ScimExpression
.
New Mapping Functions
Since version 4.0 complex types can be mapped inline.
.MapScimAttributes<AppUser>(ScimSchemas.User, mapper =>
{
mapper.MapComplex("name",u=>u.Name,im=>
{
im.Map("givenName", i => i.GivenName)
.Map("familyName", i => i.FamilyName);
})
});
Complex types can also be mapped in advanced and mapped onto multiple root attributes. The code fragment below describes how to map a complex attribute that describes a location onto a .NET type Location
. The home and office map statements, map onto properties Home and Office of type Location
.
e.g.
home.house => AppUser.Home.House
office.house => AppUser.Office.House
.MapScimAttributes<AppUser>(ScimSchemas.User, mapper =>
{
mapper.MapType<Location>(lm =>
{
lm.Map("house", h => h.House)
.Map("street", h => h.Street)
.Map("town", h => h.Town);
})
.Map("home", u => u.Home)
.Map("office", u => u.Office)
});
The same technique can also be used for mapping collections. Use MapType
for the collection type, and use the Map
function to map the navigation to the collection.
Using a Custom Validator
To use your validator as opposed to the default validation described in the installation quick-start, you need to implement IScimValidator<T>
(where T
is derived from Resource
) and also have a custom store implemented as defined above. You can provide a custom validator for your resources or the default resources. The interface IScimValidator<T>
defines two methods. The ValidateUpdate
method will be called when validating Update
(PUT
) request, and the ValidateAdd
method will be called when validating Create
(POST
) requests. This is to enable different validation based on the context of the request.
Task<IScimResult<T>> ValidateUpdate(string resourceAsString, string schema);
Task<IScimResult<T>> ValidateAdd(string resourceAsString, string schema);
To register your store with the SCIM library, you must register it alongside a resource and store in the ConfigureServices
method.
public void ConfigureServices(IServiceCollection services)
{
services.AddScimServiceProvider("/SCIM", new ScimLicensingOptions("Demo", "eyJTb2xkRm9yIjowLjAsI .... "))
.AddResource<ScimUser, ScimStore, ScimValidator>("urn:ietf:params:scim:schemas:core:2.0:User", "Users");
}