Document access

Chapter 1 — Vocabulary and policy

Declare the domain and write the document-access rules.

Choose C or Python once: the code tabs stay synchronized throughout this tutorial. The Datalog policy, diagrams and expected decisions are shared. Both versions use the current opaque consumer API; the Python import is maelys_datalog, as in the quickstart. Use the Maelys Datalog 0.11.1 setup from the quickstart. In C, include <maelys/datalog.h> for the opaque API and <maelys/datalog_builders.h> for its convenience macros.

Declare and register the domain

A domain is the vocabulary that the application permits a policy to use: predicate names, their number of arguments and their roles, plus the strings allowed in policy source. First declare that vocabulary, then register it with the engine. Creating the table or Python list does not perform registration.

Predicates, atoms and facts

The domain declares two kinds of vocabulary: predicates name relationships, while policy atoms are the string constants that the policy is allowed to name explicitly. The API calls that second list atoms in both C and Python.

Look at blocked("mallory"). in our policy:

  • Predicate: blocked names a relationship with one argument. It belongs in the predicate declarations.
  • Policy atom: "mallory" is a string constant written in the policy. It belongs in the atoms list.
  • Fact: blocked("mallory"). states that Mallory is blocked. It belongs in the policy file.

Registering an atom does not create a fact. atoms = ["mallory"] permits the policy to mention that string; only the fact in the policy says that Mallory is blocked. A quoted string used inside a rule would need the same declaration.

The list is not a whitelist of runtime users or documents:

  • "alice", "bob", "carol" and "roadmap.pdf" arrive in request facts later, so they do not need to appear in this policy's atoms list.
  • User, Doc and Level are variables, not string constants.
  • 3 is an integer, not a string atom.

The manifest loader used here is strict: if "mallory" is missing from the registered atoms, loading the policy fails with an unknown atom diagnostic, before any request is evaluated.

In Datalog terminology, a logical atom also means a predicate application such as owns(User, Doc). Do not confuse that expression with the API's atoms list: this list contains strings, not predicate applications or facts.

Why blocked does not need atoms in the quickstart

The same statement, blocked("mallory"), can come from request data or from policy source. The two examples intentionally choose different origins:

QuestionQuickstartThis tutorial
Who says Mallory is blocked?The application, in the EDB for this request.The policy author, with blocked("mallory"). in the file.
How is blocked/1 declared?EDBPOLICY_FACT
Must "mallory" be in atoms?No: it arrives as runtime data, not as a constant in the policy source.Yes: the policy source explicitly names that string.
When does the block apply?When the request's complete input includes that fact.Whenever that policy is evaluated, without the application adding a block.

In the quickstart, C supplies MAELYS_DATALOG_ADD_FACT(edb, &diagnostic, "blocked", "mallory"); Python supplies edb.add_fact("blocked", ["mallory"]). Neither requires an atom declaration. In this tutorial, do not add that EDB call: blocked has a different declared origin and its fact is already in the policy.

The condition not(blocked(User)) works with either design: it tests absence for the bound user. It does not create blocked-user facts, and the registry declaration does not create them either.

Where the string is written matters, not whether the predicate is EDB or IDB. If a rule explicitly mentions blocked("mallory"), that literal needs an atom declaration even when blocked/1 is an EDB predicate. If the rule only mentions blocked(User), the variable needs no atom declaration. The checks do not restrict the names that the application may supply in runtime facts.

Declare the vocabulary

CODEDeclare predicates and policy atoms
#include <maelys/datalog.h>
#include <maelys/datalog_builders.h>
#include <stdio.h>
#include <string.h>

static const maelys_datalog_predicate_t doc_access_preds[] = {
    MAELYS_DATALOG_EDB("owns", 2),
    MAELYS_DATALOG_EDB("shared_with", 2),
    MAELYS_DATALOG_EDB("sensitivity_level", 2),
    {"blocked", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT},
    MAELYS_DATALOG_IDB_QUERY("allow", 2),
};
static const char *const atoms[] = {"mallory"};
static const maelys_datalog_domain_t domain = {
    "doc_access", doc_access_preds,
    sizeof(doc_access_preds) / sizeof(doc_access_preds[0]),
    atoms, sizeof(atoms) / sizeof(atoms[0]),
};

The four predicate kinds are:

KindWho provides itExample
EDBRuntime caller, per requestowns("alice", "doc.pdf")
POLICY_FACTPolicy source fileblocked("mallory")
IDBSolver derives itallow("alice", "doc.pdf")
QUERYMarks IDB as inspectable— (combined with IDB)

Register the domain

This is the call that makes doc_access known to the policy loader. Run it at application startup, before loading the manifest, not for every request.

CODERegister the domain
maelys_datalog_status_t rc = maelys_datalog_domain_register(&domain);
if (rc != MAELYS_DATALOG_STATUS_OK) {
    fprintf(stderr, "register domain: %s\n", maelys_datalog_status_name(rc));
    return 1;
}

The loader looks up this registered domain by name. It does not infer the vocabulary from the first policy it sees. The complete program shows the registration call; do not paste that call twice.

Write the policy

Create a policies/ directory and save the following as policies/doc_access.dl. Use UTF-8, LF line endings and one final newline:

DATALOG
/* mallory is always blocked */
blocked("mallory").

/* allow via ownership */
allow(User, Doc) :-
    owns(User, Doc),
    not(blocked(User)),
    sensitivity_level(User, Level),
    Level >= 3.

/* allow via sharing */
allow(User, Doc) :-
    shared_with(User, Doc),
    not(blocked(User)),
    sensitivity_level(User, Level),
    Level >= 3.

Three things to notice:

Two rules, one head. Both rules derive allow/2. The solver produces an allow fact if either rule's body holds. Having two rules is not redundant — each represents a different authorization path.

not(blocked(User)) is non-binding. blocked is evaluated as a filter. User must already be bound by owns or shared_with before the not literal is checked. You cannot write not(blocked(X)) without first binding X in a positive atom.

Level >= 3 is a comparison filter. Level must be bound by sensitivity_level(User, Level) before the comparison can fire. Comparisons never bind variables — they only constrain already-bound values.

What you have learned

Keep this model

Define the vocabulary before writing the rules
  • Predicates describe the facts

    A predicate declaration gives a name, an argument count and a role: request input, policy fact or rule-derived fact. QUERY permits the application to observe it.

  • Atoms are constants used in policy source

    Declaring mallory in atoms permits that name in the policy file. It does not create blocked("mallory"); the policy fact does that. Request strings are supplied separately.

  • Register before loading

    The application registers the document domain first. The loader can then check the policy against its declared predicates and source atoms.

  • Keep the access conditions together

    Ownership or sharing grants access only when the sensitivity level is at least 3 and the user is not blocked.

Continue