Skip to main content

Outline

At a glance
  • Goal: Read and modify content ACLs programmatically for automation, integrations, and governance workflows.
  • Core objects: IContentSecurityRepository, IContentSecurityDescriptor, AccessControlList, AccessControlEntry.
  • Most common operations: list rights, grant to role/user, revoke, and break inheritance via ToLocal().
  • Safety principle: apply least privilege and log every ACL change like it’s going to be audited (because someday it will).


Introduction

Optimizely CMS 12 provides a granular, content-level security model that controls who can read, edit, publish, or administer individual items. Most teams manage permissions through the admin UI, but there are legitimate scenarios where programmatic access control is required—automated imports, custom provisioning workflows, or integrations with external identity and role management.

This page focuses on practical patterns for working with content permissions through IContentSecurityRepository. The goal is to help developers build secure automation that respects inheritance, avoids accidental privilege escalation, and remains debuggable in production.



1. Core concepts: how content security is represented

Content security in CMS 12 is expressed through ACLs (access control lists). An ACL is a set of entries, where each entry targets a user or role and grants or denies a set of access flags.

  • AccessControlList: collection of rules applied to a content item.
  • AccessControlEntry: a single rule for a user/role, including allow/deny and access flags.
  • AccessLevel: permission flags (e.g., Read, Edit, Publish, Administer).
  • SecurityEntityType: whether the principal is a User or a Role.
  • IContentSecurityDescriptor: descriptor containing the ACL + inheritance state.
  • Inheritance: most items inherit from parent until you make permissions local (ToLocal()).
Expand: Inheritance vs local ACLs (why you keep seeing ToLocal())
  • Inherited means “this item uses the parent’s rules.”
  • Local means “this item has its own explicit rules.”
  • ToLocal() breaks inheritance and (typically) brings the inherited entries down as explicit entries you can edit.
  • Breaking inheritance should be a deliberate governance choice—not an accidental side effect of automation.


2. Obtaining IContentSecurityRepository

As with other Optimizely services, obtain IContentSecurityRepository via dependency injection. In real code, you often also inject IContentLoader because you need to load the item (or validate it) before you touch security.

using EPiServer;
using EPiServer.Core;
using EPiServer.Security;

public class ContentSecurityService
{
    private readonly IContentSecurityRepository _securityRepo;
    private readonly IContentLoader _contentLoader;

    public ContentSecurityService(IContentSecurityRepository securityRepo, IContentLoader contentLoader)
    {
        _securityRepo = securityRepo;
        _contentLoader = contentLoader;
    }
}


3. Scenario 1: Reading content access rights

Start by inspecting what the content item currently has: whether it inherits permissions and what explicit ACL entries exist. This is a required step before “just changing something,” because inheritance can make your update either pointless (you changed nothing) or unexpectedly invasive (you broke inheritance).

using System;
using System.Linq;
using EPiServer;
using EPiServer.Core;
using EPiServer.Security;

public class ContentSecurityService
{
    private readonly IContentLoader _contentLoader;

    public ContentSecurityService(IContentLoader contentLoader)
    {
        _contentLoader = contentLoader;
    }

    public void ListAccessRights(ContentReference contentLink)
    {
        var content = _contentLoader.Get<IContent>(contentLink);

        if (content is not IContentSecurable securable)
        {
            Console.WriteLine($"Content '{content.Name}' (ID: {contentLink.ID}) is not securable.");
            return;
        }

        var descriptor = securable.GetContentSecurityDescriptor();

        Console.WriteLine($"Access Rights for '{content.Name}' (ID: {contentLink.ID})");
        Console.WriteLine($"  Inherits: {descriptor.IsInherited}");

        if (!descriptor.AccessControlList.Any())
        {
            Console.WriteLine("  No explicit ACEs on this item.");
            return;
        }

        foreach (var entry in descriptor.AccessControlList)
        {
            var mode = entry.IsDenied ? "Denied" : "Granted";
            Console.WriteLine($"  - {entry.Name} ({entry.EntityType}): {mode} {entry.Access}");
        }
    }
}
Expand: What “no explicit ACEs” can mean
  • If inherited: the item uses the parent’s ACL entirely.
  • If local but empty: you may have removed all local entries (danger zone). Verify actual effective permissions.
  • When in doubt, inspect the parent’s ACL and confirm what the UI shows for effective access.


4. Scenario 2: Granting access to a role or user

Granting access programmatically is common during onboarding flows (e.g., “new project folder → give Project Editors edit/publish rights”). The safe pattern is to load a writable clone of the descriptor, decide whether to break inheritance, then add/update an entry and save.

using System;
using EPiServer;
using EPiServer.Core;
using EPiServer.Security;

public class ContentSecurityService
{
    private readonly IContentSecurityRepository _securityRepo;
    private readonly IContentLoader _contentLoader;

    public ContentSecurityService(IContentSecurityRepository securityRepo, IContentLoader contentLoader)
    {
        _securityRepo = securityRepo;
        _contentLoader = contentLoader;
    }

