@leisurelink/claims

LeisureLink's object model and utilities for querying security claims in nodejs.

Usage no npm install needed!

<script type="module">
  import leisurelinkClaims from 'https://cdn.skypack.dev/@leisurelink/claims';
</script>

README

claims

LeisureLink's object model and utilities for querying security claims in nodejs.

Claims Based Security

In the context of LeisureLink's federated security, the term Claim refers to a statement that a security principal makes about itself or another security principal. Every user of the system, whether a human user or a system user, is authenticated as a security principal, and access-control, where applicable, relies upon a principal's claims.

Security Principals

Presently, there are two types of security principals; user principals and endpoint principals.

User principals represent the human users that log in to our websites, applications, and APIs and otherwise use our services.

Endpoint principals represent system users. Each of our internal API's, websites, hubs, applications, CLIs, as well as external systems that connect with us are represented in the security system as endpoint principals.

Claimsets

Within the security system, claims are organized into claimsets. A claimset is a collection of claims defined in a claimset specification and administered by a claims commissioner.

Claimsets and their constutuent claims are identified by the claimset's identfier (csid).

Claims

Claims are trusted, system-wide data points attributed to a principal. Logically, they are similar to key-value pairs; they have two parts:

  • clid - the claim's unique identifier
  • value - the claim's value

clid (Claim Identifiers)

LeisureLink's federated security is a distributed authority system. This means that there is not a single service that catalogues the entirety of a principal's claims. Instead, various services within the operating environment perform the role of claims commissioner and proffer subsets of a principal's claims to the system. This scheme enables seperation of concern in the security system; e.g. the pmc-account-api is responsible for associating principals with PMCs, and establishing a principal's permissions for conducting operations against rental properties. One of the mechanisms that enables the system to understand the separation of security concerns is the way a clid is constructed.

Claims identifiers are encoded similar to JSON Pointer fragement identifiers. This encoding enables us to namespace the security related data points and enables the security system to lookup the claim commissioner and otherwise trust the veracity of claims.

It is important to note that claim identfiers may be templated. Templated ids are frequently used to represent the relationship between a principal and another generated id within the system. For example, a PMC (property management company) may be represented by a specific entity within the system. The specification for a user's relationship with a PMC would then use a templated claim in order indicate that a claim is valid for a variety of different PMC identfiers.

Examples

The fragment identifier #/pmc/adm is a valid clid. The first segment of the clid indicates which claims commissioner is the authority over the claim. This first segment is referred to as the csid (Claimset Identifier). Additional segments in a clid denote the claim's scope and meaning to the corresponding commissioner.

Claim Types

The system understands 3 types of claims:

Fact Claim

A fact claim is a data point related to the principal that the system asserts to be factual, some of these are principalId, first-name, last-name, email-address, etc.

Facts always have a corresponding text value. Take for example the following claim specification for the principal's email address:

...
   {
     "clid": "#/sys/em",
     "kind": "fact",
     "name": "Email address"
   }

Javascript code using a principal's email address might be:

// ... assuming systemId is a principal's systemId...

Claims.forPrincipal(systemId)
  .get('#/sys/em', function (err, claim) {
    if (!err && 'me@my.com' === claim) {
      console.log('Yay, its me!');
    }
  });

Role Claim

A role claim denotes that the principal has been granted membership in the identified role.

Roles always have a boolean value of true or false, indicating whether the principal is a member. Take for example the following claim specification for the PMC administrator:

...
  {
    "clid": "#/pmc/adm",
    "kind": "role",
    "name": "PMC Administrator"
  }

Javascript code verifying a principal's 'PMC Administrator' membership:

// ... assuming systemId is a principal's systemId...

Claims.forPrincipal(systemId)
  .get('#/pmc/adm', function (err, claim) {
    if (claim) {
      console.log("Yay, I'm an admin!");
    }
  });

Role claims may frequently be specified with template parameters, given the above example we may create a claim specification for a specific PMC's administrator using the following JSON:

...
    {
        "clid": "#/pmc/{pmcId}/adm",
        "kind": "role",
        "name": "PMC Administrator"
    }
...

Verifying the presence of this claim can be done in a couple ways. The most straightforward way to query for a templated claim is done when we want to query if the principal is a related to a specific id.

