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
Marketingand Jane see the report inCreateSearchAsUserAsyncresults. - Other users get the report omitted (not a permission-denied — silently absent).
- Admin scripts using
CreateSearchAsyncstill 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
reportdoesn't restrict theAuthornode it links to — they need their own ACL. - Updates don't auto-revoke. Running
AddOrUpdateon an existing node doesn't clear its restrictions. CallRemoveAccessFromTeam/RemoveAccessFromUserexplicitly when the source revokes access. Protectedschemas need ACLs. If you mark a schema protected but the connector forgets to callRestrictAccessTo*, the data is ingested but nobody can see it.
Cross-links
- Permission-aware search — query-side enforcement with
CreateSearchAsUserAsync. - Access control model — admin-side reference.
- Schemas — marking a schema protected.
- Ingestion — where the restriction calls usually live.