Access Control

Curiosity uses ReBAC (relationship-based access control) — permissions are edges in the graph between data nodes and the internal _User / _AccessGroup nodes. Connectors mirror source permissions onto the data at ingest time; the query side (endpoints, search) enforces them automatically via CreateSearchAsUserAsync.

For the query-time half of this pattern, see Permission-aware search.

Internal node types

Type Represents Created via
_User An individual user await graph.CreateUserAsync(login, email, firstName, lastName)
_AccessGroup A team / group await graph.CreateTeamAsync(name, description = null)

Both calls are idempotent — re-running just refreshes properties on the existing node. The internal types are protected: you can't delete them, but you can manage members and admins through the API.

Managing users and teams

var alice = await graph.CreateUserAsync("alice", "alice@example.com", "Alice", "Anders");
var bob   = await graph.CreateUserAsync("bob",   "bob@example.com",   "Bob",   "Boniface");
var tier2 = await graph.CreateTeamAsync("Tier-2 Support", "Hardware escalation team");

graph.AddUserToTeam(alice, tier2);
graph.AddUserToTeam(bob,   tier2);
graph.AddAdminToTeam(alice, tier2);    // Alice can manage Tier-2 members

// Convergent membership: revoke when the source revokes.
graph.RemoveUserFromTeam(bob, tier2);

Use connectorName to mirror upstream group memberships idempotently — RemoveUserFromTeam is the lever for permission revocations.

Restricting access on data nodes

Two methods, each with a corresponding RemoveAccessFrom* to revoke:

graph.RestrictAccessToTeam(reportNode, marketingTeam);
graph.RestrictAccessToUser(reportNode, individualWatcher);

// Convergent — call when the source revokes.
graph.RemoveAccessFromTeam(reportNode, marketingTeam);
graph.RemoveAccessFromUser(reportNode, individualWatcher);

Semantics:

  • No restriction at all → default for the node type. For unprotected schemas, that's "everyone in the workspace". For protected schemas, that's "admins only".
  • Any restriction present → only listed teams' members and listed users can see the node. Restrictions are additive ("A or B"), not intersected.

To enforce per-schema defaults, mark the node type Protected under Settings → Schema. Without that, even an empty restriction list allows everyone.

Restricting a file as it is uploaded

UploadFileAsync followed by RestrictAccessToTeam leaves the file visible to everyone until the restriction is committed. Pass the owners to the upload instead, and the server writes the file and its _OwnedBy edges together:

var finance = await graph.CreateTeamAsync("Finance");

await graph.UploadFileWithOwnershipAsync(stream, "budget.xlsx", "SharePoint", new[] { finance });

UploadFileToFolderWithOwnershipAsync and UploadFileWithIdentifierAndOwnershipAsync do the same for the folder and identifier uploads. A file that belongs to a record, such as an e-mail attachment, can be linked to it in the same write with UploadFileAsAttachmentAsync(stream, fileName, sourceName, parentNode). That writes only the _AttachmentOf / _HasAttachment edges, so restrict the file separately if it needs restricting. See Graph class.

Complete example

using var graph = Graph.Connect(/* endpoint, token, connectorName */);

// 1. Make sure team/user records exist.
var team = await graph.CreateTeamAsync("Marketing");
var jane = await graph.CreateUserAsync("janedoe", "janedoe@example.com", "Jane", "Doe");

// 2. Ingest the data node.
var report = graph.AddOrUpdate(new Report
{
    Id    = "RPT-Q4",
    Title = "Q4 Marketing Plan",
});

// 3. Mirror source ACLs.
graph.RestrictAccessToTeam(report, team);
graph.RestrictAccessToUser(report, jane);

await graph.CommitPendingAsync();

After this:

  • Members of Marketing and Jane see the report in CreateSearchAsUserAsync results.
  • Other users get the report omitted (not a permission-denied — silently absent).
  • Admin scripts using CreateSearchAsync still see everything, which is the right default for batch jobs.

Pitfalls

  • Reading from a connector token doesn't enforce ACLs. The connector role is intentionally permissive so ingestion can see what it needs to. Don't repurpose a connector token for user-facing queries.
  • Restrictions stick to the node, not the edge. Restricting report doesn't restrict the Author node it links to — they need their own ACL.
  • Updates don't auto-revoke. Running AddOrUpdate on an existing node doesn't clear its restrictions. Call RemoveAccessFromTeam / RemoveAccessFromUser explicitly when the source revokes access.
  • Protected schemas need ACLs. If you mark a schema protected but the connector forgets to call RestrictAccessTo*, the data is ingested but nobody can see it.
© 2026 Curiosity. All rights reserved.