diff --git a/UPDATING.md b/UPDATING.md
index 7fd97a4dab0f6f6bf7acb45df31426660fbfe9a7..077f43cdd8992d3e0876af4ee039b6623b3e53e8 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -46,6 +46,7 @@ assists people when migrating to a new version.
 - [30284](https://github.com/apache/superset/pull/30284) Deprecated GLOBAL_ASYNC_QUERIES_REDIS_CONFIG in favor of the new GLOBAL_ASYNC_QUERIES_CACHE_BACKEND configuration. To leverage Redis Sentinel, set CACHE_TYPE to RedisSentinelCache, or use RedisCache for standalone Redis
 - [31961](https://github.com/apache/superset/pull/31961) Upgraded React from version 16.13.1 to 17.0.2. If you are using custom frontend extensions or plugins, you may need to update them to be compatible with React 17.
 - [31260](https://github.com/apache/superset/pull/31260) Docker images now use `uv pip install` instead of `pip install` to manage the python envrionment. Most docker-based deployments will be affected, whether you derive one of the published images, or have custom bootstrap script that install python libraries (drivers)
+- [32432](https://github.com/apache/superset/pull/31260) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
 
 ### Potential Downtime
 
diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts
index f6fd4b6a5c2e6842eef4a3c758d3bc4f78e35072..eda6e56c452e698c9f06e78427e74c4d7dd024b8 100644
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts
@@ -434,7 +434,7 @@ describe('Drill to detail modal', () => {
       SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
     });
 
-    describe.only('Modal actions', () => {
+    describe('Modal actions', () => {
       it('clears filters', () => {
         interceptSamples();
 
diff --git a/superset-frontend/src/components/ListView/Filters/Select.tsx b/superset-frontend/src/components/ListView/Filters/Select.tsx
index edbc5ad452b59231d00adabf22601d4e20f07410..02cd898975938118d6c17d02e2af1ff85296cb76 100644
--- a/superset-frontend/src/components/ListView/Filters/Select.tsx
+++ b/superset-frontend/src/components/ListView/Filters/Select.tsx
@@ -37,6 +37,7 @@ interface SelectFilterProps extends BaseFilter {
   onSelect: (selected: SelectOption | undefined, isClear?: boolean) => void;
   paginate?: boolean;
   selects: Filter['selects'];
+  loading?: boolean;
 }
 
 function SelectFilter(
@@ -47,6 +48,7 @@ function SelectFilter(
     initialValue,
     onSelect,
     selects = [],
+    loading = false,
   }: SelectFilterProps,
   ref: RefObject<FilterHandler>,
 ) {
@@ -115,6 +117,7 @@ function SelectFilter(
           placeholder={t('Select or type a value')}
           showSearch
           value={selectedOption}
+          loading={loading}
         />
       )}
     </FilterContainer>
diff --git a/superset-frontend/src/components/ListView/Filters/index.tsx b/superset-frontend/src/components/ListView/Filters/index.tsx
index 5733151180ea776fb17dd7a047e1d445fbb053e8..8f64b67d051f6dc6f43892a353f7dd2852b12212 100644
--- a/superset-frontend/src/components/ListView/Filters/index.tsx
+++ b/superset-frontend/src/components/ListView/Filters/index.tsx
@@ -75,6 +75,7 @@ function UIFilters(
             selects,
             toolTipDescription,
             onFilterUpdate,
+            loading,
           },
           index,
         ) => {
@@ -103,6 +104,7 @@ function UIFilters(
                 }}
                 paginate={paginate}
                 selects={selects}
+                loading={loading ?? false}
               />
             );
           }
diff --git a/superset-frontend/src/components/ListView/types.ts b/superset-frontend/src/components/ListView/types.ts
index 90de4eab18e765266186e7cc7c7e268e9479cf24..5b498070acffbd131467ee1d5f0bb8480083b796 100644
--- a/superset-frontend/src/components/ListView/types.ts
+++ b/superset-frontend/src/components/ListView/types.ts
@@ -59,6 +59,7 @@ export interface Filter {
     pageSize: number,
   ) => Promise<{ data: SelectOption[]; totalCount: number }>;
   paginate?: boolean;
+  loading?: boolean;
 }
 
 export type Filters = Filter[];
diff --git a/superset-frontend/src/features/home/SubMenu.tsx b/superset-frontend/src/features/home/SubMenu.tsx
index 7945e76da9b9e5a8ca97588dfaa152f7a727326f..dd03fba9d1326dca5c3fa074159c4faecd7e270b 100644
--- a/superset-frontend/src/features/home/SubMenu.tsx
+++ b/superset-frontend/src/features/home/SubMenu.tsx
@@ -134,6 +134,7 @@ export interface ButtonProps {
     | 'warning'
     | 'success'
     | 'tertiary';
+  loading?: boolean;
 }
 
 export interface SubMenuProps {
@@ -282,6 +283,7 @@ const SubMenuComponent: FunctionComponent<SubMenuProps> = props => {
               buttonStyle={btn.buttonStyle}
               onClick={btn.onClick}
               data-test={btn['data-test']}
+              loading={btn.loading ?? false}
             >
               {btn.name}
             </Button>
diff --git a/superset-frontend/src/features/roles/RoleFormItems.tsx b/superset-frontend/src/features/roles/RoleFormItems.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..56c4ad76f4bf6a1f110b23d7721af045b8432bc6
--- /dev/null
+++ b/superset-frontend/src/features/roles/RoleFormItems.tsx
@@ -0,0 +1,70 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { FormItem } from 'src/components/Form';
+import Select from 'src/components/Select/Select';
+import { Input } from 'src/components/Input';
+import { t } from '@superset-ui/core';
+import { FC } from 'react';
+import { FormattedPermission, UserObject } from './types';
+
+interface PermissionsFieldProps {
+  permissions: FormattedPermission[];
+}
+
+interface UsersFieldProps {
+  users: UserObject[];
+}
+
+export const RoleNameField = () => (
+  <FormItem
+    name="roleName"
+    label={t('Role Name')}
+    rules={[{ required: true, message: t('Role name is required') }]}
+  >
+    <Input name="roleName" data-test="role-name-input" />
+  </FormItem>
+);
+
+export const PermissionsField: FC<PermissionsFieldProps> = ({
+  permissions,
+}) => (
+  <FormItem name="rolePermissions" label={t('Permissions')}>
+    <Select
+      mode="multiple"
+      name="rolePermissions"
+      options={permissions.map(permission => ({
+        label: permission.label,
+        value: permission.id,
+      }))}
+      getPopupContainer={trigger => trigger.closest('.antd5-modal-content')}
+      data-test="permissions-select"
+    />
+  </FormItem>
+);
+
+export const UsersField: FC<UsersFieldProps> = ({ users }) => (
+  <FormItem name="roleUsers" label={t('Users')}>
+    <Select
+      mode="multiple"
+      name="roleUsers"
+      options={users.map(user => ({ label: user.username, value: user.id }))}
+      data-test="users-select"
+    />
+  </FormItem>
+);
diff --git a/superset-frontend/src/features/roles/RoleListAddModal.test.tsx b/superset-frontend/src/features/roles/RoleListAddModal.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..69d7eb66365c159e3780f2249454f715735169df
--- /dev/null
+++ b/superset-frontend/src/features/roles/RoleListAddModal.test.tsx
@@ -0,0 +1,92 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  render,
+  screen,
+  fireEvent,
+  waitFor,
+} from 'spec/helpers/testing-library';
+import RoleListAddModal from './RoleListAddModal';
+import { createRole } from './utils';
+
+const mockToasts = {
+  addDangerToast: jest.fn(),
+  addSuccessToast: jest.fn(),
+};
+
+jest.mock('./utils');
+const mockCreateRole = jest.mocked(createRole);
+
+jest.mock('src/components/MessageToasts/withToasts', () => ({
+  useToasts: () => mockToasts,
+}));
+
+describe('RoleListAddModal', () => {
+  const mockProps = {
+    show: true,
+    onHide: jest.fn(),
+    onSave: jest.fn(),
+    permissions: [
+      { id: 1, label: 'Permission 1', value: 'Permission_1' },
+      { id: 2, label: 'Permission 2', value: 'Permission_2' },
+    ],
+  };
+
+  it('renders modal with form fields', () => {
+    render(<RoleListAddModal {...mockProps} />);
+    expect(screen.getByText('Add Role')).toBeInTheDocument();
+    expect(screen.getByText('Role Name')).toBeInTheDocument();
+    expect(screen.getByText('Permissions')).toBeInTheDocument();
+  });
+
+  it('calls onHide when cancel button is clicked', () => {
+    render(<RoleListAddModal {...mockProps} />);
+    fireEvent.click(screen.getByTestId('modal-cancel-button'));
+    expect(mockProps.onHide).toHaveBeenCalled();
+  });
+
+  it('disables save button when role name is empty', () => {
+    render(<RoleListAddModal {...mockProps} />);
+    expect(screen.getByTestId('form-modal-save-button')).toBeDisabled();
+  });
+
+  it('enables save button when role name is entered', () => {
+    render(<RoleListAddModal {...mockProps} />);
+    fireEvent.change(screen.getByTestId('role-name-input'), {
+      target: { value: 'New Role' },
+    });
+    expect(screen.getByTestId('form-modal-save-button')).toBeEnabled();
+  });
+
+  it('calls createRole when save button is clicked', async () => {
+    render(<RoleListAddModal {...mockProps} />);
+
+    fireEvent.change(screen.getByTestId('role-name-input'), {
+      target: { value: 'New Role' },
+    });
+
+    const saveButton = screen.getByRole('button', { name: 'Save' });
+    fireEvent.click(saveButton);
+
+    await waitFor(() => {
+      expect(mockCreateRole).toHaveBeenCalledWith('New Role');
+    });
+  });
+});
diff --git a/superset-frontend/src/features/roles/RoleListAddModal.tsx b/superset-frontend/src/features/roles/RoleListAddModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c80def6724a164efcd9e8ce13c823eab5d968c34
--- /dev/null
+++ b/superset-frontend/src/features/roles/RoleListAddModal.tsx
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t } from '@superset-ui/core';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import FormModal from 'src/components/Modal/FormModal';
+import { createRole, updateRolePermissions } from './utils';
+import { PermissionsField, RoleNameField } from './RoleFormItems';
+import { BaseModalProps, FormattedPermission, RoleForm } from './types';
+
+export interface RoleListAddModalProps extends BaseModalProps {
+  permissions: FormattedPermission[];
+}
+
+function RoleListAddModal({
+  show,
+  onHide,
+  onSave,
+  permissions,
+}: RoleListAddModalProps) {
+  const { addDangerToast, addSuccessToast } = useToasts();
+
+  const handleFormSubmit = async (values: RoleForm) => {
+    try {
+      const { json: roleResponse } = await createRole(values.roleName);
+
+      if (values.rolePermissions?.length > 0) {
+        await updateRolePermissions(roleResponse.id, values.rolePermissions);
+      }
+
+      addSuccessToast(t('Role was successfully created!'));
+    } catch (err) {
+      addDangerToast(t('Error while adding role!'));
+      throw err;
+    }
+  };
+
+  return (
+    <FormModal
+      show={show}
+      onHide={onHide}
+      title={t('Add Role')}
+      onSave={onSave}
+      formSubmitHandler={handleFormSubmit}
+      requiredFields={['roleName']}
+      initialValues={{}}
+    >
+      <>
+        <RoleNameField />
+        <PermissionsField permissions={permissions} />
+      </>
+    </FormModal>
+  );
+}
+
+export default RoleListAddModal;
diff --git a/superset-frontend/src/features/roles/RoleListDuplicateModal.test.tsx b/superset-frontend/src/features/roles/RoleListDuplicateModal.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..21c56b9b30999301ba61d37f7a2d7c0d4e06ef54
--- /dev/null
+++ b/superset-frontend/src/features/roles/RoleListDuplicateModal.test.tsx
@@ -0,0 +1,100 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  render,
+  screen,
+  fireEvent,
+  waitFor,
+} from 'spec/helpers/testing-library';
+import RoleListDuplicateModal from './RoleListDuplicateModal';
+import { createRole, updateRolePermissions } from './utils';
+
+const mockToasts = {
+  addDangerToast: jest.fn(),
+  addSuccessToast: jest.fn(),
+};
+
+jest.mock('./utils');
+const mockCreateRole = jest.mocked(createRole);
+const mockUpdateRolePermissions = jest.mocked(updateRolePermissions);
+
+jest.mock('src/components/MessageToasts/withToasts', () => ({
+  useToasts: () => mockToasts,
+}));
+
+describe('RoleListDuplicateModal', () => {
+  const mockRole = {
+    id: 1,
+    name: 'Admin',
+    permission_ids: [10, 20],
+    user_ids: [1],
+  };
+
+  const mockProps = {
+    role: mockRole,
+    show: true,
+    onHide: jest.fn(),
+    onSave: jest.fn(),
+  };
+
+  it('renders modal with form fields', () => {
+    render(<RoleListDuplicateModal {...mockProps} />);
+    expect(
+      screen.getByText(`Duplicate role ${mockRole.name}`),
+    ).toBeInTheDocument();
+    expect(screen.getByText('Role Name')).toBeInTheDocument();
+  });
+
+  it('calls onHide when cancel button is clicked', () => {
+    render(<RoleListDuplicateModal {...mockProps} />);
+    fireEvent.click(screen.getByTestId('modal-cancel-button'));
+    expect(mockProps.onHide).toHaveBeenCalled();
+  });
+
+  it('disables save button when role name is empty', () => {
+    render(<RoleListDuplicateModal {...mockProps} />);
+    expect(screen.getByTestId('form-modal-save-button')).toBeDisabled();
+  });
+
+  it('enables save button when role name is entered', () => {
+    render(<RoleListDuplicateModal {...mockProps} />);
+    fireEvent.change(screen.getByTestId('role-name-input'), {
+      target: { value: 'New Role' },
+    });
+    expect(screen.getByTestId('form-modal-save-button')).toBeEnabled();
+  });
+
+  it('calls createRole when save button is clicked', async () => {
+    mockCreateRole.mockResolvedValue({ json: { id: 2 } } as any);
+
+    render(<RoleListDuplicateModal {...mockProps} />);
+
+    fireEvent.change(screen.getByTestId('role-name-input'), {
+      target: { value: 'New Role' },
+    });
+
+    fireEvent.click(screen.getByTestId('form-modal-save-button'));
+
+    await waitFor(() => {
+      expect(mockCreateRole).toHaveBeenCalledWith('New Role');
+      expect(mockUpdateRolePermissions).toHaveBeenCalledWith(2, [10, 20]);
+    });
+  });
+});
diff --git a/superset-frontend/src/features/roles/RoleListDuplicateModal.tsx b/superset-frontend/src/features/roles/RoleListDuplicateModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f7d6b7a19d03fa10762de8b0a8d464527b850ca5
--- /dev/null
+++ b/superset-frontend/src/features/roles/RoleListDuplicateModal.tsx
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { t } from '@superset-ui/core';
+import { RoleObject } from 'src/pages/RolesList';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import FormModal from 'src/components/Modal/FormModal';
+import { RoleNameField } from './RoleFormItems';
+import { BaseModalProps, RoleForm } from './types';
+import { createRole, updateRolePermissions } from './utils';
+
+export interface RoleListDuplicateModalProps extends BaseModalProps {
+  role: RoleObject;
+}
+
+function RoleListDuplicateModal({
+  role,
+  show,
+  onHide,
+  onSave,
+}: RoleListDuplicateModalProps) {
+  const { name, permission_ids } = role;
+  const { addDangerToast, addSuccessToast } = useToasts();
+
+  const handleFormSubmit = async (values: RoleForm) => {
+    try {
+      const { json: roleResponse } = await createRole(values.roleName);
+
+      if (permission_ids.length > 0) {
+        await updateRolePermissions(roleResponse.id, permission_ids);
+      }
+      addSuccessToast(t('Role was successfully duplicated!'));
+    } catch (err) {
+      addDangerToast(t('Error while duplicating role!'));
+      throw err;
+    }
+  };
+
+  return (
+    <FormModal
+      show={show}
+      onHide={onHide}
+      title={t('Duplicate role %(name)s', { name })}
+      onSave={onSave}
+      formSubmitHandler={handleFormSubmit}
+      requiredFields={['roleName']}
+      initialValues={{}}
+    >
+      <RoleNameField />
+    </FormModal>
+  );
+}
+export default RoleListDuplicateModal;
diff --git a/superset-frontend/src/features/roles/RoleListEditModal.test.tsx b/superset-frontend/src/features/roles/RoleListEditModal.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8e8c17dc0a04a9409f2f2ef471b7d41012cc0b4f
--- /dev/null
+++ b/superset-frontend/src/features/roles/RoleListEditModal.test.tsx
@@ -0,0 +1,153 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  render,
+  screen,
+  fireEvent,
+  waitFor,
+} from 'spec/helpers/testing-library';
+import RoleListEditModal from './RoleListEditModal';
+import {
+  updateRoleName,
+  updateRolePermissions,
+  updateRoleUsers,
+} from './utils';
+
+const mockToasts = {
+  addDangerToast: jest.fn(),
+  addSuccessToast: jest.fn(),
+};
+
+jest.mock('./utils');
+const mockUpdateRoleName = jest.mocked(updateRoleName);
+const mockUpdateRolePermissions = jest.mocked(updateRolePermissions);
+const mockUpdateRoleUsers = jest.mocked(updateRoleUsers);
+
+jest.mock('src/components/MessageToasts/withToasts', () => ({
+  useToasts: () => mockToasts,
+}));
+
+describe('RoleListEditModal', () => {
+  const mockRole = {
+    id: 1,
+    name: 'Admin',
+    permission_ids: [10, 20],
+    user_ids: [5, 7],
+  };
+
+  const mockPermissions = [
+    { id: 10, label: 'Permission A', value: 'perm_a' },
+    { id: 20, label: 'Permission B', value: 'perm_b' },
+  ];
+
+  const mockUsers = [
+    {
+      id: 5,
+      firstName: 'John',
+      lastName: 'Doe',
+      username: 'johndoe',
+      email: 'john@example.com',
+      isActive: true,
+      roles: [
+        {
+          id: 1,
+          name: 'Admin',
+        },
+      ],
+    },
+  ];
+
+  const mockProps = {
+    role: mockRole,
+    show: true,
+    onHide: jest.fn(),
+    onSave: jest.fn(),
+    permissions: mockPermissions,
+    users: mockUsers,
+  };
+
+  it('renders modal with correct title and fields', () => {
+    render(<RoleListEditModal {...mockProps} />);
+    expect(screen.getAllByText('Edit Role')[0]).toBeInTheDocument();
+    expect(screen.getByText('Role Name')).toBeInTheDocument();
+    expect(screen.getByText('Permissions')).toBeInTheDocument();
+    expect(screen.getAllByText('Users')[0]).toBeInTheDocument();
+  });
+
+  it('calls onHide when cancel button is clicked', () => {
+    render(<RoleListEditModal {...mockProps} />);
+    fireEvent.click(screen.getByTestId('modal-cancel-button'));
+    expect(mockProps.onHide).toHaveBeenCalled();
+  });
+
+  it('disables save button when role name is empty', () => {
+    render(<RoleListEditModal {...mockProps} />);
+    fireEvent.change(screen.getByTestId('role-name-input'), {
+      target: { value: '' },
+    });
+    expect(screen.getByTestId('form-modal-save-button')).toBeDisabled();
+  });
+
+  it('enables save button when role name is entered', () => {
+    render(<RoleListEditModal {...mockProps} />);
+    fireEvent.change(screen.getByTestId('role-name-input'), {
+      target: { value: 'Updated Role' },
+    });
+    expect(screen.getByTestId('form-modal-save-button')).toBeEnabled();
+  });
+
+  it('calls update functions when save button is clicked', async () => {
+    render(<RoleListEditModal {...mockProps} />);
+
+    fireEvent.change(screen.getByTestId('role-name-input'), {
+      target: { value: 'Updated Role' },
+    });
+
+    fireEvent.click(screen.getByTestId('form-modal-save-button'));
+
+    await waitFor(() => {
+      expect(mockUpdateRoleName).toHaveBeenCalledWith(
+        mockRole.id,
+        'Updated Role',
+      );
+      expect(mockUpdateRolePermissions).toHaveBeenCalledWith(
+        mockRole.id,
+        mockRole.permission_ids,
+      );
+      expect(mockUpdateRoleUsers).toHaveBeenCalledWith(
+        mockRole.id,
+        mockRole.user_ids,
+      );
+      expect(mockProps.onSave).toHaveBeenCalled();
+    });
+  });
+
+  it('switches tabs correctly', () => {
+    render(<RoleListEditModal {...mockProps} />);
+
+    const usersTab = screen.getByRole('tab', { name: 'Users' });
+    fireEvent.click(usersTab);
+
+    expect(screen.getByText('First Name')).toBeInTheDocument();
+    expect(screen.getByText('Last Name')).toBeInTheDocument();
+    expect(screen.getByText('User Name')).toBeInTheDocument();
+    expect(screen.getByText('Email')).toBeInTheDocument();
+  });
+});
diff --git a/superset-frontend/src/features/roles/RoleListEditModal.tsx b/superset-frontend/src/features/roles/RoleListEditModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e22f950d5e2a5eef507734e0575997b37e51f812
--- /dev/null
+++ b/superset-frontend/src/features/roles/RoleListEditModal.tsx
@@ -0,0 +1,153 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState } from 'react';
+import { t } from '@superset-ui/core';
+import Tabs from 'src/components/Tabs';
+import { RoleObject } from 'src/pages/RolesList';
+import TableView, { EmptyWrapperType } from 'src/components/TableView';
+import {
+  BaseModalProps,
+  FormattedPermission,
+  RoleForm,
+  UserObject,
+} from 'src/features/roles/types';
+import { CellProps } from 'react-table';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
+import FormModal from 'src/components/Modal/FormModal';
+import { PermissionsField, RoleNameField, UsersField } from './RoleFormItems';
+import {
+  updateRoleName,
+  updateRolePermissions,
+  updateRoleUsers,
+} from './utils';
+
+export interface RoleListEditModalProps extends BaseModalProps {
+  role: RoleObject;
+  permissions: FormattedPermission[];
+  users: UserObject[];
+}
+
+const roleTabs = {
+  edit: {
+    key: 'edit',
+    name: t('Edit Role'),
+  },
+  users: {
+    key: 'users',
+    name: t('Users'),
+  },
+};
+
+const userColumns = [
+  {
+    accessor: 'first_name',
+    Header: 'First Name',
+  },
+  {
+    accessor: 'last_name',
+    Header: 'Last Name',
+  },
+  {
+    accessor: 'username',
+    Header: 'User Name',
+  },
+  {
+    accessor: 'email',
+    Header: 'Email',
+  },
+  {
+    accessor: 'active',
+    Header: 'Is Active?',
+    Cell: ({ cell }: CellProps<{ active: boolean }>) =>
+      cell.value ? 'Yes' : 'No',
+  },
+];
+
+function RoleListEditModal({
+  show,
+  onHide,
+  role,
+  onSave,
+  permissions,
+  users,
+}: RoleListEditModalProps) {
+  const { id, name, permission_ids, user_ids } = role;
+  const [activeTabKey, setActiveTabKey] = useState(roleTabs.edit.key);
+  const { addDangerToast, addSuccessToast } = useToasts();
+  const filteredUsers = users.filter(user =>
+    user?.roles.some(role => role.id === id),
+  );
+
+  const handleFormSubmit = async (values: RoleForm) => {
+    try {
+      await updateRoleName(id, values.roleName);
+      await updateRolePermissions(id, values.rolePermissions);
+      await updateRoleUsers(id, values.roleUsers);
+      addSuccessToast(t('Role successfully updated!'));
+    } catch (err) {
+      addDangerToast(t('Error while updating role!'));
+      throw err;
+    }
+  };
+
+  const initialValues = {
+    roleName: name,
+    rolePermissions: permission_ids,
+    roleUsers: user_ids,
+  };
+
+  return (
+    <FormModal
+      show={show}
+      onHide={onHide}
+      title={t('Edit Role')}
+      onSave={onSave}
+      formSubmitHandler={handleFormSubmit}
+      initialValues={initialValues}
+      bodyStyle={{ height: '400px' }}
+      requiredFields={['roleName']}
+    >
+      <Tabs
+        activeKey={activeTabKey}
+        onChange={activeKey => setActiveTabKey(activeKey)}
+      >
+        <Tabs.TabPane
+          tab={roleTabs.edit.name}
+          key={roleTabs.edit.key}
+          forceRender
+        >
+          <>
+            <RoleNameField />
+            <PermissionsField permissions={permissions} />
+            <UsersField users={users} />
+          </>
+        </Tabs.TabPane>
+        <Tabs.TabPane tab={roleTabs.users.name} key={roleTabs.users.key}>
+          <TableView
+            columns={userColumns}
+            data={filteredUsers}
+            emptyWrapperType={EmptyWrapperType.Small}
+          />
+        </Tabs.TabPane>
+      </Tabs>
+    </FormModal>
+  );
+}
+
+export default RoleListEditModal;
diff --git a/superset-frontend/src/features/roles/types.ts b/superset-frontend/src/features/roles/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d97a8dad611bcd11e50d8f4f3275d9d663cee745
--- /dev/null
+++ b/superset-frontend/src/features/roles/types.ts
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export type PermissionView = {
+  name: string;
+};
+
+export type PermissionResource = {
+  id: number;
+  permission: PermissionView;
+  view_menu: PermissionView;
+};
+
+export type FormattedPermission = {
+  label: string;
+  value: string;
+  id: number;
+};
+
+export type RolePermissions = {
+  id: number;
+  permission_name: string;
+  view_menu_name: string;
+};
+
+export type UserObject = {
+  id: number;
+  firstName: string;
+  lastName: string;
+  username: string;
+  email: string;
+  isActive: boolean;
+  roles: Array<RoleInfo>;
+};
+
+export type RoleInfo = {
+  id: number;
+  name: string;
+};
+
+export type RoleForm = {
+  roleName: string;
+  rolePermissions: number[];
+  roleUsers: number[];
+};
+
+export interface BaseModalProps {
+  show: boolean;
+  onHide: () => void;
+  onSave: () => void;
+}
diff --git a/superset-frontend/src/features/roles/utils.ts b/superset-frontend/src/features/roles/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fcee8e6463330ae361320e00e348508607d7238d
--- /dev/null
+++ b/superset-frontend/src/features/roles/utils.ts
@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { SupersetClient } from '@superset-ui/core';
+
+export const createRole = async (name: string) =>
+  SupersetClient.post({
+    endpoint: '/api/v1/security/roles/',
+    jsonPayload: { name },
+  });
+
+export const updateRolePermissions = async (
+  roleId: number,
+  permissionIds: number[],
+) =>
+  SupersetClient.post({
+    endpoint: `/api/v1/security/roles/${roleId}/permissions`,
+    jsonPayload: { permission_view_menu_ids: permissionIds },
+  });
+
+export const updateRoleUsers = async (roleId: number, userIds: number[]) =>
+  SupersetClient.put({
+    endpoint: `/api/v1/security/roles/${roleId}/users`,
+    jsonPayload: { user_ids: userIds },
+  });
+
+export const updateRoleName = async (roleId: number, name: string) =>
+  SupersetClient.put({
+    endpoint: `/api/v1/security/roles/${roleId}`,
+    jsonPayload: { name },
+  });
diff --git a/superset-frontend/src/pages/RolesList/RolesList.test.tsx b/superset-frontend/src/pages/RolesList/RolesList.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c4c4ee25541b65682eaa0cf2c73e9b92f4aef331
--- /dev/null
+++ b/superset-frontend/src/pages/RolesList/RolesList.test.tsx
@@ -0,0 +1,206 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import fetchMock from 'fetch-mock';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import {
+  render,
+  screen,
+  fireEvent,
+  waitFor,
+  act,
+  within,
+} from 'spec/helpers/testing-library';
+import { MemoryRouter } from 'react-router-dom';
+import { QueryParamProvider } from 'use-query-params';
+import RolesList from './index';
+
+const mockStore = configureStore([thunk]);
+const store = mockStore({});
+
+const rolesEndpoint = 'glob:*/security/roles/search/?*';
+const roleEndpoint = 'glob:*/api/v1/security/roles/*';
+const permissionsEndpoint = 'glob:*/api/v1/security/permissions-resources/?*';
+const usersEndpoint = 'glob:*/api/v1/security/users/?*';
+
+const mockRoles = [...new Array(3)].map((_, i) => ({
+  id: i,
+  name: `role ${i}`,
+  user_ids: [i, i + 1],
+  permission_ids: [i, i + 1, i + 2],
+}));
+
+const mockPermissions = [...new Array(10)].map((_, i) => ({
+  id: i,
+  permission: { name: `permission_${i}` },
+  view_menu: { name: `view_menu_${i}` },
+}));
+
+const mockUsers = [...new Array(5)].map((_, i) => ({
+  id: i,
+  username: `user_${i}`,
+  first_name: `User`,
+  last_name: `${i}`,
+  roles: [
+    {
+      id: 1,
+    },
+  ],
+}));
+
+const mockUser = {
+  userId: 1,
+  firstName: 'Admin',
+  lastName: 'User',
+  roles: [
+    {
+      id: 1,
+      name: 'Admin',
+    },
+  ],
+};
+
+jest.mock('src/dashboard/util/permissionUtils', () => ({
+  ...jest.requireActual('src/dashboard/util/permissionUtils'),
+  isUserAdmin: jest.fn(() => true),
+}));
+
+fetchMock.get(rolesEndpoint, {
+  ids: [2, 0, 1],
+  result: mockRoles,
+  count: 3,
+});
+
+fetchMock.get(permissionsEndpoint, {
+  count: mockPermissions.length,
+  result: mockPermissions,
+});
+
+fetchMock.get(usersEndpoint, {
+  count: mockUsers.length,
+  result: mockUsers,
+});
+
+fetchMock.delete(roleEndpoint, {});
+fetchMock.put(roleEndpoint, {});
+
+describe('RolesList', () => {
+  async function renderAndWait() {
+    const mounted = act(async () => {
+      const mockedProps = {};
+      render(
+        <MemoryRouter>
+          <QueryParamProvider>
+            <RolesList
+              user={mockUser}
+              addDangerToast={() => {}}
+              addSuccessToast={() => {}}
+              {...mockedProps}
+            />
+          </QueryParamProvider>
+        </MemoryRouter>,
+        { useRedux: true, store },
+      );
+    });
+    return mounted;
+  }
+  beforeEach(() => {
+    fetchMock.resetHistory();
+  });
+
+  it('renders', async () => {
+    await renderAndWait();
+    expect(await screen.findByText('List Roles')).toBeInTheDocument();
+  });
+
+  it('fetches roles on load', async () => {
+    await renderAndWait();
+    await waitFor(() => {
+      const calls = fetchMock.calls(rolesEndpoint);
+      expect(calls.length).toBeGreaterThan(0);
+    });
+  });
+
+  it('fetches permissions and users on load', async () => {
+    await renderAndWait();
+    await waitFor(() => {
+      const permissionCalls = fetchMock.calls(permissionsEndpoint);
+      const userCalls = fetchMock.calls(usersEndpoint);
+      expect(permissionCalls.length).toBeGreaterThan(0);
+      expect(userCalls.length).toBeGreaterThan(0);
+    });
+  });
+
+  it('renders filters options', async () => {
+    await renderAndWait();
+
+    const typeFilter = screen.queryAllByTestId('filters-select');
+    expect(typeFilter).toHaveLength(3);
+  });
+
+  it('renders correct list columns', async () => {
+    await renderAndWait();
+
+    const table = screen.getByRole('table');
+    expect(table).toBeInTheDocument();
+
+    const nameColumn = await within(table).findByText('Name');
+    const actionsColumn = await within(table).findByText('Actions');
+
+    expect(nameColumn).toBeInTheDocument();
+    expect(actionsColumn).toBeInTheDocument();
+  });
+
+  it('opens add modal when Add Role button is clicked', async () => {
+    await renderAndWait();
+
+    const addButton = screen.getByTestId('add-role-button');
+    fireEvent.click(addButton);
+
+    expect(screen.queryByTestId('Add Role-modal')).toBeInTheDocument();
+  });
+
+  it('open duplicate modal when duplicate button is clicked', async () => {
+    await renderAndWait();
+
+    const table = screen.getByRole('table');
+    expect(table).toBeInTheDocument();
+    const duplicateAction = within(table).queryAllByTestId(
+      'role-list-duplicate-action',
+    )[0];
+    expect(duplicateAction).toBeInTheDocument();
+    fireEvent.click(duplicateAction);
+    expect(
+      screen.queryByTestId('Duplicate role role 0-modal'),
+    ).toBeInTheDocument();
+  });
+
+  it('open edit modal when edit button is clicked', async () => {
+    await renderAndWait();
+
+    const table = screen.getByRole('table');
+    expect(table).toBeInTheDocument();
+    const editAction = within(table).queryAllByTestId(
+      'role-list-edit-action',
+    )[0];
+    expect(editAction).toBeInTheDocument();
+    fireEvent.click(editAction);
+    expect(screen.queryByTestId('Edit Role-modal')).toBeInTheDocument();
+  });
+});
diff --git a/superset-frontend/src/pages/RolesList/index.tsx b/superset-frontend/src/pages/RolesList/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..be97427bd20ac4ccc4da1359a3fd2d801e5b8e16
--- /dev/null
+++ b/superset-frontend/src/pages/RolesList/index.tsx
@@ -0,0 +1,507 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { css, t, SupersetClient, useTheme } from '@superset-ui/core';
+import { useListViewResource } from 'src/views/CRUD/hooks';
+import RoleListAddModal from 'src/features/roles/RoleListAddModal';
+import RoleListEditModal from 'src/features/roles/RoleListEditModal';
+import RoleListDuplicateModal from 'src/features/roles/RoleListDuplicateModal';
+import withToasts from 'src/components/MessageToasts/withToasts';
+import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
+import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
+import ListView, {
+  ListViewProps,
+  Filters,
+  FilterOperator,
+} from 'src/components/ListView';
+import DeleteModal from 'src/components/DeleteModal';
+import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
+import {
+  FormattedPermission,
+  PermissionResource,
+  UserObject,
+} from 'src/features/roles/types';
+import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
+import { Icons } from 'src/components/Icons';
+
+const PAGE_SIZE = 25;
+
+interface RolesListProps {
+  addDangerToast: (msg: string) => void;
+  addSuccessToast: (msg: string) => void;
+  user: {
+    userId: string | number;
+    firstName: string;
+    lastName: string;
+    roles: object;
+  };
+}
+
+export type RoleObject = {
+  id: number;
+  name: string;
+  permission_ids: number[];
+  users?: Array<UserObject>;
+  user_ids: number[];
+};
+
+enum ModalType {
+  ADD = 'add',
+  EDIT = 'edit',
+  DUPLICATE = 'duplicate',
+}
+
+function RolesList({ addDangerToast, addSuccessToast, user }: RolesListProps) {
+  const theme = useTheme();
+  const {
+    state: {
+      loading,
+      resourceCount: rolesCount,
+      resourceCollection: roles,
+      bulkSelectEnabled,
+    },
+    fetchData,
+    refreshData,
+    toggleBulkSelect,
+  } = useListViewResource<RoleObject>(
+    'security/roles/search',
+    t('Role'),
+    addDangerToast,
+    false,
+  );
+  const [modalState, setModalState] = useState({
+    edit: false,
+    add: false,
+    duplicate: false,
+  });
+  const openModal = (type: ModalType) =>
+    setModalState(prev => ({ ...prev, [type]: true }));
+  const closeModal = (type: ModalType) =>
+    setModalState(prev => ({ ...prev, [type]: false }));
+
+  const [currentRole, setCurrentRole] = useState<RoleObject | null>(null);
+  const [roleCurrentlyDeleting, setRoleCurrentlyDeleting] =
+    useState<RoleObject | null>(null);
+  const [permissions, setPermissions] = useState<FormattedPermission[]>([]);
+  const [users, setUsers] = useState<UserObject[]>([]);
+  const [loadingState, setLoadingState] = useState({
+    permissions: true,
+    users: true,
+  });
+
+  const isAdmin = useMemo(() => isUserAdmin(user), [user]);
+
+  const fetchPermissions = useCallback(async () => {
+    try {
+      const pageSize = 100;
+
+      const fetchPage = async (pageIndex: number) => {
+        const response = await SupersetClient.get({
+          endpoint: `api/v1/security/permissions-resources/?q={"page_size":${pageSize}, "page":${pageIndex}}`,
+        });
+
+        return {
+          count: response.json.count,
+          results: response.json.result.map(
+            ({ permission, view_menu, id }: PermissionResource) => ({
+              label: `${permission.name.replace(/_/g, ' ')} ${view_menu.name.replace(/_/g, ' ')}`,
+              value: `${permission.name}__${view_menu.name}`,
+              id,
+            }),
+          ),
+        };
+      };
+
+      const initialResponse = await fetchPage(0);
+      const totalPermissions = initialResponse.count;
+      const firstPageResults = initialResponse.results;
+
+      if (firstPageResults.length >= totalPermissions) {
+        setPermissions(firstPageResults);
+        return;
+      }
+
+      const totalPages = Math.ceil(totalPermissions / pageSize);
+
+      const permissionRequests = Array.from(
+        { length: totalPages - 1 },
+        (_, i) => fetchPage(i + 1),
+      );
+      const remainingResults = await Promise.all(permissionRequests);
+
+      setPermissions([
+        ...firstPageResults,
+        ...remainingResults.flatMap(res => res.results),
+      ]);
+    } catch (err) {
+      addDangerToast(t('Error while fetching permissions'));
+    } finally {
+      setLoadingState(prev => ({ ...prev, permissions: false }));
+    }
+  }, []);
+
+  const fetchUsers = useCallback(async () => {
+    try {
+      const pageSize = 100;
+
+      const fetchPage = async (pageIndex: number) => {
+        const response = await SupersetClient.get({
+          endpoint: `api/v1/security/users/?q={"page_size":${pageSize},"page":${pageIndex}}`,
+        });
+        return response.json;
+      };
+
+      const initialResponse = await fetchPage(0);
+      const totalUsers = initialResponse.count;
+      const firstPageResults = initialResponse.result;
+
+      if (pageSize >= totalUsers) {
+        setUsers(firstPageResults);
+        return;
+      }
+
+      const totalPages = Math.ceil(totalUsers / pageSize);
+
+      const userRequests = Array.from({ length: totalPages - 1 }, (_, i) =>
+        fetchPage(i + 1),
+      );
+      const remainingResults = await Promise.all(userRequests);
+
+      setUsers([
+        ...firstPageResults,
+        ...remainingResults.flatMap(res => res.result),
+      ]);
+    } catch (err) {
+      addDangerToast(t('Error while fetching users'));
+    } finally {
+      setLoadingState(prev => ({ ...prev, users: false }));
+    }
+  }, []);
+
+  useEffect(() => {
+    fetchPermissions();
+  }, [fetchPermissions]);
+
+  useEffect(() => {
+    fetchUsers();
+  }, [fetchUsers]);
+
+  const handleRoleDelete = async ({ id, name }: RoleObject) => {
+    try {
+      await SupersetClient.delete({
+        endpoint: `/api/v1/security/roles/${id}`,
+      });
+
+      refreshData();
+      setRoleCurrentlyDeleting(null);
+      addSuccessToast(t('Deleted role: %s', name));
+    } catch (error) {
+      addDangerToast(t('There was an issue deleting %s', name));
+    }
+  };
+
+  const handleBulkRolesDelete = async (rolesToDelete: RoleObject[]) => {
+    const deletedRoleNames: string[] = [];
+
+    await Promise.all(
+      rolesToDelete.map(async role => {
+        try {
+          await SupersetClient.delete({
+            endpoint: `api/v1/security/roles/${role.id}`,
+          });
+
+          deletedRoleNames.push(role.name);
+        } catch (error) {
+          addDangerToast(t('Error deleting %s', role.name));
+        }
+      }),
+    );
+
+    if (deletedRoleNames.length > 0) {
+      addSuccessToast(t('Deleted roles: %s', deletedRoleNames.join(', ')));
+    }
+
+    refreshData();
+  };
+
+  const initialSort = [{ id: 'name', desc: true }];
+  const columns = useMemo(
+    () => [
+      {
+        accessor: 'name',
+        Header: t('Name'),
+        Cell: ({
+          row: {
+            original: { name },
+          },
+        }: any) => <span>{name}</span>,
+      },
+      {
+        accessor: 'user_ids',
+        Header: t('Users'),
+        hidden: true,
+        Cell: ({ row: { original } }: any) => original.user_ids.join(', '),
+      },
+      {
+        accessor: 'permission_ids',
+        Header: t('Permissions'),
+        hidden: true,
+        Cell: ({ row: { original } }: any) =>
+          original.permission_ids.join(', '),
+      },
+      {
+        Cell: ({ row: { original } }: any) => {
+          const handleEdit = () => {
+            setCurrentRole(original);
+            openModal(ModalType.EDIT);
+          };
+          const handleDelete = () => setRoleCurrentlyDeleting(original);
+          const handleDuplicate = () => {
+            setCurrentRole(original);
+            openModal(ModalType.DUPLICATE);
+          };
+
+          const actions = isAdmin
+            ? [
+                {
+                  label: 'role-list-edit-action',
+                  tooltip: t('Edit role'),
+                  placement: 'bottom',
+                  icon: 'EditOutlined',
+                  onClick: handleEdit,
+                },
+                {
+                  label: 'role-list-duplicate-action',
+                  tooltip: t('Duplicate role'),
+                  placement: 'bottom',
+                  icon: 'CopyOutlined',
+                  onClick: handleDuplicate,
+                },
+                {
+                  label: 'role-list-delete-action',
+                  tooltip: t('Delete role'),
+                  placement: 'bottom',
+                  icon: 'DeleteOutlined',
+                  onClick: handleDelete,
+                },
+              ]
+            : [];
+
+          return <ActionsBar actions={actions as ActionProps[]} />;
+        },
+        Header: t('Actions'),
+        id: 'actions',
+        disableSortBy: true,
+        hidden: !isAdmin,
+        size: 'xl',
+      },
+    ],
+    [isAdmin],
+  );
+
+  const subMenuButtons: SubMenuProps['buttons'] = [];
+
+  if (isAdmin) {
+    subMenuButtons.push(
+      {
+        name: (
+          <>
+            <Icons.PlusOutlined
+              iconColor={theme.colors.primary.light5}
+              iconSize="m"
+              css={css`
+                margin: auto ${theme.gridUnit * 2}px auto 0;
+                vertical-align: text-top;
+              `}
+            />
+            {t('Role')}
+          </>
+        ),
+        buttonStyle: 'primary',
+        onClick: () => {
+          openModal(ModalType.ADD);
+        },
+        loading: loadingState.permissions,
+        'data-test': 'add-role-button',
+      },
+      {
+        name: t('Bulk select'),
+        onClick: toggleBulkSelect,
+        buttonStyle: 'secondary',
+      },
+    );
+  }
+
+  const filters: Filters = useMemo(
+    () => [
+      {
+        Header: t('Name'),
+        key: 'name',
+        id: 'name',
+        input: 'search',
+        operator: FilterOperator.Contains,
+      },
+      {
+        Header: t('Users'),
+        key: 'user_ids',
+        id: 'user_ids',
+        input: 'select',
+        operator: FilterOperator.RelationOneMany,
+        unfilteredLabel: t('All'),
+        selects: users?.map(user => ({
+          label: user.username,
+          value: user.id,
+        })),
+        loading: loadingState.users,
+      },
+      {
+        Header: t('Permissions'),
+        key: 'permission_ids',
+        id: 'permission_ids',
+        input: 'select',
+        operator: FilterOperator.RelationOneMany,
+        unfilteredLabel: t('All'),
+        selects: permissions?.map(permission => ({
+          label: permission.label,
+          value: permission.id,
+        })),
+        loading: loadingState.permissions,
+      },
+    ],
+    [permissions, users, loadingState.users, loadingState.permissions],
+  );
+
+  const emptyState = {
+    title: t('No roles yet'),
+    image: 'filter-results.svg',
+    ...(isAdmin && {
+      buttonAction: () => {
+        openModal(ModalType.ADD);
+      },
+      buttonText: (
+        <>
+          <Icons.PlusOutlined
+            iconColor={theme.colors.primary.light5}
+            iconSize="m"
+            css={css`
+              margin: auto ${theme.gridUnit * 2}px auto 0;
+              vertical-align: text-top;
+            `}
+          />
+          {t('Role')}
+        </>
+      ),
+    }),
+  };
+
+  return (
+    <>
+      <SubMenu name={t('List Roles')} buttons={subMenuButtons} />
+      <RoleListAddModal
+        onHide={() => closeModal(ModalType.ADD)}
+        show={modalState.add}
+        onSave={() => {
+          refreshData();
+          closeModal(ModalType.ADD);
+        }}
+        permissions={permissions}
+      />
+      {modalState.edit && currentRole && (
+        <RoleListEditModal
+          role={currentRole}
+          show={modalState.edit}
+          onHide={() => closeModal(ModalType.EDIT)}
+          onSave={() => {
+            refreshData();
+            closeModal(ModalType.EDIT);
+            fetchUsers();
+          }}
+          permissions={permissions}
+          users={users}
+        />
+      )}
+      {modalState.duplicate && currentRole && (
+        <RoleListDuplicateModal
+          role={currentRole}
+          show={modalState.duplicate}
+          onHide={() => closeModal(ModalType.DUPLICATE)}
+          onSave={() => {
+            refreshData();
+            closeModal(ModalType.DUPLICATE);
+          }}
+        />
+      )}
+      {roleCurrentlyDeleting && (
+        <DeleteModal
+          description={t('This action will permanently delete the role.')}
+          onConfirm={() => {
+            if (roleCurrentlyDeleting) {
+              handleRoleDelete(roleCurrentlyDeleting);
+            }
+          }}
+          onHide={() => setRoleCurrentlyDeleting(null)}
+          open
+          title={t('Delete Role?')}
+        />
+      )}
+      <ConfirmStatusChange
+        title={t('Please confirm')}
+        description={t('Are you sure you want to delete the selected roles?')}
+        onConfirm={handleBulkRolesDelete}
+      >
+        {confirmDelete => {
+          const bulkActions: ListViewProps['bulkActions'] = isAdmin
+            ? [
+                {
+                  key: 'delete',
+                  name: t('Delete'),
+                  onSelect: confirmDelete,
+                  type: 'danger',
+                },
+              ]
+            : [];
+
+          return (
+            <ListView<RoleObject>
+              className="role-list-view"
+              columns={columns}
+              count={rolesCount}
+              data={roles}
+              fetchData={fetchData}
+              filters={filters}
+              initialSort={initialSort}
+              loading={loading}
+              pageSize={PAGE_SIZE}
+              bulkActions={bulkActions}
+              bulkSelectEnabled={bulkSelectEnabled}
+              disableBulkSelect={toggleBulkSelect}
+              addDangerToast={addDangerToast}
+              addSuccessToast={addSuccessToast}
+              emptyState={emptyState}
+              refreshData={refreshData}
+            />
+          );
+        }}
+      </ConfirmStatusChange>
+    </>
+  );
+}
+
+export default withToasts(RolesList);
diff --git a/superset-frontend/src/views/routes.tsx b/superset-frontend/src/views/routes.tsx
index 30ce65f069537aba5e44550fb45a9f5f0a62298f..a9ade6b75ee77c8d10d5275ff0553f5a30547ae2 100644
--- a/superset-frontend/src/views/routes.tsx
+++ b/superset-frontend/src/views/routes.tsx
@@ -18,6 +18,8 @@
  */
 import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
 import { lazy, ComponentType, ComponentProps } from 'react';
+import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
+import getBootstrapData from 'src/utils/getBootstrapData';
 
 // not lazy loaded since this is the home page.
 import Home from 'src/pages/Home';
@@ -123,6 +125,10 @@ const RowLevelSecurityList = lazy(
     ),
 );
 
+const RolesList = lazy(
+  () => import(/* webpackChunkName: "RolesList" */ 'src/pages/RolesList'),
+);
+
 type Routes = {
   path: string;
   Component: ComponentType;
@@ -238,6 +244,16 @@ if (isFeatureEnabled(FeatureFlag.TaggingSystem)) {
   });
 }
 
+const user = getBootstrapData()?.user;
+const isAdmin = isUserAdmin(user);
+
+if (isAdmin) {
+  routes.push({
+    path: '/roles/',
+    Component: RolesList,
+  });
+}
+
 const frontEndRoutes: Record<string, boolean> = routes
   .map(r => r.path)
   .reduce(
diff --git a/superset/config.py b/superset/config.py
index d9c35e04fab24ab84464e4f712270325d009488c..78d6567a6a996942d217e5f4935a5e35d21d232a 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -1253,6 +1253,7 @@ ENABLE_CHUNK_ENCODING = False
 SILENCE_FAB = True
 
 FAB_ADD_SECURITY_VIEWS = True
+FAB_ADD_SECURITY_API = True
 FAB_ADD_SECURITY_PERMISSION_VIEW = False
 FAB_ADD_SECURITY_VIEW_MENU_VIEW = False
 FAB_ADD_SECURITY_PERMISSION_VIEWS_VIEW = False
diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index 1a09d8b24c7d9fa26081678233d3b8b440855fac..cd3510abbf49071cb95a3bb5d3565a4f184b7a06 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -153,7 +153,7 @@ class SupersetAppInitializer:  # pylint: disable=too-many-public-methods
         from superset.reports.api import ReportScheduleRestApi
         from superset.reports.logs.api import ReportExecutionLogRestApi
         from superset.row_level_security.api import RLSRestApi
-        from superset.security.api import SecurityRestApi
+        from superset.security.api import RoleRestAPI, SecurityRestApi
         from superset.sqllab.api import SqlLabRestApi
         from superset.sqllab.permalink.api import SqlLabPermalinkRestApi
         from superset.tags.api import TagRestApi
@@ -175,6 +175,7 @@ class SupersetAppInitializer:  # pylint: disable=too-many-public-methods
         from superset.views.explore import ExplorePermalinkView, ExploreView
         from superset.views.log.api import LogRestApi
         from superset.views.log.views import LogModelView
+        from superset.views.roles import RolesListView
         from superset.views.sql_lab.views import (
             SavedQueryView,
             TableSchemaView,
@@ -265,6 +266,15 @@ class SupersetAppInitializer:  # pylint: disable=too-many-public-methods
             category_icon="",
         )
 
+        appbuilder.add_view(
+            RolesListView,
+            "List Roles",
+            label=__("List Roles"),
+            category="Security",
+            category_label=__("Security"),
+            icon="fa-lock",
+        )
+
         appbuilder.add_view(
             DynamicPluginsView,
             "Plugins",
@@ -305,6 +315,7 @@ class SupersetAppInitializer:  # pylint: disable=too-many-public-methods
         appbuilder.add_view_no_menu(TaggedObjectsModelView)
         appbuilder.add_view_no_menu(TagView)
         appbuilder.add_view_no_menu(ReportView)
+        appbuilder.add_view_no_menu(RoleRestAPI)
 
         #
         # Add links
diff --git a/superset/security/api.py b/superset/security/api.py
index 02bf6b7101ea8e1b72429c454155c02ca33cbc3a..756f1d7bbd3e1500cdd03c8a18e38ee7c31b58e1 100644
--- a/superset/security/api.py
+++ b/superset/security/api.py
@@ -19,16 +19,21 @@ from typing import Any
 
 from flask import current_app, request, Response
 from flask_appbuilder import expose
-from flask_appbuilder.api import safe
+from flask_appbuilder.api import rison, safe
+from flask_appbuilder.api.schemas import get_list_schema
 from flask_appbuilder.security.decorators import permission_name, protect
+from flask_appbuilder.security.sqla.models import Role
 from flask_wtf.csrf import generate_csrf
 from marshmallow import EXCLUDE, fields, post_load, Schema, ValidationError
+from sqlalchemy import asc, desc
+from sqlalchemy.orm import joinedload
 
 from superset.commands.dashboard.embedded.exceptions import (
     EmbeddedDashboardNotFoundError,
 )
+from superset.commands.exceptions import ForbiddenError
 from superset.exceptions import SupersetGenericErrorException
-from superset.extensions import event_logger
+from superset.extensions import db, event_logger
 from superset.security.guest_token import GuestTokenResourceType
 from superset.views.base_api import BaseSupersetApi, statsd_metrics
 
@@ -76,6 +81,19 @@ class GuestTokenCreateSchema(PermissiveSchema):
     rls = fields.List(fields.Nested(RlsRuleSchema), required=True)
 
 
+class RoleResponseSchema(PermissiveSchema):
+    id = fields.Integer()
+    name = fields.String()
+    user_ids = fields.List(fields.Integer())
+    permission_ids = fields.List(fields.Integer())
+
+
+class RolesResponseSchema(PermissiveSchema):
+    count = fields.Integer()
+    ids = fields.List(fields.Integer())
+    result = fields.List(fields.Nested(RoleResponseSchema))
+
+
 guest_token_create_schema = GuestTokenCreateSchema()
 
 
@@ -172,3 +190,146 @@ class SecurityRestApi(BaseSupersetApi):
             return self.response_400(message=error.message)
         except ValidationError as error:
             return self.response_400(message=error.messages)
+
+
+class RoleRestAPI(BaseSupersetApi):
+    """
+    APIs for listing roles with usersIds and permissionsIds and possibility to update
+    users of roles
+    """
+
+    resource_name = "security/roles"
+    allow_browser_login = True
+    openapi_spec_tag = "Security Roles"
+    openapi_spec_component_schemas = (
+        RoleResponseSchema,
+        RolesResponseSchema,
+    )
+
+    @expose("/search/", methods=["GET"])
+    @event_logger.log_this
+    @protect()
+    @safe
+    @rison(get_list_schema)
+    @statsd_metrics
+    @permission_name("list_roles")
+    def get_list(self, **kwargs: Any) -> Response:
+        """
+        List roles, including associated user IDs and permission IDs.
+
+        ---
+        get:
+          summary: List roles
+          description: Fetch a paginated list of roles with user and permission IDs.
+          parameters:
+            - in: query
+              name: q
+              schema:
+                type: object
+                properties:
+                  order_column:
+                    type: string
+                    enum: ["id", "name"]
+                    default: "id"
+                  order_direction:
+                    type: string
+                    enum: ["asc", "desc"]
+                    default: "asc"
+                  page:
+                    type: integer
+                    default: 0
+                  page_size:
+                    type: integer
+                    default: 10
+                  filters:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        col:
+                          type: string
+                          enum: ["user_ids", "permission_ids", "name"]
+                        value:
+                          type: string
+          responses:
+            200:
+              description: Successfully retrieved roles
+              content:
+                application/json:
+                  schema: RolesResponseSchema
+            400:
+              description: Bad request (invalid input)
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      error:
+                        type: string
+            403:
+              description: Forbidden
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      error:
+                        type: string
+        """
+        try:
+            args = kwargs.get("rison", {})
+            order_column = args.get("order_column", "id")
+            order_direction = args.get("order_direction", "asc")
+
+            valid_columns = ["id", "name"]
+            if order_column not in valid_columns:
+                return self.response_400(
+                    message=f"Invalid order column: {order_column}"
+                )
+
+            order_by = getattr(Role, order_column)
+            order_by = asc(order_by) if order_direction == "asc" else desc(order_by)
+
+            page = args.get("page", 0)
+            page_size = args.get("page_size", 10)
+
+            query = db.session.query(Role).options(
+                joinedload(Role.permissions), joinedload(Role.user)
+            )
+
+            filters = args.get("filters", [])
+            filter_dict = {f["col"]: f["value"] for f in filters if "col" in f}
+
+            if "user_ids" in filter_dict:
+                query = query.filter(Role.user.any(id=filter_dict["user_ids"]))
+
+            if "permission_ids" in filter_dict:
+                query = query.filter(
+                    Role.permissions.any(id=filter_dict["permission_ids"])
+                )
+
+            if "name" in filter_dict:
+                query = query.filter(Role.name.ilike(f"%{filter_dict['name']}%"))
+
+            roles = (
+                query.order_by(order_by).offset(page * page_size).limit(page_size).all()
+            )
+
+            return self.response(
+                200,
+                result=[
+                    {
+                        "id": role.id,
+                        "name": role.name,
+                        "user_ids": [user.id for user in role.user],
+                        "permission_ids": [perm.id for perm in role.permissions],
+                    }
+                    for role in roles
+                ],
+                count=query.count(),
+                ids=[role.id for role in roles],
+            )
+        except ForbiddenError as e:
+            return self.response_403(message=str(e))
+        except Exception as e:
+            return self.response_500(message=str(e))
diff --git a/superset/security/manager.py b/superset/security/manager.py
index d11e0f3e41c0c4b6e158ab81b622691a5fda2ae8..009e7f314cbcf2fc2730a37460a574f5891083a4 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -25,6 +25,7 @@ from typing import Any, Callable, cast, NamedTuple, Optional, TYPE_CHECKING
 
 from flask import current_app, Flask, g, Request
 from flask_appbuilder import Model
+from flask_appbuilder.security.sqla.apis import RoleApi
 from flask_appbuilder.security.sqla.manager import SecurityManager
 from flask_appbuilder.security.sqla.models import (
     assoc_group_role,
@@ -40,7 +41,6 @@ from flask_appbuilder.security.sqla.models import (
 from flask_appbuilder.security.views import (
     PermissionModelView,
     PermissionViewModelView,
-    RoleModelView,
     UserModelView,
     ViewMenuModelView,
 )
@@ -126,8 +126,19 @@ class SupersetRoleListWidget(ListWidget):  # pylint: disable=too-few-public-meth
         super().__init__(**kwargs)
 
 
+class SupersetRoleApi(RoleApi):
+    """
+    Overriding the RoleApi to be able to delete roles with permissions
+    """
+
+    def pre_delete(self, item: Model) -> None:
+        """
+        Overriding this method to be able to delete items when they have constraints
+        """
+        item.permissions = []
+
+
 UserModelView.list_widget = SupersetSecurityListWidget
-RoleModelView.list_widget = SupersetRoleListWidget
 PermissionViewModelView.list_widget = SupersetSecurityListWidget
 PermissionModelView.list_widget = SupersetSecurityListWidget
 
@@ -138,15 +149,10 @@ UserModelView.include_route_methods = RouteMethod.CRUD_SET | {
     RouteMethod.ACTION_POST,
     "userinfo",
 }
-RoleModelView.include_route_methods = RouteMethod.CRUD_SET
 PermissionViewModelView.include_route_methods = {RouteMethod.LIST}
 PermissionModelView.include_route_methods = {RouteMethod.LIST}
 ViewMenuModelView.include_route_methods = {RouteMethod.LIST}
 
-RoleModelView.list_columns = ["name"]
-RoleModelView.edit_columns = ["name", "permissions", "user"]
-RoleModelView.related_views = []
-
 
 def freeze_value(value: Any) -> str:
     """
@@ -218,6 +224,8 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
     userstatschartview = None
     READ_ONLY_MODEL_VIEWS = {"Database", "DynamicPlugin"}
 
+    role_api = SupersetRoleApi
+
     USER_MODEL_VIEWS = {
         "RegisterUserModelView",
         "UserDBModelView",
@@ -251,6 +259,7 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
         "User Registrations",
         "User's Statistics",
         # Guarding all AB_ADD_SECURITY_API = True REST APIs
+        "RoleRestAPI",
         "Role",
         "Permission",
         "PermissionViewMenu",
@@ -279,6 +288,8 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
     }
 
     ADMIN_ONLY_PERMISSIONS = {
+        "update_roles_users",
+        "list_roles",
         "can_update_role",
         "all_query_access",
         "can_grant_guest_token",
@@ -2749,3 +2760,23 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
         return current_app.config["AUTH_ROLE_ADMIN"] in [
             role.name for role in self.get_user_roles()
         ]
+
+    # temporal change to remove the roles view from the security menu,
+    # after migrating all views to frontend, we will set FAB_ADD_SECURITY_VIEWS = False
+    def register_views(self) -> None:
+        super().register_views()
+
+        for view in list(self.appbuilder.baseviews):
+            if (
+                isinstance(view, self.rolemodelview.__class__)
+                and getattr(view, "route_base", None) == "/roles"
+            ):
+                self.appbuilder.baseviews.remove(view)
+
+        security_menu = next(
+            (m for m in self.appbuilder.menu.get_list() if m.name == "Security"), None
+        )
+        if security_menu:
+            for item in list(security_menu.childs):
+                if item.name == "List Roles":
+                    security_menu.childs.remove(item)
diff --git a/superset/views/roles.py b/superset/views/roles.py
new file mode 100644
index 0000000000000000000000000000000000000000..29f293f51e578f745a35be03784236930aeab93d
--- /dev/null
+++ b/superset/views/roles.py
@@ -0,0 +1,34 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from flask_appbuilder import permission_name
+from flask_appbuilder.api import expose
+from flask_appbuilder.security.decorators import has_access
+
+from superset.superset_typing import FlaskResponse
+
+from .base import BaseSupersetView
+
+
+class RolesListView(BaseSupersetView):
+    route_base = "/"
+    class_permission_name = "security"
+
+    @expose("/roles/")
+    @has_access
+    @permission_name("read")
+    def list(self) -> FlaskResponse:
+        return super().render_app_template()
diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py
index 6b28ac13bfdd3d38e13414a1cc2be56eacb61d48..8fe424b1bdfc4ef8b7ef73db55bfcc8b2fe6ea83 100644
--- a/tests/integration_tests/core_tests.py
+++ b/tests/integration_tests/core_tests.py
@@ -161,7 +161,7 @@ class TestCore(SupersetTestCase):
             role = security_manager.find_role(role_name)
             view_menus = [p.view_menu.name for p in role.permissions]
             assert_func("ResetPasswordView", view_menus)
-            assert_func("RoleModelView", view_menus)
+            assert_func("RoleRestAPI", view_menus)
             assert_func("Security", view_menus)
             assert_func("SQL Lab", view_menus)
 
diff --git a/tests/integration_tests/security/api_tests.py b/tests/integration_tests/security/api_tests.py
index 3bb85c4cad40fd1bdf9c515a5e092c734c623497..5667c64ca1101080ebf0d1aa77fc1eb2d0d07758 100644
--- a/tests/integration_tests/security/api_tests.py
+++ b/tests/integration_tests/security/api_tests.py
@@ -219,6 +219,7 @@ class TestSecurityGuestTokenApiTokenValidator(SupersetTestCase):
 
 class TestSecurityRolesApi(SupersetTestCase):
     uri = "api/v1/security/roles/"  # noqa: F541
+    show_uri = "api/v1/security/roles/search/"
 
     @with_config({"FAB_ADD_SECURITY_API": True})
     def test_get_security_roles_admin(self):
@@ -276,3 +277,19 @@ class TestSecurityRolesApi(SupersetTestCase):
             content_type="application/json",
         )
         self.assert403(response)
+
+    def test_show_roles_admin(self):
+        """
+        Security API: Admin should be able to show roles with permissions and users
+        """
+        self.login(ADMIN_USERNAME)
+        response = self.client.get(self.show_uri)
+        self.assert200(response)
+
+    def test_show_roles_gamma(self):
+        """
+        Security API: Gamma should not be able to show roles
+        """
+        self.login(GAMMA_USERNAME)
+        response = self.client.get(self.show_uri)
+        self.assert403(response)
diff --git a/tests/unit_tests/security/api_test.py b/tests/unit_tests/security/api_test.py
index 73227166c212dc807d6c49877b8bad6ab94c68c1..faeec96f558769a71ecd9c453837fa1e0b32907f 100644
--- a/tests/unit_tests/security/api_test.py
+++ b/tests/unit_tests/security/api_test.py
@@ -32,4 +32,9 @@ def test_csrf_not_exempt(app_context: None) -> None:
         "MenuApi",
         "SecurityApi",
         "OpenApi",
+        "PermissionViewMenuApi",
+        "SupersetRoleApi",
+        "UserApi",
+        "PermissionApi",
+        "ViewMenuApi",
     }