Authenticate the Web Client with Azure AD

I have a few posts demonstrating on-premise ADFS with the CM Web Client but so far nothing with Azure Active Directory, today I rectify that.

The Video

Steps

The steps to configure the web client to use Azure AD are:

  • configure the site to use anonymous in IIS Admin,
  • create the Azure Ad App Registration, and
  • edit the Web Client web.config.

Sample Config

configSections

<section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral,PublicKeyToken=B77A5C561934E089" />
<section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />

appSettings

<add key="ida:FederationMetadataLocation" value="https://login.windows.net/[Your Tenant ID]/FederationMetadata/2007-06/FederationMetadata.xml" />

authorization and authentication

<authorization>
  <deny users="?" />
</authorization>
<authentication mode="None" />

system.identityModel

<system.identityModel>
<identityConfiguration>
  <audienceUris>
    <add value="[APP ID URI]" />
  </audienceUris>
  <securityTokenHandlers>
    <add type="System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    <remove type="System.IdentityModel.Tokens.SessionSecurityTokenHandler, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  </securityTokenHandlers>
  <certificateValidation certificateValidationMode="None" />
  <issuerNameRegistry type="System.IdentityModel.Tokens.ValidatingIssuerNameRegistry, System.IdentityModel.Tokens.ValidatingIssuerNameRegistry">
    <authority name="https://sts.windows.net/[Tenant ID]/">   
      <validIssuers>
        <add name="https://sts.windows.net/[Tenant ID]/" />
      </validIssuers>
    </authority>
  </issuerNameRegistry>
</identityConfiguration>
</system.identityModel>
<system.identityModel.services>
<federationConfiguration>
  <cookieHandler requireSsl="true" />
  <wsFederation passiveRedirectEnabled="true" issuer="https://login.windows.net/[Domain]/wsfed" realm="[APP ID URI]" requireHttps="true" />
</federationConfiguration>
</system.identityModel.services>

Use Powershell to import a folder of files

There are a variety of ways to import files to Content Manager.  If you want granular control you may choose to write some code.  This Powershell script uses a CheckinStyle to import all EML files from a folder.

Add-Type -Path "c:\[CM Binary Path]\HP.HPTRIM.SDK.dll"
$database = New-Object HP.HPTRIM.SDK.Database
$database.Id = "L1"
$database.WorkgroupServerName = "local"
$database.Connect()

$checkinStyle = New-Object HP.HPTRIM.SDK.CheckinStyle($database, "test sec");

$files = [System.IO.Directory]::GetFiles("c:\\junk\\testimport", "*.eml", [System.IO.SearchOption]::TopDirectoryOnly);

foreach ($file in $files)
{
Try
{
    $inputDoc = New-Object HP.HPTRIM.SDK.InputDocument($file);
    $rec = $checkinStyle.SetupNewRecord($inputDoc)
    $rec.Save()

    [System.IO.File]::Move($file, [System.IO.Path]::Combine("c:\\junk\\imported\\", [System.IO.Path]::GetFileName($file)))

    Write-Host "Imported: " $rec.Title + " / " + $rec.Uri
    }
    Catch 
    {
        $ErrorMessage = $_.Exception.Message
        Write-Host "Error: " $ErrorMessage + " / " + $file

    }
}

$database.Dispose()

To use this script:

  • set the location of your Content Manager binaries in the first line,
  • use your database Id in the $database,
  • if your work-group server is not on the local machine set the WorkGroupServerName,
  • replace the name in the Checkin Style  Style constructor,
  • if you want to import files other than EML change the '*.eml' to something else,
  • set the source folder name, also the destination folder in the Move method (ensure this destination folder exists), then
  • run the script from Powershell.

Stream a document in the .Net SDK

The standard methods of getting a document from in the SDK (e.g. Record.GetDocument()) fetch the entire document from the document store before giving you access to the document.  DownloadNotifier allows you to:

  • not write a file to the file system but to a stream,
  • fetch the document in chunks rather than waiting for the entire document, and
  • start downloading part way through a file.

