Groups

A group is a named set of principals. Name a group in an AuthorizationRule actor, and the rule applies to every member — so you grant a permission once and manage who holds it in one place.

Membership can come from a Group document you store on the host, the identity provider's roles, or the built-in groups. However it arises, it resolves to the group name used in actor, so a rule need not know where the membership came from.

The Group document

The platform-native group is an HTML Microdata item of type https://pagelove.org/Group, stored anywhere on the host. It carries a name and one member per email address. Membership is managed as data — editing the document is the whole operation.

<div itemscope itemtype="https://pagelove.org/Group">
  <meta itemprop="name" content="editors">
  <meta itemprop="member" content="alice@example.com">
  <meta itemprop="member" content="bob@example.com">
</div>

A request whose verified email matches one of the member values is granted the group's name (editors) as an actor.

Fields

Field Cardinality Description
name 1..1 The group name — the value an AuthorizationRule actor names to grant the group's members. A group with a blank name is ignored.
member 0..n A member's email address, matched exactly against the request principal's verified email.

Verified email only

A member is matched against the principal's email only when that email is verified — the OIDC email_verified claim must be true. An unverified or absent email is never a member, so it can never inherit a group's grants. This is the same trust gate as a verified-email actor.

Custom membership: subtype Group and override includes()

Membership is decided by the group's includes(email) method. The base Group's implementation is the exact-email match over member values described above — and because it is an ordinary schema method, you can define your own group type (a schema whose parent is https://pagelove.org/Group) and override includes() to implement any membership policy Sessel can express. A subtype group participates in authorization exactly like a base Group — its name grants as an actor — with or without member entries.

For example, a group that includes everyone at an email domain:

<div itemscope itemtype="https://pagelove.org/Schema">
  <meta itemprop="type" content="https://example.com/TeamGroup">
  <meta itemprop="name" content="TeamGroup">
  <meta itemprop="parent" content="https://pagelove.org/Group">
  <div itemprop="property" itemscope itemtype="https://pagelove.org/Property">
    <meta itemprop="name" content="domain">
    <meta itemprop="type" content="https://schema.host/Text">
    <meta itemprop="cardinality" content="1..1">
  </div>
  <li itemprop="property" itemscope itemtype="https://pagelove.org/Method">
    <meta itemprop="name" content="includes">
    <meta itemprop="returns" content="https://schema.host/Boolean">
    <li itemprop="parameter" itemscope itemtype="https://pagelove.org/Parameter">
      <meta itemprop="name" content="email">
      <meta itemprop="type" content="https://schema.host/Text">
    </li>
    <div itemprop="implementation" itemscope itemtype="https://pagelove.org/Sessel">
      <script itemprop="source" type="text/sessel">email.endsWith(self.domain)</script>
    </div>
  </li>
</div>

Store the schema under /system/schemas/, then store instances of it like any group:

<div itemscope itemtype="https://example.com/TeamGroup">
  <span itemprop="name">example-staff</span>
  <meta itemprop="domain" content="@example.com">
</div>

Every request whose verified email ends in @example.com is now a member of example-staff — no member list to maintain. A subtype that does not override includes() inherits the base exact-email behaviour.

Other sources of membership

A principal can also belong to a group without a Group document:

So actor editors matches whether that membership came from a Group document, an OIDC role, or was named directly.

How a group actor is matched

A group name sits at the same specificity tier as a role or a verified email — below an exact user name, above the wildcard *. The usual allow-list pattern therefore composes: a blanket * … Deny alongside a specific editors … Allow grants the group and no one else. See Conflict resolution for how overlapping rules settle.

<div itemscope itemtype="https://pagelove.org/AuthorizationRule">
  <meta itemprop="actor" content="editors">
  <meta itemprop="resource" content="/blog/*">
  <meta itemprop="method" content="POST">
  <meta itemprop="method" content="PUT">
  <meta itemprop="method" content="DELETE">
  <meta itemprop="action" content="Allow">
</div>

Membership beyond verified email

The built-in Group matches on verified email only. When you need membership keyed on something else — internal user ids, a service principal, a directory lookup — keep that membership in your own documents and expand it with a schema @read resolver on AuthorizationRule's actor property. The Group-based permissions recipe walks through it.

See also