// ... assuming systemId is a principal's systemId...

Claims.forPrincipal(systemId)
  .get('#/pmc/12/adm', function (err, claim) {
    if (claim) {
      console.log("Yay, I'm an admin!");
    }
  });

Alternatively, we may want to know if the principal is just an Administrator of some PMC rather than any specific id, this may be accomplished with the following JS:

// ... assuming systemId is a principal's systemId...

Claims.forPrincipal(systemId)
  .get('#/pmc/{pmcId}/adm', function (err, claim) {
    if (claim) {
      console.log("Yay, I'm an admin!");
    }
  });

Permissions Claim

A permissions claim is composed of a set of permissions that have been granted to a principal.

Permissions have a value that is the set of permissions defined in the claim's specification that have been granted a principal. Take for example the following claim specification for a PMC's rental unit's data permissions:

...
  {
    "clid": "#/pmc/{pmcId}/units/[]",
    "kind": "permissions",
    "name": "PMC Rental Unit Permissions",
    "permissions": [{
      "flag": "c",
      "description": "Permission to create the PMC's unit data."
    }, {
      "flag": "r",
      "description": "Permission to read the PMC's unit data."
    }, {
      "flag": "u",
      "description": "Permission to update the PMC's unit data."
    }, {
      "flag": "d",
      "description": "Permission to delete the PMC's unit data."
    }],
    "parameters": [{
      "name": "pmcId",
      "position": 1,
      "type": "string"
    }]

Each discreet permission is defined in the claim's specification. Javascript code testing whether a principal has permission to read and update rental units might be:

// ... assuming systemId is a principal's systemId...

Claims.forPrincipal(systemId)
  .get('#/pmc/123/units/[ru]', function (err, claim) {
    if (!err && claim === '[ru]') {
      console.log("Whew, I can read and update PMC 123's rental units!");
    }
  });

Object Model

The object model implements the business logic necessary to pull claim-specs and claims close to where security demands are being made. It embodies logic that implements select-through caching at many levels in order to reduce unnecessary network requests. The caching adheres to time-to-live declarations made in the claimset specifications.

claims defines the following objects:

Claims Class

The Claims class is the entrypoint for client code working with claims.

  • #forPrincipal - factory method; creates a principal specific claims context (PrincipalClaims) for the principal specified by systemId
  • #getClaimetSpecs - gets the claimset specification corresponding to the specified csid and constructs a ClaimsetSpec
  • #getClaimSpecs - gets the claim specification corresponding to the specified clid and constructs a ClaimSpec

The Claims class' constructor requires that three resolver functions be specified, one that resolves claimsets and two others that resolves a principal's claims:

// assuming the two resolvers are already defined in the runspace:

var claims = new Claims({
  claimsetSpecResolver: getClaimsetSpecs,
  claimResolver: getClaim,
  claimsResolver: getClaims
});

The Claims class intentionally does not care how these functions are implemented; it only cares about the functions' contract as specified below.

Required Claimset Specification Resolver

A claimset specification resolver retrieves a specified claimset. It is a function that takes two parameters:

  • csid - the claimset's identifier
  • callback - a nodejs style callback function where results are returned
function getClaimsetSpecById( csid, callback ) {
  //... implementation elided.
}

During normal operation, the resolver must not throw an exception. Error conditions may be communicated back to the caller via the callback's first argument.

The resolver's success response must be a javascript object constructed from the requested claimset's specification. Since claimsets are specified in JSON Schema format, the direct result of JSON.parse is what is expected as the top-level success result provided as the callback's second argument.

Required Claim Resolvers

A claim resolver retrieves a principal's claims.

  • clid - the claim's identifier (or claims' identifiers). Please note that this method should support being called with an array.
  • systemId - the principal's systemId
  • callback - a nodejs style callback function where results are returned
function getClaim( clid, systemId, callback ) {
  //... implementation elided
  // _N.B._ clid may be a single value _or_ an array.
}

During normal operation, the resolver must not throw an exception. Error conditions may be communicated back to the caller via the callback's first argument.

The resolver's success response must be the claim's value, according the the claim's type, and returned as the callback's second argument.