Knowledge bases, categories, members, and access rules describe how content is organized and who should theoretically reach it. The current model persists that information, but member and ACL data are not part of request admission. This chapter separates the model from effective security.
1. Three independent hierarchies
KnowledgeBase.ParentId organizes spaces, Category.ParentId organizes navigation within a knowledge base, and Document.ParentId organizes documents. The database and handlers do not universally validate these identifiers.
2. Creating a knowledge base
POST /api/v1/knowledgebases HTTP/1.1Authorization: Bearer eyJhbGciOiJSUzI1NiJ9Content-Type: application/json
{ "name": "Support operations", "description": "Maintained by support and operations", "type": "KnowledgeBase", "icon": "headset", "color": "#2864DC", "parentId": null, "sortOrder": 10, "isPublic": false, "defaultAccessLevel": "View"}The caller must be a User; TenantId and CreateBy come from the current identity. DefaultAccessLevel has a null! C# default, so clients should always send a registered value.
Creation does not check same-tenant name uniqueness, parent existence/tenant, cycles, or the validity of IsPublic plus DefaultAccessLevel. The tree builder recursively follows ParentId without a visited set. A detached cycle can disappear from the root result; a reachable cycle can recurse indefinitely.
3. Members are persisted aggregate data
Members live in MembersJson. Add/Remove maintains MemberCount and rejects duplicates/missing members.
// The member and level are appended to the knowledge-base MembersJson collection.using var response = await api.PostAsJsonAsync( "/api/v1/knowledgebases/kb-support/members", new { userId = "user-2048", accessLevel = "Edit" }, cancellationToken);
// Success proves persistence only; reads and writes do not consult this member yet.response.EnsureSuccessStatusCode();SetKnowledgeBaseAccessCommand can change IsPublic and DefaultAccessLevel but has no generated endpoint. Detail, list, tree, and document queries do not filter by members, public state, or default access.
4. Category uniqueness and references
Categories have a unique index on (TenantId, KnowledgeBaseId, ParentId, Name). The handler first queries for duplicates, and the database index closes concurrent races.
The handler does not verify that KnowledgeBaseId exists. Delete trusts DocumentCount > 0 and does not check child categories. Since document handlers do not maintain category count, the guard may be stale.
5. Document access rules
An access rule contains PrincipalId, PrincipalType, and AccessLevel. A principal/type pair is unique. Document.HasAccess grants its User creator all levels and otherwise compares the stored level numerically.
// Current HTTP handlers do not call HasAccess; this illustrates the aggregate method only.var canEdit = document.HasAccess( currentUserId, PrincipalType.User, AccessLevel.Edit);
// Once integrated, denial should flow through the framework Problem Details mapping.if (!canEdit) return Result.Failure(DocsErrors.Document.AccessDenied);No production caller of HasAccess was found. Adding a rule changes the DTO and database but not the reachability of detail, list, versions, diff, update, publish, or delete.
6. Effective four-layer model
| Path | Authenticated | Tenant | Generic permission | Member/ACL |
|---|---|---|---|---|
| Document writes | Yes | User.TenantId | Yes | No |
| Document GET/version/diff | Yes | User.TenantId | No | No |
| Knowledge-base writes | Yes | User.TenantId | Yes | No |
| Knowledge-base reads | Yes | User.TenantId | No | No |
| Category writes | Yes | User.TenantId | Message yes, catalog missing | No |
| Category tree | Yes | User.TenantId | No | No |
| Whiteboard equivalents | Yes | User.TenantId | Writes yes/reads no; catalog missing | No |
The catalog contains document view/create/update/delete/publish/manage and knowledge-base view/create/manage/delete. Category and whiteboard resource constants exist without permission definitions. Publish, tag, and ACL commands use Update, leaving publish/manage constants disconnected from those actions.
7. Target resource policy
public interface IDocumentAccessPolicy{ // Detail, list, version, search, and hub paths share this operation model. Task<Result> AuthorizeAsync( DocumentResourceSnapshot resource, DocumentOperation operation, CurrentUser caller, CancellationToken cancellationToken);}
// Carry only immutable decision facts instead of exposing a mutable aggregate.public sealed record DocumentResourceSnapshot( string TenantId, string DocumentId, string KnowledgeBaseId, string? CreatorId, bool KnowledgeBaseIsPublic, AccessLevel KnowledgeBaseDefault, IReadOnlyList<KnowledgeBaseMember> Members, IReadOnlyList<DocumentAccessRule> Rules);A production policy should fail tenant mismatch first, define audited administrative bypass, use a deterministic merge order, push visibility into list/search storage predicates, and serve detail/version/diff/download/search/hub consistently. Member or ACL changes must invalidate caches and active sessions.
8. Referential-integrity orchestration
public async Task<Result<Document>> CreateDocumentAsync( CreateDocument request, CurrentUser caller, CancellationToken cancellationToken){ // Resolve every parent through the trusted tenant boundary. var kb = await knowledgeBases.FindAsync( caller.TenantId, request.KnowledgeBaseId, cancellationToken); if (kb is null) return Result.Failure(DocsErrors.KnowledgeBase.NotFound);
if (request.CategoryId is not null) { var category = await categories.FindAsync( caller.TenantId, request.CategoryId, cancellationToken); if (category is null || category.KnowledgeBaseId != kb.Id) return Result.Failure(DocsErrors.Category.ParentKnowledgeBaseMismatch); }
// Apply resource policy before constructing or saving the new aggregate. var allowed = await access.AuthorizeCreateAsync(kb, caller, cancellationToken); if (allowed.IsFailure) return Result.Failure<Document>(allowed.Error);
return Document.Create(/* validated ids and server-derived tenant */);}This is a target orchestration example for the current gap, not a copy of the existing handler.
9. Delete behavior
| Resource | Current guard | Risk |
|---|---|---|
| Knowledge base | Repository query for documents | No child KB/category/whiteboard checks; soft delete |
| Category | Stored DocumentCount | Count is not maintained; no child-category check |
| Document | Immediate soft delete | KB/category counts unchanged; no observed integration-event publish |
| Whiteboard | Immediate soft delete | Members and KB status ignored |
Use authoritative reference queries before delete. Denormalized counts may accelerate displays, but they are not integrity unless maintained and reconciled.
10. Required tests
- unauthorized principals for every GET, version, diff, search, and hub path;
- same-tenant outsider, cross-tenant member, creator, user/role principal types;
- cache and live-connection behavior after ACL/member revocation;
- cross-KB category parents, concurrent duplicate names, orphans, and cycles;
- delete with documents, child categories, child knowledge bases, or whiteboards;
- impersonation where User.TenantId and EffectiveTenantId differ;
- permission-catalog parity for every
IAuthorizedRequest.