I just updated the SDK docs to include some information on using it, also, here is a video if you want to watch me use it.

Checkin Style and Email deletion

This is a note to clarify what happens to emails processed by CM EmailLink.

Content Manager 9.1

 In CM 9.1 there are two check boxes which impact what happens to the email after it has been filed in CM.

In CM 9.1 there are two check boxes which impact what happens to the email after it has been filed in CM.

Content Manager 9.2

 In CM 9.2 there is one combo-box to choose what happens to the email after filing.

In CM 9.2 there is one combo-box to choose what happens to the email after filing.

Mapping 9.1 to 9.2

The check boxes uses in 9.1 map to the (I believe) simpler UI in 9.2 like this:

  • Permanent Delete - no check boxes selected.
  • Move to Deleted Items - 'Keep email in the mail system' NOT selected and 'Move to deleted' selected.
  • Retain in Mail System - 'Keep email in the mail system' selected and 'Move to deleted' NOT selected.

What actually happens

After the email has been filed in Content Manager one of three things happens:

  • Permanent delete - the mail is hard deleted and is not available in the deleted items folder
  • Move to deleted items - if a mail prefix is set in System Options it is pre-pended to the subject and the unique Id of the Record is set in a custom property on the email.  Then the email is moved to the Deleted items folder.
  • Retain in Mail System - same as 'Move to deleted items' except the email is not moved to the Deleted Items folder.

Recursive searching in WebDrawer and ServiceAPI

The CM search syntax allows for recursive searching, for example returning results from folders contained within folders.  The operator that specifies recursion is the plus sign (+), the problem is that in a URL the plus sign is reserved to indicate white space.

To overcome this the ServiceAPI (and WebDrawer) use the tilde (~) instead of the plus sign, so instead of this:

container:[title:asia]+

Do this:

container:[title:asia]~

If you would rather use a character (or sequence of characters) other than the tilde you can customise this in the hptrim.config.  The Web Client, for example, uses _$_ instead of tilde, which is set in the  'searching' element in its hprmServiceAPI.config.

<searching pageSize="30" searchRecursiveOption="_$_"/>

The 'All' property set in the ServiceAPI

Overview

In the ServiceAPI there is a concept of a propertySet, this allows for the retrieval of a pre-defined set of properties, for example:

/Record?q=all&propertySets=Grid&format=json

The problem with 'All'

One of the property sets is 'All' which means that the above URL could look like this:

/Record?pageSize=20&q=all&propertySets=All&format=json

Fetching all properties for many object types (such as Classification, SavedSearch, Activity etc) can be a good idea, being simpler than pre-determining which properties you need.  Using 'All' for Records is problematic, two problems being:

  • some Record properties are very expensive so response times will be unnecessarily slow, and
  • some properties will throw an error if the related feature is not enabled (e.g. NextTaskDue requires the Vital Records feature be enabled).

The solution

The solution is not to use 'All'. The alternatives are:

  • use the 'ViewPane' property set instead, which returns a list of all properties that might be interesting to an end user, or
  • construct a custom property set in hptrim.config which can contain all the properties that are of interest to your application.

Creating a child object (Record 'Alt in' Relationship)

The code

To add a new object to one of the child object collections on a TrimMainObject, for example adding a RecordRelationship to a Record, requires sending an array containing one or more objects to the appropriate collection. For Example, posting the code below to the Record endpoint will cause Record 9000000002 to be 'Alternately contained within' Record 9000008050.

{
    "Uri":9000000002, 
    "ChildRelationships":[
    {
        "RecordRelationshipRelatedRecord":9000008050,
        "RecordRelationshipRelationType": "IsAltIn"
    }]
}

And a video...

Using a search query instead of a Uri in a URL

