diff --git a/core/core.services.yml b/core/core.services.yml
index afccb31..9908126 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -36,7 +36,7 @@ parameters:
 services:
   # Simple cache contexts, directly derived from the request context.
   cache_context.ip:
-    class: Drupal\Core\Cache\Context\IpCacheContext
+    class: Drupal\Core\Cache\Context\IpCacheContextn
     arguments: ['@request_stack']
     tags:
       - { name: cache.context }
@@ -579,6 +579,9 @@ services:
   plugin.manager.menu.link:
     class: Drupal\Core\Menu\MenuLinkManager
     arguments: ['@menu.tree_storage', '@menu_link.static.overrides', '@module_handler']
+  menu.link_access:
+    class: Drupal\Core\Menu\MenuLinkAccessController
+    arguments: ['@current_user']
   menu.link_tree:
     class: Drupal\Core\Menu\MenuLinkTree
     arguments: ['@menu.tree_storage', '@plugin.manager.menu.link', '@router.route_provider', '@menu.active_trail', '@controller_resolver']
@@ -592,7 +595,7 @@ services:
       - { name: needs_destruction }
   menu.parent_form_selector:
     class: Drupal\Core\Menu\MenuParentFormSelector
-    arguments: ['@menu.link_tree', '@entity.manager', '@string_translation']
+    arguments: ['@menu.link_tree', '@plugin.manager.menu.link', '@entity_type.manager', '@string_translation']
   plugin.manager.menu.local_action:
     class: Drupal\Core\Menu\LocalActionManager
     arguments: ['@controller_resolver', '@request_stack', '@current_route_match', '@router.route_provider', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user']
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkAccessController.php b/core/lib/Drupal/Core/Menu/MenuLinkAccessController.php
new file mode 100644
index 0000000..ec72343
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkAccessController.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuLinkAccessController.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\system\MenuInterface;
+
+/**
+ * Provides a menu link access controller.
+ */
+class MenuLinkAccessController implements MenuLinkAccessControllerInterface {
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   */
+  public function __construct(AccountInterface $current_user) {
+    $this->currentUser = $current_user;
+  }
+
+  /**
+   * Gets the default access result.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkInterface|null $menu_link
+   *   The menu link access is checked for or NULL if no link exists yet.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The account that is to perform the operation.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   */
+  protected function getDefaultAccessResult(MenuLinkInterface $menu_link = NULL, AccountInterface $account) {
+    return AccessResult::allowedIfHasPermissions($account, ['administer menu'])->addCacheableDependency($menu_link);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkAddAccess(MenuInterface $menu, MenuLinkInterface $menu_link = NULL, AccountInterface $account = NULL) {
+    $account = $account ?: $this->currentUser;
+    $access = $this->getDefaultAccessResult($menu_link, $account);
+
+    // Check if the user has access by default.
+    if ($access->isAllowed()) {
+      return $access;
+    }
+
+    // Links can always be 'added' to the menus they already belong to.
+    if ($menu_link && $menu_link->getMenuName() === $menu->id()) {
+      return $access->orIf(AccessResult::allowed());
+    }
+
+    // Check if the menu link can be added to the target menu.
+    $add_access = AccessResult::allowedIfHasPermissions($account, [sprintf('system.menu.%s.menu_link.add', $menu->id())]);
+    // If the menu link is moved to another menu, check if it can be removed
+    // from its current menu.
+    $delete_access = $menu_link ? $this->checkDeleteAccess($menu_link, $account) : AccessResult::allowed();
+    return $access->orIf($add_access->andIf($delete_access));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkUpdateAccess(MenuLinkInterface $menu_link, AccountInterface $account = NULL) {
+    $account = $account ?: $this->currentUser;
+    $access = $this->getDefaultAccessResult($menu_link, $account);
+
+    // Check if the user has access by default.
+    if ($access->isAllowed()) {
+      return $access;
+    }
+
+    return $access->orIf(AccessResult::allowedIfHasPermissions($account, [sprintf('system.menu.%s.menu_link.update', $menu_link->getMenuName())]));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkDeleteAccess(MenuLinkInterface $menu_link, AccountInterface $account = NULL) {
+    $account = $account ?: $this->currentUser;
+    $access = $this->getDefaultAccessResult($menu_link, $account);
+
+    // Check if the user has access by default.
+    if ($access->isAllowed()) {
+      return $access;
+    }
+
+    return $access->orIf(AccessResult::allowedIfHasPermissions($account, [sprintf('system.menu.%s.menu_link.delete', $menu_link->getMenuName())]));
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkAccessControllerInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkAccessControllerInterface.php
new file mode 100644
index 0000000..803ad6e
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkAccessControllerInterface.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuLinkAccessControllerInterface.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Core\Session\AccountInterface;
+use Drupal\system\MenuInterface;
+
+/**
+ * Defines a menu link access controller.
+ */
+interface MenuLinkAccessControllerInterface {
+
+  /**
+   * Checks whether an account can add a menu link to a menu.
+   *
+   * @param \Drupal\system\MenuInterface $menu
+   *   The menu the menu link is to be added to.
+   * @param \Drupal\Core\Menu\MenuLinkInterface|null $menu_link
+   *   The menu link the operation is to be performed upon or NULL if a new link
+   *   is to be added.
+   * @param \Drupal\Core\Session\AccountInterface|null $account
+   *   The account that is to perform the operation or NULL to use the current
+   *   user.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   */
+  public function checkAddAccess(MenuInterface $menu, MenuLinkInterface $menu_link = NULL, AccountInterface $account = NULL);
+
+  /**
+   * Checks whether an account can update a menu link in its menu.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkInterface $menu_link
+   *   The menu link the operation is to be performed upon.
+   * @param \Drupal\Core\Session\AccountInterface|null $account
+   *   The account that is to perform the operation or NULL to use the current
+   *   user.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   */
+  public function checkUpdateAccess(MenuLinkInterface $menu_link, AccountInterface $account = NULL);
+
+  /**
+   * Checks whether an account can delete a menu link from its menu.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkInterface $menu_link
+   *   The menu link the operation is to be performed upon.
+   * @param \Drupal\Core\Session\AccountInterface|null $account
+   *   The account that is to perform the operation or NULL to use the current
+   *   user.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   */
+  public function checkDeleteAccess(MenuLinkInterface $menu_link, AccountInterface $account = NULL);
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php b/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php
index c014112..8c91dcd 100644
--- a/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php
+++ b/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
 
@@ -22,6 +23,20 @@ class MenuParentFormSelector implements MenuParentFormSelectorInterface {
   use StringTranslationTrait;
 
   /**
+   * The menu link manager.
+   *
+   * @var \Drupal\Core\Menu\MenuLinkManagerInterface
+   */
+  protected $menuLinkManager;
+
+  /**
+   * The menu link access controller.
+   *
+   * @var \Drupal\Core\Menu\MenuLinkAccessControllerInterface
+   */
+  protected $menuLinkAccessController;
+
+  /**
    * The menu link tree service.
    *
    * @var \Drupal\Core\Menu\MenuLinkTreeInterface
@@ -29,39 +44,63 @@ class MenuParentFormSelector implements MenuParentFormSelectorInterface {
   protected $menuLinkTree;
 
   /**
-   * The entity manager.
+   * The menu storage.
    *
-   * @var \Drupal\Core\Entity\EntityManagerInterface
+   * @var \Drupal\Core\Entity\EntityStorageInterface
    */
-  protected $entityManager;
+  protected $menuStorage;
 
   /**
    * Constructs a \Drupal\Core\Menu\MenuParentFormSelector
    *
    * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
    *   The menu link tree service.
-   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   * @param \Drupal\Core\Menu\MenuLinkmanagerInterface
+   *   The menu link manager.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    *   The entity manager.
    * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
    *   The string translation service.
    */
-  public function __construct(MenuLinkTreeInterface $menu_link_tree, EntityManagerInterface $entity_manager, TranslationInterface $string_translation) {
+  public function __construct(MenuLinkTreeInterface $menu_link_tree, MenuLinkManagerInterface $menu_link_manager, EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
+    $this->menuLinkManager = $menu_link_manager;
     $this->menuLinkTree = $menu_link_tree;
-    $this->entityManager = $entity_manager;
+    $this->menuStorage = $entity_type_manager->getStorage('menu');
     $this->stringTranslation = $string_translation;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getParentSelectOptions($id = '', array $menus = NULL, CacheableMetadata &$cacheability = NULL) {
-    if (!isset($menus)) {
-      $menus = $this->getMenuOptions();
+  public function getParentSelectOptions($id = '', array $menu_options = NULL, CacheableMetadata &$cacheability = NULL) {
+    $menus = $this->menuStorage->loadMultiple(is_array($menu_options) ? array_keys($menu_options) : NULL);
+
+    if (!isset($menu_options)) {
+      $menu_options = [];
+      foreach ($menus as $menu) {
+        $menu_options[$menu->id()] = $menu->label();
+      }
+      return $menu_options;
     }
 
+    // Filter the available menus by the ones the current user has access to.
+    $menu_link = NULL;
+    if ($id) {
+      $menu_link = $this->menuLinkManager->createInstance($id);
+    }
+    array_filter($menu_options, function($menu_name) use ($menus, $menu_link, $cacheability) {
+      $menu = $menus[$menu_name];
+      $access = $this->menuLinkAccessController->checkAddAccess($menu, $menu_link);
+      if ($menu_link) {
+        $access->andIf($this->menuLinkAccessController->checkDeleteAccess($menu, $menu_link));
+      }
+      $cacheability->addCacheableDependency($access);
+      return $access->isAllowed();
+    }, ARRAY_FILTER_USE_KEY);
+
     $options = array();
     $depth_limit = $this->getParentDepthLimit($id);
-    foreach ($menus as $menu_name => $menu_title) {
+    foreach ($menu_options as $menu_name => $menu_title) {
       $options[$menu_name . ':'] = '<' . $menu_title . '>';
 
       $parameters = new MenuTreeParameters();
@@ -177,23 +216,4 @@ protected function parentSelectOptionsTreeWalk(array $tree, $menu_name, $indent,
     }
   }
 
-  /**
-   * Gets a list of menu names for use as options.
-   *
-   * @param array $menu_names
-   *   (optional) Array of menu names to limit the options, or NULL to load all.
-   *
-   * @return array
-   *   Keys are menu names (ids) values are the menu labels.
-   */
-  protected function getMenuOptions(array $menu_names = NULL) {
-    $menus = $this->entityManager->getStorage('menu')->loadMultiple($menu_names);
-    $options = array();
-    /** @var \Drupal\system\MenuInterface[] $menus */
-    foreach ($menus as $menu) {
-      $options[$menu->id()] = $menu->label();
-    }
-    return $options;
-  }
-
 }
diff --git a/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php b/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php
index 8e096e5..0aa3e15 100644
--- a/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php
+++ b/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php
@@ -20,7 +20,7 @@
    * @param string $id
    *   Optional ID of a link plugin. This will exclude the link and its
    *   children from the select options.
-   * @param array $menus
+   * @param array $menu_options
    *   Optional array of menu names as keys and titles as values to limit
    *   the select options.  If NULL, all menus will be included.
    * @param \Drupal\Core\Cache\CacheableMetadata|NULL &$cacheability
@@ -31,7 +31,7 @@
    *   Keyed array where the keys are contain a menu name and parent ID and
    *   the values are a menu name or link title indented by depth.
    */
-  public function getParentSelectOptions($id = '', array $menus = NULL, CacheableMetadata &$cacheability = NULL);
+  public function getParentSelectOptions($id = '', array $menu_options = NULL, CacheableMetadata &$cacheability = NULL);
 
   /**
    * Gets a form element to choose a menu and parent.
@@ -46,7 +46,7 @@ public function getParentSelectOptions($id = '', array $menus = NULL, CacheableM
    * @param string $id
    *   (optional) ID of a link plugin. This will exclude the link and its
    *   children from being selected.
-   * @param array $menus
+   * @param array|null $menus
    *   (optional) array of menu names as keys and titles as values to limit
    *   the values that may be selected. If NULL, all menus will be included.
    *
diff --git a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
index dc14109..1aa9643 100644
--- a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
+++ b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
@@ -33,7 +33,6 @@
  *       "delete" = "Drupal\menu_link_content\Form\MenuLinkContentDeleteForm"
  *     }
  *   },
- *   admin_permission = "administer menu",
  *   base_table = "menu_link_content",
  *   data_table = "menu_link_content_data",
  *   translatable = TRUE,
diff --git a/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php b/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php
index d3c0998..d901218 100644
--- a/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php
+++ b/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Entity\EntityAccessControlHandler;
 use Drupal\Core\Entity\EntityHandlerInterface;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Session\AccountInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -28,30 +29,62 @@ class MenuLinkContentAccessControlHandler extends EntityAccessControlHandler imp
   protected $accessManager;
 
   /**
+   * The menu storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $menuStorage;
+
+  /**
    * Creates a new MenuLinkContentAccessControlHandler.
    *
    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
    *   The entity type definition.
    * @param \Drupal\Core\Access\AccessManagerInterface $access_manager
    *   The access manager to check routes by name.
+   * @param \Drupal\Core\Entity\EntityStorageInterface
+   *   The menu storage.
    */
-  public function __construct(EntityTypeInterface $entity_type, AccessManagerInterface $access_manager) {
+  public function __construct(EntityTypeInterface $entity_type, AccessManagerInterface $access_manager, EntityStorageInterface $menu_storage) {
     parent::__construct($entity_type);
 
     $this->accessManager = $access_manager;
+    $this->menuStorage = $menu_storage;
   }
 
   /**
    * {@inheritdoc}
    */
   public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
-    return new static($entity_type, $container->get('access_manager'));
+    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
+    $entity_type_manager = $container->get('entity_type.manager');
+
+    return new static($entity_type, $container->get('access_manager'), $entity_type_manager->getStorage('menu'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    // We only need the IDs of the available menus and an entity query is the
+    // fastest way to get them.
+    $query = $this->menuStorage->getQuery();
+    $query->condition('status', TRUE);
+    $menu_ids = $query->execute();
+    $permissions = [];
+    foreach ($menu_ids as $menu_id) {
+      $permissions[] = sprintf('system.menu.%s.menu_link.add', $menu_id);
+    }
+
+    return AccessResult::allowedIfHasPermissions($account, $permissions);
   }
 
   /**
    * {@inheritdoc}
    */
   protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    /** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
+
     switch ($operation) {
       case 'view':
         // There is no direct viewing of a menu link, but still for purposes of
@@ -59,13 +92,12 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
         return AccessResult::allowedIfHasPermission($account, 'administer menu');
 
       case 'update':
-        if (!$account->hasPermission('administer menu')) {
+        if (!$account->hasPermission('administer menu') && !$account->hasPermission(sprintf('system.menu.%s.menu_link.update', $entity->getMenuName()))) {
           return AccessResult::neutral()->cachePerPermissions();
         }
         else {
           // If there is a URL, this is an external link so always accessible.
           $access = AccessResult::allowed()->cachePerPermissions()->cacheUntilEntityChanges($entity);
-          /** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
           // We allow access, but only if the link is accessible as well.
           if (($url_object = $entity->getUrlObject()) && $url_object->isRouted()) {
             $link_access = $this->accessManager->checkNamedRoute($url_object->getRouteName(), $url_object->getRouteParameters(), $account, TRUE);
@@ -75,7 +107,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
         }
 
       case 'delete':
-        return AccessResult::allowedIf(!$entity->isNew() && $account->hasPermission('administer menu'))->cachePerPermissions()->cacheUntilEntityChanges($entity);
+        return AccessResult::allowedIf(!$entity->isNew())->allowedIfHasPermissions($account, ['administer menu', sprintf('system.menu.%s.menu_link.delete', $entity->getMenuName())])->cacheUntilEntityChanges($entity);
     }
   }
 
diff --git a/core/modules/menu_ui/menu_ui.routing.yml b/core/modules/menu_ui/menu_ui.routing.yml
index 7f94a6d..51984a4 100644
--- a/core/modules/menu_ui/menu_ui.routing.yml
+++ b/core/modules/menu_ui/menu_ui.routing.yml
@@ -23,7 +23,7 @@ menu_ui.link_edit:
       menu_link_plugin:
         type: menu_link_plugin
   requirements:
-    _permission: 'administer menu'
+    _custom_access: '\Drupal\Core\Menu\MenuLinkAccessController::checkUpdateAccess'
 
 menu_ui.link_reset:
   path: '/admin/structure/menu/link/{menu_link_plugin}/reset'
diff --git a/core/modules/menu_ui/src/Controller/MenuController.php b/core/modules/menu_ui/src/Controller/MenuController.php
index 32fb743..de8af1c 100644
--- a/core/modules/menu_ui/src/Controller/MenuController.php
+++ b/core/modules/menu_ui/src/Controller/MenuController.php
@@ -18,7 +18,8 @@
 /**
  * Returns responses for Menu routes.
  */
-class MenuController extends ControllerBase {
+class
+MenuController extends ControllerBase {
 
   /**
    * The menu parent form service.
diff --git a/core/modules/system/src/MenuAccessControlHandler.php b/core/modules/system/src/MenuAccessControlHandler.php
index 789577b..de3c34c 100644
--- a/core/modules/system/src/MenuAccessControlHandler.php
+++ b/core/modules/system/src/MenuAccessControlHandler.php
@@ -24,7 +24,7 @@ class MenuAccessControlHandler extends EntityAccessControlHandler {
    */
   protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
     if ($operation === 'view') {
-      return AccessResult::allowed();
+      return AccessResult::allowedIfHasPermissions($account, [sprintf('system.menu.%s.view', $entity->id()), $this->entityType->getAdminPermission()], 'OR');
     }
     // Locked menus could not be deleted.
     elseif ($operation == 'delete') {
diff --git a/core/modules/system/src/MenuPermissions.php b/core/modules/system/src/MenuPermissions.php
new file mode 100644
index 0000000..8b48c93
--- /dev/null
+++ b/core/modules/system/src/MenuPermissions.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\MenuPermissions.
+ */
+
+namespace Drupal\system;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides dynamic permissions for the menu system.
+ */
+class MenuPermissions implements ContainerInjectionInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The menu storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $menuStorage;
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The string translator.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $menu_storage
+   *   The menu storage.
+   */
+  public function __construct(TranslationInterface $string_translation, EntityStorageInterface $menu_storage) {
+    $this->menuStorage = $menu_storage;
+    $this->stringTranslation = $string_translation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
+    $entity_type_manager = $container->get('entity_type.manager');
+
+    return new static($container->get('string_translation'), $entity_type_manager->getStorage('menu'));
+  }
+
+  /**
+   * Builds dynamic permissions for the menu system.
+   *
+   * @return mixed[]
+   *   The output is identical to that of
+   *   \Drupal\user\PermissionHandlerInterface::getPermissions().
+   */
+  public function buildPermissions() {
+    $permissions = [];
+    foreach ($this->menuStorage->loadMultiple() as $menu) {
+      $permissions += $this->buildPermissionsPerMenu($menu);
+    }
+
+    return $permissions;
+  }
+
+  /**
+   * Builds dynamic permissions for a single menu.
+   *
+   * @param \Drupal\system\MenuInterface $menu
+   *   The menu.
+   *
+   * @return mixed[]
+   *   The output is identical to that of
+   *   \Drupal\user\PermissionHandlerInterface::getPermissions().
+   */
+  protected function buildPermissionsPerMenu(MenuInterface $menu) {
+    return [
+      sprintf('system.menu.%s.menu_link.add', $menu->id()) => [
+        'title' => $this->t('Add links to the %menu_label menu', [
+          '%menu_label' => $menu->label(),
+        ]),
+      ],
+      sprintf('system.menu.%s.menu_link.update', $menu->id()) => [
+        'title' => $this->t('Update links in the %menu_label menu', [
+          '%menu_label' => $menu->label(),
+        ]),
+      ],
+      sprintf('system.menu.%s.menu_link.delete', $menu->id()) => [
+        'title' => $this->t('Delete links from the %menu_label menu', [
+          '%menu_label' => $menu->label(),
+        ]),
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/system/system.permissions.yml b/core/modules/system/system.permissions.yml
index 726c295..41d8f13 100644
--- a/core/modules/system/system.permissions.yml
+++ b/core/modules/system/system.permissions.yml
@@ -24,3 +24,5 @@ link to any page:
   description: 'This allows to bypass access checking when linking to internal paths.'
 administer menu:
   title: 'Administer menus and menu items'
+permission_callbacks:
+  - \Drupal\system\MenuPermissions::buildPermissions
diff --git a/core/tests/Drupal/Tests/Core/Menu/MenuLinkAccessControllerTest.php b/core/tests/Drupal/Tests/Core/Menu/MenuLinkAccessControllerTest.php
new file mode 100644
index 0000000..3ea84d3
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Menu/MenuLinkAccessControllerTest.php
@@ -0,0 +1,307 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Menu\MenuLinkAccessControllerTest.
+ */
+
+namespace Drupal\Tests\Core\Menu;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Menu\MenuLinkAccessController;
+use Drupal\Core\Menu\MenuLinkInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\system\MenuInterface;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Menu\MenuLinkAccessController
+ */
+class MenuLinkAccessControllerTest extends UnitTestCase {
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * The subject under test.
+   *
+   * @var \Drupal\Core\Menu\MenuLinkAccessController
+   */
+  protected $menuLinkAccessController;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    // \Drupal\Core\Cache\Cache accesses \Drupal in an assert(), which we should
+    // not have to mock in a unit test.
+    assert_options(ASSERT_ACTIVE, FALSE);
+
+    $this->currentUser = $this->prophesize(AccountInterface::class);
+
+    $this->menuLinkAccessController = new MenuLinkAccessController($this->currentUser->reveal());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function tearDown() {
+    parent::tearDown();
+    // Asserts were disabled in self::setUp().
+    assert_options(ASSERT_ACTIVE, TRUE);
+  }
+
+  /**
+   * @covers ::checkAddAccess
+   *
+   * @dataProvider providerCheckAddAccess
+   *
+   * @param bool|null $expected_access
+   *   TRUE if access is allowed, FALSE if access is forbidden, or NULL if the
+   *   result is neutral.
+   * @param string $menu_id
+   *   The ID of the menu the link is to be added to.
+   * @param string|null $menu_link_menu_id
+   *   The ID of the link's current menu, or NULL if no link exists yet.
+   * @param bool $use_current_user
+   *   Whether or not to use the current user.
+   * @param string[] $account_permissions
+   *   The permissions the account has.
+   */
+  public function testCheckAddAccess($expected_access, $menu_id, $menu_link_menu_id, $use_current_user, array $account_permissions) {
+    $account = NULL;
+    /** @var \Drupal\Core\Session\AccountInterface|\Prophecy\Prophecy\ObjectProphecy $mocked_account */
+    if ($use_current_user) {
+      $mocked_account = $this->currentUser;
+      $account = NULL;
+    }
+    else {
+      $mocked_account = $this->prophesize(AccountInterface::class);
+      $account = $mocked_account->reveal();
+    }
+
+    foreach ($account_permissions as $permission) {
+      $mocked_account->hasPermission($permission)->willReturn(TRUE);
+    }
+    $mocked_account->hasPermission(Argument::any())->willReturn(FALSE);
+
+    $menu = $this->prophesize(MenuInterface::class);
+    $menu->id()->willReturn($menu_id);
+
+    $menu_link = NULL;
+    if ($menu_link_menu_id) {
+      /** @var \Drupal\Core\Menu\MenuLinkInterface|\Prophecy\Prophecy\ObjectProphecy $menu_link */
+      $mocked_menu_link = $this->prophesize(MenuLinkInterface::class);
+      $mocked_menu_link->getMenuName()->willReturn($menu_link_menu_id);
+      $mocked_menu_link->getCacheContexts()->willReturn([]);
+      $mocked_menu_link->getCacheTags()->willReturn([]);
+      $mocked_menu_link->getCacheMaxAge()->willReturn(Cache::PERMANENT);
+      $menu_link = $mocked_menu_link->reveal();
+    }
+
+    $access = $this->menuLinkAccessController->checkAddAccess($menu->reveal(), $menu_link, $account);
+    $this->assertSame($expected_access === TRUE, $access->isAllowed());
+    $this->assertSame($expected_access === FALSE, $access->isForbidden());
+    $this->assertSame($expected_access === NULL, $access->isNeutral());
+  }
+
+  /**
+   * Provides data to self::testCheckAddAccess()
+   */
+  public function providerCheckAddAccess() {
+    return [
+      // The link exists and already belongs to the menu it is to be added to.
+      [TRUE, 'foo', 'foo', TRUE, []],
+      [TRUE, 'foo', 'foo', FALSE, []],
+
+      // No link exists yet, and the current user has the bypass permission.
+      [TRUE, 'foo', NULL, TRUE, ['administer menu']],
+      // No link exists yet, and the given user has the bypass permission.
+      [TRUE, 'foo', NULL, FALSE, ['administer menu']],
+      // No link exists yet, and the current user has permission to add a new
+      // link to this specific menu.
+      [TRUE, 'foo', NULL, TRUE, ['system.menu.foo.menu_link.add']],
+      // No link exists yet, and the given user has permission to add a new link
+      // to this specific menu.
+      [TRUE, 'foo', NULL, FALSE, ['system.menu.foo.menu_link.add']],
+
+      // A link exists, and the current user has the bypass permission.
+      [TRUE, 'foo', 'bar', TRUE, ['administer menu']],
+      // A link exist, and the given user has the bypass permission.
+      [TRUE, 'foo', 'bar', FALSE, ['administer menu']],
+      // A link exist, and the current user has permission to add a new link to
+      // this specific menu.
+      [TRUE, 'foo', 'bar', TRUE, ['system.menu.foo.menu_link.add', 'system.menu.bar.menu_link.delete']],
+      // A link exist, and the given user has permission to add a new link to
+      // this specific menu.
+      [TRUE, 'foo', 'bar', FALSE, ['system.menu.foo.menu_link.add', 'system.menu.bar.menu_link.delete']],
+
+      // No link exists yet, and the current user has no permissions.
+      [NULL, 'foo', NULL, TRUE, []],
+      // No link exists yet, and the given user has no permissions.
+      [NULL, 'foo', NULL, FALSE, []],
+      // A link exists, and the current user has no permissions.
+      [NULL, 'foo', 'bar', TRUE, []],
+      // A link exists, and the given user has no permissions.
+      [NULL, 'foo', 'bar', FALSE, []],
+      // A link exist, and the current user has permission to add a new link to
+      // a menu the link is not located in.
+      [NULL, 'foo', 'bar', TRUE, ['system.menu.foo.menu_link.add']],
+      // A link exist, and the given user has permission to add a new link to
+      // a menu the link is not located in.
+      [NULL, 'foo', 'bar', FALSE, ['system.menu.foo.menu_link.add']],
+      // A link exist, and the current user has permission to delete a link from
+      // its current menu.
+      [NULL, 'foo', 'bar', TRUE, ['system.menu.bar.menu_link.delete']],
+      // A link exist, and the given user has permission to delete a link from
+      // its menu.
+      [NULL, 'foo', 'bar', FALSE, ['system.menu.bar.menu_link.delete']],
+    ];
+  }
+
+  /**
+   * @covers ::checkUpdateAccess
+   *
+   * @dataProvider providerCheckUpdateAccess
+   *
+   * @param bool|null $expected_access
+   *   TRUE if access is allowed, FALSE if access is forbidden, or NULL if the
+   *   result is neutral.
+   * @param string $menu_link_menu_id
+   *   The ID of the link's current menu.
+   * @param bool $use_current_user
+   *   Whether or not to use the current user.
+   * @param string[] $account_permissions
+   *   The permissions the account has.
+   */
+  public function testCheckUpdateAccess($expected_access, $menu_link_menu_id, $use_current_user, array $account_permissions) {
+    $account = NULL;
+    /** @var \Drupal\Core\Session\AccountInterface|\Prophecy\Prophecy\ObjectProphecy $mocked_account */
+    if ($use_current_user) {
+      $mocked_account = $this->currentUser;
+      $account = NULL;
+    }
+    else {
+      $mocked_account = $this->prophesize(AccountInterface::class);
+      $account = $mocked_account->reveal();
+    }
+
+
+    foreach ($account_permissions as $permission) {
+      $mocked_account->hasPermission($permission)->willReturn(TRUE);
+    }
+    $mocked_account->hasPermission(Argument::any())->willReturn(FALSE);
+
+    /** @var \Drupal\Core\Menu\MenuLinkInterface|\Prophecy\Prophecy\ObjectProphecy $menu_link */
+    $menu_link = $this->prophesize(MenuLinkInterface::class);
+    $menu_link->getMenuName()->willReturn($menu_link_menu_id);
+    $menu_link->getCacheContexts()->willReturn([]);
+    $menu_link->getCacheTags()->willReturn([]);
+    $menu_link->getCacheMaxAge()->willReturn(Cache::PERMANENT);
+
+    $access = $this->menuLinkAccessController->checkUpdateAccess($menu_link->reveal(), $account);
+    $this->assertSame($expected_access === TRUE, $access->isAllowed());
+    $this->assertSame($expected_access === FALSE, $access->isForbidden());
+    $this->assertSame($expected_access === NULL, $access->isNeutral());
+  }
+
+  /**
+   * Provides data to self::testCheckUpdateAccess()
+   */
+  public function providerCheckUpdateAccess() {
+    return [
+      // The current user has the bypass permission.
+      [TRUE, 'foo', TRUE, ['administer menu']],
+      // The given user has the bypass permission.
+      [TRUE, 'foo', FALSE, ['administer menu']],
+      // The current user has permission to update from this specific menu.
+      [TRUE, 'foo', TRUE, ['system.menu.foo.menu_link.update']],
+      // The given user has permission to update from this specific menu.
+      [TRUE, 'foo', FALSE, ['system.menu.foo.menu_link.update']],
+
+      // The current user has none of the required permissions.
+      [NULL, 'foo', TRUE, []],
+      // The given user has none of the required permissions.
+      [NULL, 'foo', FALSE, []],
+      // The menu link is not in the menu the user has permission to update
+      // from.
+      [NULL, 'foo', TRUE, ['system.menu.bar.menu_link.update']],
+    ];
+  }
+
+  /**
+   * @covers ::checkDeleteAccess
+   *
+   * @dataProvider providerCheckDeleteAccess
+   *
+   * @param bool|null $expected_access
+   *   TRUE if access is allowed, FALSE if access is forbidden, or NULL if the
+   *   result is neutral.
+   * @param string $menu_link_menu_id
+   *   The ID of the link's current menu.
+   * @param bool $use_current_user
+   *   Whether or not to use the current user.
+   * @param string[] $account_permissions
+   *   The permissions the account has.
+   */
+  public function testCheckDeleteAccess($expected_access, $menu_link_menu_id, $use_current_user, array $account_permissions) {
+    $account = NULL;
+    /** @var \Drupal\Core\Session\AccountInterface|\Prophecy\Prophecy\ObjectProphecy $mocked_account */
+    if ($use_current_user) {
+      $mocked_account = $this->currentUser;
+      $account = NULL;
+    }
+    else {
+      $mocked_account = $this->prophesize(AccountInterface::class);
+      $account = $mocked_account->reveal();
+    }
+
+    foreach ($account_permissions as $permission) {
+      $mocked_account->hasPermission($permission)->willReturn(TRUE);
+    }
+    $mocked_account->hasPermission(Argument::any())->willReturn(FALSE);
+
+    /** @var \Drupal\Core\Menu\MenuLinkInterface|\Prophecy\Prophecy\ObjectProphecy $menu_link */
+    $menu_link = $this->prophesize(MenuLinkInterface::class);
+    $menu_link->getMenuName()->willReturn($menu_link_menu_id);
+    $menu_link->getCacheContexts()->willReturn([]);
+    $menu_link->getCacheTags()->willReturn([]);
+    $menu_link->getCacheMaxAge()->willReturn(Cache::PERMANENT);
+
+    $access = $this->menuLinkAccessController->checkDeleteAccess($menu_link->reveal(), $account);
+    $this->assertSame($expected_access === TRUE, $access->isAllowed());
+    $this->assertSame($expected_access === FALSE, $access->isForbidden());
+    $this->assertSame($expected_access === NULL, $access->isNeutral());
+  }
+
+  /**
+   * Provides data to self::testCheckDeleteAccess()
+   */
+  public function providerCheckDeleteAccess() {
+    return [
+      // The current user has the bypass permission.
+      [TRUE, 'foo', TRUE, ['administer menu']],
+      // The given user has the bypass permission.
+      [TRUE, 'foo', FALSE, ['administer menu']],
+      // The current user has permission to delete from this specific menu.
+      [TRUE, 'foo', TRUE, ['system.menu.foo.menu_link.delete']],
+      // The given user has permission to delete from this specific menu.
+      [TRUE, 'foo', FALSE, ['system.menu.foo.menu_link.delete']],
+
+      // The current user has none of the required permissions.
+      [NULL, 'foo', TRUE, []],
+      // The given user has none of the required permissions.
+      [NULL, 'foo', FALSE, []],
+      // The menu link is not in the menu the user has permission to delete
+      // from.
+      [NULL, 'foo', TRUE, ['system.menu.bar.menu_link.delete']],
+    ];
+  }
+
+}
