You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ma...@apache.org on 2023/01/22 02:15:55 UTC

[superset] 01/01: Revert "feat(RLS): RESTful apis and react view for RLS (#22325)"

This is an automated email from the ASF dual-hosted git repository.

mayurnewase pushed a commit to branch revert-22325-feat/restful-rls
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 53a2801472122d5a02b344c2cbbca9d29e67ddb1
Author: Mayur <ma...@gmail.com>
AuthorDate: Sun Jan 22 07:45:46 2023 +0530

    Revert "feat(RLS): RESTful apis and react view for RLS (#22325)"
    
    This reverts commit 159dcd7e62e9466e2da4ad81cd25c06770fb4a5e.
---
 UPDATING.md                                        |   1 -
 .../rowlevelsecurity/RowLevelSecurityList.test.tsx | 259 -----------
 .../CRUD/rowlevelsecurity/RowLevelSecurityList.tsx | 351 ---------------
 .../RowLevelSecurityModal.test.tsx                 | 295 -------------
 .../rowlevelsecurity/RowLevelSecurityModal.tsx     | 480 ---------------------
 .../src/views/CRUD/rowlevelsecurity/constants.ts   |  31 --
 .../src/views/CRUD/rowlevelsecurity/types.ts       |  51 ---
 superset-frontend/src/views/routes.tsx             |  10 -
 superset/config.py                                 |  14 +-
 superset/connectors/sqla/views.py                  | 117 ++++-
 superset/dao/base.py                               |  11 -
 superset/initialization/__init__.py                |  22 +-
 superset/row_level_security/api.py                 | 349 ---------------
 superset/row_level_security/commands/__init__.py   |  16 -
 .../row_level_security/commands/bulk_delete.py     |  52 ---
 superset/row_level_security/commands/create.py     |  57 ---
 superset/row_level_security/commands/exceptions.py |  29 --
 superset/row_level_security/commands/update.py     |  63 ---
 superset/row_level_security/dao.py                 |  23 -
 superset/row_level_security/schemas.py             | 154 -------
 .../security/row_level_security_tests.py           | 419 ++----------------
 21 files changed, 161 insertions(+), 2643 deletions(-)

