In the HCMS, you can use the HCMS roles and HCMS permission groups model to controll access to content.

Introduction

There are two authorization models to specify access control rules in the Headless CMS that can be combined to create accumulated permissions: Headless CMS roles and permission groups. For each of those authorization models there a different authorization providers that grant roles and permission groups to individual requests.

Headless CMS Role Model

A role is not an entity and does not have to be created or registered; it is always just string. The Headless CMS keeps for each endpoint operation a set of roles, called the "required roles" and for each request there is a set of "granted roles". If the intersection of these sets is empty, an HTTP response 403 Forbidden is returned instead of the regular result, see flags below. Note that Headless CMS roles are not related to the censhare Clients roles. Here are some key properties of the role model:

  • Coarse-grained, only restricts whole endpoint operations such as 'create comment entity'
    • It is, however, possible to combine role checking with a custom query expressions to achieve a fine-grained entity-level access control.
  • The only security model available for administration parts of the API (/hcms/v1.0/schema)
  • Users can be defined in service configuration, allowing for a simple setup

There are several standard roles defined and required by standard endpoints, but a custom configuration can use custom roles.

Required Roles for Schemas

For the schema endpoints the the following roles are defined and cannot be changed:

  • schema-rw is required to create and change the schema
  • schema-ro is required to read the schema and list all schemas

Required Roles for Entities

The required roles can be specified in the schema in the property cs:roles.required, containing sub-properties for the individual operations read,create,update and delete that define required roles respectively. The values of the sub properties can be one of the following types:

  • array of string : List of required roles
  • string : Same as an array with one entry
  • false : Same as an empty array: No role allowed, but note since the permission models are accumulative permission can be granted by a different authorization model
  • true : No role required, this operation is always allowed (i.e. public)

For example:

{
    "cs:asset.type": "text.comment.",
    "title": "comment",
    "type": "object",
    "cs:roles.required": {
        "read": ["external-user", "internal-user"]
        "create": "internal-user",
        "delete": false
    },
    "properties": {
        "text": {"type": "string"}
    }
}

Operations that are not explicitly specified are set to default roles:

"cs:roles.required": {
  "read": true,
  "create": "rw",
  "update": "rw",
  "delete": "rw"
}

Therefore, the following specification has the same effect as the one above:

"cs:roles.required": {}

Declaration of the required roles is always at the root level of the schema. Any other occurrences are silently ignored, including permissions from the entity schema embedded via "$ref". This can be used to prevent direct access to entities that are not supposed to be standalone; for example, the entity type article_content is not publicly accessible ("cs:roles.required": {"read": "rw"},), but the whole article is, including its content.

Advanced Permission definition

Advanced permission checking can be specified in the schema in property cs:permissions, containing sub-properties for operations (like cs:roles.required) and for the operation an array of conditions. Each condition can be defined either directly (as a JSON object) or indirectly as JSON pointer (resolvable in the schema itself; this allows shared condition definition between permissions and property-level conditions).

A condition is the combination of a query expression and a set of roles; both of these parts are optional and any missing part is considered "true". An empty object, thus, represents an "always true" condition (similar to true in cs:required.roles).

  • Roles are defined by the property roles (role is also accepted) with an array of strings as a value
    • This condition part is true if the request has at least one role from the set
    • This is similar to the values in cs:roles.required, but no special boolean values are allowed here
    • An empty array is still allowed and it is never checked for a match; such a condition is, however, not particularly useful
  • A query is defined by the property query, its value is always string

Conditions are always evaluated in the order they are declared, until one of them succeeds or until the end of the list is reached. This is important for performance reasons: "roles-only" conditions must be declared first in order so that the unnecessary queries are skipped.

When the schema contains both cs:roles.required and cs:permissions with the same operations, roles from cs:roles.required are automatically converted to a "roles-only" condition and inserted at the beginning of the condition list. It is not allowed to have true or false, a special value in cs:roles.required, and a non-empty list of conditions for the same operation in cs:permissions.

Default roles are used only for operations they are neither in cs:roles.required nor in cs:permissions.

How to represent special cases:

  • The operation is always allowed: and array with an single empty condition
  • The operation is never allowed: an empty array (no condition)
    • Note that the permission group model is still applied
{
    "$comment": "this entity is publicly visible by everyone, but cannot be used by anyone",
    "cs:permissions": {
        "READ": [{}],
        "UPDATE": []
    }
}

Permission Group Model

Permission groups are censhare assets of the type module.oc.permission-group. and represent groups of users that share permissions. User assets (e.g. asset type Account) are assigned to permission groups by placing asset reference features of type censhare:module.oc.permissions.granted on them pointing to the groups. Similarly, the feature censhare:module.oc.permission.group-ref can be placed on assets to indicate that users that share the referenced permission group have access to it. Note that all involved assets need to be tagged with an output channel, that is configured in the data store configuration.

