You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by aa...@apache.org on 2022/04/11 20:51:08 UTC

[superset] branch master updated: fix: update Permissions for right nav (#19051)

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

aafghahi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 4bf4d58423 fix: update Permissions for right nav (#19051)
4bf4d58423 is described below

commit 4bf4d58423e39c3cf3b592adece41049984ffced
Author: AAfghahi <48...@users.noreply.github.com>
AuthorDate: Mon Apr 11 16:50:59 2022 -0400

    fix: update Permissions for right nav (#19051)
    
    * draft pr
    
    * finished styling
    
    * add filter
    
    * added testing
    
    * added tests
    
    * added permissions tests
    
    * Empty-Commit
    
    * new test
    
    * Update superset-frontend/src/views/components/MenuRight.tsx
    
    Co-authored-by: Elizabeth Thompson <es...@gmail.com>
    
    * revisions
    
    * added to CRUD view
    
    Co-authored-by: Elizabeth Thompson <es...@gmail.com>
---
 .../src/dashboard/util/findPermission.ts           |   2 +-
 .../views/CRUD/data/database/DatabaseList.test.jsx |  36 ++-
 .../src/views/CRUD/data/database/DatabaseList.tsx  |  42 ++-
 .../src/views/components/Menu.test.tsx             |   6 +
 superset-frontend/src/views/components/Menu.tsx    |   1 +
 .../src/views/components/MenuRight.tsx             |  94 +++++-
 superset-frontend/src/views/components/SubMenu.tsx |  25 +-
 superset/databases/api.py                          |  10 +-
 superset/databases/filters.py                      |  73 ++++-
 tests/integration_tests/databases/api_tests.py     | 358 +++++++++++++++++++++
 tests/integration_tests/security_tests.py          |   3 +-
 11 files changed, 602 insertions(+), 48 deletions(-)

diff --git a/superset-frontend/src/dashboard/util/findPermission.ts b/superset-frontend/src/dashboard/util/findPermission.ts
index 8f28a03c99..d3a8b61eca 100644
--- a/superset-frontend/src/dashboard/util/findPermission.ts
+++ b/superset-frontend/src/dashboard/util/findPermission.ts
@@ -36,7 +36,7 @@ export default findPermission;
 // but is hardcoded in backend logic already, so...
 const ADMIN_ROLE_NAME = 'admin';
 
-const isUserAdmin = (user: UserWithPermissionsAndRoles) =>
+export const isUserAdmin = (user: UserWithPermissionsAndRoles) =>
   Object.keys(user.roles).some(role => role.toLowerCase() === ADMIN_ROLE_NAME);
 
 const isUserDashboardOwner = (
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseList.test.jsx b/superset-frontend/src/views/CRUD/data/database/DatabaseList.test.jsx
index fa8721e9e3..5fe6ead7fd 100644
--- a/superset-frontend/src/views/CRUD/data/database/DatabaseList.test.jsx
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseList.test.jsx
@@ -18,7 +18,7 @@
  */
 import React from 'react';
 import thunk from 'redux-thunk';
-import * as redux from 'react-redux';
+import * as reactRedux from 'react-redux';
 import configureStore from 'redux-mock-store';
 import fetchMock from 'fetch-mock';
 import { Provider } from 'react-redux';
@@ -34,6 +34,7 @@ import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
 import { act } from 'react-dom/test-utils';
 
 // store needed for withToasts(DatabaseList)
+
 const mockStore = configureStore([thunk]);
 const store = mockStore({});
 
@@ -63,10 +64,6 @@ jest.mock('react-redux', () => ({
   useSelector: jest.fn(),
 }));
 
-const mockUser = {
-  userId: 1,
-};
-
 fetchMock.get(databasesInfoEndpoint, {
   permissions: ['can_write'],
 });
@@ -91,7 +88,13 @@ fetchMock.get(databaseRelatedEndpoint, {
   },
 });
 
-const useSelectorMock = jest.spyOn(redux, 'useSelector');
+fetchMock.get(
+  'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
+  {},
+);
+
+const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
+const userSelectorMock = jest.spyOn(reactRedux, 'useSelector');
 
 describe('DatabaseList', () => {
   useSelectorMock.mockReturnValue({
@@ -100,10 +103,27 @@ describe('DatabaseList', () => {
     COLUMNAR_EXTENSIONS: ['parquet', 'zip'],
     ALLOWED_EXTENSIONS: ['parquet', 'zip', 'xls', 'xlsx', 'csv'],
   });
+  userSelectorMock.mockReturnValue({
+    createdOn: '2021-04-27T18:12:38.952304',
+    email: 'admin',
+    firstName: 'admin',
+    isActive: true,
+    lastName: 'admin',
+    permissions: {},
+    roles: {
+      Admin: [
+        ['can_sqllab', 'Superset'],
+        ['can_write', 'Dashboard'],
+        ['can_write', 'Chart'],
+      ],
+    },
+    userId: 1,
+    username: 'admin',
+  });
 
   const wrapper = mount(
     <Provider store={store}>
-      <DatabaseList user={mockUser} />
+      <DatabaseList />
     </Provider>,
   );
 
@@ -129,7 +149,7 @@ describe('DatabaseList', () => {
 
   it('fetches Databases', () => {
     const callsD = fetchMock.calls(/database\/\?q/);
-    expect(callsD).toHaveLength(1);
+    expect(callsD).toHaveLength(2);
     expect(callsD[0][0]).toMatchInlineSnapshot(
       `"http://localhost/api/v1/database/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
     );
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
index f980295cc2..df4ef3cf02 100644
--- a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
@@ -17,7 +17,8 @@
  * under the License.
  */
 import { SupersetClient, t, styled } from '@superset-ui/core';
-import React, { useState, useMemo } from 'react';
+import React, { useState, useMemo, useEffect } from 'react';
+import rison from 'rison';
 import { useSelector } from 'react-redux';
 import Loading from 'src/components/Loading';
 import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
@@ -28,6 +29,7 @@ import SubMenu, { SubMenuProps } from 'src/views/components/SubMenu';
 import DeleteModal from 'src/components/DeleteModal';
 import { Tooltip } from 'src/components/Tooltip';
 import Icons from 'src/components/Icons';
+import { isUserAdmin } from 'src/dashboard/util/findPermission';
 import ListView, { FilterOperator, Filters } from 'src/components/ListView';
 import { commonMenuData } from 'src/views/CRUD/data/common';
 import handleResourceExport from 'src/utils/export';
@@ -85,16 +87,22 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
     t('database'),
     addDangerToast,
   );
+  const user = useSelector<any, UserWithPermissionsAndRoles>(
+    state => state.user,
+  );
+
   const [databaseModalOpen, setDatabaseModalOpen] = useState<boolean>(false);
   const [databaseCurrentlyDeleting, setDatabaseCurrentlyDeleting] =
     useState<DatabaseDeleteObject | null>(null);
   const [currentDatabase, setCurrentDatabase] = useState<DatabaseObject | null>(
     null,
   );
+  const [allowUploads, setAllowUploads] = useState<boolean>(false);
+  const isAdmin = isUserAdmin(user);
+  const showUploads = allowUploads || isAdmin;
+
   const [preparingExport, setPreparingExport] = useState<boolean>(false);
-  const { roles } = useSelector<any, UserWithPermissionsAndRoles>(
-    state => state.user,
-  );
+  const { roles } = user;
   const {
     CSV_EXTENSIONS,
     COLUMNAR_EXTENSIONS,
@@ -163,6 +171,8 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
     ALLOWED_EXTENSIONS,
   );
 
+  const isDisabled = isAdmin && !allowUploads;
+
   const uploadDropdownMenu = [
     {
       label: t('Upload file to database'),
@@ -171,24 +181,42 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
           label: t('Upload CSV'),
           name: 'Upload CSV file',
           url: '/csvtodatabaseview/form',
-          perm: canUploadCSV,
+          perm: canUploadCSV && showUploads,
+          disable: isDisabled,
         },
         {
           label: t('Upload columnar file'),
           name: 'Upload columnar file',
           url: '/columnartodatabaseview/form',
-          perm: canUploadColumnar,
+          perm: canUploadColumnar && showUploads,
+          disable: isDisabled,
         },
         {
           label: t('Upload Excel file'),
           name: 'Upload Excel file',
           url: '/exceltodatabaseview/form',
-          perm: canUploadExcel,
+          perm: canUploadExcel && showUploads,
+          disable: isDisabled,
         },
       ],
     },
   ];
 
+  const hasFileUploadEnabled = () => {
+    const payload = {
+      filters: [
+        { col: 'allow_file_upload', opr: 'upload_is_enabled', value: true },
+      ],
+    };
+    SupersetClient.get({
+      endpoint: `/api/v1/database/?q=${rison.encode(payload)}`,
+    }).then(({ json }: Record<string, any>) => {
+      setAllowUploads(json.count >= 1);
+    });
+  };
+
+  useEffect(() => hasFileUploadEnabled(), [databaseModalOpen]);
+
   const filteredDropDown = uploadDropdownMenu.map(link => {
     // eslint-disable-next-line no-param-reassign
     link.childs = link.childs.filter(item => item.perm);
diff --git a/superset-frontend/src/views/components/Menu.test.tsx b/superset-frontend/src/views/components/Menu.test.tsx
index d13275fbc0..a80a43a22f 100644
--- a/superset-frontend/src/views/components/Menu.test.tsx
+++ b/superset-frontend/src/views/components/Menu.test.tsx
@@ -18,6 +18,7 @@
  */
 import React from 'react';
 import * as reactRedux from 'react-redux';
+import fetchMock from 'fetch-mock';
 import { render, screen } from 'spec/helpers/testing-library';
 import userEvent from '@testing-library/user-event';
 import { Menu } from './Menu';
@@ -235,6 +236,11 @@ const notanonProps = {
 
 const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
 
+fetchMock.get(
+  'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
+  {},
+);
+
 beforeEach(() => {
   // setup a DOM element as a render target
   useSelectorMock.mockClear();
diff --git a/superset-frontend/src/views/components/Menu.tsx b/superset-frontend/src/views/components/Menu.tsx
index 55d929e7b7..d26742096a 100644
--- a/superset-frontend/src/views/components/Menu.tsx
+++ b/superset-frontend/src/views/components/Menu.tsx
@@ -75,6 +75,7 @@ export interface MenuObjectChildProps {
   isFrontendRoute?: boolean;
   perm?: string | boolean;
   view?: string;
+  disable?: boolean;
 }
 
 export interface MenuObjectProps extends MenuObjectChildProps {
diff --git a/superset-frontend/src/views/components/MenuRight.tsx b/superset-frontend/src/views/components/MenuRight.tsx
index 4628a47e24..9d657aa9b7 100644
--- a/superset-frontend/src/views/components/MenuRight.tsx
+++ b/superset-frontend/src/views/components/MenuRight.tsx
@@ -16,12 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { Fragment, useState } from 'react';
+import React, { Fragment, useState, useEffect } from 'react';
+import rison from 'rison';
 import { MainNav as Menu } from 'src/components/Menu';
-import { t, styled, css, SupersetTheme } from '@superset-ui/core';
+import {
+  t,
+  styled,
+  css,
+  SupersetTheme,
+  SupersetClient,
+} from '@superset-ui/core';
+import { Tooltip } from 'src/components/Tooltip';
 import { Link } from 'react-router-dom';
 import Icons from 'src/components/Icons';
-import findPermission from 'src/dashboard/util/findPermission';
+import findPermission, { isUserAdmin } from 'src/dashboard/util/findPermission';
 import { useSelector } from 'react-redux';
 import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
 import LanguagePicker from './LanguagePicker';
@@ -45,6 +53,15 @@ const StyledI = styled.div`
   color: ${({ theme }) => theme.colors.primary.dark1};
 `;
 
+const styledDisabled = (theme: SupersetTheme) => css`
+  color: ${theme.colors.grayscale.base};
+  backgroundColor: ${theme.colors.grayscale.light2}};
+  .ant-menu-item:hover {
+    color: ${theme.colors.grayscale.base};
+    cursor: default;
+  }
+`;
+
 const StyledDiv = styled.div<{ align: string }>`
   display: flex;
   flex-direction: row;
@@ -69,9 +86,11 @@ const RightMenu = ({
   navbarRight,
   isFrontendRoute,
 }: RightMenuProps) => {
-  const { roles } = useSelector<any, UserWithPermissionsAndRoles>(
+  const user = useSelector<any, UserWithPermissionsAndRoles>(
     state => state.user,
   );
+
+  const { roles } = user;
   const {
     CSV_EXTENSIONS,
     COLUMNAR_EXTENSIONS,
@@ -96,6 +115,9 @@ const RightMenu = ({
 
   const canUpload = canUploadCSV || canUploadColumnar || canUploadExcel;
   const showActionDropdown = canSql || canChart || canDashboard;
+  const [allowUploads, setAllowUploads] = useState<boolean>(false);
+  const isAdmin = isUserAdmin(user);
+  const showUploads = allowUploads || isAdmin;
   const dropdownItems: MenuObjectProps[] = [
     {
       label: t('Data'),
@@ -115,19 +137,19 @@ const RightMenu = ({
           label: t('Upload CSV to database'),
           name: 'Upload a CSV',
           url: '/csvtodatabaseview/form',
-          perm: canUploadCSV,
+          perm: CSV_EXTENSIONS && showUploads,
         },
         {
           label: t('Upload columnar file to database'),
           name: 'Upload a Columnar file',
           url: '/columnartodatabaseview/form',
-          perm: canUploadColumnar,
+          perm: COLUMNAR_EXTENSIONS && showUploads,
         },
         {
           label: t('Upload Excel file to database'),
           name: 'Upload Excel',
           url: '/exceltodatabaseview/form',
-          perm: canUploadExcel,
+          perm: EXCEL_EXTENSIONS && showUploads,
         },
       ],
     },
@@ -154,6 +176,21 @@ const RightMenu = ({
     },
   ];
 
+  const hasFileUploadEnabled = () => {
+    const payload = {
+      filters: [
+        { col: 'allow_file_upload', opr: 'upload_is_enabled', value: true },
+      ],
+    };
+    SupersetClient.get({
+      endpoint: `/api/v1/database/?q=${rison.encode(payload)}`,
+    }).then(({ json }: Record<string, any>) => {
+      setAllowUploads(json.count >= 1);
+    });
+  };
+
+  useEffect(() => hasFileUploadEnabled(), []);
+
   const menuIconAndLabel = (menu: MenuObjectProps) => (
     <>
       <i data-test={`menu-item-${menu.label}`} className={`fa ${menu.icon}`} />
@@ -175,6 +212,34 @@ const RightMenu = ({
     setShowModal(false);
   };
 
+  const isDisabled = isAdmin && !allowUploads;
+
+  const tooltipText = t(
+    "Enable 'Allow data upload' in any database's settings",
+  );
+
+  const buildMenuItem = (item: Record<string, any>) => {
+    const disabledText = isDisabled && item.url;
+    return disabledText ? (
+      <Menu.Item key={item.name} css={styledDisabled}>
+        <Tooltip placement="top" title={tooltipText}>
+          {item.label}
+        </Tooltip>
+      </Menu.Item>
+    ) : (
+      <Menu.Item key={item.name}>
+        {item.url ? <a href={item.url}> {item.label} </a> : item.label}
+      </Menu.Item>
+    );
+  };
+
+  const onMenuOpen = (openKeys: string[]) => {
+    if (openKeys.length) {
+      return hasFileUploadEnabled();
+    }
+    return null;
+  };
+
   return (
     <StyledDiv align={align}>
       <DatabaseModal
@@ -182,7 +247,12 @@ const RightMenu = ({
         show={showModal}
         dbEngine={engine}
       />
-      <Menu selectable={false} mode="horizontal" onClick={handleMenuSelection}>
+      <Menu
+        selectable={false}
+        mode="horizontal"
+        onClick={handleMenuSelection}
+        onOpenChange={onMenuOpen}
+      >
         {!navbarRight.user_is_anonymous && showActionDropdown && (
           <SubMenu
             data-test="new-dropdown"
@@ -203,13 +273,7 @@ const RightMenu = ({
                       typeof item !== 'string' && item.name && item.perm ? (
                         <Fragment key={item.name}>
                           {idx === 2 && <Menu.Divider />}
-                          <Menu.Item key={item.name}>
-                            {item.url ? (
-                              <a href={item.url}> {item.label} </a>
-                            ) : (
-                              item.label
-                            )}
-                          </Menu.Item>
+                          {buildMenuItem(item)}
                         </Fragment>
                       ) : null,
                     )}
diff --git a/superset-frontend/src/views/components/SubMenu.tsx b/superset-frontend/src/views/components/SubMenu.tsx
index 4ad3cfe42e..9dc5b41cdc 100644
--- a/superset-frontend/src/views/components/SubMenu.tsx
+++ b/superset-frontend/src/views/components/SubMenu.tsx
@@ -18,8 +18,9 @@
  */
 import React, { ReactNode, useState, useEffect } from 'react';
 import { Link, useHistory } from 'react-router-dom';
-import { styled } from '@superset-ui/core';
+import { styled, SupersetTheme, css, t } from '@superset-ui/core';
 import cx from 'classnames';
+import { Tooltip } from 'src/components/Tooltip';
 import { debounce } from 'lodash';
 import { Row } from 'src/components';
 import { Menu, MenuMode, MainNav as DropdownMenu } from 'src/components/Menu';
@@ -144,6 +145,15 @@ const StyledHeader = styled.div`
   }
 `;
 
+const styledDisabled = (theme: SupersetTheme) => css`
+  color: ${theme.colors.grayscale.base};
+  backgroundColor: ${theme.colors.grayscale.light2}};
+  .ant-menu-item:hover {
+    color: ${theme.colors.grayscale.base};
+    cursor: default;
+  }
+`;
+
 type MenuChild = {
   label: string;
   name: string;
@@ -271,7 +281,18 @@ const SubMenuComponent: React.FunctionComponent<SubMenuProps> = props => {
               >
                 {link.childs?.map(item => {
                   if (typeof item === 'object') {
-                    return (
+                    return item.disable ? (
+                      <DropdownMenu.Item key={item.label} css={styledDisabled}>
+                        <Tooltip
+                          placement="top"
+                          title={t(
+                            "Enable 'Allow data upload' in any database's settings",
+                          )}
+                        >
+                          {item.label}
+                        </Tooltip>
+                      </DropdownMenu.Item>
+                    ) : (
                       <DropdownMenu.Item key={item.label}>
                         <a href={item.url}>{item.label}</a>
                       </DropdownMenu.Item>
diff --git a/superset/databases/api.py b/superset/databases/api.py
index 0de8bcf83e..63fcecedc4 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -51,7 +51,7 @@ from superset.databases.commands.update import UpdateDatabaseCommand
 from superset.databases.commands.validate import ValidateDatabaseParametersCommand
 from superset.databases.dao import DatabaseDAO
 from superset.databases.decorators import check_datasource_access
-from superset.databases.filters import DatabaseFilter
+from superset.databases.filters import DatabaseFilter, DatabaseUploadEnabledFilter
 from superset.databases.schemas import (
     database_schemas_query_schema,
     DatabaseFunctionNamesResponse,
@@ -166,8 +166,16 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         "encrypted_extra",
         "server_cert",
     ]
+
     edit_columns = add_columns
 
+    search_columns = ["allow_file_upload", "expose_in_sqllab"]
+
+    search_filters = {
+        "allow_file_upload": [DatabaseUploadEnabledFilter],
+        "expose_in_sqllab": [DatabaseFilter],
+    }
+
     list_select_columns = list_columns + ["extra", "sqlalchemy_uri", "password"]
     order_columns = [
         "allow_file_upload",
diff --git a/superset/databases/filters.py b/superset/databases/filters.py
index bd0729767e..228abbc3bf 100644
--- a/superset/databases/filters.py
+++ b/superset/databases/filters.py
@@ -16,32 +16,37 @@
 # under the License.
 from typing import Any, Set
 
+from flask import g
+from flask_babel import lazy_gettext as _
 from sqlalchemy import or_
 from sqlalchemy.orm import Query
+from sqlalchemy.sql.expression import cast
+from sqlalchemy.sql.sqltypes import JSON
 
-from superset import security_manager
+from superset import app, security_manager
+from superset.models.core import Database
 from superset.views.base import BaseFilter
 
 
-class DatabaseFilter(BaseFilter):
-    # TODO(bogdan): consider caching.
+def can_access_databases(
+    view_menu_name: str,
+) -> Set[str]:
+    return {
+        security_manager.unpack_database_and_schema(vm).database
+        for vm in security_manager.user_view_menu_names(view_menu_name)
+    }
+
 
-    def can_access_databases(  # noqa pylint: disable=no-self-use
-        self,
-        view_menu_name: str,
-    ) -> Set[str]:
-        return {
-            security_manager.unpack_database_and_schema(vm).database
-            for vm in security_manager.user_view_menu_names(view_menu_name)
-        }
+class DatabaseFilter(BaseFilter):  # pylint: disable=too-few-public-methods
+    # TODO(bogdan): consider caching.
 
     def apply(self, query: Query, value: Any) -> Query:
         if security_manager.can_access_all_databases():
             return query
         database_perms = security_manager.user_view_menu_names("database_access")
-        schema_access_databases = self.can_access_databases("schema_access")
+        schema_access_databases = can_access_databases("schema_access")
 
-        datasource_access_databases = self.can_access_databases("datasource_access")
+        datasource_access_databases = can_access_databases("datasource_access")
 
         return query.filter(
             or_(
@@ -51,3 +56,45 @@ class DatabaseFilter(BaseFilter):
                 ),
             )
         )
+
+
+class DatabaseUploadEnabledFilter(BaseFilter):  # pylint: disable=too-few-public-methods
+    """
+    Custom filter for the GET list that filters all databases based on allow_file_upload
+    """
+
+    name = _("Upload Enabled")
+    arg_name = "upload_is_enabled"
+
+    def apply(self, query: Query, value: Any) -> Query:
+        filtered_query = query.filter(Database.allow_file_upload)
+
+        database_perms = security_manager.user_view_menu_names("database_access")
+        schema_access_databases = can_access_databases("schema_access")
+        datasource_access_databases = can_access_databases("datasource_access")
+
+        if hasattr(g, "user"):
+            allowed_schemas = [
+                app.config["ALLOWED_USER_CSV_SCHEMA_FUNC"](db, g.user)
+                for db in datasource_access_databases
+            ]
+
+            if len(allowed_schemas):
+                return filtered_query
+
+        filtered_query = filtered_query.filter(
+            or_(
+                cast(Database.extra, JSON)["schemas_allowed_for_file_upload"]
+                is not None,
+                cast(Database.extra, JSON)["schemas_allowed_for_file_upload"] != [],
+            )
+        )
+
+        return filtered_query.filter(
+            or_(
+                self.model.perm.in_(database_perms),
+                self.model.database_name.in_(
+                    [*schema_access_databases, *datasource_access_databases]
+                ),
+            )
+        )
diff --git a/tests/integration_tests/databases/api_tests.py b/tests/integration_tests/databases/api_tests.py
index 4f29600bda..0c1dc27538 100644
--- a/tests/integration_tests/databases/api_tests.py
+++ b/tests/integration_tests/databases/api_tests.py
@@ -80,6 +80,7 @@ class TestDatabaseApi(SupersetTestCase):
         encrypted_extra: str = "",
         server_cert: str = "",
         expose_in_sqllab: bool = False,
+        allow_file_upload: bool = False,
     ) -> Database:
         database = Database(
             database_name=database_name,
@@ -88,6 +89,7 @@ class TestDatabaseApi(SupersetTestCase):
             encrypted_extra=encrypted_extra,
             server_cert=server_cert,
             expose_in_sqllab=expose_in_sqllab,
+            allow_file_upload=allow_file_upload,
         )
         db.session.add(database)
         db.session.commit()
@@ -864,6 +866,362 @@ class TestDatabaseApi(SupersetTestCase):
         # TODO(bkyryliuk): investigate why presto returns 500
         self.assertEqual(rv.status_code, 404 if example_db.backend != "presto" else 500)
 
+    def test_get_allow_file_upload_filter(self):
+        """
+        Database API: Test filter for allow file upload checks for schemas
+        """
+        with self.create_app().app_context():
+            example_db = get_example_database()
+
+            extra = {
+                "metadata_params": {},
+                "engine_params": {},
+                "metadata_cache_timeout": {},
+                "schemas_allowed_for_file_upload": ["public"],
+            }
+            self.login(username="admin")
+            database = self.insert_database(
+                "database_with_upload",
+                example_db.sqlalchemy_uri_decrypted,
+                extra=json.dumps(extra),
+                allow_file_upload=True,
+            )
+            db.session.commit()
+            yield database
+
+            arguments = {
+                "columns": ["allow_file_upload"],
+                "filters": [
+                    {
+                        "col": "allow_file_upload",
+                        "opr": "upload_is_enabled",
+                        "value": True,
+                    }
+                ],
+            }
+            uri = f"api/v1/database/?q={prison.dumps(arguments)}"
+            rv = self.client.get(uri)
+            data = json.loads(rv.data.decode("utf-8"))
+            assert data["count"] == 1
+            db.session.delete(database)
+            db.session.commit()
+
+    def test_get_allow_file_upload_filter_no_schema(self):
+        """
+        Database API: Test filter for allow file upload checks for schemas.
+        This test has allow_file_upload but no schemas.
+        """
+        with self.create_app().app_context():
+            example_db = get_example_database()
+
+            extra = {
+                "metadata_params": {},
+                "engine_params": {},
+                "metadata_cache_timeout": {},
+                "schemas_allowed_for_file_upload": [],
+            }
+            self.login(username="admin")
+            database = self.insert_database(
+                "database_with_upload",
+                example_db.sqlalchemy_uri_decrypted,
+                extra=json.dumps(extra),
+                allow_file_upload=True,
+            )
+            db.session.commit()
+            yield database
+
+            arguments = {
+                "columns": ["allow_file_upload"],
+                "filters": [
+                    {
+                        "col": "allow_file_upload",
+                        "opr": "upload_is_enabled",
+                        "value": True,
+                    }
+                ],
+            }
+            uri = f"api/v1/database/?q={prison.dumps(arguments)}"
+            rv = self.client.get(uri)
+            data = json.loads(rv.data.decode("utf-8"))
+            assert data["count"] == 0
+            db.session.delete(database)
+            db.session.commit()
+
+    def test_get_allow_file_upload_filter_allow_file_false(self):
+        """
+        Database API: Test filter for allow file upload checks for schemas.
+        This has a schema but does not allow_file_upload
+        """
+        with self.create_app().app_context():
+            example_db = get_example_database()
+
+            extra = {
+                "metadata_params": {},
+                "engine_params": {},
+                "metadata_cache_timeout": {},
+                "schemas_allowed_for_file_upload": ["public"],
+            }
+            self.login(username="admin")
+            database = self.insert_database(
+                "database_with_upload",
+                example_db.sqlalchemy_uri_decrypted,
+                extra=json.dumps(extra),
+                allow_file_upload=False,
+            )
+            db.session.commit()
+            yield database
+
+            arguments = {
+                "columns": ["allow_file_upload"],
+                "filters": [
+                    {
+                        "col": "allow_file_upload",
+                        "opr": "upload_is_enabled",
+                        "value": True,
+                    }
+                ],
+            }
+            uri = f"api/v1/database/?q={prison.dumps(arguments)}"
+            rv = self.client.get(uri)
+            data = json.loads(rv.data.decode("utf-8"))
+            assert data["count"] == 0
+            db.session.delete(database)
+            db.session.commit()
+
+    def test_get_allow_file_upload_false(self):
+        """
+        Database API: Test filter for allow file upload checks for schemas.
+        Both databases have false allow_file_upload
+        """
+        with self.create_app().app_context():
+            example_db = get_example_database()
+
+            extra = {
+                "metadata_params": {},
+                "engine_params": {},
+                "metadata_cache_timeout": {},
+                "schemas_allowed_for_file_upload": [],
+            }
+            self.login(username="admin")
+            database = self.insert_database(
+                "database_with_upload",
+                example_db.sqlalchemy_uri_decrypted,
+                extra=json.dumps(extra),
+                allow_file_upload=False,
+            )
+            db.session.commit()
+            yield database
+            arguments = {
+                "columns": ["allow_file_upload"],
+                "filters": [
+                    {
+                        "col": "allow_file_upload",
+                        "opr": "upload_is_enabled",
+                        "value": True,
+                    }
+                ],
+            }
+            uri = f"api/v1/database/?q={prison.dumps(arguments)}"
+            rv = self.client.get(uri)
+            data = json.loads(rv.data.decode("utf-8"))
+            assert data["count"] == 0
+            db.session.delete(database)
+            db.session.commit()
+
+    def test_get_allow_file_upload_false_no_extra(self):
+        """
+        Database API: Test filter for allow file upload checks for schemas.
+        Both databases have false allow_file_upload
+        """
+        with self.create_app().app_context():
+            example_db = get_example_database()
+
+            self.login(username="admin")
+            database = self.insert_database(
+                "database_with_upload",
+                example_db.sqlalchemy_uri_decrypted,
+                allow_file_upload=False,
+            )
+            db.session.commit()
+            yield database
+            arguments = {
+                "columns": ["allow_file_upload"],
+                "filters": [
+                    {
+                        "col": "allow_file_upload",
+                        "opr": "upload_is_enabled",
+                        "value": True,
+                    }
+                ],
+            }
+            uri = f"api/v1/database/?q={prison.dumps(arguments)}"
+            rv = self.client.get(uri)
+            data = json.loads(rv.data.decode("utf-8"))
+            assert data["count"] == 0
+            db.session.delete(database)
+            db.session.commit()
+
+    def mock_csv_function(d, user):
+        return d.get_all_schema_names()
+
+    @mock.patch(
+        "superset.views.core.app.config",
+        {**app.config, "ALLOWED_USER_CSV_SCHEMA_FUNC": mock_csv_function},
+    )
+    def test_get_allow_file_upload_true_csv(self):
+        """
+        Database API: Test filter for allow file upload checks for schemas.
+        Both databases have false allow_file_upload
+        """
+        with self.create_app().app_context():
+            example_db = get_example_database()
+
+            extra = {
+                "metadata_params": {},
+                "engine_params": {},
+                "metadata_cache_timeout": {},
+                "schemas_allowed_for_file_upload": [],
+            }
+            self.login(username="admin")
+            database = self.insert_database(
+                "database_with_upload",
+                example_db.sqlalchemy_uri_decrypted,
+                extra=json.dumps(extra),
+                allow_file_upload=True,
+            )
+            db.session.commit()
+            yield database
+            arguments = {
+                "columns": ["allow_file_upload"],
+                "filters": [
+                    {
+                        "col": "allow_file_upload",
+                        "opr": "upload_is_enabled",
+                        "value": True,
+                    }
+                ],
+            }
+            uri = f"api/v1/database/?q={prison.dumps(arguments)}"
+            rv = self.client.get(uri)
+            data = json.loads(rv.data.decode("utf-8"))
+            assert data["count"] == 1
+            db.session.delete(database)
+            db.session.commit()
+
+    def mock_empty_csv_function(d, user):
+        return []
+
+    @mock.patch(
+        "superset.views.core.app.config",
+        {**app.config, "ALLOWED_USER_CSV_SCHEMA_FUNC": mock_empty_csv_function},
+    )
+    def test_get_allow_file_upload_false_csv(self):
+        """
+        Database API: Test filter for allow file upload checks for schemas.
+        Both databases have false allow_file_upload
+        """
+        with self.create_app().app_context():
+            self.login(username="admin")
+            arguments = {
+                "columns": ["allow_file_upload"],
+                "filters": [
+                    {
+                        "col": "allow_file_upload",
+                        "opr": "upload_is_enabled",
+                        "value": True,
+                    }
+                ],
+            }
+            uri = f"api/v1/database/?q={prison.dumps(arguments)}"
+            rv = self.client.get(uri)
+            data = json.loads(rv.data.decode("utf-8"))
+            assert data["count"] == 0
+
+    def test_get_allow_file_upload_filter_no_permission(self):
+        """
+        Database API: Test filter for allow file upload checks for schemas
+        """
+        with self.create_app().app_context():
+            example_db = get_example_database()
+
+            extra = {
+                "metadata_params": {},
+                "engine_params": {},
+                "metadata_cache_timeout": {},
+                "schemas_allowed_for_file_upload": ["public"],
+            }
+            self.login(username="gamma")
+            database = self.insert_database(
+                "database_with_upload",
+                example_db.sqlalchemy_uri_decrypted,
+                extra=json.dumps(extra),
+                allow_file_upload=True,
+            )
+            db.session.commit()
+            yield database
+
+            arguments = {
+                "columns": ["allow_file_upload"],
+                "filters": [
+                    {
+                        "col": "allow_file_upload",
+                        "opr": "upload_is_enabled",
+                        "value": True,
+                    }
+                ],
+            }
+            uri = f"api/v1/database/?q={prison.dumps(arguments)}"
+            rv = self.client.get(uri)
+            data = json.loads(rv.data.decode("utf-8"))
+            assert data["count"] == 0
+            db.session.delete(database)
+            db.session.commit()
+
+    def test_get_allow_file_upload_filter_with_permission(self):
+        """
+        Database API: Test filter for allow file upload checks for schemas
+        """
+        with self.create_app().app_context():
+            main_db = get_main_database()
+            main_db.allow_file_upload = True
+            session = db.session
+            table = SqlaTable(
+                schema="public",
+                table_name="ab_permission",
+                database=get_main_database(),
+            )
+
+            session.add(table)
+            session.commit()
+            tmp_table_perm = security_manager.find_permission_view_menu(
+                "datasource_access", table.get_perm()
+            )
+            gamma_role = security_manager.find_role("Gamma")
+            security_manager.add_permission_role(gamma_role, tmp_table_perm)
+
+            self.login(username="gamma")
+
+            arguments = {
+                "columns": ["allow_file_upload"],
+                "filters": [
+                    {
+                        "col": "allow_file_upload",
+                        "opr": "upload_is_enabled",
+                        "value": True,
+                    }
+                ],
+            }
+            uri = f"api/v1/database/?q={prison.dumps(arguments)}"
+            rv = self.client.get(uri)
+            data = json.loads(rv.data.decode("utf-8"))
+            assert data["count"] == 1
+
+            # rollback changes
+            security_manager.del_permission_role(gamma_role, tmp_table_perm)
+            db.session.delete(table)
+            db.session.delete(main_db)
+            db.session.commit()
+
     def test_database_schemas(self):
         """
         Database API: Test database schemas
diff --git a/tests/integration_tests/security_tests.py b/tests/integration_tests/security_tests.py
index 0b3a8b1d82..82b4d8717d 100644
--- a/tests/integration_tests/security_tests.py
+++ b/tests/integration_tests/security_tests.py
@@ -595,15 +595,16 @@ class TestRolePermission(SupersetTestCase):
         for pvm in current_app.config["FAB_ROLES"]["TestRole"]:
             assert pvm in public_role_resource_names
 
+    @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
     def test_sqllab_gamma_user_schema_access_to_sqllab(self):
         session = db.session
-
         example_db = session.query(Database).filter_by(database_name="examples").one()
         example_db.expose_in_sqllab = True
         session.commit()
 
         arguments = {
             "keys": ["none"],
+            "columns": ["expose_in_sqllab"],
             "filters": [{"col": "expose_in_sqllab", "opr": "eq", "value": True}],
             "order_columns": "database_name",
             "order_direction": "asc",