    public void GrantAccess(ContentReference contentLink, string principalName, SecurityEntityType entityType, AccessLevel access)
    {
        var content = _contentLoader.Get<IContent>(contentLink);

        if (content is not IContentSecurable securable)
            throw new InvalidOperationException("Content is not securable.");

        var descriptor = securable.GetContentSecurityDescriptor().CreateWritableClone();

        // Decide deliberately: do you want a local override or should this remain inherited?
        if (descriptor.IsInherited)
        {
            descriptor.ToLocal();
        }

        descriptor.AddEntry(new AccessControlEntry(principalName, access, entityType));

        _securityRepo.Save(contentLink, descriptor, SecuritySaveType.Replace);
    }
}
Governance warning

Breaking inheritance creates a long-term maintenance obligation. If your organization expects permissions to be managed at section level, avoid creating “snowflake ACLs” on thousands of items unless you have a strong reason.



5. Scenario 3: Revoking access from a role or user

Revocation is not just “remove entry.” If the item is inherited, removing a local entry does nothing unless you first make permissions local. In practice, you either (a) keep inheritance and revoke at the parent level, or (b) break inheritance and manage the item explicitly.

using System;
using System.Linq;
using EPiServer;
using EPiServer.Core;
using EPiServer.Security;

public class ContentSecurityService
{
    private readonly IContentSecurityRepository _securityRepo;
    private readonly IContentLoader _contentLoader;

    public ContentSecurityService(IContentSecurityRepository securityRepo, IContentLoader contentLoader)
    {
        _securityRepo = securityRepo;
        _contentLoader = contentLoader;
    }

    public void RevokeAccess(ContentReference contentLink, string principalName, SecurityEntityType entityType)
    {
        var content = _contentLoader.Get<IContent>(contentLink);

        if (content is not IContentSecurable securable)
            throw new InvalidOperationException("Content is not securable.");

        var descriptor = securable.GetContentSecurityDescriptor().CreateWritableClone();

        if (descriptor.IsInherited)
        {
            descriptor.ToLocal();
        }

        var entry = descriptor.Entries.FirstOrDefault(e =>
            e.Name.Equals(principalName, StringComparison.OrdinalIgnoreCase) &&
            e.EntityType == entityType);

        if (entry == null)
            return;

        descriptor.RemoveEntry(entry);

        _securityRepo.Save(contentLink, descriptor, SecuritySaveType.Replace);
    }
}
Expand: Revocation strategies that scale
  • Prefer role changes over item-by-item ACL edits when possible.
  • Revoke at the correct level: if a whole subtree should change, update the parent instead of each item.
  • Audit after changes: log what changed and verify effective access in the UI for at least one sample item.


6. Scenario 4: Breaking inheritance and setting explicit rights

Breaking inheritance is the “point of no return” moment where the item becomes independent from its parent’s permission strategy. Do it intentionally and document why. The mechanics are simple: make a writable clone, call ToLocal(), then modify entries and save.

using EPiServer;
using EPiServer.Core;
using EPiServer.Security;

public class ContentSecurityService
{
    private readonly IContentSecurityRepository _securityRepo;
    private readonly IContentLoader _contentLoader;

    public ContentSecurityService(IContentSecurityRepository securityRepo, IContentLoader contentLoader)
    {
        _securityRepo = securityRepo;
        _contentLoader = contentLoader;
    }

    public void BreakInheritanceAndSetRoleAccess(ContentReference contentLink, string roleName, AccessLevel access)
    {
        var content = _contentLoader.Get<IContent>(contentLink);

        if (content is not IContentSecurable securable)
            throw new System.InvalidOperationException("Content is not securable.");

        var descriptor = securable.GetContentSecurityDescriptor().CreateWritableClone();

        if (descriptor.IsInherited)
        {
            descriptor.ToLocal();
        }

        // Optional: clear existing local entries if you want a clean slate (use carefully).
        // descriptor.AccessControlList.Clear();

        descriptor.AddEntry(new AccessControlEntry(roleName, access, SecurityEntityType.Role));

        _securityRepo.Save(contentLink, descriptor, SecuritySaveType.Replace);
    }
}


7. Best practices for programmatic content security

  • Least privilege: grant only what’s necessary; avoid FullAccess/Administer unless required.
  • Prefer RBAC: grant roles, not users, unless you have a strong exception case.
  • Be deliberate about inheritance: don’t silently “ToLocal() everything” in bulk jobs.
  • Log every ACL mutation: include content ID, principal, before/after, and correlation IDs.
  • Test in non-prod: security mistakes are annoying in Dev and catastrophic in Prod.
  • Batch responsibly: large-scale ACL updates can be expensive; reduce saves and avoid unnecessary churn.
  • Be careful with bypass patterns: any “ignore permissions” capability is a loaded foot-gun—use only in tightly controlled contexts.


Conclusion

IContentSecurityRepository enables controlled, programmatic management of content permissions in Optimizely CMS 12. When implemented with clear intent—especially around inheritance—these patterns support automation and governance without undermining the security model. Keep changes minimal, observable, and auditable, and your future self (and your security reviewers) will sleep better.