Skip to main content
Skip table of contents

HCMS Authorization

Explains the authorization management at the Headless CMS REST interface.

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/v2.1/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:

JSON
{
    "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:

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

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

JSON
"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
  • Required variables are defined by property variables-all or variables-any; both are optional list of strings.
    • Each string item is a variable name as used by queries or request logging. Variable is considered present if any non-null value is available (even if it is an empty string).
    • "variables-all" requires that all the specified variables are present. Even one single non-present variable means that the whole condition is false (and the query part, if any, is skipped).
      • Several aliases are also accepted: "all-variables", "variables"
    • "variables-any" requires that at least one the specified variables is present. If none of them is present, the whole condition is false (and the query part, if any, is skipped).
      • Several aliases are also accepted (mostly to allow for typos): "variable-any", "any-variable", "any-variables"
    • Instead of an array, the value can be just a string. This is the same as single-element array, and it means that just one single variable is required. There is no difference between variables-all and variables-any in this case.
  • A query is defined by the property query, its value is always string
    • At the time of evaluation, this query is executed and the if the asset is part of the result, this condition succeeds.
      • Implementation note: when checking single asset, executed query is actually conjunction (AND) of the specified query and condition for asset ID.
    • Queries do not work for CREATE operation, because the entity is not searchable at the time of evaluation; result is automatically true.
    • Note that if the query uses some query variable, it is generally good idea to also add this variable as an item in "variables-all" (so the query evaluation would not be even attempted if the value is not available).

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
JSON
{
    "$comment": "this entity is publicly visible by everyone, but cannot be updated by anyone",
    "cs:permissions": {
        "READ": [{}],
        "UPDATE": []
    }
}

Note: it is generally possible to declare cs:$conditions (see "Property-level Permissions") at the root level, with the same conditions.
This is strongly discouraged because of different behavior:

  • property-level conditions are the same for READ, UPDATE, CREATE
  • DELETE operation is not affected at all
  • roles from cs:roles.required are not part of the evaluation
  • there is no special handling in listings
    • entities with no satisfied root condition are found, but their serialized value is just empty JSON object!
    • performance is significantly worse, because property-level conditions are evaluated separately for each entity in listing
  • in single-entity endpoints (GET, PUT), failed condition check does not cause HTTP error code - it just silently uses empty document
  • unlike schema permissions, property-level conditions are evaluated even when the schema is inlined in other schema
  • property-level conditions cannot be overridden by permission groups

Permission Group Model

Warning: Since HCMS version 4.0, following mechanism is considered legacy and disabled by default. Please do not use it except in cases where compatibility is required.

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)

Enabling

To use it in a specific schema, it must contain special property "cs:permgroups.ignore": false (note that the value is counter-intuitively reversed).

In earlier version (1.x, 2.x, 3.x), false used to be the default value and this mechanism had to be explicitly disabled by setting the property "cs:permgroups.ignore" to true. Please note that this change of behavior is part of the schema compilation and is not influenced by the version path component (ie /hcms/v1.0/ vs /hcms/v4.0/).

Permission Checking Algorithm

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

  1. Determine all permission conditions:
    1. If cs:required.roles is present and contains this operation, evaluate the value as the first condition
    2. If cs:permissions is present and contains this operation, evaluate the conditions
    3. If neither of above applies, check the default roles for this operation
  2. If at least one condition in previous step evaluates true, 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).
Unauthenticated request can get 401 or empty list too, see below.
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. Determine conditions for READ operation:
    1. If cs:required.roles is present and it contains READ, evaluate the value as the first condition
    2. If cs:permissions is present and contains this operation, evaluate all the non-query conditions
      • Note: any condition with query automatically fails here, but it is properly handled in following steps.
    3. If neither of above applies, use the default = true
  3. If the request has a required role (or no role is required for this operation), execute the full query and return the result
  4. If the request has no authenticated user, no query is executed and the request returns empty list or 401. The behavior is quite confusing before 4.3 and configurable after that. See below for detail.
  5. Add one disjunction (OR) operand to the query, with following disjuncts:
    1. Read set of assigned group from user's entity; add condition that entity has at least one assigned group from this set
    2. If cs:permissions is present and contains this operation, add all applicable query conditions
      • Only those that either have no roles part or the roles part is satisfied.
  6. Execute query and return list

