Document access

Chapter 4 — Integrity and policy selection

Reject altered packages and select independent policies with separate sessions.

Continue from Chapter 3 — Evaluation and sharing. Keep the declarations, policy files and helpers from the earlier chapters.

Verify that an altered package is rejected

The manifest check already runs at startup. To observe it, change a comment in policies/doc_access.dl without updating manifest.json, then run the complete program again. In both languages, loading must fail before any access decision is printed. This is a package error, not an ordinary DENY from evaluating the rules.

Change to the package or startupWhy loading must stop
Skip domain registrationThe named vocabulary is not available.
Rename or remove the policy fileThe manifest's source file cannot be read.
Edit the source without updating its digestThe file no longer matches the reviewed package.
Use invalid Datalog with its correct new digestMatching bytes do not make an invalid policy valid.

Restore the original file to continue. After an intentional policy update, compute its new digest during packaging or review—not automatically at runtime just to make a mismatch disappear. Neither the request helper nor the application's compiled code needs a duplicate of the policy source.

One policy or a set of policies?

A policy is one Datalog program: it may contain several rules and policy facts. A policy set groups independent programs for loading and selection; it does not merge their rules or their derived facts.

Yes, you can keep a session for policy 0 and another session for policy 1 open at the same time. Let us build that case: the existing policy accepts ownership or sharing; the new policy accepts ownership only. Both still reject blocked users and require a sensitivity level of at least 3.

Write the second policy file

Keep policies/doc_access.dl exactly as written in the policy section. Save this complete second program as policies/owners_only.dl, again with UTF-8, LF line endings and one final newline:

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

/* ownership is required; sharing alone is not enough */
allow(User, Doc) :-
    owns(User, Doc),
    not(blocked(User)),
    sensitivity_level(User, Level),
    Level >= 3.

This file has its own blocked("mallory"). fact and its own allow rule. It does not inherit either from the first file. The shared_with predicate remains declared in the common domain, but this second policy has no rule that grants access from sharing. No new domain or atom declaration is needed.

Package both policies in one manifest

Save the following as policies/manifest-two.json. This separate manifest leaves the one-policy example above runnable unchanged:

CODE
policies/
├── doc_access.dl
├── owners_only.dl
├── manifest.json       # original, one policy
└── manifest-two.json   # new, two policies

The two digests below match the exact files shown in this tutorial. If you edit either file, compute and review its new digest with the commands in the manifest chapter; each manifest entry has its own sha256.

CODE
{
  "policy_set_id": "documents",
  "policy_set_version": "2",
  "manifest_version": "1",
  "default_profile": "enforce",
  "created_for": "document-access-tutorial",
  "strict_loading": true,
  "fail_closed": true,
  "capabilities": [],
  "policies": [
    {
      "policy_id": "doc_access.main",
      "domain": "doc_access",
      "file": "doc_access.dl",
      "sha256": "64deaed81401b21788260e73452fe424a963dababe7e142aac7d5d73bbd1bd12",
      "mode": "enforce",
      "enabled": true,
      "description": "Ownership, sharing and sensitivity",
      "queries": [
        {
          "name": "allow",
          "arity": 2
        }
      ]
    },
    {
      "policy_id": "doc_access.owners_only",
      "domain": "doc_access",
      "file": "owners_only.dl",
      "sha256": "f03889630918c42cdb1a4fc39dd5454a080fd2a28ed4e3f30682839b96b6eacb",
      "mode": "enforce",
      "enabled": true,
      "description": "Ownership only, with block and sensitivity checks",
      "queries": [
        {
          "name": "allow",
          "arity": 2
        }
      ]
    }
  ]
}

With both entries enabled in this order:

  • Index 0 selects doc_access.main, loaded from doc_access.dl.
  • Index 1 selects doc_access.owners_only, loaded from owners_only.dl.

These are positions in the loaded set, not policy names and not the policy_set_version. Disabled entries are skipped; changing the enabled entries or their order can change the indices. Select from the trusted manifest and check the loaded count.

Load once and prepare two sessions

Loading manifest-two.json loads both programs. Creating a session with index 1 then selects the second program; it does not load another file or replace the session for index 0.

For this separate example, reuse the domain declarations. In C, also reuse check_access() and use the new main() below instead of the complete program’s main(). Do not use the earlier load_policy() helper: it deliberately prepares only policy 0. The Python example below supplies and solves the request directly, without that helper.

