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 <eschutho@gmail.com>

* revisions

* added to CRUD view

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>
This commit is contained in:
AAfghahi 2022-04-11 16:50:59 -04:00 committed by GitHub
parent d693f4e970
commit 4bf4d58423
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 602 additions and 48 deletions

View File

@ -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 = (

View File

@ -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)"`,
);

View File

@ -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);

View File

@ -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();

View File

@ -75,6 +75,7 @@ export interface MenuObjectChildProps {
isFrontendRoute?: boolean;
perm?: string | boolean;
view?: string;
disable?: boolean;
}
export interface MenuObjectProps extends MenuObjectChildProps {

View File

@ -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,
)}

View File

@ -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>

View File

@ -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",

View File

@ -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]
),
)
)

View File

@ -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

View File

@ -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",