Browse all documentation

Access control

Allow, deny, or filter operations with transaction-scoped Go rules.

Access decisions

A collection rule receives the actor, operation, target ID, submitted data, locale, and local API. Return ridu.Allow() or ridu.Deny() for any rule. Return ridu.Where(expression) only where Ridu can attach a document predicate to an atomic document read, version-history read, update, delete, or unlock query.

Keep reusable rules in an ordinary Go file beside your content model. This example allows anyone to read, requires a session to create, and builds an ownership predicate from the authenticated document ID.

content/access.go
package content

import (
  "github.com/riducms/ridu"
  "github.com/riducms/ridu/query"
  "github.com/riducms/ridu/store"
)

func publicRead(ridu.AccessContext) (ridu.AccessDecision, error) {
  return ridu.Allow(), nil
}

func signedIn(ctx ridu.AccessContext) (ridu.AccessDecision, error) {
  if ctx.Actor == nil {
    return ridu.Deny(), nil
  }
  return ridu.Allow(), nil
}

func ownDocuments(path query.Path) ridu.AccessRule {
  return func(ctx ridu.AccessContext) (ridu.AccessDecision, error) {
    if ctx.Actor == nil {
      return ridu.Deny(), nil
    }
    return ridu.Where(
      query.Equal(path, query.String(ctx.Actor.ID)),
    ), nil
  }
}

func actorHasRole(actor *store.Document, allowed ...string) bool {
  if actor == nil {
    return false
  }
  role, ok := actor.Values["role"].StringValue()
  if !ok {
    return false
  }
  for _, candidate := range allowed {
    if role == candidate {
      return true
    }
  }
  return false
}

Collection rules

Attach those rules to the collection operation they protect. The path supplied to ownDocuments is the stored relationship field, so an author can update or delete only rows whose author ID matches their own document ID.

content/posts.go
package content

import (
  "github.com/riducms/ridu"
  "github.com/riducms/ridu/field"
  "github.com/riducms/ridu/query"
)

func Posts() ridu.Collection {
  authorPath, err := query.NewPath("author")
  if err != nil {
    panic(err)
  }

  return ridu.Collection{
    Slug: "posts",
    Fields: []field.Definition{
      field.Text("title", field.Required()),
      field.Relationship("author", field.To("users"), field.Required()),
      field.Textarea("internalNotes"),
    },
    Access: ridu.CollectionAccess{
      Create: signedIn,
      Read:   publicRead,
      Update: ownDocuments(authorPath),
      Delete: ownDocuments(authorPath),
    },
    FieldAccess: postFieldAccess(),
  }
}

This example keeps Posts as a function because it performs fallible path construction before it can return the collection. Ordinary static collection definitions should be package variables.

Field access

Field rules return a boolean. A denied write is rejected; a denied read is redacted from the returned document. Presentation options such as field.ReadOnly() are not authorization.

Field-access map keys are authored field paths. postFieldAccess() is called by Posts() above, keeping the policy readable without hiding it in an anonymous collection literal.

content/field_access.go
package content

import "github.com/riducms/ridu"

func postFieldAccess() map[string]ridu.FieldAccess {
  return map[string]ridu.FieldAccess{
    "internalNotes": {
      Create: func(ctx ridu.FieldAccessContext) (bool, error) {
        return actorHasRole(ctx.Actor, "admin"), nil
      },
      Read: func(ctx ridu.FieldAccessContext) (bool, error) {
        return actorHasRole(ctx.Actor, "editor", "admin"), nil
      },
      Update: func(ctx ridu.FieldAccessContext) (bool, error) {
        return actorHasRole(ctx.Actor, "admin"), nil
      },
    },
  }
}

Here only administrators may create or update internalNotes; editors may read it. Omitting the Create rule would leave that write allowed by default, even if Update were restricted.

Read sibling values in a field rule

ctx.SiblingData contains the values beside the field currently being authorized. For a root field, that is the document’s root value map. For a nested field, Ridu narrows it to that field’s Group, Array row, or Block instance.

This collection makes a link URL visible to everyone unless the checkbox in the same row marks it as members-only:

content/pages.go
package content

import (
  "github.com/riducms/ridu"
  "github.com/riducms/ridu/field"
)

var Pages = ridu.Collection{
  Slug: "pages",
  Fields: []field.Definition{
    field.Array("links", field.Fields(
      field.Text("label", field.Required()),
      field.Text("url", field.Required()),
      field.Checkbox("membersOnly", field.Default(false)),
    )),
  },
  FieldAccess: map[string]ridu.FieldAccess{
    "links.url": {
      Read: func(ctx ridu.FieldAccessContext) (bool, error) {
        membersOnly, _ := ctx.SiblingData["membersOnly"].BooleanValue()
        if !membersOnly {
          return true, nil
        }
        return ctx.Actor != nil, nil
      },
    },
  },
}

The map key remains the authored path links.url. When Ridu evaluates a particular row, ctx.RuntimePath is concrete—for example, links.2.url—and SiblingData is that row’s label, url, and membersOnly values. It is a detached snapshot: changing it does not change the document. Use a hook when you intend to mutate submitted data.

See ridu.FieldAccessContext for the current value, document, original document, locale, and transaction-reusing Local API available alongside sibling data.