# Technical Interfaces
Interface Layering
The ACL design uses two distinct interface layers that must not be conflated.
Domain interfaces — I<Entity>
Pure domain contracts with no external dependencies. They represent the logical model and can be used anywhere in the codebase regardless of the underlying data store.
Examples: IUser, IDossier, IDocument, IGroup
These interfaces express what an entity is. Collections and relationships are modelled as direct properties (e.g. Dossier.resources: Resource[]).
Neo4j entity interfaces — INeo4j<Entity>
Graph-database–specific contracts that extend or parallel the domain interfaces. They reflect the constraints and conventions of Neo4j: nodes have flat property maps, and relationships are not node attributes — they are edges in the graph.
Examples: INeo4jUser, INeo4jDossier, INeo4jDocument, INeo4jGroup
Important —
Dossier.resourcesvs. Neo4j graph edges:The domain interface
Dossierexposes aresources: Resource[]collection for convenience. In Neo4j this collection does not exist as a property on theDossiernode. It is materialized at query time by traversing outgoing[:CONTAINS]relationships:
cypher (d:Dossier)-[:CONTAINS]->(child:Document) (d:Dossier)-[:CONTAINS]->(child:Dossier)
INeo4jDossiertherefore has noresourcesproperty. Children are fetched separately viagetChildren(dossierId).
The same pattern applies to any other relationship-backed collection: the domain interface may expose it as a list, while the Neo4j layer resolves it through graph traversal.
Resource Entities & Service
Resource = documents, dataset and folders
Interfaces
type DossierType =
| 'root' // root hidden dossier of a tenant — managed by the system
| 'user' // user hidden dossier of a user — managed by the system
| 'session' // user's session hidden dossier — managed by the system
| 'visible'; // visible dossier created by the user
interface Resource {
id: string;
// parentId and ownerGroupId are NOT properties on the node.
// Parent dossier is resolved via (Dossier)-[:CONTAINS]->(Resource).
// Ownership is resolved via (Group)-[:OWNS]->(Resource).
isProtected: boolean; // break inheritance
createdAt: Date;
updatedAt: Date;
isDeleted: boolean;
deletedAt: Date | null;
deletedBy: string | null;
}
interface Document extends Resource {}
interface Dossier extends Resource {
type: DossierType;
resources: Resource[];
}
Resource Service
CreateDossier(
parentId: string,
name: string,
ownerGroupId: string,
type: DossierType
): Promise<Resource>;
createDocument(
parentId: string,
name: string,
ownerGroupId: string
): Promise<Resource>;
moveResource(
resourceId: string,
newParentId: string,
effectiveUserId: string
): Promise<void>;
renameResource(
resourceId: string,
newName: string
): Promise<void>;
deleteResource(
resourceId: string,
effectiveUserId: string
): Promise<void>;
restoreResource(resourceId: string): Promise<void>;
purgeResource(resourceId: string): Promise<void>; // hard-delete after recovery window (cron)
getResource(resourceId: string): Promise<Resource>;
getChildren(resourceId: string): Promise<Resource[]>;
setProtected(resourceId: string, effectiveUserId: string): Promise<void>;
unprotect(resourceId: string, effectiveUserId: string): Promise<void>;
User & Group Entities
interface User {
id: string;
email: string;
fullname: string;
username: string;
// Neo4j authorization node uses labels :User:SystemAcl.
// Hidden system group: navigate via (User:SystemAcl)-[:MEMBER_OF]->(Group:SystemAcl {isUserGroup:true})
// Hidden user dossier: navigate via (Group)-[:OWNS]->(Dossier {type:'user'})
}
interface Group {
id: string;
name: string;
isUserGroup: boolean; // true for hidden single-user groups — managed entirely by the system
// Neo4j authorization node uses labels :Group:SystemAcl.
// Imported diagram/entity groups remain :Group:Entity and are outside ACL.
// tenant group carries NO property flag. It is identified by graph traversal:
// (Group:SystemAcl)-[:HAS_ROLE]->(:UserRole:SystemAcl)-[:HAS_CAPABILITY]->(:UserCapability:SystemAcl {id: 'tenant-admin'})
}
Group Service
addMember(groupId: string, userId: string, effectiveUserId: string): Promise<void>;
removeMember(groupId: string, userId: string, effectiveUserId: string): Promise<void>;
getGroups(userId: string): Promise<Group[]>;
getMembers(groupId: string): Promise<User[]>;
// Called internally by UserService.addUser — creates the hidden system group (isUserGroup=true)
// and user dossier (type='user'), linking them via MEMBER_OF and OWNS edges.
provisionHiddenGroupAndDossierForUser(user: User): Promise<void>;
ACL Entities
type Permission =
| 'READ'
| 'SHARING'
| 'DELETE'
| 'ADD'
| 'UPDATE'
| 'OWNERSHIP';
type RoleType =
| 'SUPER_ADMIN'
| 'OWNER'
| 'ADMIN'
| 'MANAGER'
| 'VIEWER';
interface Role {
name: RoleType;
permissions: Permission[];
}
// Only groups can own or be granted permissions on resources.
// Users interact with resources exclusively through their group membership.
// Every user has a hidden system group (isUserGroup: true) containing only themselves,
// reached via (User:SystemAcl)-[:MEMBER_OF]->(Group:SystemAcl {isUserGroup:true}).
//
// NOTE — Role is a C# abstraction only and is NEVER stored in Neo4j.
// When an ACLEntry is persisted, one [:GRANT:<PERMISSION>] typed relationship is created
// per effective permission (e.g. [:GRANT:READ], [:GRANT:SHARING]).
// When read back, the permission set is reconstructed from the typed relationships.
interface ACLEntry {
resourceId: string;
groupId: string; // always a Group ID — users are never granted directly
role: Role;
permissionOverrides?: Permission[]; // null = use role's canonical permissions; [] = deny all
// Derived: permissionOverrides if set, otherwise role.permissions
readonly effectivePermissions: Permission[];
updatedAt: Date;
grantedBy?: string; // effective user who created this grant
}
ACL Service
// Returns true if the user holds the specified permission on the resource.
// Evaluates inherited permissions (via CONTAINS ancestry) and SUPER_ADMIN bypass.
checkAccess(
userId: string,
resourceId: string,
permission: Permission
): Promise<boolean>;
// Returns the full set of permissions the user holds on the resource
// after evaluating inheritance, isProtected barriers, and SUPER_ADMIN bypass.
getEffectivePermissions(
userId: string,
resourceId: string
): Promise<Permission[]>;
// Returns all ACL entries directly assigned to the resource (does not include inherited).
getACL(resourceId: string): Promise<ACLEntry[]>;
// Replaces ALL ACL entries on the resource atomically.
// Requires SHARING permission — caller is responsible for checking before calling.
setACL(
resourceId: string,
entries: ACLEntry[],
effectiveUserId: string
): Promise<void>;
// Upserts a single grant for the entry's group on the resource (MERGE semantics).
// Requires SHARING permission — caller is responsible for checking before calling.
addPermission(entry: ACLEntry, effectiveUserId: string): Promise<void>;
// Removes all [:GRANT:<PERMISSION>] relationships for the group on the resource.
// No-op if no grant exists. Requires SHARING permission.
removePermission(
resourceId: string,
groupId: string,
effectiveUserId: string
): Promise<void>;
Audit Entities & Service
type AuditEventType =
| 'RESOURCE_CREATED'
| 'RESOURCE_MOVED'
| 'RESOURCE_RENAMED'
| 'RESOURCE_DELETED'
| 'RESOURCE_RESTORED'
| 'PERMISSION_GRANTED'
| 'PERMISSION_REVOKED'
| 'RESOURCE_PROTECTED'
| 'RESOURCE_UNPROTECTED'
| 'OWNERSHIP_CHANGED'
| 'SHARING_ACTION_PERFORMED'
| 'ACCESS_DENIED'
| 'ACCESS_GRANTED'
| 'SUPER_ADMIN_ACCESS_USED'
| 'GROUP_MEMBERSHIP_CHANGED'
| 'USER_REMOVED_FROM_TENANT';
interface AuditEvent {
id: string;
type: AuditEventType;
effectiveUserId: string; // user who performed the action
resourceId?: string; // resource involved (if applicable)
targetId?: string; // secondary target (e.g. new parent, group, subject)
metadata?: Record<string, unknown>; // additional context (e.g. old name, permission)
occurredAt: Date;
}
Audit Service
log(event: Omit<AuditEvent, 'id' | 'occurredAt'>): Promise<void>;