Skip to content

Commit

Permalink
fix: Alpha should not be able to edit datasets that they don't own (#…
Browse files Browse the repository at this point in the history
…19854)

* fix api for checking owners

* fix styles for disabling

* fix styles for disabling

* fix lint

* fix lint

* add owners key

* plzz

* remove

* update test

* add tooltip

* add type

* fix test

* fix user reference

* lit

* fix test

* work

(cherry picked from commit 8b15b68)
  • Loading branch information
hughhhh authored and michael-s-molina committed May 26, 2022
1 parent 94d4179 commit 2bd89d1
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 30 deletions.
Expand Up @@ -91,6 +91,11 @@ export const PRIMARY_COLOR = { r: 0, g: 122, b: 135, a: 1 };
const ROW_LIMIT_OPTIONS = [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000];
const SERIES_LIMITS = [5, 10, 25, 50, 100, 500];

const appContainer = document.getElementById('app');
const { user } = JSON.parse(
appContainer?.getAttribute('data-bootstrap') || '{}',
);

type Control = {
savedMetrics?: Metric[] | null;
default?: unknown;
Expand Down Expand Up @@ -167,6 +172,7 @@ const datasourceControl: SharedControlConfig<'DatasourceControl'> = {
mapStateToProps: ({ datasource, form_data }) => ({
datasource,
form_data,
user,
}),
};

Expand Down
Expand Up @@ -47,7 +47,22 @@ const datasource = {
main_dttm_col: 'None',
datasource_name: 'table1',
description: 'desc',
owners: [{ username: 'admin', userId: 1 }],
};

const mockUser = {
createdOn: '2021-04-27T18:12:38.952304',
email: 'admin',
firstName: 'admin',
isActive: true,
lastName: 'admin',
permissions: {},
roles: { Admin: Array(173) },
userId: 1,
username: 'admin',
isAnonymous: false,
};

const props: DatasourcePanelProps = {
datasource,
controls: {
Expand All @@ -57,6 +72,7 @@ const props: DatasourcePanelProps = {
type: DatasourceControl,
label: 'hello',
datasource,
user: mockUser,
},
},
actions: {
Expand Down Expand Up @@ -154,6 +170,7 @@ test('should render a warning', async () => {
datasource: {
...props.controls.datasource,
datasource: deprecatedDatasource,
user: mockUser,
},
},
}),
Expand Down
Expand Up @@ -31,12 +31,14 @@ import { FAST_DEBOUNCE } from 'src/constants';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { ExploreActions } from 'src/explore/actions/exploreActions';
import Control from 'src/explore/components/Control';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import DatasourcePanelDragOption from './DatasourcePanelDragOption';
import { DndItemType } from '../DndItemType';
import { StyledColumnOption, StyledMetricOption } from '../optionRenderers';

interface DatasourceControl extends ControlConfig {
datasource?: DatasourceMeta;
user: UserWithPermissionsAndRoles;
}

export interface Props {
Expand Down
Expand Up @@ -41,6 +41,7 @@ const defaultProps = {
id: 1,
columns: [],
metrics: [],
owners: [{ username: 'admin', userId: 1 }],
database: {
backend: 'mysql',
name: 'main',
Expand All @@ -51,6 +52,17 @@ const defaultProps = {
setDatasource: sinon.spy(),
},
onChange: sinon.spy(),
user: {
createdOn: '2021-04-27T18:12:38.952304',
email: 'admin',
firstName: 'admin',
isActive: true,
lastName: 'admin',
permissions: {},
roles: { Admin: Array(173) },
userId: 1,
username: 'admin',
},
};

describe('DatasourceControl', () => {
Expand Down Expand Up @@ -107,6 +119,7 @@ describe('DatasourceControl', () => {
id: 1,
columns: [],
metrics: [],
owners: [{ username: 'admin', userId: 1 }],
database: {
backend: 'druid',
name: 'main',
Expand Down
Expand Up @@ -48,6 +48,17 @@ const createProps = () => ({
name: 'datasource',
actions: {},
isEditable: true,
user: {
createdOn: '2021-04-27T18:12:38.952304',
email: 'admin',
firstName: 'admin',
isActive: true,
lastName: 'admin',
permissions: {},
roles: { Admin: Array(173) },
userId: 1,
username: 'admin',
},
onChange: jest.fn(),
onDatasourceSave: jest.fn(),
});
Expand Down
Expand Up @@ -35,6 +35,7 @@ import Button from 'src/components/Button';
import ErrorAlert from 'src/components/ErrorMessage/ErrorAlert';
import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip';
import { URL_PARAMS } from 'src/constants';
import { isUserAdmin } from 'src/dashboard/util/findPermission';

const propTypes = {
actions: PropTypes.object.isRequired,
Expand Down Expand Up @@ -195,12 +196,32 @@ class DatasourceControl extends React.PureComponent {
}

const isSqlSupported = datasource.type === 'table';
const { user } = this.props;
const allowEdit =
datasource.owners.map(o => o.id).includes(user.userId) ||
isUserAdmin(user);

const editText = t('Edit dataset');

const datasourceMenu = (
<Menu onClick={this.handleMenuItemClick}>
{this.props.isEditable && (
<Menu.Item key={EDIT_DATASET} data-test="edit-dataset">
{t('Edit dataset')}
<Menu.Item
key={EDIT_DATASET}
data-test="edit-dataset"
disabled={!allowEdit}
>
{!allowEdit ? (
<Tooltip
title={t(
'You must be a dataset owner in order to edit. Please reach out to a dataset owner to request modifications or edit access.',
)}
>
{editText}
</Tooltip>
) : (
editText
)}
</Menu.Item>
)}
<Menu.Item key={CHANGE_DATASET}>{t('Change dataset')}</Menu.Item>
Expand Down
Expand Up @@ -55,6 +55,7 @@ const mockdatasets = [...new Array(3)].map((_, i) => ({
id: i,
schema: `schema ${i}`,
table_name: `coolest table ${i}`,
owners: [{ username: 'admin', userId: 1 }],
}));

const mockUser = {
Expand Down
40 changes: 36 additions & 4 deletions superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
Expand Up @@ -56,7 +56,9 @@ import InfoTooltip from 'src/components/InfoTooltip';
import ImportModelsModal from 'src/components/ImportModal/index';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip';
import { isUserAdmin } from 'src/dashboard/util/findPermission';
import AddDatasetModal from './AddDatasetModal';

import {
PAGE_SIZE,
SORT_BY,
Expand All @@ -75,6 +77,25 @@ const FlexRowContainer = styled.div`

const Actions = styled.div`
color: ${({ theme }) => theme.colors.grayscale.base};
.disabled {
svg,
i {
&:hover {
path {
fill: ${({ theme }) => theme.colors.grayscale.light1};
}
}
}
color: ${({ theme }) => theme.colors.grayscale.light1};
.ant-menu-item:hover {
color: ${({ theme }) => theme.colors.grayscale.light1};
cursor: default;
}
&::after {
color: ${({ theme }) => theme.colors.grayscale.light1};
}
}
`;

type Dataset = {
Expand Down Expand Up @@ -344,6 +365,11 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
},
{
Cell: ({ row: { original } }: any) => {
// Verify owner or isAdmin
const allowEdit =
original.owners.map((o: Owner) => o.id).includes(user.userId) ||
isUserAdmin(user);

const handleEdit = () => openDatasetEditModal(original);
const handleDelete = () => openDatasetDeleteModal(original);
const handleExport = () => handleBulkDatasetExport([original]);
Expand Down Expand Up @@ -387,14 +413,20 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
{canEdit && (
<Tooltip
id="edit-action-tooltip"
title={t('Edit')}
placement="bottom"
title={
allowEdit
? t('Edit')
: t(
'You must be a dataset owner in order to edit. Please reach out to a dataset owner to request modifications or edit access.',
)
}
placement="bottomRight"
>
<span
role="button"
tabIndex={0}
className="action-button"
onClick={handleEdit}
className={allowEdit ? 'action-button' : 'disabled'}
onClick={allowEdit ? handleEdit : undefined}
>
<Icons.EditAlt />
</span>
Expand Down
34 changes: 10 additions & 24 deletions superset/views/datasource/views.py
Expand Up @@ -27,7 +27,7 @@
from sqlalchemy.exc import NoSuchTableError
from sqlalchemy.orm.exc import NoResultFound

from superset import app, db, event_logger
from superset import db, event_logger
from superset.commands.utils import populate_owners
from superset.connectors.connector_registry import ConnectorRegistry
from superset.connectors.sqla.utils import get_physical_table_metadata
Expand Down Expand Up @@ -81,29 +81,15 @@ def save(self) -> FlaskResponse:

if "owners" in datasource_dict and orm_datasource.owner_class is not None:
# Check ownership
if app.config.get("OLD_API_CHECK_DATASET_OWNERSHIP"):
# mimic the behavior of the new dataset command that
# checks ownership and ensures that non-admins aren't locked out
# of the object
try:
check_ownership(orm_datasource)
except SupersetSecurityException as ex:
raise DatasetForbiddenError() from ex
user = security_manager.get_user_by_id(g.user.id)
datasource_dict["owners"] = populate_owners(
user, datasource_dict["owners"], default_to_user=False
)
else:
# legacy behavior
datasource_dict["owners"] = (
db.session.query(orm_datasource.owner_class)
.filter(
orm_datasource.owner_class.id.in_(
datasource_dict["owners"] or []
)
)
.all()
)
try:
check_ownership(orm_datasource)
except SupersetSecurityException as ex:
raise DatasetForbiddenError() from ex

user = security_manager.get_user_by_id(g.user.id)
datasource_dict["owners"] = populate_owners(
user, datasource_dict["owners"], default_to_user=False
)

duplicates = [
name
Expand Down
8 changes: 8 additions & 0 deletions tests/integration_tests/datasource_tests.py
Expand Up @@ -278,6 +278,7 @@ def test_save(self):

datasource_post = get_datasource_post()
datasource_post["id"] = tbl_id
datasource_post["owners"] = [1]
data = dict(data=json.dumps(datasource_post))
resp = self.get_json_resp("/datasource/save/", data)
for k in datasource_post:
Expand All @@ -299,11 +300,14 @@ def save_datasource_from_dict(self, datasource_post):
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_change_database(self):
self.login(username="admin")
admin_user = self.get_user("admin")

tbl = self.get_table(name="birth_names")
tbl_id = tbl.id
db_id = tbl.database_id
datasource_post = get_datasource_post()
datasource_post["id"] = tbl_id
datasource_post["owners"] = [admin_user.id]

new_db = self.create_fake_db()
datasource_post["database"]["id"] = new_db.id
Expand All @@ -318,10 +322,12 @@ def test_change_database(self):

def test_save_duplicate_key(self):
self.login(username="admin")
admin_user = self.get_user("admin")
tbl_id = self.get_table(name="birth_names").id

datasource_post = get_datasource_post()
datasource_post["id"] = tbl_id
datasource_post["owners"] = [admin_user.id]
datasource_post["columns"].extend(
[
{
Expand All @@ -346,10 +352,12 @@ def test_save_duplicate_key(self):

def test_get_datasource(self):
self.login(username="admin")
admin_user = self.get_user("admin")
tbl = self.get_table(name="birth_names")

datasource_post = get_datasource_post()
datasource_post["id"] = tbl.id
datasource_post["owners"] = [admin_user.id]
data = dict(data=json.dumps(datasource_post))
self.get_json_resp("/datasource/save/", data)
url = f"/datasource/get/{tbl.type}/{tbl.id}/"
Expand Down

0 comments on commit 2bd89d1

Please sign in to comment.