Note that the special "shortcut" case of unauthenticated user is used only when there is no custom query-based rule.
The exact result in that case is configuration since version 4.3, which added new configuration option unauthorized-list (attribute of the <api> element) with following values:
- compatibility: the only available behavior before version 5, result depends on the value of "cs:permgroups.ignore" (or its default value, which changed in 4.0, see above):
- When true (permission groups ignored, default for 4.0 and later), the result is empty list.
- When false (permission groups used, default before 4.0), the result is 401 Unauthenticated.
- empty: always empty list
- 401: always 401
- 404: always 404 with no such schema:  - this allows API to hide existence of this schema

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
<?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>
          <ip>
              <range start="127.0.0.1" end="127.255.255.255">
                <role>rw</role>
                <role>schema-ro</role>
              </range>
          </ip>
        </auth>
      </config>
    </Xml>
  </property>
</dictionary>

Disable Security Authorization Provider

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

Example:

XML
<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:

XML
<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.
    • Key can be present directly in the configuration file, via <hmac> and <pem> elements.
    • Both can be used at the same time, and each can be even used multiple times with different secrets/keys.
      • Note that this setup is considered advanced and isn't supported by the commandline tool (direct editing of configuration XML is needed).
    • It is also possible to configure JWKS URL that is used to obtain keys dynamically.
      • Note that this setup is also considered advanced and isn't supported by the commandline tool (direct editing of configuration XML is needed).
    • Note that when validating token, all configured secrets and keys are tried in the order they are present in the configuration, until first success. Using many keys at once might have negative impact on performance!
  • Special claim roles contains list of roles
    • Some of those roles might be ignored by using "blacklist" configuration element(s). This element is considered advanced and cannot be set via commandline tool (direct editing of configuration XML is needed).
    • Name of this claim can be change in the configuration, attribute roles-claim-name.
  • 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)
  • Token must contain standard expiration claim (exp); tokens with infinite validity are not accepted.

Example for HMAC

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

Example for RSA

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

Example for JWKS URL (OpenID)

XML
<jwks url="https://www.googleapis.com/oauth2/v3/certs"/>

In addition to the "claims" token, it is also possible to configure automatic assignment of roles based on any claim (or even without any claim, just based on the fact that token is valid). This feature is useful when the authority issuing JWT tokens cannot be fully configured to issue project-specific tokens.
This is especially useful when the token is generated by an OpenID server, which cannot be configured to provide roles claim directly.

XML
<jwt>
  <jwks url="https://keycloak.censhare.com/auth/realms/someRealm/protocol/openid-connect/certs"/>
  <!-- without any claim- attributes, this role is always added (as long as the request has valid JWT token) -->
  <role>logged</role>
  <!-- any claim can be used, with value matched by regular expression -->
  <role claim-name="usrGrp" claim-regex="CONTENT-.*">rw</role>
  <!-- is is also possible to map one role to another - this might simplify cases when roles are actually user groups -->
  <role claim-name="roles" claim-regex="CONTENT-.*">rw</role>
  <!-- asterisk (wildcard) value is also supported, just like in other providers  -->
  <role claim-name="admin" claim-regex="true">*</role>
</jwt>

Note that <role> element is considered advanced and isn't supported by the commandline tool (direct editing of configuration XML is needed).
See HeadlessCMSConfiguration.xsd

External session support