Recently I wanted to link to a Record using the external ID, not the Uri or the number.  This was from a customised WeDrawer page so I could have done a search to find the Uri and then used that, but I didn't want that extra step.  A little reflection (and poking around in the code) and I realised that I could use the external id in the URL after all.

If it was a search

A search by external id would look like this, problem was I want to open the actual record page, not a list of one record.

/WebDrawer/record?q=recExternal:abc123

As a URL

As long as a search returns only one result then you can substitute the query for the Uri in the URL, as long as you can compose the query without using a colon (colons are reserved characters in URLs).  So below I use equals instead of colon to either open the details page or go directly to the document via the external id.

http://localhost/WebDrawer/record/recExternal=abc123
http://localhost/WebDrawer/record/recExternal=abc123/file/document

Note

As is my habit I use the internal name for the search clause (recExternal) not the name (external) to minimise the possibility of breakages due to either custom captions or use of different languages.

SAML signout

Overview

In a previous post I show how to use Component Space to add SAML support to the Web Client, here I add a sign-out button.

Steps

useADFS == true

Set useADFS to true in hprmServiceAPI.config, for example:

<setup databaseId="J1"  searchAhead="false" advancedSearch="false" workpath="C:\HP Records Manager\ServiceAPIWorkpath\Uploads" useADFS="true"/>

Setup signing certificate

If you plan to sign your logout request then you will need a certificate, either encrypt the password or store the certificate in the Windows certificate store as described in this Component Space document.

Add keep-alive

To allow for notification when the user's session has expired add the keep alive loop.  To do this edit _Initialisation.cshtml (or _Shared.cshtml in later versions) and comment out the RMStayALive function, then add this new version:

var RMStayALive2 = function () {
    var makeRequest = function () {
        $.getJSON(HP.HPTRIM.TrimClient.getServiceAPIUrl() + "/Location/me", function (data, status, xhr) {
                
        }).fail(function () { 
            if (confirm("Your session has expired, do you wish to re-authenticate?")) {
                top.location = HPRMWebConfig.virtualDirectory;
            }
            
        });
    }
    setInterval(makeRequest, (60 * 1000));

}();

Video

The process above is shown in this video.

Capture all email using Email Manager

Overview

Occasionally you might want to capture all email sent by (and to) particular users.  This is achieved using a combination of Exchange Journaling and the Content Manager 'Email Management' module. 

Details

There are a few things that need to be done in order to use the Email Management import tool, these are:

  1. setup an SMTP server which can store incoming email in a drop folder, probably the IIS SMTP server,
  2. configure journaling on your Exchange server to journal email to your SMTP server, and
  3. configure an Origin in Content Manager to monitor the drop folder on the SMTP server.

Watch

In this video I take a tour of Content Manager Email Management installed on an Azure VM and using Exchange Email Online.

NOTE: In the video I state the bulk loading is not supported when using SQL Azure.  I am reliably informed that this has been fixed in Content Manager 9.2.

Java ServiceAPI and additional fields

Previous posts have described the Java client and file upload.  This post details how to fetch the value of additional fields.

Field Names

As described elsewhere the name to use to fetch an additional field is its search clause name, as seen in the client.

adf.PNG

Get field values as text

Once you know the correct names for your fields you need to fetch them.  A full sample is provided in the samples repo, in short you need to:

  1. request the fields by name
  2. request string values be returned
  3. parse the string value as necessary.

Example

See the samples repo for the full sample. 

Records request = new Records();
request.q = "number:rec_222";
// tell the search request to return both string and actual values for the fields and properties
request.setPropertyValue(PropertyType.Both);
request.setStringDisplayType(StringDisplayType.WebService);     

request.Properties = makePropertyList("RoadSurface", "DateOfIssue", "PoliceOfficer");

RecordsResponse response = client.get(request);
Record rec = response.Results.get(0);

ITrimProperty dateOfIssue = rec.Fields.get("DateOfIssue");

SimpleDateFormat parser = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss zzz");
Date date = parser.parse(dateOfIssue.getStringValue());
System.out.println(date);