When using the censhare Online Channel compatibility mode, entities without any permission group assigned are publicly available. It can be enabled by setting the property "cs:permgroups.oc-compatible" on the root level of a schema to true.

Permission groups only grant read operations by default. The create, update and delete operations can be also granted on a per schema basis by setting the property "cs:permgroups.cs:permgroups.writable" to true.

Note that the user management is not part of the Headless CMS itself. Among the key properties of the permission groups based authorization model are:

  • Fine-grained, allows access control to single entity instances
  • Only applicable for entity endpoints
  • Requires authentication method that uses user entities (censhare assets)

Permission Checking Algorithm

For each operation on a single entity, the permission check is done in the following order:

  1. Determine the required role, as declared in the entity schema (or use the default)
  2. If the request has a required role (or no role is required for this operation), execute the operation
  3. If the request has no authenticated user, block the access with a 403 Forbidden
  4. Read the set of the assigned group from the user's entity
  5. Read the set of the assigned groups from the entity
  6. If the intersection of both sets is empty, block the access with a 403 Forbidden; otherwise execute the operation

Listing endpoints (the full list and the queries) works in a slightly different way: authenticated users never get a 403 Forbidden from these endpoints, they instead receive a filtered list with all entities they have access to (even if this list is empty). The precise algorithm is similar to the following steps:

  1. Determine the required role for the READ operation, as declared in the entity schema (or use the default)
  2. If the request has a required role (or no role is required for this operation), execute the full query and return the result
  3. If the request has no authenticated user, block the access with a 403 Forbidden
  4. Read the set of the assigned group from a user's entity
  5. Add a query condition that the entity has at least one assigned group from this set
  6. Execute the query and return the list

Authorization Providers

The Headless WCMS configuration must contain a list of authorization providers as child of the authelement. On each request, these providers are executed in order to provide roles and authenticated user. Roles are always collected from all providers, but the authenticated user is only one - from the first provider that returns one, this is why order is important. A configuration looks similar to the example below:

<?xml version="1.0" encoding="UTF-8"?>
<dictionary>
  <factorypid>com.censhare.oc.hcms.service.impl.HeadlessCMSServiceImpl</factorypid>
  <instanceid>sample</instanceid>
  <property>
    <key>config</key>
    <Xml>
      <config version="1">
        <datastore name="sample-db"/>
        <schemaregistry resourcekey="sample-schema" outputchannel="root.hcms."/>
        <hostmappings>
          <hostmapping name="sample"/>
        </hostmappings>
        <api/>
        <auth>
          <basic>
            <user name="system" password="abcd">
              <role>*</role>
            </user>
          </basic>
          <jwt>
            <hmac secret="1234"/>
          </jwt>
          <range start="127.0.0.1" end="127.255.255.255">
            <role>rw</role>
            <role>schema-ro</role>
          </range>
        </auth>
      </config>
    </Xml>
  </property>
</dictionary>

Disable Security Authorization Provider

  • disable-security provides all roles and no user
  • Intended mainly for development

Example:

<disable-security/>

Basic Authorization Provider

  • basic provides HTTP basic authentication using users and passwords with a set of roles that are specified directly in the service configuration
  • Intended mainly to authenticate intermediate services and for development and testing
  • Does not support permission groups
  • "*" can be used as a placeholder for all groups

Example:

<basic>
  <user name="system" password="abcd">
    <role>*</role>
  </user>
  <user name="dev" password="abcd">
    <role>schema-rw</role>
    <role>schema-ro</role>
  </user>
  <user name="content" password="abcd">
    <role>rw</role>
  </user>
  <user name="user1" password="abcd"/>
</basic>

JSON Web Token Authorization Provider

  • jwt accepts Authorization: Bearer with JSON Web Token (JWT).
  • Supports HMAC with plain text key and RSA/EC with a PEM encoded public key
  • Special claim roles contains list of roles
  • Subject claim (sub) is expected to be id of the user entity (asset id)
  • Both claims are optional (but any useful JWT token has at least one)

Example for HMAC

<jwt>
  <hmac secret="abcd"/>
</jwt>

Example for RSA

<jwt>
  <pem>
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQ0dOofkxscy5pEZMrk2QqAQ/q
H4kBuLCBEs6rYETBuiAxoxLUdfQlMhJCHi4cKfm0svVVDdlCMlWppWb7jjq0kxPu
pU5fFZ0nv9m0cyUV9KBHTXplsJeQGeI45c35lk/rmuBaklA5TYkOqKcVQREG4Yxj
WNz1abp4qzMv6jjy5QIDAQAB
-----END PUBLIC KEY-----
  </pem>
</jwt>

IP Authorization Provider

  • ip assigns roles based on client's IP address
  • Accepts either IPv4 or IPv6 addresses per range
  • No user authentication

Example:

<range start="127.0.0.1" end="127.255.255.255">
  <role>rw</role>
  <role>schema-ro</role>
</range>
<range start="127.0.0.1" end="127.255.255.255">
  <role>rw</role>
  <role>schema-ro</role>
</range>