diff --git a/UPDATING.md b/UPDATING.md
index ee489ec5cd..22e879b1e7 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -36,7 +36,6 @@ assists people when migrating to a new version.
 
 - [22328](https://github.com/apache/superset/pull/22328): For deployments that have enabled the "THUMBNAILS" feature flag, the function that calculates dashboard digests has been updated to consider additional properties to more accurately identify changes in the dashboard metadata. This change will invalidate all currently cached dashboard thumbnails.
 - [21765](https://github.com/apache/superset/pull/21765): For deployments that have enabled the "ALERT_REPORTS" feature flag, Gamma users will no longer have read and write access to Alerts & Reports by default. To give Gamma users the ability to schedule reports from the Dashboard and Explore view like before, create an additional role with "can read on ReportSchedule" and "can write on ReportSchedule" permissions. To further give Gamma users access to the "Alerts & Reports" menu and CR [...]
-- [22325](https://github.com/apache/superset/pull/22325): "RLS_FORM_QUERY_REL_FIELDS" is replaced by "RLS_BASE_RELATED_FIELD_FILTERS" feature flag.Its value format stays same.
 
 ### Potential Downtime
 
diff --git a/superset-frontend/src/views/CRUD/rowlevelsecurity/RowLevelSecurityList.test.tsx b/superset-frontend/src/views/CRUD/rowlevelsecurity/RowLevelSecurityList.test.tsx
deleted file mode 100644
index 8a948bd997..0000000000
--- a/superset-frontend/src/views/CRUD/rowlevelsecurity/RowLevelSecurityList.test.tsx
+++ /dev/null
@@ -1,259 +0,0 @@
-/**
- * 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 React from 'react';
-import fetchMock from 'fetch-mock';
-import RowLevelSecurityList from 'src/views/CRUD/rowlevelsecurity/RowLevelSecurityList';
-import { render, screen, within } from 'spec/helpers/testing-library';
-import { act } from 'react-dom/test-utils';
-import { MemoryRouter } from 'react-router-dom';
-import { QueryParamProvider } from 'use-query-params';
-import { styledMount as mount } from 'spec/helpers/theming';
-import { Provider } from 'react-redux';
-import configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
-import ListView from 'src/components/ListView/ListView';
-import userEvent from '@testing-library/user-event';
-
-const ruleListEndpoint = 'glob:*/api/v1/rowlevelsecurity/?*';
-const ruleInfoEndpoint = 'glob:*/api/v1/rowlevelsecurity/_info*';
-
-const mockRules = [
-  {
-    changed_on_delta_humanized: '1 days ago',
-    clause: '1=1',
-    description: 'some description',
-    filter_type: 'Regular',
-    group_key: 'group-1',
-    id: 1,
-    name: 'rule 1',
-    roles: [
-      {
-        id: 3,
-        name: 'Alpha',
-      },
-      {
-        id: 5,
-        name: 'granter',
-      },
-    ],
-    tables: [
-      {
-        id: 6,
-        table_name: 'flights',
-      },
-      {
-        id: 13,
-        table_name: 'messages',
-      },
-    ],
-  },
-  {
-    changed_on_delta_humanized: '2 days ago',
-    clause: '2=2',
-    description: 'some description 2',
-    filter_type: 'Base',
-    group_key: 'group-1',
-    id: 2,
-    name: 'rule 2',
-    roles: [
-      {
-        id: 3,
-        name: 'Alpha',
-      },
-      {
-        id: 5,
-        name: 'granter',
-      },
-    ],
-    tables: [
-      {
-        id: 6,
-        table_name: 'flights',
-      },
-      {
-        id: 13,
-        table_name: 'messages',
-      },
-    ],
-  },
-];
-fetchMock.get(ruleListEndpoint, { result: mockRules, count: 2 });
-fetchMock.get(ruleInfoEndpoint, { permissions: ['can_read', 'can_write'] });
-global.URL.createObjectURL = jest.fn();
-
-const mockUser = {
-  userId: 1,
-};
-
-const mockedProps = {};
-
-const mockStore = configureStore([thunk]);
-const store = mockStore({});
-
-describe('RulesList Enzyme', () => {
-  let wrapper: any;
-
-  beforeAll(async () => {
-    fetchMock.resetHistory();
-    wrapper = mount(
-      <MemoryRouter>
-        <Provider store={store}>
-          <RowLevelSecurityList {...mockedProps} user={mockUser} />
-        </Provider>
-      </MemoryRouter>,
-    );
-
-    await waitForComponentToPaint(wrapper);
-  });
-
-  it('renders', () => {
-    expect(wrapper.find(RowLevelSecurityList)).toExist();
-  });
-  it('renders a ListView', () => {
-    expect(wrapper.find(ListView)).toExist();
-  });
-  it('fetched data', () => {
-    // wrapper.update();
-    const apiCalls = fetchMock.calls(/rowlevelsecurity\/\?q/);
-    expect(apiCalls).toHaveLength(1);
-    expect(apiCalls[0][0]).toMatchInlineSnapshot(
-      `"http://localhost/api/v1/rowlevelsecurity/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
-    );
-  });
-});
-
-describe('RuleList RTL', () => {
-  async function renderAndWait() {
-    const mounted = act(async () => {
-      const mockedProps = {};
-      render(
-        <MemoryRouter>
-          <QueryParamProvider>
-            <RowLevelSecurityList {...mockedProps} user={mockUser} />
-          </QueryParamProvider>
-        </MemoryRouter>,
-        { useRedux: true },
-      );
-    });
-    return mounted;
-  }
-
-  it('renders add rule button on empty state', async () => {
-    fetchMock.get(
-      ruleListEndpoint,
-      { result: [], count: 0 },
-      { overwriteRoutes: true },
-    );
-    await renderAndWait();
-
-    const emptyAddRuleButton = await screen.findByTestId('add-rule-empty');
-    expect(emptyAddRuleButton).toBeInTheDocument();
-    fetchMock.get(
-      ruleListEndpoint,
-      { result: mockRules, count: 2 },
-      { overwriteRoutes: true },
-    );
-  });
-
-  it('renders a "Rule" button to add a rule in bulk action', async () => {
-    await renderAndWait();
-
-    const addRuleButton = await screen.findByTestId('add-rule');
-    const emptyAddRuleButton = screen.queryByTestId('add-rule-empty');
-    expect(addRuleButton).toBeInTheDocument();
-    expect(emptyAddRuleButton).not.toBeInTheDocument();
-  });
-
-  it('renders filter options', async () => {
-    await renderAndWait();
-
-    const searchFilters = screen.queryAllByTestId('filters-search');
-    expect(searchFilters).toHaveLength(2);
-
-    const typeFilter = await screen.findByTestId('filters-select');
-    expect(typeFilter).toBeInTheDocument();
-  });
-
-  it('renders correct list columns', async () => {
-    await renderAndWait();
-
-    const table = screen.getByRole('table');
-    expect(table).toBeInTheDocument();
-
-    const nameColumn = await within(table).findByText('Name');
-    const fitlerTypeColumn = await within(table).findByText('Filter Type');
-    const groupKeyColumn = await within(table).findByText('Group Key');
-    const clauseColumn = await within(table).findByText('Clause');
-    const modifiedColumn = await within(table).findByText('Modified');
-    const actionsColumn = await within(table).findByText('Actions');
-
-    expect(nameColumn).toBeInTheDocument();
-    expect(fitlerTypeColumn).toBeInTheDocument();
-    expect(groupKeyColumn).toBeInTheDocument();
-    expect(clauseColumn).toBeInTheDocument();
-    expect(modifiedColumn).toBeInTheDocument();
-    expect(actionsColumn).toBeInTheDocument();
-  });
-
-  it('renders correct action buttons with write permission', async () => {
-    await renderAndWait();
-
-    const deleteActionIcon = screen.queryAllByTestId('rls-list-trash-icon');
-    expect(deleteActionIcon).toHaveLength(2);
-
-    const editActionIcon = screen.queryAllByTestId('edit-alt');
-    expect(editActionIcon).toHaveLength(2);
-  });
-
-  it('should not renders correct action buttons without write permission', async () => {
-    fetchMock.get(
-      ruleInfoEndpoint,
-      { permissions: ['can_read'] },
-      { overwriteRoutes: true },
-    );
-
-    await renderAndWait();
-
-    const deleteActionIcon = screen.queryByTestId('rls-list-trash-icon');
-    expect(deleteActionIcon).not.toBeInTheDocument();
-
-    const editActionIcon = screen.queryByTestId('edit-alt');
-    expect(editActionIcon).not.toBeInTheDocument();
-
-    fetchMock.get(
-      ruleInfoEndpoint,
-      { permissions: ['can_read', 'can_write'] },
-      { overwriteRoutes: true },
-    );
-  });
-
-  it('renders popover on new clicking rule button', async () => {
-    await renderAndWait();
-
-    const modal = screen.queryByTestId('rls-modal-title');
-    expect(modal).not.toBeInTheDocument();
-
-    const addRuleButton = await screen.findByTestId('add-rule');
-    userEvent.click(addRuleButton);
-
-    const modalAfterClick = screen.queryByTestId('rls-modal-title');
-    expect(modalAfterClick).toBeInTheDocument();
-  });
-});
diff --git a/superset-frontend/src/views/CRUD/rowlevelsecurity/RowLevelSecurityList.tsx b/superset-frontend/src/views/CRUD/rowlevelsecurity/RowLevelSecurityList.tsx
deleted file mode 100644
index a7dfa2058c..0000000000
--- a/superset-frontend/src/views/CRUD/rowlevelsecurity/RowLevelSecurityList.tsx
+++ /dev/null
@@ -1,351 +0,0 @@
-/**
- * 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, styled, SupersetClient } from '@superset-ui/core';
-import React, { useMemo, useState } from 'react';
-import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
-import Icons from 'src/components/Icons';
-import ListView, {
-  FetchDataConfig,
-  FilterOperator,
-  ListViewProps,
-  Filters,
-} from 'src/components/ListView';
-import withToasts from 'src/components/MessageToasts/withToasts';
-import { Tooltip } from 'src/components/Tooltip';
-import SubMenu, { SubMenuProps } from 'src/views/components/SubMenu';
-import rison from 'rison';
-import { useListViewResource } from '../hooks';
-import RowLevelSecurityModal from './RowLevelSecurityModal';
-import { RLSObject } from './types';
-import { createErrorHandler } from '../utils';
-
-const Actions = styled.div`
-  color: ${({ theme }) => theme.colors.grayscale.base};
-`;
-
-interface RLSProps {
-  addDangerToast: (msg: string) => void;
-  addSuccessToast: (msg: string) => void;
-  user: {
-    userId?: string | number;
-    firstName: string;
-    lastName: string;
-  };
-}
-
-function RowLevelSecurityList(props: RLSProps) {
-  const { addDangerToast, addSuccessToast, user } = props;
-  const [ruleModalOpen, setRuleModalOpen] = useState<boolean>(false);
-  const [currentRule, setCurrentRule] = useState(null);
-
-  const {
-    state: {
-      loading,
-      resourceCount: rulesCount,
-      resourceCollection: rules,
-      bulkSelectEnabled,
-    },
-    hasPerm,
-    fetchData,
-    refreshData,
-    toggleBulkSelect,
-  } = useListViewResource<RLSObject>(
-    'rowlevelsecurity',
-    t('Row Level Security'),
-    addDangerToast,
-    true,
-    undefined,
-    undefined,
-    true,
-  );
-
-  function handleRuleEdit(rule: null) {
-    setCurrentRule(rule);
-    setRuleModalOpen(true);
-  }
-
-  function handleRuleDelete(
-    { id, name }: RLSObject,
-    refreshData: (arg0?: FetchDataConfig | null) => void,
-    addSuccessToast: (arg0: string) => void,
-    addDangerToast: (arg0: string) => void,
-  ) {
-    return SupersetClient.delete({
-      endpoint: `/api/v1/rowlevelsecurity/${id}`,
-    }).then(
-      () => {
-        refreshData();
-        addSuccessToast(t('Deleted %s', name));
-      },
-      createErrorHandler(errMsg =>
-        addDangerToast(t('There was an issue deleting %s: %s', name, errMsg)),
-      ),
-    );
-  }
-  function handleBulkRulesDelete(rulesToDelete: RLSObject[]) {
-    const ids = rulesToDelete.map(({ id }) => id);
-    return SupersetClient.delete({
-      endpoint: `/api/v1/rowlevelsecurity/?q=${rison.encode(ids)}`,
-    }).then(
-      () => {
-        refreshData();
-        addSuccessToast(t(`Deleted`));
-      },
-      createErrorHandler(errMsg =>
-        addDangerToast(t('There was an issue deleting rules: %s', errMsg)),
-      ),
-    );
-  }
-
-  function handleRuleModalHide() {
-    setCurrentRule(null);
-    setRuleModalOpen(false);
-    refreshData();
-  }
-
-  const canWrite = hasPerm('can_write');
-  const canEdit = hasPerm('can_write');
-  const canExport = hasPerm('can_export');
-
-  const columns = useMemo(
-    () => [
-      {
-        accessor: 'name',
-        Header: t('Name'),
-      },
-      {
-        accessor: 'filter_type',
-        Header: t('Filter Type'),
-        size: 'xl',
-      },
-      {
-        accessor: 'group_key',
-        Header: t('Group Key'),
-        size: 'xl',
-      },
-      {
-        accessor: 'clause',
-        Header: t('Clause'),
-      },
-      {
-        Cell: ({
-          row: {
-            original: { changed_on_delta_humanized: changedOn },
-          },
-        }: any) => <span className="no-wrap">{changedOn}</span>,
-        Header: t('Modified'),
-        accessor: 'changed_on_delta_humanized',
-        size: 'xl',
-      },
-      {
-        Cell: ({ row: { original } }: any) => {
-          const handleDelete = () =>
-            handleRuleDelete(
-              original,
-              refreshData,
-              addSuccessToast,
-              addDangerToast,
-            );
-          const handleEdit = () => handleRuleEdit(original);
-          return (
-            <Actions className="actions">
-              {canWrite && (
-                <ConfirmStatusChange
-                  title={t('Please confirm')}
-                  description={
-                    <>
-                      {t('Are you sure you want to delete')}{' '}
-                      <b>{original.name}</b>
-                    </>
-                  }
-                  onConfirm={handleDelete}
-                >
-                  {confirmDelete => (
-                    <Tooltip
-                      id="delete-action-tooltip"
-                      title={t('Delete')}
-                      placement="bottom"
-                    >
-                      <span
-                        role="button"
-                        tabIndex={0}
-                        className="action-button"
-                        onClick={confirmDelete}
-                      >
-                        <Icons.Trash data-test="rls-list-trash-icon" />
-                      </span>
-                    </Tooltip>
-                  )}
-                </ConfirmStatusChange>
-              )}
-              {canEdit && (
-                <Tooltip
-                  id="edit-action-tooltip"
-                  title={t('Edit')}
-                  placement="bottom"
-                >
-                  <span
-                    role="button"
-                    tabIndex={0}
-                    className="action-button"
-                    onClick={handleEdit}
-                  >
-                    <Icons.EditAlt data-test="edit-alt" />
-                  </span>
-                </Tooltip>
-              )}
-            </Actions>
-          );
-        },
-        Header: t('Actions'),
-        id: 'actions',
-        hidden: !canEdit && !canWrite && !canExport,
-        disableSortBy: true,
-      },
-    ],
-    [
-      user.userId,
-      canEdit,
-      canWrite,
-      canExport,
-      hasPerm,
-      refreshData,
-      addDangerToast,
-      addSuccessToast,
-    ],
-  );
-
-  const emptyState = {
-    title: t('No Rules yet'),
-    image: 'filter-results.svg',
-    buttonAction: () => handleRuleEdit(null),
-    buttonText: canEdit ? (
-      <>
-        <i className="fa fa-plus" data-test="add-rule-empty" /> {'Rule'}{' '}
-      </>
-    ) : null,
-  };
-
-  const filters: Filters = useMemo(
-    () => [
-      {
-        Header: t('Name'),
-        key: 'search',
-        id: 'name',
-        input: 'search',
-        operator: FilterOperator.startsWith,
-      },
-      {
-        Header: t('Filter Type'),
-        key: 'filter_type',
-        id: 'filter_type',
-        input: 'select',
-        operator: FilterOperator.equals,
-        unfilteredLabel: t('Any'),
-        selects: [
-          { label: t('Regular'), value: 'Regular' },
-          { label: t('Base'), value: 'Base' },
-        ],
-      },
-      {
-        Header: t('Group Key'),
-        key: 'search',
-        id: 'group_key',
-        input: 'search',
-        operator: FilterOperator.startsWith,
-      },
-    ],
-    [user],
-  );
-
-  const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
-  const PAGE_SIZE = 25;
-
-  const subMenuButtons: SubMenuProps['buttons'] = [];
-
-  if (canWrite) {
-    subMenuButtons.push({
-      name: (
-        <>
-          <i className="fa fa-plus" data-test="add-rule" /> {t('Rule')}
-        </>
-      ),
-      buttonStyle: 'primary',
-      onClick: () => handleRuleEdit(null),
-    });
-    subMenuButtons.push({
-      name: t('Bulk select'),
-      buttonStyle: 'secondary',
-      'data-test': 'bulk-select',
-      onClick: toggleBulkSelect,
-    });
-  }
-
-  return (
-    <>
-      <SubMenu name={t('Row Level Security')} buttons={subMenuButtons} />
-      <ConfirmStatusChange
-        title={t('Please confirm')}
-        description={t('Are you sure you want to delete the selected rules?')}
-        onConfirm={handleBulkRulesDelete}
-      >
-        {confirmDelete => {
-          const bulkActions: ListViewProps['bulkActions'] = [];
-          if (canWrite) {
-            bulkActions.push({
-              key: 'delete',
-              name: t('Delete'),
-              type: 'danger',
-              onSelect: confirmDelete,
-            });
-          }
-          return (
-            <>
-              <RowLevelSecurityModal
-                rule={currentRule}
-                addDangerToast={addDangerToast}
-                onHide={handleRuleModalHide}
-                addSuccessToast={addSuccessToast}
-                show={ruleModalOpen}
-              />
-              <ListView<RLSObject>
-                className="rls-list-view"
-                bulkActions={bulkActions}
-                bulkSelectEnabled={bulkSelectEnabled}
-                disableBulkSelect={toggleBulkSelect}
-                columns={columns}
-                count={rulesCount}
-                data={rules}
-                emptyState={emptyState}
-                fetchData={fetchData}
-                filters={filters}
-                initialSort={initialSort}
-                loading={loading}
-                pageSize={PAGE_SIZE}
-              />
-            </>
-          );
-        }}
-      </ConfirmStatusChange>
-    </>
-  );
-}
-
-export default withToasts(RowLevelSecurityList);
diff --git a/superset-frontend/src/views/CRUD/rowlevelsecurity/RowLevelSecurityModal.test.tsx b/superset-frontend/src/views/CRUD/rowlevelsecurity/RowLevelSecurityModal.test.tsx
deleted file mode 100644
index 6253c42c82..0000000000
--- a/superset-frontend/src/views/CRUD/rowlevelsecurity/RowLevelSecurityModal.test.tsx
+++ /dev/null
@@ -1,295 +0,0 @@
-/**
- * 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 React from 'react';
-import fetchMock from 'fetch-mock';
-import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
-import { act } from 'react-dom/test-utils';
-import userEvent from '@testing-library/user-event';
-import RowLevelSecurityModal, {
-  RowLevelSecurityModalProps,
-} from './RowLevelSecurityModal';
-import { FilterType } from './types';
-
-const getRuleEndpoint = 'glob:*/api/v1/rowlevelsecurity/1';
-const getRelatedRolesEndpoint =
-  'glob:*/api/v1/rowlevelsecurity/related/roles?q*';
-const getRelatedTablesEndpoint =
-  'glob:*/api/v1/rowlevelsecurity/related/tables?q*';
-const postRuleEndpoint = 'glob:*/api/v1/rowlevelsecurity/*';
-const putRuleEndpoint = 'glob:*/api/v1/rowlevelsecurity/1';
-
-const mockGetRuleResult = {
-  description_columns: {},
-  id: 1,
-  label_columns: {
-    clause: 'Clause',
-    description: 'Description',
-    filter_type: 'Filter Type',
-    group_key: 'Group Key',
-    name: 'Name',
-    'roles.id': 'Roles Id',
-    'roles.name': 'Roles Name',
-    'tables.id': 'Tables Id',
-    'tables.table_name': 'Tables Table Name',
-  },
-  result: {
-    clause: 'gender="girl"',
-    description: 'test rls rule with RTL',
-    filter_type: 'Base',
-    group_key: 'g1',
-    id: 1,
-    name: 'rls 1',
-    roles: [
-      {
-        id: 1,
-        name: 'Admin',
-      },
-    ],
-    tables: [
-      {
-        id: 2,
-        table_name: 'birth_names',
-      },
-    ],
-  },
-  show_columns: [
-    'name',
-    'description',
-    'filter_type',
-    'tables.id',
-    'tables.table_name',
-    'roles.id',
-    'roles.name',
-    'group_key',
-    'clause',
-  ],
-  show_title: 'Show Row Level Security Filter',
-};
-
-const mockGetRolesResult = {
-  count: 3,
-  result: [
-    {
-      extra: {},
-      text: 'Admin',
-      value: 1,
-    },
-    {
-      extra: {},
-      text: 'Public',
-      value: 2,
-    },
-    {
-      extra: {},
-      text: 'Alpha',
-      value: 3,
-    },
-  ],
-};
-
-const mockGetTablesResult = {
-  count: 3,
-  result: [
-    {
-      extra: {},
-      text: 'wb_health_population',
-      value: 1,
-    },
-    {
-      extra: {},
-      text: 'birth_names',
-      value: 2,
-    },
-    {
-      extra: {},
-      text: 'long_lat',
-      value: 3,
-    },
-  ],
-};
-
-fetchMock.get(getRuleEndpoint, mockGetRuleResult);
-fetchMock.get(getRelatedRolesEndpoint, mockGetRolesResult);
-fetchMock.get(getRelatedTablesEndpoint, mockGetTablesResult);
-fetchMock.post(postRuleEndpoint, {});
-fetchMock.put(putRuleEndpoint, {});
-
-global.URL.createObjectURL = jest.fn();
-
-const NOOP = () => {};
-
-const addNewRuleDefaultProps: RowLevelSecurityModalProps = {
-  addDangerToast: NOOP,
-  addSuccessToast: NOOP,
-  show: true,
-  rule: null,
-  onHide: NOOP,
-};
-
-describe('Rule modal', () => {
-  async function renderAndWait(props: RowLevelSecurityModalProps) {
-    const mounted = act(async () => {
-      render(<RowLevelSecurityModal {...props} />, { useRedux: true });
-    });
-    return mounted;
-  }
-
-  it('Sets correct title for adding new rule', async () => {
-    await renderAndWait(addNewRuleDefaultProps);
-    const title = screen.getByText('Add Rule');
-    expect(title).toBeInTheDocument();
-    expect(fetchMock.calls(getRuleEndpoint)).toHaveLength(0);
-    expect(fetchMock.calls(getRelatedTablesEndpoint)).toHaveLength(0);
-    expect(fetchMock.calls(getRelatedRolesEndpoint)).toHaveLength(0);
-  });
-
-  it('Sets correct title for editing existing rule', async () => {
-    await renderAndWait({
-      ...addNewRuleDefaultProps,
-      rule: {
-        id: 1,
-        name: 'test rule',
-        filter_type: FilterType.BASE,
-        tables: [{ key: 1, id: 1, value: 'birth_names' }],
-        roles: [],
-      },
-    });
-    const title = screen.getByText('Edit Rule');
-    expect(title).toBeInTheDocument();
-    expect(fetchMock.calls(getRuleEndpoint)).toHaveLength(1);
-    expect(fetchMock.calls(getRelatedTablesEndpoint)).toHaveLength(0);
-    expect(fetchMock.calls(getRelatedRolesEndpoint)).toHaveLength(0);
-  });
-
-  it('Fills correct values when editing rule', async () => {
-    await renderAndWait({
-      ...addNewRuleDefaultProps,
-      rule: {
-        id: 1,
-        name: 'rls 1',
-        filter_type: FilterType.BASE,
-      },
-    });
-
-    const name = await screen.findByTestId('rule-name-test');
-    expect(name).toHaveDisplayValue('rls 1');
-    userEvent.type(name, 'rls 2');
-    expect(name).toHaveDisplayValue('rls 2');
-
-    const filterType = await screen.findByText('Base');
-    expect(filterType).toBeInTheDocument();
-
-    const roles = await screen.findByText('Admin');
-    expect(roles).toBeInTheDocument();
-
-    const tables = await screen.findByText('birth_names');
-    expect(tables).toBeInTheDocument();
-
-    const groupKey = await screen.findByTestId('group-key-test');
-    expect(groupKey).toHaveValue('g1');
-    userEvent.clear(groupKey);
-    userEvent.type(groupKey, 'g2');
-    expect(groupKey).toHaveValue('g2');
-
-    const clause = await screen.findByTestId('clause-test');
-    expect(clause).toHaveValue('gender="girl"');
-    userEvent.clear(clause);
-    userEvent.type(clause, 'gender="boy"');
-    expect(clause).toHaveValue('gender="boy"');
-
-    const description = await screen.findByTestId('description-test');
-    expect(description).toHaveValue('test rls rule with RTL');
-    userEvent.clear(description);
-    userEvent.type(description, 'test description');
-    expect(description).toHaveValue('test description');
-  });
-
-  it('Does not allow to create rule without name, tables and clause', async () => {
-    await renderAndWait(addNewRuleDefaultProps);
-
-    const addButton = screen.getByRole('button', { name: /add/i });
-    expect(addButton).toBeDisabled();
-
-    const nameTextBox = screen.getByTestId('rule-name-test');
-    userEvent.type(nameTextBox, 'name');
-
-    expect(addButton).toBeDisabled();
-
-    const getSelect = () => screen.getByRole('combobox', { name: 'Tables' });
-    const getElementByClassName = (className: string) =>
-      document.querySelector(className)! as HTMLElement;
-
-    const findSelectOption = (text: string) =>
-      waitFor(() =>
-        within(getElementByClassName('.rc-virtual-list')).getByText(text),
-      );
-    const open = () => waitFor(() => userEvent.click(getSelect()));
-    await open();
-    userEvent.click(await findSelectOption('birth_names'));
-    expect(addButton).toBeDisabled();
-
-    const clause = await screen.findByTestId('clause-test');
-    userEvent.type(clause, 'gender="girl"');
-
-    expect(addButton).toBeEnabled();
-  });
-
-  it('Creates a new rule', async () => {
-    await renderAndWait(addNewRuleDefaultProps);
-
-    const addButton = screen.getByRole('button', { name: /add/i });
-
-    const nameTextBox = screen.getByTestId('rule-name-test');
-    userEvent.type(nameTextBox, 'name');
-
-    const getSelect = () => screen.getByRole('combobox', { name: 'Tables' });
-    const getElementByClassName = (className: string) =>
-      document.querySelector(className)! as HTMLElement;
-
-    const findSelectOption = (text: string) =>
-      waitFor(() =>
-        within(getElementByClassName('.rc-virtual-list')).getByText(text),
-      );
-    const open = () => waitFor(() => userEvent.click(getSelect()));
-    await open();
-    userEvent.click(await findSelectOption('birth_names'));
-
-    const clause = await screen.findByTestId('clause-test');
-    userEvent.type(clause, 'gender="girl"');
-
-    await waitFor(() => userEvent.click(addButton));
-
-    expect(fetchMock.calls(postRuleEndpoint)).toHaveLength(1);
-  });
-
-  it('Updates existing rule', async () => {
-    await renderAndWait({
-      ...addNewRuleDefaultProps,
-      rule: {
-        id: 1,
-        name: 'rls 1',
-        filter_type: FilterType.BASE,
-      },
-    });
-
-    const addButton = screen.getByRole('button', { name: /save/i });
-    await waitFor(() => userEvent.click(addButton));
-    expect(fetchMock.calls(putRuleEndpoint)).toHaveLength(4);
-  });
-});
diff --git a/superset-frontend/src/views/CRUD/rowlevelsecurity/RowLevelSecurityModal.tsx b/superset-frontend/src/views/CRUD/rowlevelsecurity/RowLevelSecurityModal.tsx
deleted file mode 100644
index 1498527c1f..0000000000
--- a/superset-frontend/src/views/CRUD/rowlevelsecurity/RowLevelSecurityModal.tsx
+++ /dev/null
@@ -1,480 +0,0 @@
-/**
- * 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 {
-  css,
-  styled,
-  SupersetClient,
-  SupersetTheme,
-  t,
-} from '@superset-ui/core';
-import Modal from 'src/components/Modal';
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import Icons from 'src/components/Icons';
-import Select from 'src/components/Select/Select';
-import AsyncSelect from 'src/components/Select/AsyncSelect';
-import rison from 'rison';
-import { LabeledErrorBoundInput } from 'src/components/Form';
-import { noBottomMargin } from 'src/components/ReportModal/styles';
-import InfoTooltip from 'src/components/InfoTooltip';
-import { useSingleViewResource } from '../hooks';
-import { FilterOptions } from './constants';
-import { FilterType, RLSObject, RoleObject, TableObject } from './types';
-
-const StyledModal = styled(Modal)`
-  max-width: 1200px;
-  width: 100%;
-  .ant-modal-body {
-    overflow: initial;
-  }
-`;
-const StyledIcon = (theme: SupersetTheme) => css`
-  margin: auto ${theme.gridUnit * 2}px auto 0;
-  color: ${theme.colors.grayscale.base};
-`;
-
-const StyledSectionContainer = styled.div`
-  display: flex;
-  flex-direction: column;
-  padding: ${({ theme }) =>
-    `${theme.gridUnit * 3}px ${theme.gridUnit * 4}px ${theme.gridUnit * 2}px`};
-
-  label {
-    font-size: ${({ theme }) => theme.typography.sizes.s}px;
-    color: ${({ theme }) => theme.colors.grayscale.light1};
-  }
-}
-`;
-
-const StyledInputContainer = styled.div`
-  display: flex;
-  flex-direction: column;
-  margin: ${({ theme }) => theme.gridUnit}px;
-
-  margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
-
-  .input-container {
-    display: flex;
-    align-items: center;
-
-    > div {
-      width: 100%;
-    }
-
-    label {
-      display: flex;
-      margin-right: ${({ theme }) => theme.gridUnit * 2}px;
-    }
-  }
-
-  input,
-  textarea {
-    flex: 1 1 auto;
-  }
-
-  textarea {
-    height: 100px;
-    resize: none;
-  }
-
-  .required {
-    margin-left: ${({ theme }) => theme.gridUnit / 2}px;
-    color: ${({ theme }) => theme.colors.error.base};
-  }
-`;
-
-export interface RowLevelSecurityModalProps {
-  rule: RLSObject | null;
-  addSuccessToast: (msg: string) => void;
-  addDangerToast: (msg: string) => void;
-  onAdd?: (alert?: any) => void;
-  onHide: () => void;
-  show: boolean;
-}
-
-const DEAFULT_RULE = {
-  name: '',
-  filter_type: FilterType.REGULAR,
-  tables: [],
-  roles: [],
-  clause: '',
-  group_key: '',
-  description: '',
-};
-
-function RowLevelSecurityModal(props: RowLevelSecurityModalProps) {
-  const { rule, addDangerToast, addSuccessToast, onHide, show } = props;
-
-  const [currentRule, setCurrentRule] = useState<RLSObject>({
-    ...DEAFULT_RULE,
-  });
-  const [disableSave, setDisableSave] = useState<boolean>(true);
-
-  const isEditMode = rule !== null;
-
-  // * hooks *
-  const {
-    state: { loading, resource, error: fetchError },
-    fetchResource,
-    createResource,
-    updateResource,
-    clearError,
-  } = useSingleViewResource<RLSObject>(
-    `rowlevelsecurity`,
-    t('rowlevelsecurity'),
-    addDangerToast,
-  );
-
-  // initialize
-  useEffect(() => {
-    if (!isEditMode) {
-      setCurrentRule({ ...DEAFULT_RULE });
-    } else if (rule?.id !== null && !loading && !fetchError) {
-      fetchResource(rule.id as number);
-    }
-  }, [rule]);
-
-  useEffect(() => {
-    if (resource) {
-      setCurrentRule({ ...resource, id: rule?.id });
-      const selectedTableAndRoles = getSelectedData();
-      updateRuleState('tables', selectedTableAndRoles?.tables || []);
-      updateRuleState('roles', selectedTableAndRoles?.roles || []);
-    }
-  }, [resource]);
-
-  // find selected tables and roles
-  const getSelectedData = useCallback(() => {
-    if (!resource) {
-      return null;
-    }
-    const tables: TableObject[] = [];
-    const roles: RoleObject[] = [];
-
-    resource.tables?.forEach(selectedTable => {
-      tables.push({
-        key: selectedTable.id,
-        label: selectedTable.schema
-          ? `${selectedTable.schema}.${selectedTable.table_name}`
-          : selectedTable.table_name,
-        value: selectedTable.id,
-      });
-    });
-
-    resource.roles?.forEach(selectedRole => {
-      roles.push({
-        key: selectedRole.id,
-        label: selectedRole.name,
-        value: selectedRole.id,
-      });
-    });
-
-    return { tables, roles };
-  }, [resource?.tables, resource?.roles]);
-
-  // validate
-  const currentRuleSafe = currentRule || {};
-  useEffect(() => {
-    validate();
-  }, [currentRuleSafe.name, currentRuleSafe.clause, currentRuleSafe?.tables]);
-
-  // * event handlers *
-  type SelectValue = {
-    value: string;
-    label: string;
-  };
-
-  const updateRuleState = (name: string, value: any) => {
-    setCurrentRule(currentRuleData => ({
-      ...currentRuleData,
-      [name]: value,
-    }));
-  };
-
-  const onTextChange = (target: HTMLInputElement | HTMLTextAreaElement) => {
-    updateRuleState(target.name, target.value);
-  };
-
-  const onFilterChange = (type: string) => {
-    updateRuleState('filter_type', type);
-  };
-
-  const onTablesChange = (tables: Array<SelectValue>) => {
-    updateRuleState('tables', tables || []);
-  };
-
-  const onRolesChange = (roles: Array<SelectValue>) => {
-    updateRuleState('roles', roles || []);
-  };
-
-  const hide = () => {
-    clearError();
-    setCurrentRule({ ...DEAFULT_RULE });
-    onHide();
-  };
-
-  const onSave = () => {
-    const tables: number[] = [];
-    const roles: number[] = [];
-
-    currentRule.tables?.forEach(table => tables.push(table.key));
-    currentRule.roles?.forEach(role => roles.push(role.key));
-
-    const data: any = { ...currentRule, tables, roles };
-
-    if (isEditMode && currentRule.id) {
-      const updateId = currentRule.id;
-      delete data.id;
-      updateResource(updateId, data).then(response => {
-        if (!response) {
-          return;
-        }
-        addSuccessToast(`Rule updated`);
-        hide();
-      });
-    } else if (currentRule) {
-      createResource(data).then(response => {
-        if (!response) return;
-        addSuccessToast(t('Rule added'));
-        hide();
-      });
-    }
-  };
-
-  // * data loaders *
-  const loadTableOptions = useMemo(
-    () =>
-      (input = '', page: number, pageSize: number) => {
-        const query = rison.encode({
-          filter: input,
-          page,
-          page_size: pageSize,
-        });
-        return SupersetClient.get({
-          endpoint: `/api/v1/rowlevelsecurity/related/tables?q=${query}`,
-        }).then(response => {
-          const list = response.json.result.map(
-            (item: { value: number; text: string }) => ({
-              label: item.text,
-              value: item.value,
-            }),
-          );
-          return { data: list, totalCount: response.json.count };
-        });
-      },
-    [],
-  );
-
-  const loadRoleOptions = useMemo(
-    () =>
-      (input = '', page: number, pageSize: number) => {
-        const query = rison.encode({
-          filter: input,
-          page,
-          page_size: pageSize,
-        });
-        return SupersetClient.get({
-          endpoint: `/api/v1/rowlevelsecurity/related/roles?q=${query}`,
-        }).then(response => {
-          const list = response.json.result.map(
-            (item: { value: number; text: string }) => ({
-              label: item.text,
-              value: item.value,
-            }),
-          );
-          return { data: list, totalCount: response.json.count };
-        });
-      },
-    [],
-  );
-
-  // * state validators *
-  const validate = () => {
-    if (
-      currentRule?.name &&
-      currentRule?.clause &&
-      currentRule.tables?.length
-    ) {
-      setDisableSave(false);
-    } else {
-      setDisableSave(true);
-    }
-  };
-
-  return (
-    <StyledModal
-      className="no-content-padding"
-      responsive
-      show={show}
-      onHide={hide}
-      primaryButtonName={isEditMode ? t('Save') : t('Add')}
-      disablePrimaryButton={disableSave}
-      onHandledPrimaryAction={onSave}
-      width="30%"
-      maxWidth="1450px"
-      title={
-        <h4 data-test="rls-modal-title">
-          {isEditMode ? (
-            <Icons.EditAlt css={StyledIcon} />
-          ) : (
-            <Icons.PlusLarge css={StyledIcon} />
-          )}
-          {isEditMode ? t('Edit Rule') : t('Add Rule')}
-        </h4>
-      }
-    >
-      <StyledSectionContainer>
-        <div className="main-section">
-          <StyledInputContainer>
-            <LabeledErrorBoundInput
-              id="name"
-              name="name"
-              className="labeled-input"
-              value={currentRule ? currentRule.name : ''}
-              required
-              validationMethods={{
-                onChange: ({ target }: { target: HTMLInputElement }) =>
-                  onTextChange(target),
-              }}
-              css={noBottomMargin}
-              label={t('Rule Name')}
-              data-test="rule-name-test"
-            />
-          </StyledInputContainer>
-
-          <StyledInputContainer>
-            <div className="control-label">
-              {t('Filter Type')}{' '}
-              <InfoTooltip
-                tooltip={t(
-                  'Regular filters add where clauses to queries if a user belongs to a role referenced in the filter, base filters apply filters to all queries except the roles defined in the filter, and can be used to define what users can see if no RLS filters within a filter group apply to them.',
-                )}
-              />
-            </div>
-            <div className="input-container">
-              <Select
-                name="filter_type"
-                ariaLabel={t('Filter Type')}
-                placeholder={t('Filter Type')}
-                onChange={onFilterChange}
-                value={currentRule?.filter_type}
-                options={FilterOptions}
-                data-test="rule-filter-type-test"
-              />
-            </div>
-          </StyledInputContainer>
-
-          <StyledInputContainer>
-            <div className="control-label">
-              {t('Tables')} <span className="required">*</span>
-              <InfoTooltip
-                tooltip={t(
-                  'These are the tables this filter will be applied to.',
-                )}
-              />
-            </div>
-            <div className="input-container">
-              <AsyncSelect
-                ariaLabel={t('Tables')}
-                mode="multiple"
-                onChange={onTablesChange}
-                value={(currentRule?.tables as SelectValue[]) || []}
-                options={loadTableOptions}
-              />
-            </div>
-          </StyledInputContainer>
-
-          <StyledInputContainer>
-            <div className="control-label">
-              {t('Roles')}{' '}
-              <InfoTooltip
-                tooltip={t(
-                  'For regular filters, these are the roles this filter will be applied to. For base filters, these are the roles that the filter DOES NOT apply to, e.g. Admin if admin should see all data.',
-                )}
-              />
-            </div>
-            <div className="input-container">
-              <AsyncSelect
-                ariaLabel={t('Roles')}
-                mode="multiple"
-                onChange={onRolesChange}
-                value={(currentRule?.roles as SelectValue[]) || []}
-                options={loadRoleOptions}
-              />
-            </div>
-          </StyledInputContainer>
-          <StyledInputContainer>
-            <LabeledErrorBoundInput
-              id="group_key"
-              name="group_key"
-              value={currentRule ? currentRule.group_key : ''}
-              validationMethods={{
-                onChange: ({ target }: { target: HTMLInputElement }) =>
-                  onTextChange(target),
-              }}
-              css={noBottomMargin}
-              label={t('Group Key')}
-              hasTooltip
-              tooltipText={t(
-                `Filters with the same group key will be ORed together within the group, while different filter groups will be ANDed together. Undefined group keys are treated as unique groups, i.e. are not grouped together. For example, if a table has three filters, of which two are for departments Finance and Marketing (group key = 'department'), and one refers to the region Europe (group key = 'region'), the filter clause would apply the filter (department = 'Finance' OR department =  [...]
-              )}
-              data-test="group-key-test"
-            />
-          </StyledInputContainer>
-
-          <StyledInputContainer>
-            <div className="control-label">
-              <LabeledErrorBoundInput
-                id="clause"
-                name="clause"
-                value={currentRule ? currentRule.clause : ''}
-                required
-                validationMethods={{
-                  onChange: ({ target }: { target: HTMLInputElement }) =>
-                    onTextChange(target),
-                }}
-                css={noBottomMargin}
-                label={t('Clause')}
-                hasTooltip
-                tooltipText={t(
-                  'This is the condition that will be added to the WHERE clause. For example, to only return rows for a particular client, you might define a regular filter with the clause `client_id = 9`. To display no rows unless a user belongs to a RLS filter role, a base filter can be created with the clause `1 = 0` (always false).',
-                )}
-                data-test="clause-test"
-              />
-            </div>
-          </StyledInputContainer>
-
-          <StyledInputContainer>
-            <div className="control-label">{t('Description')}</div>
-            <div className="input-container">
-              <textarea
-                name="description"
-                value={currentRule ? currentRule.description : ''}
-                onChange={event => onTextChange(event.target)}
-                data-test="description-test"
-              />
-            </div>
-          </StyledInputContainer>
-        </div>
-      </StyledSectionContainer>
-    </StyledModal>
-  );
-}
-
-export default RowLevelSecurityModal;
diff --git a/superset-frontend/src/views/CRUD/rowlevelsecurity/constants.ts b/superset-frontend/src/views/CRUD/rowlevelsecurity/constants.ts
deleted file mode 100644
index ceb0982c5f..0000000000
--- a/superset-frontend/src/views/CRUD/rowlevelsecurity/constants.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * 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';
-
-export const FilterOptions = [
-  {
-    label: t('Regular'),
-    value: 'Regular',
-  },
-  {
-    label: t('Base'),
-    value: 'Base',
-  },
-];
diff --git a/superset-frontend/src/views/CRUD/rowlevelsecurity/types.ts b/superset-frontend/src/views/CRUD/rowlevelsecurity/types.ts
deleted file mode 100644
index ae0166c8e1..0000000000
--- a/superset-frontend/src/views/CRUD/rowlevelsecurity/types.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * 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 enum FilterType {
-  REGULAR = 'Regular',
-  BASE = 'Base',
-}
-
-export type RLSObject = {
-  id?: number;
-  name: string;
-  filter_type: FilterType;
-  tables?: TableObject[];
-  roles?: RoleObject[];
-  group_key?: string;
-  clause?: string;
-  description?: string;
-};
-
-export type TableObject = {
-  key: any;
-  id?: number;
-  label?: string;
-  value?: number | string;
-  schema?: string;
-  table_name?: string;
-};
-
-export type RoleObject = {
-  key: any;
-  id?: number;
-  label?: string;
-  value?: number | string;
-  name?: string;
-};
diff --git a/superset-frontend/src/views/routes.tsx b/superset-frontend/src/views/routes.tsx
index da4b8312c9..cbab3f09fd 100644
--- a/superset-frontend/src/views/routes.tsx
+++ b/superset-frontend/src/views/routes.tsx
@@ -105,12 +105,6 @@ const SavedQueryList = lazy(
       /* webpackChunkName: "SavedQueryList" */ 'src/views/CRUD/data/savedquery/SavedQueryList'
     ),
 );
-const RowLevelSecurity = lazy(
-  () =>
-    import(
-      /* webpackChunkName: "RowLevelSecurity" */ 'src/views/CRUD/rowlevelsecurity/RowLevelSecurityList'
-    ),
-);
 
 type Routes = {
   path: string;
@@ -206,10 +200,6 @@ export const routes: Routes = [
     path: '/dataset/:datasetId',
     Component: AddDataset,
   },
-  {
-    path: '/rowlevelsecurity/list',
-    Component: RowLevelSecurity,
-  },
 ];
 
 const frontEndRoutes = routes
diff --git a/superset/config.py b/superset/config.py
index 42dbeda852..922d4a981f 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -53,7 +53,6 @@ from cachelib.base import BaseCache
 from celery.schedules import crontab
 from dateutil import tz
 from flask import Blueprint
-from flask_appbuilder.models.filters import BaseFilter
 from flask_appbuilder.security.manager import AUTH_DB
 from pandas._libs.parsers import STR_NA_VALUES  # pylint: disable=no-name-in-module
 from sqlalchemy.orm.query import Query
@@ -1364,16 +1363,15 @@ TALISMAN_CONFIG = {
 }
 
 # It is possible to customize which tables and roles are featured in the RLS
-# dropdown. When set, this dict is assigned to `filter_rel_fields`
-# on `RLSRestApi`. Example:
+# dropdown. When set, this dict is assigned to `add_form_query_rel_fields` and
+# `edit_form_query_rel_fields` on `RowLevelSecurityFiltersModelView`. Example:
 #
 # from flask_appbuilder.models.sqla import filters
-
-# RLS_BASE_RELATED_FIELD_FILTERS = {
-#     "tables": [["table_name", filters.FilterStartsWith, "birth"]],
-#     "roles": [["name", filters.FilterContains, "Admin"]]
+# RLS_FORM_QUERY_REL_FIELDS = {
+#     "roles": [["name", filters.FilterStartsWith, "RlsRole"]]
+#     "tables": [["table_name", filters.FilterContains, "rls"]]
 # }
-RLS_BASE_RELATED_FIELD_FILTERS: Dict[str, BaseFilter] = {}
+RLS_FORM_QUERY_REL_FIELDS: Optional[Dict[str, List[List[Any]]]] = None
 
 #
 # Flask session cookie options
diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py
index eca84fc646..c502f527ac 100644
--- a/superset/connectors/sqla/views.py
+++ b/superset/connectors/sqla/views.py
@@ -17,9 +17,10 @@
 """Views used by the SqlAlchemy connector"""
 import logging
 import re
+from typing import Any, cast
 
-from flask import flash, Markup, redirect
-from flask_appbuilder import CompactCRUDMixin, expose, permission_name
+from flask import current_app, flash, Markup, redirect
+from flask_appbuilder import CompactCRUDMixin, expose
 from flask_appbuilder.fieldwidgets import Select2Widget
 from flask_appbuilder.models.sqla.interface import SQLAInterface
 from flask_appbuilder.security.decorators import has_access
@@ -27,18 +28,18 @@ from flask_babel import lazy_gettext as _
 from wtforms.ext.sqlalchemy.fields import QuerySelectField
 from wtforms.validators import DataRequired, Regexp
 
-from superset import db
+from superset import app, db
 from superset.connectors.base.views import DatasourceModelView
 from superset.connectors.sqla import models
 from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod
 from superset.superset_typing import FlaskResponse
 from superset.utils import core as utils
 from superset.views.base import (
-    BaseSupersetView,
     create_table_permissions,
     DatasourceFilter,
     DeleteMixin,
     ListWidgetWithCheckboxes,
+    SupersetListWidget,
     SupersetModelView,
     YamlExportMixin,
 )
@@ -270,15 +271,107 @@ class SqlMetricInlineView(  # pylint: disable=too-many-ancestors
     edit_form_extra_fields = add_form_extra_fields
 
 
-class RowLevelSecurityView(BaseSupersetView):
-    route_base = "/rowlevelsecurity"
-    class_permission_name = "RowLevelSecurity"
+class RowLevelSecurityListWidget(
+    SupersetListWidget
+):  # pylint: disable=too-few-public-methods
+    template = "superset/models/rls/list.html"
 
-    @expose("/list/")
-    @has_access
-    @permission_name("read")
-    def list(self) -> FlaskResponse:
-        return super().render_app_template()
+    def __init__(self, **kwargs: Any):
+        kwargs["appbuilder"] = current_app.appbuilder
+        super().__init__(**kwargs)
+
+
+class RowLevelSecurityFiltersModelView(  # pylint: disable=too-many-ancestors
+    SupersetModelView, DeleteMixin
+):
+    datamodel = SQLAInterface(models.RowLevelSecurityFilter)
+
+    list_widget = cast(SupersetListWidget, RowLevelSecurityListWidget)
+
+    list_title = _("Row level security filter")
+    show_title = _("Show Row level security filter")
+    add_title = _("Add Row level security filter")
+    edit_title = _("Edit Row level security filter")
+
+    list_columns = [
+        "name",
+        "filter_type",
+        "tables",
+        "roles",
+        "clause",
+        "creator",
+        "modified",
+    ]
+    order_columns = ["name", "filter_type", "clause", "modified"]
+    edit_columns = [
+        "name",
+        "description",
+        "filter_type",
+        "tables",
+        "roles",
+        "group_key",
+        "clause",
+    ]
+    show_columns = edit_columns
+    search_columns = (
+        "name",
+        "description",
+        "filter_type",
+        "tables",
+        "roles",
+        "group_key",
+        "clause",
+    )
+    add_columns = edit_columns
+    base_order = ("changed_on", "desc")
+    description_columns = {
+        "name": _("Choose a unique name"),
+        "description": _("Optionally add a detailed description"),
+        "filter_type": _(
+            "Regular filters add where clauses to queries if a user belongs to a "
+            "role referenced in the filter. Base filters apply filters to all queries "
+            "except the roles defined in the filter, and can be used to define what "
+            "users can see if no RLS filters within a filter group apply to them."
+        ),
+        "tables": _("These are the tables this filter will be applied to."),
+        "roles": _(
+            "For regular filters, these are the roles this filter will be "
+            "applied to. For base filters, these are the roles that the "
+            "filter DOES NOT apply to, e.g. Admin if admin should see all "
+            "data."
+        ),
+        "group_key": _(
+            "Filters with the same group key will be ORed together within the group, "
+            "while different filter groups will be ANDed together. Undefined group "
+            "keys are treated as unique groups, i.e. are not grouped together. "
+            "For example, if a table has three filters, of which two are for "
+            "departments Finance and Marketing (group key = 'department'), and one "
+            "refers to the region Europe (group key = 'region'), the filter clause "
+            "would apply the filter (department = 'Finance' OR department = "
+            "'Marketing') AND (region = 'Europe')."
+        ),
+        "clause": _(
+            "This is the condition that will be added to the WHERE clause. "
+            "For example, to only return rows for a particular client, "
+            "you might define a regular filter with the clause `client_id = 9`. To "
+            "display no rows unless a user belongs to a RLS filter role, a base "
+            "filter can be created with the clause `1 = 0` (always false)."
+        ),
+    }
+    label_columns = {
+        "name": _("Name"),
+        "description": _("Description"),
+        "tables": _("Tables"),
+        "roles": _("Roles"),
+        "clause": _("Clause"),
+        "creator": _("Creator"),
+        "modified": _("Modified"),
+    }
+    validators_columns = {"tables": [SelectDataRequired()]}
+
+    if app.config["RLS_FORM_QUERY_REL_FIELDS"]:
+        add_form_query_rel_fields = app.config["RLS_FORM_QUERY_REL_FIELDS"]
+        edit_form_query_rel_fields = add_form_query_rel_fields
 
 
 class TableModelView(  # pylint: disable=too-many-ancestors
diff --git a/superset/dao/base.py b/superset/dao/base.py
index 660b4fe601..c6890e53a5 100644
--- a/superset/dao/base.py
+++ b/superset/dao/base.py
@@ -188,14 +188,3 @@ class BaseDAO:
             db.session.rollback()
             raise DAODeleteFailedError(exception=ex) from ex
         return model
-
-    @classmethod
-    def bulk_delete(cls, models: List[Model], commit: bool = True) -> None:
-        try:
-            for model in models:
-                cls.delete(model, False)
-            if commit:
-                db.session.commit()
-        except SQLAlchemyError as ex:
-            db.session.rollback()
-            raise DAODeleteFailedError(exception=ex) from ex
diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index 7e37781502..1cffbd0dc2 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -123,7 +123,7 @@ class SupersetAppInitializer:  # pylint: disable=too-many-public-methods
         from superset.charts.api import ChartRestApi
         from superset.charts.data.api import ChartDataRestApi
         from superset.connectors.sqla.views import (
-            RowLevelSecurityView,
+            RowLevelSecurityFiltersModelView,
             SqlMetricInlineView,
             TableColumnInlineView,
             TableModelView,
@@ -147,7 +147,6 @@ class SupersetAppInitializer:  # pylint: disable=too-many-public-methods
         from superset.queries.saved_queries.api import SavedQueryRestApi
         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.views.access_requests import AccessRequestsModelView
         from superset.views.alerts import AlertView, ReportView
@@ -216,7 +215,6 @@ class SupersetAppInitializer:  # pylint: disable=too-many-public-methods
         appbuilder.add_api(QueryRestApi)
         appbuilder.add_api(ReportScheduleRestApi)
         appbuilder.add_api(ReportExecutionLogRestApi)
-        appbuilder.add_api(RLSRestApi)
         appbuilder.add_api(SavedQueryRestApi)
         #
         # Setup regular views
@@ -282,6 +280,14 @@ class SupersetAppInitializer:  # pylint: disable=too-many-public-methods
             category_label=__("Manage"),
             category_icon="",
         )
+        appbuilder.add_view(
+            RowLevelSecurityFiltersModelView,
+            "Row Level Security",
+            label=__("Row Level Security"),
+            category="Security",
+            category_label=__("Security"),
+            icon="fa-lock",
+        )
 
         #
         # Setup views with no menu
@@ -403,16 +409,6 @@ class SupersetAppInitializer:  # pylint: disable=too-many-public-methods
             menu_cond=lambda: bool(self.config["ENABLE_ACCESS_REQUEST"]),
         )
 
-        appbuilder.add_view(
-            RowLevelSecurityView,
-            "Row Level Security",
-            href="/rowlevelsecurity/list/",
-            label=__("Row Level Security"),
-            category="Security",
-            category_label=__("Security"),
-            icon="fa-lock",
-        )
-
     def init_app_in_ctx(self) -> None:
         """
         Runs init logic in the context of the app
diff --git a/superset/row_level_security/api.py b/superset/row_level_security/api.py
deleted file mode 100644
index 76a04d93ab..0000000000
--- a/superset/row_level_security/api.py
+++ /dev/null
@@ -1,349 +0,0 @@
-# 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 logging
-from typing import Any
-
-from flask import request, Response
-from flask_appbuilder.api import expose, protect, rison, safe
-from flask_appbuilder.models.sqla.interface import SQLAInterface
-from flask_babel import ngettext
-from marshmallow import ValidationError
-
-from superset import app
-from superset.commands.exceptions import (
-    DatasourceNotFoundValidationError,
-    RolesNotFoundValidationError,
-)
-from superset.connectors.sqla.models import RowLevelSecurityFilter
-from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
-from superset.dao.exceptions import DAOCreateFailedError, DAOUpdateFailedError
-from superset.extensions import event_logger
-from superset.row_level_security.commands.bulk_delete import BulkDeleteRLSRuleCommand
-from superset.row_level_security.commands.create import CreateRLSRuleCommand
-from superset.row_level_security.commands.exceptions import RLSRuleNotFoundError
-from superset.row_level_security.commands.update import UpdateRLSRuleCommand
-from superset.row_level_security.schemas import (
-    get_delete_ids_schema,
-    RLSListSchema,
-    RLSPostSchema,
-    RLSPutSchema,
-    RLSShowSchema,
-)
-from superset.views.base_api import (
-    BaseSupersetModelRestApi,
-    requires_json,
-    statsd_metrics,
-)
-
-logger = logging.getLogger(__name__)
-
-
-class RLSRestApi(BaseSupersetModelRestApi):
-    datamodel = SQLAInterface(RowLevelSecurityFilter)
-    include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | {
-        RouteMethod.RELATED,
-        "bulk_delete",
-    }
-    resource_name = "rowlevelsecurity"
-    class_permission_name = "Row Level Security"
-    openapi_spec_tag = "Row Level Security"
-    method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
-    allow_browser_login = True
-
-    list_columns = [
-        "id",
-        "name",
-        "filter_type",
-        "tables.id",
-        "tables.table_name",
-        "roles.id",
-        "roles.name",
-        "clause",
-        "changed_on_delta_humanized",
-        "group_key",
-    ]
-    order_columns = [
-        "name",
-        "filter_type",
-        "clause",
-        "changed_on_delta_humanized",
-        "group_key",
-    ]
-    add_columns = [
-        "name",
-        "description",
-        "filter_type",
-        "tables",
-        "roles",
-        "group_key",
-        "clause",
-    ]
-    show_columns = [
-        "name",
-        "description",
-        "filter_type",
-        "tables.id",
-        "tables.schema",
-        "tables.table_name",
-        "roles.id",
-        "roles.name",
-        "group_key",
-        "clause",
-    ]
-    search_columns = (
-        "name",
-        "description",
-        "filter_type",
-        "tables",
-        "roles",
-        "group_key",
-        "clause",
-    )
-    edit_columns = add_columns
-
-    show_model_schema = RLSShowSchema()
-    list_model_schema = RLSListSchema()
-    add_model_schema = RLSPostSchema()
-    edit_model_schema = RLSPutSchema()
-
-    allowed_rel_fields = {"tables", "roles"}
-    base_related_field_filters = app.config["RLS_BASE_RELATED_FIELD_FILTERS"]
-
-    @expose("/", methods=["POST"])
-    @protect()
-    @safe
-    @statsd_metrics
-    @requires_json
-    @event_logger.log_this_with_context(
-        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
-        log_to_statsd=False,
-    )
-    def post(self) -> Response:
-        """Creates a new RLS rule
-        ---
-        post:
-          description: >-
-            Create a new RLS Rule
-          requestBody:
-            description: RLS schema
-            required: true
-            content:
-              application/json:
-                schema:
-                  $ref: '#/components/schemas/{{self.__class__.__name__}}.post'
-          responses:
-            201:
-              description: RLS Rule added
-              content:
-                application/json:
-                  schema:
-                    type: object
-                    properties:
-                      id:
-                        type: number
-                      result:
-                        $ref: '#/components/schemas/{{self.__class__.__name__}}.post'
-            400:
-              $ref: '#/components/responses/400'
-            401:
-              $ref: '#/components/responses/401'
-            404:
-              $ref: '#/components/responses/404'
-            422:
-              $ref: '#/components/responses/422'
-            500:
-              $ref: '#/components/responses/500'
-        """
-        try:
-            item = self.add_model_schema.load(request.json)
-        except ValidationError as error:
-            return self.response_400(message=error.messages)
-
-        try:
-            new_model = CreateRLSRuleCommand(item).run()
-            return self.response(201, id=new_model.id, result=item)
-        except RolesNotFoundValidationError as ex:
-            logger.error(
-                "Role not found while creating RLS rule %s: %s",
-                self.__class__.__name__,
-                str(ex),
-                exc_info=True,
-            )
-            return self.response_422(message=str(ex))
-        except DatasourceNotFoundValidationError as ex:
-            logger.error(
-                "Table not found while creating RLS rule %s: %s",
-                self.__class__.__name__,
-                str(ex),
-                exc_info=True,
-            )
-            return self.response_422(message=str(ex))
-        except DAOCreateFailedError as ex:
-            logger.error(
-                "Error creating RLS rule %s: %s",
-                self.__class__.__name__,
-                str(ex),
-                exc_info=True,
-            )
-            return self.response_422(message=str(ex))
-
-    @expose("/<int:pk>", methods=["PUT"])
-    @protect()
-    @safe
-    @statsd_metrics
-    @requires_json
-    @event_logger.log_this_with_context(
-        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.put",
-        log_to_statsd=False,
-    )
-    def put(self, pk: int) -> Response:
-        """Updates an RLS Rule
-        ---
-        put:
-          description: >-
-            Updates an RLS Rule
-          parameters:
-          - in: path
-            schema:
-              type: integer
-            name: pk
-            description: The Rule pk
-          requestBody:
-            description: RLS schema
-            required: true
-            content:
-              application/json:
-                schema:
-                  $ref: '#/components/schemas/{{self.__class__.__name__}}.put'
-          responses:
-            200:
-              description: Rule changed
-              content:
-                application/json:
-                  schema:
-                    type: object
-                    properties:
-                      id:
-                        type: number
-                      result:
-                        $ref: '#/components/schemas/{{self.__class__.__name__}}.put'
-            400:
-              $ref: '#/components/responses/400'
-            401:
-              $ref: '#/components/responses/401'
-            403:
-              $ref: '#/components/responses/403'
-            404:
-              $ref: '#/components/responses/404'
-            422:
-              $ref: '#/components/responses/422'
-            500:
-              $ref: '#/components/responses/500'
-        """
-
-        try:
-            item = self.edit_model_schema.load(request.json)
-        except ValidationError as error:
-            return self.response_400(message=error.messages)
-
-        try:
-            new_model = UpdateRLSRuleCommand(pk, item).run()
-            return self.response(201, id=new_model.id, result=item)
-        except RolesNotFoundValidationError as ex:
-            logger.error(
-                "Role not found while updating RLS rule %s: %s",
-                self.__class__.__name__,
-                str(ex),
-                exc_info=True,
-            )
-            return self.response_422(message=str(ex))
-        except DatasourceNotFoundValidationError as ex:
-            logger.error(
-                "Table not found while updating RLS rule %s: %s",
-                self.__class__.__name__,
-                str(ex),
-                exc_info=True,
-            )
-            return self.response_422(message=str(ex))
-        except DAOUpdateFailedError as ex:
-            logger.error(
-                "Error updating RLS rule %s: %s",
-                self.__class__.__name__,
-                str(ex),
-                exc_info=True,
-            )
-            return self.response_422(message=str(ex))
-        except RLSRuleNotFoundError as ex:
-            return self.response_404()
-
-    @expose("/", methods=["DELETE"])
-    @protect()
-    @safe
-    @statsd_metrics
-    @rison(get_delete_ids_schema)
-    @event_logger.log_this_with_context(
-        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.bulk_delete",
-        log_to_statsd=False,
-    )
-    def bulk_delete(self, **kwargs: Any) -> Response:
-        """Delete bulk RLS rules
-        ---
-        delete:
-          description: >-
-            Deletes multiple RLS rules in a bulk operation.
-          parameters:
-          - in: query
-            name: q
-            content:
-              application/json:
-                schema:
-                  $ref: '#/components/schemas/get_delete_ids_schema'
-          responses:
-            200:
-              description: RLS Rule bulk delete
-              content:
-                application/json:
-                  schema:
-                    type: object
-                    properties:
-                      message:
-                        type: string
-            401:
-              $ref: '#/components/responses/401'
-            403:
-              $ref: '#/components/responses/403'
-            404:
-              $ref: '#/components/responses/404'
-            422:
-              $ref: '#/components/responses/422'
-            500:
-              $ref: '#/components/responses/500'
-        """
-        item_ids = kwargs["rison"]
-        try:
-            BulkDeleteRLSRuleCommand(item_ids).run()
-            return self.response(
-                200,
-                message=ngettext(
-                    "Deleted %(num)d rules",
-                    "Deleted %(num)d rules",
-                    num=len(item_ids),
-                ),
-            )
-        except RLSRuleNotFoundError:
-            return self.response_404()
diff --git a/superset/row_level_security/commands/__init__.py b/superset/row_level_security/commands/__init__.py
deleted file mode 100644
index 13a83393a9..0000000000
--- a/superset/row_level_security/commands/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# 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.
diff --git a/superset/row_level_security/commands/bulk_delete.py b/superset/row_level_security/commands/bulk_delete.py
deleted file mode 100644
index 90a37cc6d2..0000000000
--- a/superset/row_level_security/commands/bulk_delete.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# 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 logging
-from typing import List, Optional
-
-from superset import security_manager
-from superset.commands.base import BaseCommand
-from superset.dao.exceptions import DAODeleteFailedError
-from superset.reports.models import ReportSchedule
-from superset.row_level_security.commands.exceptions import (
-    RLSRuleNotFoundError,
-    RuleBulkDeleteFailedError,
-)
-from superset.row_level_security.dao import RLSDAO
-
-logger = logging.getLogger(__name__)
-
-
-class BulkDeleteRLSRuleCommand(BaseCommand):
-    def __init__(self, model_ids: List[int]):
-        self._model_ids = model_ids
-        self._models: Optional[List[ReportSchedule]] = None
-
-    def run(self) -> None:
-        self.validate()
-        try:
-            RLSDAO.bulk_delete(self._models)
-            return None
-        except DAODeleteFailedError as ex:
-            logger.exception(ex.exception)
-            raise RuleBulkDeleteFailedError() from ex
-
-    def validate(self) -> None:
-        # Validate/populate model exists
-        self._models = RLSDAO.find_by_ids(self._model_ids)
-        if not self._models or len(self._models) != len(self._model_ids):
-            raise RLSRuleNotFoundError()
diff --git a/superset/row_level_security/commands/create.py b/superset/row_level_security/commands/create.py
deleted file mode 100644
index 2c1d4f7b6a..0000000000
--- a/superset/row_level_security/commands/create.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# 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 logging
-from typing import Any, Dict
-
-from superset.commands.base import BaseCommand
-from superset.commands.exceptions import DatasourceNotFoundValidationError
-from superset.commands.utils import populate_roles
-from superset.connectors.sqla.models import SqlaTable
-from superset.dao.exceptions import DAOCreateFailedError
-from superset.extensions import appbuilder, db, security_manager
-from superset.row_level_security.dao import RLSDAO
-
-logger = logging.getLogger(__name__)
-
-
-class CreateRLSRuleCommand(BaseCommand):
-    def __init__(self, data: Dict[str, Any]):
-        self._properties = data.copy()
-        self._tables = self._properties.get("tables", [])
-        self._roles = self._properties.get("roles", [])
-
-    def run(self) -> Any:
-        self.validate()
-        try:
-            rule = RLSDAO.create(self._properties)
-        except DAOCreateFailedError as ex:
-            logger.exception(ex.exception)
-            raise DAOCreateFailedError
-
-        return rule
-
-    def validate(self) -> None:
-        roles = populate_roles(self._roles)
-        tables = (
-            db.session.query(SqlaTable).filter(SqlaTable.id.in_(self._tables)).all()
-        )
-        if len(tables) != len(self._tables):
-            raise DatasourceNotFoundValidationError()
-        self._properties["roles"] = roles
-        self._properties["tables"] = tables
diff --git a/superset/row_level_security/commands/exceptions.py b/superset/row_level_security/commands/exceptions.py
deleted file mode 100644
index 40f8e4af81..0000000000
--- a/superset/row_level_security/commands/exceptions.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# 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_babel import lazy_gettext as _
-
-from superset.commands.exceptions import CommandException, DeleteFailedError
-
-
-class RLSRuleNotFoundError(CommandException):
-    status = 404
-    message = _("RLS Rule not found.")
-
-
-class RuleBulkDeleteFailedError(DeleteFailedError):
-    message = _("RLS Rule could not be deleted.")
diff --git a/superset/row_level_security/commands/update.py b/superset/row_level_security/commands/update.py
deleted file mode 100644
index 08964601b3..0000000000
--- a/superset/row_level_security/commands/update.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# 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 logging
-from typing import Any, Dict, Optional
-
-from superset.commands.base import BaseCommand
-from superset.commands.exceptions import DatasourceNotFoundValidationError
-from superset.commands.utils import populate_roles
-from superset.connectors.sqla.models import RowLevelSecurityFilter, SqlaTable
-from superset.dao.exceptions import DAOUpdateFailedError
-from superset.extensions import appbuilder, db, security_manager
-from superset.row_level_security.commands.exceptions import RLSRuleNotFoundError
-from superset.row_level_security.dao import RLSDAO
-
-logger = logging.getLogger(__name__)
-
-
-class UpdateRLSRuleCommand(BaseCommand):
-    def __init__(self, model_id: int, data: Dict[str, Any]):
-        self._model_id = model_id
-        self._properties = data.copy()
-        self._tables = self._properties.get("tables", [])
-        self._roles = self._properties.get("roles", [])
-        self._model: Optional[RowLevelSecurityFilter] = None
-
-    def run(self) -> Any:
-        self.validate()
-        try:
-            rule = RLSDAO.update(self._model, self._properties)
-        except DAOUpdateFailedError as ex:
-            logger.exception(ex.exception)
-            raise DAOUpdateFailedError
-
-        return rule
-
-    def validate(self) -> None:
-        self._model = RLSDAO.find_by_id(int(self._model_id))
-        if not self._model:
-            raise RLSRuleNotFoundError()
-        roles = populate_roles(self._roles)
-        tables = (
-            db.session.query(SqlaTable).filter(SqlaTable.id.in_(self._tables)).all()
-        )
-        if len(tables) != len(self._tables):
-            raise DatasourceNotFoundValidationError()
-        self._properties["roles"] = roles
-        self._properties["tables"] = tables
diff --git a/superset/row_level_security/dao.py b/superset/row_level_security/dao.py
deleted file mode 100644
index 1226e4d549..0000000000
--- a/superset/row_level_security/dao.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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 superset.connectors.sqla.models import RowLevelSecurityFilter
-from superset.dao.base import BaseDAO
-
-
-class RLSDAO(BaseDAO):
-    model_cls = RowLevelSecurityFilter
diff --git a/superset/row_level_security/schemas.py b/superset/row_level_security/schemas.py
deleted file mode 100644
index 63f9c8d6bc..0000000000
--- a/superset/row_level_security/schemas.py
+++ /dev/null
@@ -1,154 +0,0 @@
-# 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 marshmallow import fields, Schema
-from marshmallow.validate import Length, OneOf
-
-from superset.connectors.sqla.models import RowLevelSecurityFilter
-from superset.utils.core import RowLevelSecurityFilterType
-
-id_description = "Unique if of rls filter"
-name_description = "Name of rls filter"
-description_description = "Detailed description"
-filter_type_description = "Regular filters add where clauses to queries if a user belongs to a role referenced in the filter, base filters apply filters to all queries except the roles defined in the filter, and can be used to define what users can see if no RLS filters within a filter group apply to them."
-tables_description = "These are the tables this filter will be applied to."
-roles_description = "For regular filters, these are the roles this filter will be applied to. For base filters, these are the roles that the filter DOES NOT apply to, e.g. Admin if admin should see all data."
-group_key_description = "Filters with the same group key will be ORed together within the group, while different filter groups will be ANDed together. Undefined group keys are treated as unique groups, i.e. are not grouped together. For example, if a table has three filters, of which two are for departments Finance and Marketing (group key = 'department'), and one refers to the region Europe (group key = 'region'), the filter clause would apply the filter (department = 'Finance' OR depar [...]
-clause_description = "This is the condition that will be added to the WHERE clause. For example, to only return rows for a particular client, you might define a regular filter with the clause `client_id = 9`. To display no rows unless a user belongs to a RLS filter role, a base filter can be created with the clause `1 = 0` (always false)."
-
-get_delete_ids_schema = {"type": "array", "items": {"type": "integer"}}
-
-
-class RolesSchema(Schema):
-    name = fields.String()
-    id = fields.Integer()
-
-
-class TablesSchema(Schema):
-    schema = fields.String()
-    table_name = fields.String()
-    id = fields.Integer()
-
-
-class RLSListSchema(Schema):
-    id = fields.Integer(description=id_description)
-    name = fields.String(description=name_description)
-    filter_type = fields.String(
-        description=filter_type_description,
-        validate=OneOf(
-            [filter_type.value for filter_type in RowLevelSecurityFilterType]
-        ),
-    )
-    roles = fields.List(fields.Nested(RolesSchema))
-    tables = fields.List(fields.Nested(TablesSchema))
-    clause = fields.String(description=clause_description)
-    changed_on_delta_humanized = fields.Function(
-        RowLevelSecurityFilter.created_on_delta_humanized
-    )
-    group_key = fields.String(description=group_key_description)
-    description = fields.String(description=description_description)
-
-
-class RLSShowSchema(Schema):
-    id = fields.Integer(description=id_description)
-    name = fields.String(description=name_description)
-    filter_type = fields.String(
-        description=filter_type_description,
-        validate=OneOf(
-            [filter_type.value for filter_type in RowLevelSecurityFilterType]
-        ),
-    )
-    roles = fields.List(fields.Nested(RolesSchema))
-    tables = fields.List(fields.Nested(TablesSchema))
-    clause = fields.String(description=clause_description)
-    group_key = fields.String(description=group_key_description)
-    description = fields.String(description=description_description)
-
-
-class RLSPostSchema(Schema):
-    name = fields.String(
-        description=name_description,
-        required=True,
-        allow_none=False,
-        validate=Length(1, 255),
-    )
-    description = fields.String(
-        description=description_description, required=False, allow_none=True
-    )
-    filter_type = fields.String(
-        description=filter_type_description,
-        required=True,
-        allow_none=False,
-        validate=OneOf(
-            [filter_type.value for filter_type in RowLevelSecurityFilterType]
-        ),
-    )
-    tables = fields.List(
-        fields.Integer(),
-        description=tables_description,
-        required=True,
-        allow_none=False,
-        validate=Length(1),
-    )
-    roles = fields.List(
-        fields.Integer(), description=roles_description, required=True, allow_none=False
-    )
-    group_key = fields.String(
-        description=group_key_description, required=False, allow_none=True
-    )
-    clause = fields.String(
-        description=clause_description, required=True, allow_none=False
-    )
-
-
-class RLSPutSchema(Schema):
-    name = fields.String(
-        description=name_description,
-        required=False,
-        allow_none=False,
-        validate=Length(1, 255),
-    )
-    description = fields.String(
-        description=description_description, required=False, allow_none=True
-    )
-    filter_type = fields.String(
-        description=filter_type_description,
-        required=False,
-        allow_none=False,
-        validate=OneOf(
-            [filter_type.value for filter_type in RowLevelSecurityFilterType]
-        ),
-    )
-    tables = fields.List(
-        fields.Integer(),
-        description=tables_description,
-        required=False,
-        allow_none=False,
-    )
-    roles = fields.List(
-        fields.Integer(),
-        description=roles_description,
-        required=False,
-        allow_none=False,
-    )
-    group_key = fields.String(
-        description=group_key_description, required=False, allow_none=True
-    )
-    clause = fields.String(
-        description=clause_description, required=False, allow_none=False
-    )
diff --git a/tests/integration_tests/security/row_level_security_tests.py b/tests/integration_tests/security/row_level_security_tests.py
index ef0870d32d..ebd95cae39 100644
--- a/tests/integration_tests/security/row_level_security_tests.py
+++ b/tests/integration_tests/security/row_level_security_tests.py
@@ -21,18 +21,13 @@ from unittest import mock
 
 import pytest
 from flask import g
-import json
-import prison
 
-from superset import db, security_manager, app
+from superset import db, security_manager
 from superset.connectors.sqla.models import RowLevelSecurityFilter, SqlaTable
 from superset.security.guest_token import (
     GuestTokenResourceType,
     GuestUser,
 )
-from flask_babel import lazy_gettext as _
-from flask_appbuilder.models.sqla import filters
-from ..conftest import with_config
 from ..base_tests import SupersetTestCase
 from tests.integration_tests.fixtures.birth_names_dashboard import (
     load_birth_names_dashboard_with_slices,
@@ -43,7 +38,6 @@ from tests.integration_tests.fixtures.energy_dashboard import (
     load_energy_table_data,
 )
 from tests.integration_tests.fixtures.unicode_dashboard import (
-    UNICODE_TBL_NAME,
     load_unicode_dashboard_with_slice,
     load_unicode_data,
 )
@@ -180,18 +174,19 @@ class TestRowLevelSecurity(SupersetTestCase):
         self.login(username="admin")
         test_dataset = self._get_test_dataset()
         rv = self.client.post(
-            "/api/v1/rowlevelsecurity/",
-            json={
-                "name": "rls1",
-                "description": "Some description",
-                "filter_type": "Regular",
-                "tables": [test_dataset.id],
-                "roles": [security_manager.find_role("Alpha").id],
-                "group_key": "group_key_1",
-                "clause": "client_id=1",
-            },
+            "/rowlevelsecurityfiltersmodelview/add",
+            data=dict(
+                name="rls1",
+                description="Some description",
+                filter_type="Regular",
+                tables=[test_dataset.id],
+                roles=[security_manager.find_role("Alpha").id],
+                group_key="group_key_1",
+                clause="client_id=1",
+            ),
+            follow_redirects=True,
         )
-        self.assertEqual(rv.status_code, 201)
+        self.assertEqual(rv.status_code, 200)
         rls1 = (
             db.session.query(RowLevelSecurityFilter).filter_by(name="rls1")
         ).one_or_none()
@@ -206,39 +201,41 @@ class TestRowLevelSecurity(SupersetTestCase):
         self.login(username="admin")
         test_dataset = self._get_test_dataset()
         rv = self.client.post(
-            "/api/v1/rowlevelsecurity/",
-            json={
-                "name": "rls_entry1",
-                "description": "Some description",
-                "filter_type": "Regular",
-                "tables": [test_dataset.id],
-                "roles": [security_manager.find_role("Alpha").id],
-                "group_key": "group_key_1",
-                "clause": "client_id=1",
-            },
+            "/rowlevelsecurityfiltersmodelview/add",
+            data=dict(
+                name="rls_entry1",
+                description="Some description",
+                filter_type="Regular",
+                tables=[test_dataset.id],
+                roles=[security_manager.find_role("Alpha").id],
+                group_key="group_key_1",
+                clause="client_id=1",
+            ),
+            follow_redirects=True,
         )
-        self.assertEqual(rv.status_code, 422)
-        data = json.loads(rv.data.decode("utf-8"))
-        assert "Create failed" in data["message"]
+        self.assertEqual(rv.status_code, 200)
+        data = rv.data.decode("utf-8")
+        assert "Already exists." in data
 
     @pytest.mark.usefixtures("create_dataset")
     def test_model_view_rls_add_tables_required(self):
         self.login(username="admin")
         rv = self.client.post(
-            "/api/v1/rowlevelsecurity/",
-            json={
-                "name": "rls1",
-                "description": "Some description",
-                "filter_type": "Regular",
-                "tables": [],
-                "roles": [security_manager.find_role("Alpha").id],
-                "group_key": "group_key_1",
-                "clause": "client_id=1",
-            },
+            "/rowlevelsecurityfiltersmodelview/add",
+            data=dict(
+                name="rls1",
+                description="Some description",
+                filter_type="Regular",
+                tables=[],
+                roles=[security_manager.find_role("Alpha").id],
+                group_key="group_key_1",
+                clause="client_id=1",
+            ),
+            follow_redirects=True,
         )
-        self.assertEqual(rv.status_code, 400)
-        data = json.loads(rv.data.decode("utf-8"))
-        assert data["message"] == {"tables": ["Shorter than minimum length 1."]}
+        self.assertEqual(rv.status_code, 200)
+        data = rv.data.decode("utf-8")
+        assert "This field is required." in data
 
     @pytest.mark.usefixtures("load_energy_table_with_slice")
     def test_rls_filter_alters_energy_query(self):
@@ -307,340 +304,6 @@ class TestRowLevelSecurity(SupersetTestCase):
         assert not self.BASE_FILTER_REGEX.search(sql)
 
 
-class TestRowLevelSecurityCreateAPI(SupersetTestCase):
-    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    def test_invalid_role_failure(self):
-        self.login("Admin")
-        payload = {
-            "name": "rls 1",
-            "clause": "1=1",
-            "filter_type": "Base",
-            "tables": [1],
-            "roles": [999999],
-        }
-        rv = self.client.post("/api/v1/rowlevelsecurity/", json=payload)
-        status_code, data = rv.status_code, json.loads(rv.data.decode("utf-8"))
-        self.assertEqual(status_code, 422)
-        self.assertEqual(data["message"], "[l'Some roles do not exist']")
-
-    def test_invalid_table_failure(self):
-        self.login("Admin")
-        payload = {
-            "name": "rls 1",
-            "clause": "1=1",
-            "filter_type": "Base",
-            "tables": [999999],
-            "roles": [1],
-        }
-        rv = self.client.post("/api/v1/rowlevelsecurity/", json=payload)
-        status_code, data = rv.status_code, json.loads(rv.data.decode("utf-8"))
-        self.assertEqual(status_code, 422)
-        self.assertEqual(data["message"], "[l'Datasource does not exist']")
-
-    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    def test_post_success(self):
-        table = db.session.query(SqlaTable).first()
-        self.login("Admin")
-        payload = {
-            "name": "rls 1",
-            "clause": "1=1",
-            "filter_type": "Base",
-            "tables": [table.id],
-            "roles": [1],
-        }
-        rv = self.client.post("/api/v1/rowlevelsecurity/", json=payload)
-        status_code, data = rv.status_code, json.loads(rv.data.decode("utf-8"))
-
-        self.assertEqual(status_code, 201)
-
-        rls = (
-            db.session.query(RowLevelSecurityFilter)
-            .filter(RowLevelSecurityFilter.id == data["id"])
-            .one_or_none()
-        )
-
-        assert rls
-        self.assertEqual(rls.name, "rls 1")
-        self.assertEqual(rls.clause, "1=1")
-        self.assertEqual(rls.filter_type, "Base")
-        self.assertEqual(rls.tables[0].id, table.id)
-        self.assertEqual(rls.roles[0].id, 1)
-
-        db.session.delete(rls)
-        db.session.commit()
-
-
-class TestRowLevelSecurityUpdateAPI(SupersetTestCase):
-    def test_invalid_id_failure(self):
-        self.login("Admin")
-        payload = {
-            "name": "rls 1",
-            "clause": "1=1",
-            "filter_type": "Base",
-            "tables": [1],
-            "roles": [1],
-        }
-        rv = self.client.put("/api/v1/rowlevelsecurity/99999999", json=payload)
-        status_code, data = rv.status_code, json.loads(rv.data.decode("utf-8"))
-        self.assertEqual(status_code, 404)
-        self.assertEqual(data["message"], "Not found")
-
-    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    def test_invalid_role_failure(self):
-        table = db.session.query(SqlaTable).first()
-
-        rls = RowLevelSecurityFilter(
-            name="rls test invalid role",
-            clause="1=1",
-            filter_type="Regular",
-            tables=[table],
-        )
-        db.session.add(rls)
-        db.session.commit()
-
-        self.login("Admin")
-        payload = {
-            "roles": [999999],
-        }
-        rv = self.client.put(f"/api/v1/rowlevelsecurity/{rls.id}", json=payload)
-        status_code, data = rv.status_code, json.loads(rv.data.decode("utf-8"))
-        self.assertEqual(status_code, 422)
-        self.assertEqual(data["message"], "[l'Some roles do not exist']")
-
-        db.session.delete(rls)
-        db.session.commit()
-
-    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    def test_invalid_table_failure(self):
-        table = db.session.query(SqlaTable).first()
-
-        rls = RowLevelSecurityFilter(
-            name="rls test invalid role",
-            clause="1=1",
-            filter_type="Regular",
-            tables=[table],
-        )
-        db.session.add(rls)
-        db.session.commit()
-
-        self.login("Admin")
-        payload = {
-            "name": "rls 1",
-            "clause": "1=1",
-            "filter_type": "Base",
-            "tables": [999999],
-            "roles": [1],
-        }
-        rv = self.client.put(f"/api/v1/rowlevelsecurity/{rls.id}", json=payload)
-        status_code, data = rv.status_code, json.loads(rv.data.decode("utf-8"))
-        self.assertEqual(status_code, 422)
-        self.assertEqual(data["message"], "[l'Datasource does not exist']")
-
-        db.session.delete(rls)
-        db.session.commit()
-
-    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    @pytest.mark.usefixtures("load_energy_table_with_slice")
-    def test_put_success(self):
-        tables = db.session.query(SqlaTable).limit(2).all()
-        roles = db.session.query(security_manager.role_model).limit(2).all()
-
-        rls = RowLevelSecurityFilter(
-            name="rls 1",
-            clause="1=1",
-            filter_type="Regular",
-            tables=[tables[0]],
-            roles=[roles[0]],
-        )
-        db.session.add(rls)
-        db.session.commit()
-
-        self.login("Admin")
-        payload = {
-            "name": "rls put success",
-            "clause": "2=2",
-            "filter_type": "Base",
-            "tables": [tables[1].id],
-            "roles": [roles[1].id],
-        }
-        rv = self.client.put(f"/api/v1/rowlevelsecurity/{rls.id}", json=payload)
-        status_code, data = rv.status_code, json.loads(rv.data.decode("utf-8"))
-
-        self.assertEqual(status_code, 201)
-
-        rls = (
-            db.session.query(RowLevelSecurityFilter)
-            .filter(RowLevelSecurityFilter.id == rls.id)
-            .one_or_none()
-        )
-
-        self.assertEqual(rls.name, "rls put success")
-        self.assertEqual(rls.clause, "2=2")
-        self.assertEqual(rls.filter_type, "Base")
-        self.assertEqual(rls.tables[0].id, tables[1].id)
-        self.assertEqual(rls.roles[0].id, roles[1].id)
-
-        db.session.delete(rls)
-        db.session.commit()
-
-
-class TestRowLevelSecurityBulkDeleteAPI(SupersetTestCase):
-    def test_invalid_id_failure(self):
-        self.login("Admin")
-
-        ids_to_delete = prison.dumps([10000, 10001, 100002])
-        rv = self.client.delete(f"/api/v1/rowlevelsecurity/?q={ids_to_delete}")
-        status_code, data = rv.status_code, json.loads(rv.data.decode("utf-8"))
-
-        self.assertEqual(status_code, 404)
-        self.assertEqual(data["message"], "Not found")
-
-    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    @pytest.mark.usefixtures("load_energy_table_with_slice")
-    def test_bulk_delete_success(self):
-        tables = db.session.query(SqlaTable).limit(2).all()
-        roles = db.session.query(security_manager.role_model).limit(2).all()
-
-        rls_1 = RowLevelSecurityFilter(
-            name="rls 1",
-            clause="1=1",
-            filter_type="Regular",
-            tables=[tables[0]],
-            roles=[roles[0]],
-        )
-        rls_2 = RowLevelSecurityFilter(
-            name="rls 2",
-            clause="2=2",
-            filter_type="Base",
-            tables=[tables[1]],
-            roles=[roles[1]],
-        )
-        db.session.add_all([rls_1, rls_2])
-        db.session.commit()
-
-        self.login("Admin")
-
-        ids_to_delete = prison.dumps([rls_1.id, rls_2.id])
-        rv = self.client.delete(f"/api/v1/rowlevelsecurity/?q={ids_to_delete}")
-        status_code, data = rv.status_code, json.loads(rv.data.decode("utf-8"))
-
-        self.assertEqual(status_code, 200)
-        self.assertEqual(data["message"], "Deleted 2 rules")
-
-
-class TestRowLevelSecurityWithRelatedAPI(SupersetTestCase):
-    @pytest.mark.usefixtures("load_birth_names_data")
-    @pytest.mark.usefixtures("load_energy_table_data")
-    def test_rls_tables_related_api(self):
-        self.login("Admin")
-
-        params = prison.dumps({"page": 0, "page_size": 100})
-
-        rv = self.client.get(f"/api/v1/rowlevelsecurity/related/tables?q={params}")
-        self.assertEqual(rv.status_code, 200)
-        data = json.loads(rv.data.decode("utf-8"))
-        result = data["result"]
-
-        db_tables = db.session.query(SqlaTable).all()
-
-        db_table_names = set([t.name for t in db_tables])
-        received_tables = set([table["text"] for table in result])
-
-        assert data["count"] == len(db_tables)
-        assert len(result) == len(db_tables)
-        assert db_table_names == received_tables
-
-    def test_rls_roles_related_api(self):
-        self.login("Admin")
-        params = prison.dumps({"page": 0, "page_size": 100})
-
-        rv = self.client.get(f"/api/v1/rowlevelsecurity/related/roles?q={params}")
-        self.assertEqual(rv.status_code, 200)
-        data = json.loads(rv.data.decode("utf-8"))
-        result = data["result"]
-
-        db_role_names = set([r.name for r in security_manager.get_all_roles()])
-        received_roles = set([role["text"] for role in result])
-
-        assert data["count"] == len(db_role_names)
-        assert len(result) == len(db_role_names)
-        assert db_role_names == received_roles
-
-    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    @pytest.mark.usefixtures("load_energy_table_with_slice")
-    @mock.patch(
-        "superset.row_level_security.api.RLSRestApi.base_related_field_filters",
-        {"tables": [["table_name", filters.FilterStartsWith, "birth"]]},
-    )
-    def test_table_related_filter(self):
-        self.login("Admin")
-
-        params = prison.dumps({"page": 0, "page_size": 10})
-
-        rv = self.client.get(f"/api/v1/rowlevelsecurity/related/tables?q={params}")
-        self.assertEqual(rv.status_code, 200)
-        data = json.loads(rv.data.decode("utf-8"))
-        result = data["result"]
-        received_tables = set([table["text"].split(".")[-1] for table in result])
-
-        assert data["count"] == 1
-        assert len(result) == 1
-        assert {"birth_names"} == received_tables
-
-    @mock.patch(
-        "superset.row_level_security.api.RLSRestApi.base_related_field_filters",
-        {"roles": [["name", filters.FilterEqual, "Admin"]]},
-    )
-    def test_role_related_filter(self):
-        self.login("Admin")
-
-        params = prison.dumps({"page": 0, "page_size": 10})
-
-        rv = self.client.get(f"/api/v1/rowlevelsecurity/related/roles?q={params}")
-        self.assertEqual(rv.status_code, 200)
-        data = json.loads(rv.data.decode("utf-8"))
-        result = data["result"]
-        received_roles = set([role["text"] for role in result])
-
-        assert data["count"] == 1
-        assert len(result) == 1
-        assert {"Admin"} == received_roles
-
-    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    @pytest.mark.usefixtures("load_energy_table_with_slice")
-    @mock.patch(
-        "superset.row_level_security.api.RLSRestApi.base_related_field_filters",
-        {
-            "tables": [["table_name", filters.FilterStartsWith, "birth"]],
-            "roles": [["name", filters.FilterEqual, "Admin"]],
-        },
-    )
-    def test_table_and_role_related_filter(self):
-        self.login("Admin")
-
-        params = prison.dumps({"page": 0, "page_size": 10})
-
-        rv = self.client.get(f"/api/v1/rowlevelsecurity/related/tables?q={params}")
-        self.assertEqual(rv.status_code, 200)
-        data = json.loads(rv.data.decode("utf-8"))
-        result = data["result"]
-        received_tables = set([table["text"].split(".")[-1] for table in result])
-
-        assert data["count"] == 1
-        assert len(result) == 1
-        assert {"birth_names"} == received_tables
-
-        rv = self.client.get(f"/api/v1/rowlevelsecurity/related/roles?q={params}")
-        self.assertEqual(rv.status_code, 200)
-        data = json.loads(rv.data.decode("utf-8"))
-        result = data["result"]
-        received_roles = set([role["text"] for role in result])
-
-        assert data["count"] == 1
-        assert len(result) == 1
-        assert {"Admin"} == received_roles
-
-
 RLS_ALICE_REGEX = re.compile(r"name = 'Alice'")
 RLS_GENDER_REGEX = re.compile(r"AND \(gender = 'girl'\)")