I’d say negate would be useful for conditions.. but then why stop at just that.. matchAll vs matchAny for instance on the array of conditions the same as IIS:UrlRewrite? 
On the startNode you mentioned (multiple site roots for me).. I just moved that into the property guard, so it wasn’t constrained by the Condition basis, below if of use (yes it’s hardcoded aliases and guids… but serves the purpose, and also never quite sure if any benefit to using the core repositories/contexts over creating a custom api endpoint)
import { UMB_WORKSPACE_CONDITION_ALIAS } from "@umbraco-cms/backoffice/workspace";
import { UMB_DOCUMENT_WORKSPACE_ALIAS } from "@umbraco-cms/backoffice/document";
const manifest: UmbExtensionManifest = {
type: "workspaceContext",
name: "SM Property Permission Workspace Context",
alias: "SM.WorkspaceContext.DocumentPropertyPermission",
js: () => import("./visibility-guards.element.js"),
conditions: [
{
alias: UMB_WORKSPACE_CONDITION_ALIAS,
match: UMB_DOCUMENT_WORKSPACE_ALIAS,
},
],
};
export const manifests = [manifest];
import { UmbControllerBase } from "@umbraco-cms/backoffice/class-api";
import type { UmbControllerHost } from "@umbraco-cms/backoffice/controller-api";
import { UMB_DOCUMENT_WORKSPACE_CONTEXT, type UmbDocumentWorkspaceContext, UmbDocumentItemRepository } from "@umbraco-cms/backoffice/document";
import { UMB_ANCESTORS_ENTITY_CONTEXT, type UmbAncestorsEntityContext } from "@umbraco-cms/backoffice/entity";
export class MyDocumentPropertyPermissionWorkspaceContext extends UmbControllerBase {
#workspace?: UmbDocumentWorkspaceContext;
#ancestors?: UmbAncestorsEntityContext;
readonly #docItemRepo: UmbDocumentItemRepository;
// Toggle via console: localStorage.setItem('VISIBILTY_GUARDS_DEBUG', 'true')
readonly #isDebug = localStorage.getItem('VISIBILTY_GUARDS_DEBUG') === 'true';
constructor(host: UmbControllerHost) {
super(host);
this.#log("DEBUG: Permission Controller Initialized", null, 'info');
// In v17, we instantiate the repository directly.
// It uses the host (this) to access the central API clients.
this.#docItemRepo = new UmbDocumentItemRepository(this);
// 1. Setup flat context consumers
this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (context) => {
this.#log("DEBUG: Workspace Context Captured");
this.#workspace = context;
this.#checkInitialization();
});
this.consumeContext(UMB_ANCESTORS_ENTITY_CONTEXT, (context) => {
this.#log("DEBUG: Ancestors Context Captured");
this.#ancestors = context;
this.#checkInitialization();
});
}
/**
* Centralized Debug Logger
*/
#log(message: string, data?: any, type: 'info' | 'log' | 'warn' | 'error' | 'dir' = 'log') {
if (!this.#isDebug) return;
if (type === 'dir' && data) {
console.groupCollapsed(`[PermissionGuard] ${message}`);
console.dir(data);
console.groupEnd();
} else {
const logFn = console[type] || console.log;
logFn(`[PermissionGuard] ${message}`, data ?? '');
}
}
#checkInitialization() {
if (this.#workspace && this.#ancestors && this.#docItemRepo) {
this.#log("DEBUG: All contexts ready. Setting up rules...", null, 'info');
this.#setupRules();
} else {
this.#log(`DEBUG: Initialization Status - Workspace: ${!!this.#workspace}, Ancestors: ${!!this.#ancestors}`);
}
}
#setupRules() {
// List of property aliases you want to guard
const restrictedAliases = ['promotionalMessage', 'developmentIntroduction', 'housetypeIntroduction'];
// Observe the lineage
this.observe(this.#ancestors!.ancestors, async (ancestors) => {
this.#log("DEBUG: Ancestors Array Updated:", ancestors);
const currentUnique = this.#workspace!.getUnique();
// Logic: If ancestors is empty [], currentUnique is our Root.
const rootUnique = (ancestors && ancestors.length > 0)
? ancestors[0].unique
: currentUnique;
this.#log("DEBUG: Current Node Unique:", currentUnique);
this.#log("DEBUG: Evaluated Root Unique:", rootUnique);
if (!rootUnique) {
this.#log("DEBUG: Root Unique is null (Document likely not yet saved).");
return;
}
const { data: documentItems } = await this.#docItemRepo.requestItems([rootUnique]);
this.#log(`DEBUG: documentItems for ${rootUnique}`, documentItems);
this.#log(`DEBUG: documentItems for ${rootUnique}`, { documentItems }, 'dir');
const llpHomePage = "06d46507-6e05-448e-bea9-b1a0467e284f";
const isAllowed = documentItems?.[0]?.documentType?.unique.toLowerCase() === llpHomePage.toLowerCase();
this.#log("DEBUG: Has a matching llpHomePage DocumentType:", isAllowed, 'info');
// Loop through each alias to apply the rules
for (const alias of restrictedAliases) {
// Fetch property structure
const propertyStructure = await this.#workspace!.structure.propertyStructureByAlias(alias);
// If the property is missing, the observable itself might be null,
// or it might emit null inside the observe.
if (!propertyStructure) {
this.#log("DEBUG: propertyStructure observable is null. The alias 'promotionalMessage' is likely incorrect or the property isn't on this DocType.", null, 'warn');
// Look at the underlying data in the workspace structure manager
this.#log("DEBUG: Inspecting workspace structure owner for available aliases:", (this.#workspace!.structure as any)._owner, 'dir');
}
// Create a unique observer key for each property to avoid collisions
const observerKey = `guard_observer_${alias}`;
this.observe(propertyStructure, (property) => {
if (property) {
this.#log(`DEBUG: Applying Rule for ${alias}. Permitted:`, isAllowed);
this.#workspace!.propertyViewGuard.addRule({
unique: `${alias}_permission`,
permitted: isAllowed,
message: "This property is restricted to the designated site section.",
propertyType: {
unique: property.unique
}
});
} else {
this.#log("DEBUG: Property 'promotionalMessage' structure not found.", null, 'warn');
}
}, observerKey);
}
}, 'ancestor_observer');
}
}
export { MyDocumentPropertyPermissionWorkspaceContext as api };