Working in Craft 3, I have a requirement to restrict user access (in the CP) per Entry rather than per Section.

In this case Users in Craft will belong to Department (Channel) and Resources (Channel). They should only be able to administer the Entries in those Channels for which they are responsible.

I suppose the ideal would be to create a User Group per Department and then be able to select the individual Entries within the Department & Resources Channels here:

enter image description here

Is that possible via a Module/Plugin and if so can anyone provide any pointers on how to get started? I presume I'd need to loop through all Entries in the Channels and output them in this list to be selectable and then have access permissions applied. Any examples or pointers would be appreciated.

Any other suggestions or workarounds also welcome.


2 Answers 2


There is a way - it's actually quite easy but you need to dig deep into Craft 3 to do it. In general: there is only one single Javascript function that fetches all elements for all index pages

updateElements: function() {
    // Ignore if we're not fully initialized yet
    if (!this.initialized) {


    var params = this.getViewParams();

    Craft.postActionRequest(this.settings.updateElementsAction, params, $.proxy(function(response, textStatus) {

        if (textStatus === 'success') {
            this._updateView(params, response);
        else {
            Craft.cp.displayError(Craft.t('app', 'An unknown error occurred.'));

    }, this));

As you can see: the route is a variable in the defaults

updateElementsAction: 'element-indexes/get-elements'

In order to change this route you'll have to overwrite it in a JS file

Craft.BaseElementIndex.defaults.updateElementsAction = 'your-awesome-plugin-route';

Not let's take a look at the controller: After certain other functions the final call that searches for all elements is the following ElementIndexController line 379

$responseData['html'] = $elementType::indexHtml(
    $this->_elementQuery,  // <-- this is the important part

All you have to do is to extend this Controller, build a custom one, link the route to your custom controller and change the $this->_elementQuery. You can include your custom search parameters, include hidden content fields, create custom relation fields for groups and search for elements related to this user group or whatever you want.

It seems a little bit tricky at first but it's actually pretty easy. I've done it several times

  • Thanks Robin! Where is the JS file you mention above? And how can I create and inject a new JS file to overwrite it to use the new route?
    – Ian Young
    Apr 16, 2018 at 13:36
  • @Ian You need to register a new Assetbundle github.com/craftcms/docs/blob/v3/en/asset-bundles.md somewhere in your plugin Apr 16, 2018 at 14:45
  • Thanks again @Robin. That gives me something to go on. And I see what you were talking about in regard to the JS file in Craft.js on line 3466. Do you have any examples of a working plugin btw? Whilst learning it can often be useful to pick apart something very similar
    – Ian Young
    Apr 16, 2018 at 16:29
  • @Ian I can only provide code snippeds because I never build a "single" plugin with that ability. I have a "default" plugin that is used in every single project that contains the functionality of many plugins at the store... Including permissions based on entries, certain fields and on sites. But it replaces many Craft files with closures and the Yii2 classmap and its specialized to my customers - it digs really deep in Craft in order to handle things quite complex. It's not good for leaning purpose. But you can ping me on slack if you have further questions Apr 16, 2018 at 18:17
  • @Robin, just checking that I understand correctly. Add a plugin with a controller. All admin pages get the data that they display through ajax using javascript. Change the js file to use the route of my custom controller. Add logic to my controller to achieve displaying what I want. Is this correct? May 1, 2018 at 0:11

I had a similar problem where I needed to manage national and regional users groups where regional users should only see their own content. I didn’t like the idea of overriding the whole ElementIndexesController controller as most of its properties and methods are private and that could impede upgrades to Craft.

I managed to have this work by using ElementQuery::EVENT_BEFORE_PREPARE in my site module.

Regional content is assigned a region category with an attribute (numeroRegion) that match the last 2 characters of the regional group handle (ex.: "01").

Note that I’m using the Collections plugin to filter and map the groups.

if ($user && $user->id && !$user->identity->admin && $request->getIsCpRequest()) {
  Event::on(ElementQuery::class, ElementQuery::EVENT_BEFORE_PREPARE, function(Event $event) use ($user) {

    if ($event->sender->elementType === 'craft\elements\Entry') {
        $userGroups = Collections::$plugin->collectionsService->collect(Craft::$app->userGroups->getGroupsByUserId($user->id));
      $isNational = $userGroups->filter(function($group) {
        return (int)$group->id === 1;

      if (!$isNational) {
        $userRegionGroups = $userGroups->map(function($group) {
          return substr($group->handle, -2);
        $regions = Category::find()->numeroRegion($userRegionGroups)->all();
        Craft::configure($event->sender, [
          'relatedTo' => [
            'element' => $regions

Disclaimer: it works, but this is my first attempt at this kind of hack, and it may have side effects I haven’t noticed yet.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.