diff --git a/core/modules/user/src/AccountForm.php b/core/modules/user/src/AccountForm.php
index d6959681f4..bd20dd1d66 100644
--- a/core/modules/user/src/AccountForm.php
+++ b/core/modules/user/src/AccountForm.php
@@ -191,7 +191,7 @@ public function form(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Roles'),
       '#default_value' => (!$register ? $account->getRoles() : []),
       '#options' => $roles,
-      '#access' => $roles && $user->hasPermission('administer permissions'),
+      '#access' => $roles && $account->roles->access('edit', $user, TRUE),
     ];
 
     // Special handling for the inevitable "Authenticated user" role.
diff --git a/core/modules/user/src/UserAccessControlHandler.php b/core/modules/user/src/UserAccessControlHandler.php
index 8ff01d1447..8072175f85 100644
--- a/core/modules/user/src/UserAccessControlHandler.php
+++ b/core/modules/user/src/UserAccessControlHandler.php
@@ -82,9 +82,10 @@ protected function checkFieldAccess($operation, FieldDefinitionInterface $field_
     // Fields that are not implicitly allowed to administrative users.
     $explicit_check_fields = [
       'pass',
+      'roles',
     ];
 
-    // Administrative users are allowed to edit and view all fields.
+    // Administrative users can edit all fields except password and roles.
     if (!in_array($field_definition->getName(), $explicit_check_fields) && $account->hasPermission('administer users')) {
       return AccessResult::allowed()->cachePerPermissions();
     }
@@ -130,6 +131,8 @@ protected function checkFieldAccess($operation, FieldDefinitionInterface $field_
         return ($operation == 'view') ? AccessResult::allowed() : AccessResult::forbidden();
 
       case 'roles':
+        return AccessResult::allowedIfHasPermission($account, 'administer permissions');
+
       case 'status':
       case 'access':
       case 'login':
diff --git a/core/modules/user/tests/src/Functional/UserLoginHttpTest.php b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php
index b12a72b58e..89d54138b0 100644
--- a/core/modules/user/tests/src/Functional/UserLoginHttpTest.php
+++ b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php
@@ -114,9 +114,9 @@ public function testLogin() {
   protected function doTestLogin($format) {
     $client = \Drupal::httpClient();
     // Create new user for each iteration to reset flood.
-    // Grant the user administer users permissions to they can see the
-    // 'roles' field.
-    $account = $this->drupalCreateUser(['administer users']);
+    // Grant the user 'administer permissions' so they can see the 'roles'
+    // field.
+    $account = $this->drupalCreateUser(['administer permissions']);
     $name = $account->getUsername();
     $pass = $account->passRaw;
 
diff --git a/core/modules/user/tests/src/Functional/Views/BulkFormTest.php b/core/modules/user/tests/src/Functional/Views/BulkFormTest.php
index 29c0e83e6b..90c06ac063 100644
--- a/core/modules/user/tests/src/Functional/Views/BulkFormTest.php
+++ b/core/modules/user/tests/src/Functional/Views/BulkFormTest.php
@@ -3,7 +3,7 @@
 namespace Drupal\Tests\user\Functional\Views;
 
 use Drupal\user\Entity\User;
-use Drupal\user\RoleInterface;
+use Drupal\user\Entity\Role;
 use Drupal\views\Views;
 
 /**
@@ -32,8 +32,6 @@ class BulkFormTest extends UserTestBase {
    * Tests the user bulk form.
    */
   public function testBulkForm() {
-    // Log in as a user without 'administer users'.
-    $this->drupalLogin($this->drupalCreateUser(['administer permissions']));
     $user_storage = $this->container->get('entity.manager')->getStorage('user');
 
     // Create an user which actually can change users.
@@ -49,32 +47,7 @@ public function testBulkForm() {
     $this->drupalPostForm('test-user-bulk-form', $edit, t('Apply to selected items'));
     $this->assertText(t('No users selected.'));
 
-    // Assign a role to a user.
     $account = $user_storage->load($this->users[0]->id());
-    $roles = user_role_names(TRUE);
-    unset($roles[RoleInterface::AUTHENTICATED_ID]);
-    $role = key($roles);
-
-    $this->assertFalse($account->hasRole($role), 'The user currently does not have a custom role.');
-    $edit = [
-      'user_bulk_form[1]' => TRUE,
-      'action' => 'user_add_role_action.' . $role,
-    ];
-    $this->drupalPostForm(NULL, $edit, t('Apply to selected items'));
-    // Re-load the user and check their roles.
-    $user_storage->resetCache([$account->id()]);
-    $account = $user_storage->load($account->id());
-    $this->assertTrue($account->hasRole($role), 'The user now has the custom role.');
-
-    $edit = [
-      'user_bulk_form[1]' => TRUE,
-      'action' => 'user_remove_role_action.' . $role,
-    ];
-    $this->drupalPostForm(NULL, $edit, t('Apply to selected items'));
-    // Re-load the user and check their roles.
-    $user_storage->resetCache([$account->id()]);
-    $account = $user_storage->load($account->id());
-    $this->assertFalse($account->hasRole($role), 'The user no longer has the custom role.');
 
     // Block a user using the bulk form.
     $this->assertTrue($account->isActive(), 'The user is not blocked.');
@@ -110,7 +83,9 @@ public function testBulkForm() {
 
     // Test the list of available actions with a value that contains a dot.
     $this->drupalLogin($this->drupalCreateUser(['administer permissions', 'administer views', 'administer users']));
-    $action_id = 'user_add_role_action.' . $role;
+    $role_id = strtolower($this->randomMachineName());
+    Role::create(['id' => $role_id])->save();
+    $action_id = 'user_add_role_action.' . $role_id;
     $edit = [
       'options[include_exclude]' => 'exclude',
       "options[selected_actions][$action_id]" => $action_id,
@@ -127,6 +102,78 @@ public function testBulkForm() {
   }
 
   /**
+   * Tests role assignment/de-assignment actions in user bulk forms.
+   */
+  public function testBulkFormRoles() {
+    $this->drupalLogin($this->drupalCreateUser(['administer users']));
+
+    $user_storage = $this->container->get('entity_type.manager')->getStorage('user');
+
+    $role = Role::create([
+      'id' => strtolower($this->randomMachineName()),
+      'label' => $this->randomMachineName(),
+    ]);
+    $role->save();
+    $role_label = $role->label();
+
+    $user = $this->drupalCreateUser();
+    $uid = $user->id();
+    $username = $user->label();
+    $expected_roles = [];
+    $this->assertEqual($expected_roles, $user->getRoles(TRUE), 'The user does not have any custom roles.');
+
+    // Attempt to assign role (without permission).
+    $edit = [
+      'user_bulk_form[3]' => TRUE,
+      'action' => 'user_add_role_action.' . $role->id(),
+    ];
+    $this->drupalPostForm('test-user-bulk-form', $edit, t('Apply to selected items'));
+
+    $this->assertText("No access to execute Add the $role_label role to the selected users on the User $username");
+    $user_storage->resetCache([$uid]);
+    $user = User::load($uid);
+    $this->assertEqual($expected_roles, $user->getRoles(TRUE), 'Roles are unchanged.');
+
+    // Attempt to remove role (without permission).
+    $edit = [
+      'user_bulk_form[3]' => TRUE,
+      'action' => 'user_remove_role_action.' . $role->id(),
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Apply to selected items'));
+    $this->assertText("No access to execute Remove the $role_label role from the selected users on the User $username");
+    $user_storage->resetCache([$uid]);
+    $user = User::load($uid);
+    $this->assertEqual($expected_roles, $user->getRoles(TRUE), 'Roles are unchanged.');
+
+    // Login with a user who can modify roles.
+    $this->drupalLogin($this->drupalCreateUser(['administer users', 'administer permissions']));
+
+    // Assign role to user.
+    $edit = [
+      'user_bulk_form[3]' => TRUE,
+      'action' => 'user_add_role_action.' . $role->id(),
+    ];
+    $this->drupalPostForm('test-user-bulk-form', $edit, t('Apply to selected items'));
+    $this->assertText("Add the $role_label role to the selected users was applied to 1 item.");
+    $user_storage->resetCache([$uid]);
+    $user = User::load($uid);
+    $expected_roles = [$role->id()];
+    $this->assertEqual($expected_roles, $user->getRoles(TRUE), 'Role was added.');
+
+    // Remove role from user.
+    $edit = [
+      'user_bulk_form[3]' => TRUE,
+      'action' => 'user_remove_role_action.' . $role->id(),
+    ];
+    $this->drupalPostForm('test-user-bulk-form', $edit, t('Apply to selected items'));
+    $this->assertText("Remove the $role_label role from the selected users was applied to 1 item.");
+    $user_storage->resetCache([$uid]);
+    $user = User::load($uid);
+    $expected_roles = [];
+    $this->assertEqual($expected_roles, $user->getRoles(TRUE), 'Role was removed.');
+  }
+
+  /**
    * Tests the user bulk form with a combined field filter on the bulk column.
    */
   public function testBulkFormCombineFilter() {
diff --git a/core/modules/user/tests/src/Unit/UserAccessControlHandlerTest.php b/core/modules/user/tests/src/Unit/UserAccessControlHandlerTest.php
index 216e8d6522..f1e1ef0081 100644
--- a/core/modules/user/tests/src/Unit/UserAccessControlHandlerTest.php
+++ b/core/modules/user/tests/src/Unit/UserAccessControlHandlerTest.php
@@ -40,11 +40,18 @@ class UserAccessControlHandlerTest extends UnitTestCase {
   protected $owner;
 
   /**
-   * The mock administrative test user.
+   * A user with 'administer users' permission.
    *
    * @var \Drupal\Core\Session\AccountInterface
    */
-  protected $admin;
+  protected $admin_users;
+
+  /**
+   * A user with 'administer permissions' permission.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $admin_roles;
 
   /**
    * The mocked test field items.
@@ -83,6 +90,7 @@ protected function setUp() {
       ->will($this->returnValueMap([
         ['administer users', FALSE],
         ['change own username', TRUE],
+        ['administer permissions', FALSE],
       ]));
 
     $this->owner
@@ -90,11 +98,23 @@ protected function setUp() {
       ->method('id')
       ->will($this->returnValue(2));
 
-    $this->admin = $this->getMock('\Drupal\Core\Session\AccountInterface');
-    $this->admin
+    $this->admin_users = $this->getMock('\Drupal\Core\Session\AccountInterface');
+    $this->admin_users
+      ->expects($this->any())
+      ->method('hasPermission')
+      ->will($this->returnValueMap([
+        ['administer users', TRUE],
+        ['administer permissions', FALSE],
+      ]));
+
+    $this->admin_roles = $this->getMock('\Drupal\Core\Session\AccountInterface');
+    $this->admin_roles
       ->expects($this->any())
       ->method('hasPermission')
-      ->will($this->returnValue(TRUE));
+      ->will($this->returnValueMap([
+        ['administer users', FALSE],
+        ['administer permissions', TRUE],
+      ]));
 
     $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
 
@@ -177,7 +197,7 @@ public function userNameProvider() {
       ],
       // The users-administrator user has full access.
       [
-        'viewer' => 'admin',
+        'viewer' => 'admin_users',
         'target' => 'owner',
         'view' => TRUE,
         'edit' => TRUE,
@@ -235,11 +255,18 @@ public function hiddenUserSettingsProvider() {
       ];
       $access_info[] = [
         'field' => $field,
-        'viewer' => 'admin',
+        'viewer' => 'admin_users',
         'target' => 'owner',
         'view' => TRUE,
         'edit' => TRUE,
       ];
+      $access_info[] = [
+        'field' => $field,
+        'viewer' => 'admin_roles',
+        'target' => 'owner',
+        'view' => FALSE,
+        'edit' => TRUE,
+      ];
     }
 
     return $access_info;
@@ -261,7 +288,6 @@ public function adminFieldAccessProvider() {
     $access_info = [];
 
     $fields = [
-      'roles',
       'status',
       'access',
       'login',
@@ -285,7 +311,7 @@ public function adminFieldAccessProvider() {
       ];
       $access_info[] = [
         'field' => $field,
-        'viewer' => 'admin',
+        'viewer' => 'admin_users',
         'target' => 'owner',
         'view' => TRUE,
         'edit' => TRUE,
@@ -305,7 +331,7 @@ public function testPasswordAccess($viewer, $target, $view, $edit) {
   }
 
   /**
-   * Provides test data for passwordAccessProvider().
+   * Provides test data for testPasswordAccess().
    */
   public function passwordAccessProvider() {
     $pass_access = [
@@ -331,13 +357,67 @@ public function passwordAccessProvider() {
         'edit' => TRUE,
       ],
       [
-        'viewer' => 'admin',
+        'viewer' => 'admin_users',
+        'target' => 'owner',
+        'view' => FALSE,
+        'edit' => TRUE,
+      ],
+      [
+        'viewer' => 'admin_roles',
+        'target' => 'owner',
+        'view' => FALSE,
+        'edit' => TRUE,
+      ],
+     ];
+     return $pass_access;
+   }
+
+  /**
+   * Tests that roles can be only be edited by users with permission.
+   *
+   * @dataProvider rolesAccessProvider
+   */
+  public function testRolesAccess($viewer, $target, $view, $edit) {
+    $this->assertFieldAccess('roles', $viewer, $target, $view, $edit);
+  }
+
+  /**
+   * Provides test data for testRolesAccess().
+   */
+  public function rolesAccessProvider() {
+    $role_access = [
+      [
+        'viewer' => 'viewer',
+        'target' => 'viewer',
+        'view' => FALSE,
+        'edit' => FALSE,
+      ],
+      [
+        'viewer' => 'viewer',
         'target' => 'owner',
         'view' => FALSE,
+        'edit' => FALSE,
+      ],
+      [
+        'viewer' => 'owner',
+        'target' => 'viewer',
+        'view' => FALSE,
+        'edit' => FALSE,
+      ],
+      [
+        'viewer' => 'admin_users',
+        'target' => 'owner',
+        'view' => FALSE,
+        'edit' => FALSE,
+      ],
+      [
+        'viewer' => 'admin_roles',
+        'target' => 'owner',
+        'view' => TRUE,
         'edit' => TRUE,
       ],
     ];
-    return $pass_access;
+    return $role_access;
   }
 
   /**
@@ -367,11 +447,17 @@ public function createdAccessProvider() {
         'edit' => FALSE,
       ],
       [
-        'viewer' => 'admin',
+        'viewer' => 'admin_users',
         'target' => 'owner',
         'view' => TRUE,
         'edit' => TRUE,
       ],
+      [
+        'viewer' => 'admin_roles',
+        'target' => 'owner',
+        'view' => TRUE,
+        'edit' => FALSE,
+      ],
     ];
     return $created_access;
   }
@@ -406,7 +492,13 @@ public function NonExistingFieldAccessProvider() {
         'edit' => TRUE,
       ],
       [
-        'viewer' => 'admin',
+        'viewer' => 'admin_users',
+        'target' => 'owner',
+        'view' => TRUE,
+        'edit' => TRUE,
+      ],
+      [
+        'viewer' => 'admin_roles',
         'target' => 'owner',
         'view' => TRUE,
         'edit' => TRUE,