Despite the fact that HCMS is completely stateless, it can participate in a cookie-based session provided by an external system. This external system must provide a special HTTP endpoint ("webhook") that confirms if the specified cookie value represents a valid session, and in the case it does, what kind of authentication / authorization this session provides.

This webhook is invoked upon each request (with possible short-term cache to improve performance) and can provide roles or variables in a way similar to JWT tokens.

The complete documentation is provided in a separate document

IP Authorization Provider

  • ip assigns roles based on client's IP address
  • Each range has starting and ending address and both of them are included in the range.
  • Accepts both IPv4 or IPv6 addresses (but no hostnames)
    • Note: IPv4 is internally represented as its IPv6 equivalent (::ffff:<ipv4>).
  • Multiple ranges can match; in that case, roles are merged.
  • No user authentication

Example:

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

Property-level Permissions

Custom conditions can be used to hide some parts of the data entity ("prune" the tree) in runtime, by supplied code. Each property can have several conditions specified; they are evaluated during export and import and if all of them are negative, this property (and all sub-properties, if any) is completely ignored.

Conditions can be specified on any property at any level, regardless of mapping - even data-less objects and constant properties can have condition. It is also possible to define conditions at root level, but that is almost never a good idea (cs:permissions is the correct way to defined schema permissions; for differences, see documentation above).

Conditions are defined by property "cs:$conditions", value is array of conditions.
Each condition is either JSON object or string with JSON pointer that evaluates to JSON object. The pointer support allows simple definition of shared conditions used by many properties (and maybe even schema permissions).

HCMS conditions are the same as used in "Advanced Permission definition" (see above), supporting both query and roles parts (role is also accepted as variant of roles). Object that contains neither of these represents always-true condition (this is not really useful for property-level condition - it's better just to not define conditions at all).

Important note: conditions are evaluated only on export and import; queries always use the full schema.
This must be considered when using conditions for security purposes (sensitive data might be omitted from the payload, but they can be determined by carefully crafted queries).

Complex example from real-world project: "person" entity shown to everyone, but with name (and other contact info, omitted for brevity here) hidden for some users.
Authentication data (hashed password) available only for holders of special role (authentication server).

JSON
{
  "type": "object",
  "cs:asset.type": "person.account.",
  "cs:roles.required": { "READ": "r" },
  "cs:$$cnd": {
    "admin": {
      "role": ["custom:admin", "auth"]
    },
    "SA": {
      "role": ["custom:default", "custom:supply", "custom:demand", "custom:analyst"],
      "query": "accountCategory=\"supply\" | accountCategory=\"analyst\""
    },
    "demand-DA": {
      "comment": "demand, confirmed; user is demand or analyst",
      "role": ["custom:demand", "custom:analyst"],
      "query": "accountCategory=\"demand\""
    }
  },
  "properties": {
    "type": {
      "type": "string",
      "const": "contact",
      "cs:$const": true
    },
    "name": {
      "cs:$conditions": ["#/cs:$$cnd/admin","#/cs:$$cnd/SA","#/cs:$$cnd/demand-DA"],
      "cs:feature.key": "censhare:asset.name",
      "type": "string"
    },
    "accountCategory": {
      "type": "string",
      "enum": [
        "default",
        "analyst",
        "demand",
        "supply"
      ],
      "cs:feature.key": "custom:account-category"
    },
    "auth-data": {
      "cs:$conditions": [{ "role": ["auth"] }],
      "type": "object",
      "cs:feature.key": "censhare:address/censhare:address.user-type",
      "cs:feature.$value_property": "type",
      "properties": {
        "type": {"type": "string"},
        "login": {
          "cs:feature.key": "censhare:address.user-login",
          "type": "string"
        },
        "password": {
          "cs:feature.key": "censhare:address.user-password",
          "type": "string"
        }
      },
      "required": [ "type", "login" ]
    },
    "id": {
      "cs:feature.key": "censhare:asset.id",
      "type": "integer"
    }
  }
}    
JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.