Gå till huvudinnehållet Gå till huvudmenyn

Restrict admin access

Av Mia Holmberg Lästid: 3 minuter

Too many editors with admin rights? Let's explore how to split the admin roles in Optimizely CMS for better control and security of the administrative interface.

Optimizely CMS restricts access to the Settings area to admin users by default, which often results in editors being given unnecessary administrative privileges. They get access to critical functionality though they only need access to a few features. To address this, I explored how to divide admin responsibilities into distinct editorial and administrative roles.Screenshot of admin area

I did not find a way to change the built-in access policies to the settings menu, I did however find a way to override the menu items in it and set our custom role to them.

First, I added a new policy to startup.cs and configured so that users with ‘SystemAdmin’-role belong to the new policy.

services.AddAuthorization(options =>
{
  options.AddPolicy(
    "SystemAdminPolicy", policy =>
       policy.RequireRole("SystemAdmin")
  );

});

I also added some role mapping in appsettings.json so that SystemAdmin role also become CmsAdmin so users won’t need multiple admin roles:

"SystemAdmin": {
  "MappedRoles": [ "SystemAdmin" ]
},
"CmsAdmins": {
  "MappedRoles": [ "SystemAdmin", "WebAdmins" ]
}

Then I created a custom menu provider where I added the same items as the original EPiServer.Cms.UI.Admin.MenuProvider does, the only thing different here is changing the AuthorizationPolicy to our custom one. It is important that the paths are the same as the original for the assembler to override the items.

[MenuProvider]
public class CmsMenuProvider : IMenuProvider
{
  private readonly LocalizationService _localizationService;
 
  public CmsMenuProvider(LocalizationService localizationService)
  {
      _localizationService = localizationService;
  }
 
  public IEnumerable<MenuItem> GetMenuItems()
  {
    var policy = "SystemAdminPolicy";
    var menuItems = new List<MenuItem>
    {
      // Content Types
      new UrlMenuItem(
        _localizationService.GetString("/admin/menutabs/contenttypestitle"),
        "/global/cms/admin/contenttypes",
        Paths.ToResource(typeof(MenuProvider), "default#/ContentTypes"))
      {
          SortIndex = 10,
          AuthorizationPolicy = policy,
          Alignment = MenuItemAlignment.Left,
          Behavior = MenuItemBehavior.HideForChildNavigation
      },
      // Set Access Rights
      new UrlMenuItem(
        _localizationService.GetString("/admin/menu/setsecurity"),
        "/global/cms/admin/accessrights/setaccessrights",
        Paths.ToResource(typeof(MenuProvider),
        "default#/AccessRights/SetAccessRights"))

      {
          SortIndex = 21,
          AuthorizationPolicy = policy,
      },
      // Permissions for Functions
      new UrlMenuItem(
        _localizationService.GetString("/admin/menu/setpermission"),
        "/global/cms/admin/accessrights/permissionsforfunctions",
        Paths.ToResource(typeof(MenuProvider),
        "default#/AccessRights/PermissionsForFunctions"))

      {
          SortIndex = 24,
          AuthorizationPolicy = policy,
      },
      // Config heading
      new UrlMenuItem(
        _localizationService.GetString("/admin/menutabs/config"),
        "/global/cms/admin/configurations",
        Paths.ToResource(typeof(MenuProvider), "default#/Configurations"))
      {
          SortIndex = 40,
          AuthorizationPolicy = policy,
          Alignment = MenuItemAlignment.Left
      },
      // Manage Websites
      new UrlMenuItem(
        _localizationService.GetString("/admin/menu/siteinformation"),
        "/global/cms/admin/configurations/managesites",
        Paths.ToResource(typeof(MenuProvider),
        "default#/Configurations/ManageSites"))

      {
          SortIndex = 41,
          AuthorizationPolicy = policy,
      },
      // Manage Website Languages
      new UrlMenuItem(
        _localizationService.GetString("/admin/editlanguagebranches/heading"),
        "/global/cms/admin/configurations/languages",
        Paths.ToResource(typeof(MenuProvider),
        "default#/Configurations/Languages"))

      {
          SortIndex = 42,
          AuthorizationPolicy = policy,
      },
      // Categories
      new UrlMenuItem(
        _localizationService.GetString("/admin/categories/heading"),
        "/global/cms/admin/configurations/categories",
        Paths.ToResource(typeof(MenuProvider),
        "default#/Configurations/Categories"))

      {
          SortIndex = 43,
          AuthorizationPolicy = policy,
      },
      // Plug-in Manager
      new UrlMenuItem(
        _localizationService.GetString("/admin/plugin/heading"),
        "/global/cms/admin/configurations/pluginmanager",
        Paths.ToResource(typeof(MenuProvider),
        "default#/Configurations/PluginManager"))

      {
          SortIndex = 44,
          AuthorizationPolicy = policy,
      },
      // Edit Tabs
      new UrlMenuItem(
        _localizationService.GetString("/admin/headings/heading"),
        "/global/cms/admin/configurations/tabs",
        Paths.ToResource(typeof(MenuProvider), "default#/Configurations/Tabs"))
      {
          SortIndex = 45,
          AuthorizationPolicy = policy,
      }
    };

    return menuItems;
  }
}

 

The Settings menu now looks like this for an admin who is not assigned the SystemAdmin role:
Screenshot of the result

We also need to set up the new role in CMS and the assign it to user(s) that should have the more administrative role. When logged in as SystemAdmin I can now see the full Settings menu. 
Screenshot of administer groups

This worked great locally, BUT then in the integration environment, my custom menu were not used, and it took a while before I figured out that it depends on the initialization order of the MenuProviders. The custom one must be initialized before the original one.

To solve this I made an extension that I call from startup.cs. In this code I first add our custom provider, then check if any EPiServer.Cms.UI.Admin.MenuProvider has been added to services, if so, I remove it from the list and then re-add it to make sure it is in the bottom of the list.

public static IServiceCollection AddCmsMenuProvider(this IServiceCollection services)
{
  // Make sure our menu provider is registered before
  // the shell menu provider to override menu items

  services.AddSingleton<IMenuProvider, CmsMenuProvider>();
       
  var epiAdminMenu =
    services.FirstOrDefault(
      d => d.ServiceType == typeof(IMenuProvider)
      && d.ImplementationType == typeof(EPiServer.Cms.UI.Admin.MenuProvider)
   );

    if (epiAdminMenu is not null)
  {
    services.Remove(epiAdminMenu);
  }
 
  services.AddSingleton<IMenuProvider, EPiServer.Cms.UI.Admin.MenuProvider>();
 
  return services;
}

When I added above extension I also needed to remove the [MenuProvider] attribute from our custom CmsMenuProvider so that it would not be added twice to services.

[MenuProvider]
public class CmsMenuProvider : IMenuProvider

There, now we can separate editorial and administrative access in Optimizely CMS, giving editors just what they need without handing over the keys to everything.

Reflections:

  • As always when building on top of external code, this must be tested out after every package update.
  • Additional URL restrictions could be needed to restrict access completely since with this fix we only hide the menu links, I left the URLs accessible for this poc since our purpose is to prevent someone just clicking around and accidentally changing something important.

Vi vill gärna höra vad du tycker om inlägget

Mia Holmberg

Mia Holmberg

Utvecklare

Läs alla blogginlägg av Mia Holmberg