Database connection caching

Previously if you wanted to avoid the overhead of Database.Connect() in a web service application you had to write some sort of connection pool. In 9.2 this is no longer required as the SDK itself caches connections.  The requirements are that:

  • the previous connection must be Disposed,
  • the subsequent connection must have the same Database Id, work group server name  and trusted user.

Sample Code

private static Database getDatabase(string user = null)
{
    Database database = new Database();
    database.WorkgroupServerName = "local";
    database.Id = "L1";
    if (user != null)
    {
        database.TrustedUser = user;
    }
    _watch.Reset();
    _watch.Start();
    database.Connect();
    _watch.Stop();

    Console.WriteLine(_watch.ElapsedMilliseconds);

    return database;
}


private static void connectDatabase()
{
    string trustedUser = "itu_tadmin";
    using (var db = getDatabase(trustedUser))
    {
    }

    using (var db2 = getDatabase(trustedUser))
    {
    }

    using (var db2 = getDatabase(trustedUser))
    {
    }
}



static void Main(string[] args)
{
    TrimApplication.TrimBinariesLoadPath = @"Your bin folder";
    TrimApplication.Initialize();
    TrimApplication.SetAsWebService("c:\\junk");

    connectDatabase();

}

Demonstration

Event Filtering in 92

Event processor add-ins are great for responding to events on the workgroup server but and now we can optimise them to only respond to the events (and object types) that we are interested in.  Use the 'Configure Events' button in the  'Custom Processes' tab in Enterprise Studio, or watch this video for more information.  For more details on the event processor addins see my previous post and the sample.

Where to store config, web.config or not...

For those who customise WebDrawer on one machine and then push it to a production machine this may prove handy.  I wanted to change the setup of IIS on my dev machine to not update the web.config when I change the authentication settings.  Here is what I did...

1. Go to Feature delegation in IIS Manager

feature_delegation_icon.PNG

2. Select Custom Sites

custom_sites.PNG

3. Set the authentication settings to Read Only

read_only.PNG

4. Clean modified web.configs

Remove the <security /> section from all web.config files that have already been updated, otherwise you will get an error when you attempt to open these applications.  Also check c:\inetpub\wwwroot\web.config to ensure that authentication has not been set here.

5. Update applications in IIS

Go through each application in IIS and ensure the authentication settings are correct, this will update applicationHost.config.

Summary

Setting these settings to Read Only prevents them being updated in the application specific web.config, instead they will be set in the applicationHost.config

Email Link - Delete Check in Style

The CM Rambler showed how to improve the Email Link Admin console, this post shows how you might add a facility to delete a user's Check in Style from within the admin console.  My main motivation for this is that there is a version of Content Manager out there which makes it difficult for a user to delete a user's check in style from the client, which is a problem when, for example, a user leaves the organisation.

 

The code

As per the video above

An empty TH

<th></th>

A table cell containing a button

<td><button class="btn btn-small" data-place-uri="@record.Uri">Delete</button></td> 

The script

$("button[data-place-uri!=''][data-place-uri]").on("click", function (event) {
    var placeUri = $(this).attr('data-place-uri');

    $.ajax({
        url: "CheckinPlace?q=Uri:" + placeUri + "&resultsOnly=true",
        data: {
            properties: "CheckinPlaceCheckinAs"
        },
        dataType: 'json'
    })
    .done(function (data) {

        for (var n = 0; n < data.Results.length; n++) {
            var place = data.Results[n];

            if (place.CheckinPlaceCheckinAs) {
                $.ajax({
                    url: "CheckinStyle/" + place.CheckinPlaceCheckinAs.Uri + "/Delete",
                    type: 'POST',
                    dataType: 'json',
                    contentType: 'application/json'
                })
                .done(function (response, statusText) {
                    if (statusText === "success") {
                        window.location.reload();
                    }
                })
            }
        }

    });

});