MarkLogic Security Model

Roles, Privileges, Permissions, and Amps

personClever Llamas
CleverLlamasMinimum Llamaverse Version: 2
databaseMinimum MarkLogic Version: 6

MarkLogic security is easiest to reason about when you keep three concepts clearly separated:

  1. Roles group capabilities.
  2. Privileges control whether protected actions may execute.
  3. Document permissions control who may read, update, insert, execute, or node-update specific documents.

Amps are a fourth concept layered on top: controlled privilege escalation for one specific function in one specific module.

If you mix these ideas together, security behaviour feels mysterious. If you keep them separate, the model is predictable and auditable.

The examples in this article assume the llamaverse (v2.0+) is deployed. The llamaverse sample data is freely available from github.com/cleverllamas/llamaverse — see the llamaverse article for full setup instructions.

Roles: Your Primary Security Building Block

Roles are the unit of assignment for users. Users should not receive privileges ad hoc; they should receive roles that represent clear responsibility boundaries.

In practice, small role sets keep administration manageable. Some systems use explicit role inheritance; others, like the current llamaverse setup, assign a small combination of focused roles directly to the user.

The current llamaverse security configuration is small enough to view directly as a role model. That is more useful here than a generic sec:create-role() example because it shows how real users, roles, privileges, and URI scope fit together.

This is intentionally compact:

  • cleverllamas-llama gets the reader role.
  • cleverllamas-mother gets both the reader and writer roles.
  • The writer role carries the execute privileges and the /cleverllamas/llamaverse/ URI privilege needed for broader maintenance work.
  • The reader and writer roles also contribute the default permission shape seen on llamaverse content documents.

That is the pattern to copy: keep roles small, name them after responsibility, and make the diagram of who gets what easy to explain in one minute.

Privileges: Guarding Protected Operations

Privileges control whether protected operations may run. Most application security designs rely heavily on execute privileges and explicit assertions in code.

URI privileges follow the same underlying mechanism. Internally, MarkLogic still performs a privilege check against the caller's effective roles; the difference is that the check is triggered by URI rules (for example, protected URI patterns) rather than by directly asserting an execute privilege in your code.

In practice, this means URI privileges are not a separate security model. They are another way to invoke the same privilege-evaluation engine, with URI scope providing the guard condition.

'use strict';

// Protect sensitive operations with a custom execute privilege.
// Call xdmp.securityAssert before performing the operation.

function exportLlamaData() {
  xdmp.securityAssert('http://example.com/privilege/llama-export', 'execute');

  return cts.search(
    cts.collectionQuery('wild-llamas')
  ).toArray().slice(0, 10);
}

exportLlamaData();

That pattern does two things well:

  • It keeps protection close to the operation.
  • It fails early and clearly when callers are under-privileged.

Document Permissions: Data-Level Access Control

Document permissions are separate from execute privileges. A caller can pass privilege checks and still fail because the target document does not grant the required capability.

The read-only introspection query below shows the real role/capability map on one wild-llamas document:

xquery version "1.0-ml";
declare namespace sec = "http://marklogic.com/xdmp/security";

(: Read-only introspection: show one document URI and its role/capability map. :)

let $uri := fn:head(cts:uris((), (), cts:collection-query("wild-llamas")))
return (
  concat("uri:", $uri),
  for $perm in xdmp:document-get-permissions($uri)
  return concat(
    xdmp:role-name(xs:unsignedLong($perm/sec:role-id)),
    ":",
    string($perm/sec:capability)
  )
)
uri:/cleverllamas/llamaverse/raw/wild-llamas/llamas/00048384-cb13-4557-805e-a6b4e57f7eab.json
cleverllamas-llamaverse-reader:read
cleverllamas-llamaverse-writer:insert
cleverllamas-llamaverse-writer:node-update
cleverllamas-llamaverse-writer:update

This output is a useful reminder that permissions are explicit metadata. They are not inferred from role naming conventions or module location.

Execute Privileges vs Document Permissions

This distinction is where many production bugs begin.

  • Execute privilege answers: "May this caller run this protected function?"
  • Document permission answers: "May this caller perform this capability on this specific document?"

Both checks must pass for write workflows.

'use strict';

// Execute privilege and document permissions are separate checks.
// Both must pass for a write operation to succeed.

function updateLlama(uri, replacementDoc) {
  xdmp.securityAssert('http://example.com/privilege/llama-update', 'execute');
  xdmp.nodeReplace(fn.doc(uri), replacementDoc);
}

updateLlama('/cleverllamas/llamaverse/raw/wild-llamas/llamas/sample.json', { name: 'Sample' });