Both sessions are created before either request is evaluated. The input is the same in both cases: Carol has a sensitivity level of 3 and a document shared with her, but no ownership fact.

CODELoad two policies and create two sessions
int main(void) {
    maelys_datalog_policy_t *policies = NULL;
    maelys_datalog_session_t *normal_session = NULL;
    maelys_datalog_session_t *strict_session = NULL;
    maelys_datalog_diagnostic_t diagnostic = MAELYS_DATALOG_DIAGNOSTIC_INIT;
    size_t count = 0;
    int exit_code = 1;

    maelys_datalog_status_t rc = maelys_datalog_domain_register(&domain);
    if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
    rc = maelys_datalog_policy_load_manifest(
        "policies/manifest-two.json", MAELYS_DATALOG_PUBLIC_ALLOW_NONE, &policies, &diagnostic);
    if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
    rc = maelys_datalog_policy_count(policies, &count);
    if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
    if (count != 2u) {
        fprintf(stderr, "Expected two enabled policies; loaded %zu\n", count);
        goto cleanup;
    }

    /* Prepare both sessions from the same loaded set. */
    rc = maelys_datalog_session_create(policies, 0u, &normal_session);
    if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
    rc = maelys_datalog_session_create(policies, 1u, &strict_session);
    if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;

    printf("loaded policies: %zu\n", count);
    /* Same request: Carol, shared roadmap.pdf, sensitivity level 3. */
    printf("policy 0 (ownership or sharing): %s\n",
        check_access(normal_session, "carol", "roadmap.pdf", 3, 1)
            ? "ALLOW" : "DENY");
    printf("policy 1 (owners only): %s\n",
        check_access(strict_session, "carol", "roadmap.pdf", 3, 1)
            ? "ALLOW" : "DENY");
    exit_code = 0;

cleanup:
    if (rc != MAELYS_DATALOG_STATUS_OK)
        fprintf(stderr, "policy selection: %s: %s\n",
            maelys_datalog_status_name(rc), diagnostic.message);
    if (strict_session != NULL) (void)maelys_datalog_session_free(strict_session);
    if (normal_session != NULL) (void)maelys_datalog_session_free(normal_session);
    if (policies != NULL) (void)maelys_datalog_policy_free(policies);
    return exit_code;
}

Run from the directory containing policies/, using the same C build or Python environment as before. Both programs print:

CODE
loaded policies: 2
policy 0 (ownership or sharing): ALLOW
policy 1 (owners only): DENY

Policy 0 derives allow("carol", "roadmap.pdf") through its sharing rule. Policy 1 cannot derive it: Carol has no owns fact, and that policy has no sharing rule. The same query and inputs produce different answers because the selected rules differ. This is an ordinary policy denial, not a loading or query error.

Each session remains fixed to its selected policy and can be reused for later requests. A session allows one live result at a time; release that result before solving another request on the same session. Two separate sessions do not share that result slot. Keeping both open does not imply that they may be used from arbitrary threads; the example evaluates them sequentially.

No automatic combination: an ALLOW from policy 0 does not override a DENY from policy 1. The application decides which policy governs a request, or explicitly requires both to allow. Index 2 is invalid for this two-policy set: C returns MAELYS_DATALOG_STATUS_NOT_FOUND; Python raises IndexError.

Can one inline string contain two independent policies?

No. Every rule in an inline string belongs to the single policy named by the loader's policy_id argument. Adding another rule—even one with a different head predicate—does not start a second policy. There is no multi-policy delimiter in the inline source language.

Two separate inline calls instead return two separate loaded objects, each containing one policy. Each object's only valid index is 0: indices are local to a loaded object, not a counter shared across loader calls. Use a manifest when you want several independent policies in one loaded set.

What you have learned

Keep this model

Verify packages and select independent policies
  • Loading failure is not an ordinary denial

    An altered policy with the old digest is rejected before evaluation. Protect the manifest as well: changing both the file and its expected digest defeats that integrity check.

  • A policy set contains separate programs

    The two-entry manifest loads the sharing policy and the owners-only policy. Sessions at indices 0 and 1 keep those programs separate; their rules are never merged.

  • The same facts can produce different decisions

    Carol is allowed by the sharing policy and denied by the owners-only policy. Each session has its own result, and that result must be released before solving again on that session.

  • Indices belong to the loaded set

    An inline load contains one policy at index 0. Two separate inline loads each have their own index 0; a manifest is how this tutorial packages multiple policies in one set.

Continue