Treat these as independent gates, not alternative gates.

Amps: Controlled, Narrow Elevation

Amps allow a specific function in a specific module to run with additional privileges. They are powerful and risky.

Use amps only when you can prove a clear need and keep the amped function narrowly scoped with strict input validation.

One subtle point matters in production: when code crosses an execution boundary, amped roles are not something you should treat as ambient background state. For the eval/invoke family, you can choose whether called code should continue with inherited amp elevation or deliberately drop it. That means amp propagation should be an explicit design choice, not an accident.

This is especially relevant for:

  • xdmp:eval()
  • xdmp:invoke()
  • xdmp:invoke-function() because it uses xdmp:invoke()-style options
  • transform and XSLT-style execution paths that run through the same broader execution-boundary model

If you need the called code to run without inherited amp elevation, set that intent explicitly with the relevant options instead of assuming the callee will behave like the caller. The same caution applies when you move through invoke-style or XSLT-style boundaries: treat amp carrying as something to decide and document.

Also add a caller-side security assertion inside the amped function. An amp controls which function is elevated, but it does not automatically prove that the caller came through the intended application path. A defensive assertion gives you an explicit allow condition before privileged work runs.

In practice, assert one narrow prerequisite such as:

  • a specific execute or URI privilege required for entry,
  • or a dedicated application role expected to invoke the function.
xquery version "1.0-ml";
import module namespace sec = "http://marklogic.com/xdmp/security"
  at "/MarkLogic/security.xqy";

(: Amp template for one specific function.                                    :)
(: Keep the function narrow and grant the minimum required privilege.         :)

sec:create-amp(
  "http://example.com/audit",
  "log-event",
  "/lib/audit.xqy",
  xdmp:database("Security"),
  xdmp:privilege("unprotected-collections", "execute")
)

A safe amp pattern usually has all of these properties:

PropertyWhy it matters
One narrow functionReduces blast radius
Minimal privilege setLimits escalation scope
No dynamic code executionPrevents privilege abuse vectors
Strict input validationStops malicious or malformed inputs
Caller assertion inside amped functionEnforces intended invocation path before elevation logic
Explicit audit loggingImproves incident traceability

Container Security: Platform Boundary vs Database Boundary

Container hardening is important, but it does not replace MarkLogic security controls.

  • Container and orchestrator controls protect runtime boundaries: image provenance, pod or container isolation, network policy, secret handling, and who may deploy or exec into workloads.
  • MarkLogic controls protect data and protected operations inside the database: roles, privileges, amps, and document permissions.

Use both layers together. A hardened container with weak MarkLogic roles is still risky; a strong MarkLogic model in an over-permissive cluster is also risky.

For containerised deployments, verify all of the following:

  1. Application services authenticate as least-privilege MarkLogic users, not shared admin-like users.
  2. Credentials and certificates come from secret stores, not image layers or source control.
  3. Management endpoints are network-restricted and not exposed broadly.
  4. Role and privilege mappings are versioned and reviewed alongside deployment manifests.
  5. Audit logs from both platform and MarkLogic are retained and correlated for incident analysis.

Common Security Mistakes

This section is operational guidance derived from how MarkLogic security features behave in practice. The underlying mechanics are factual MarkLogic behaviour; the framing here is the recommended design interpretation, not a verbatim product rule table.

MistakeConsequenceBetter approach
Granting broad privileges directly to many rolesPrivilege creep and hard-to-audit accessUse layered role hierarchies and least privilege
Using amps for general utility functionsHigh-risk privilege abuseAmp only narrow, validated functions
Assuming an amp alone proves caller intentUnintended invocation paths may reach elevated logicAdd explicit caller privilege or role assertions in the amped function
Treating document permissions and execute privileges as interchangeableUnexpected permission failuresModel them as separate checks
Skipping periodic permission auditsDrift and hidden over-permissioningRun scheduled URI and permission audits
Using shared "admin-like" app roles for convenienceExcessive privilege concentrationSplit operational and application roles clearly

Practical Audit Checklist

Use this checklist in every security review:

  1. Verify each application role has a single, clear purpose.
  2. Confirm execute privileges map to explicit protected operations.
  3. Inspect document permission templates for sensitive content classes.
  4. Review amp definitions and remove any broad or obsolete entries.
  5. Test least-privilege user paths for both read and write workflows.
  6. Record findings and remediation actions as part of release governance.

Security quality comes from repeatable discipline, not one-time setup.

Need Some Help?


Looking for more information on this subject or any other topic related to MarkLogic? Contact Us (info@cleverllamas.com) to find out how we can assist you with consulting or training!