chore: remove deprecated apis and ENABLE_BROAD_ACTIVITY_ACCESS (#24400)

This commit is contained in:
Daniel Vaz Gaspar 2023-06-15 22:11:24 +01:00 committed by GitHub
parent dc042c6c3d
commit 23bb1c48a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 170 additions and 698 deletions

View File

@ -61,30 +61,21 @@
|can my queries on SqlLab|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| |can my queries on SqlLab|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|can log on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can log on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can schemas access for csv upload on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can schemas access for csv upload on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can user slices on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can favstar on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can import dashboards on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can import dashboards on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can schemas on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can schemas on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can sqllab history on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| |can sqllab history on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|can publish on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can publish on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can csv on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| |can csv on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|can fave dashboards by username on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can slice on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can slice on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can sync druid source on Superset|:heavy_check_mark:|O|O|O| |can sync druid source on Superset|:heavy_check_mark:|O|O|O|
|can explore on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can explore on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can fave slices on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can slice json on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can slice json on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can approve on Superset|:heavy_check_mark:|O|O|O| |can approve on Superset|:heavy_check_mark:|O|O|O|
|can explore json on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can explore json on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can fetch datasource metadata on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can fetch datasource metadata on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can override role permissions on Superset|:heavy_check_mark:|O|O|O|
|can created dashboards on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can csrf token on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can csrf token on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can created slices on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can annotation json on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can annotation json on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can fave dashboards on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can sqllab on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| |can sqllab on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|can recent activity on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can select star on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can select star on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can warm up cache on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can warm up cache on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can sqllab table viz on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| |can sqllab table viz on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|

View File

@ -34,6 +34,7 @@ assists people when migrating to a new version.
### Breaking Changes ### Breaking Changes
- [24400](https://github.com/apache/superset/pull/24400): Removed deprecated APIs `/superset/recent_activity/...`, `/superset/fave_dashboards_by_username/...`, `/superset/fave_dashboards/...`, `/superset/created_dashboards/...`, `/superset/user_slices/`, `/superset/created_slices/...`, `/superset/fave_slices/...`, `/superset/favstar/...`,
- [24401](https://github.com/apache/superset/pull/24401): Removes the deprecated `metrics` column (which was blossomed in [20732](https://github.com/apache/superset/pull/20732)) from the `/api/v1/dataset/` API. - [24401](https://github.com/apache/superset/pull/24401): Removes the deprecated `metrics` column (which was blossomed in [20732](https://github.com/apache/superset/pull/20732)) from the `/api/v1/dataset/` API.
- [24375](https://github.com/apache/superset/pull/24375): Removed deprecated API `/superset/get_or_create_table/...`, `/superset/sqllab_viz` - [24375](https://github.com/apache/superset/pull/24375): Removed deprecated API `/superset/get_or_create_table/...`, `/superset/sqllab_viz`
- [24360](https://github.com/apache/superset/pull/24360): Removed deprecated APIs `/superset/stop_query/...`, `/superset/queries/...`, `/superset/search_queries` - [24360](https://github.com/apache/superset/pull/24360): Removed deprecated APIs `/superset/stop_query/...`, `/superset/queries/...`, `/superset/search_queries`

View File

@ -40,7 +40,6 @@ export enum FeatureFlag {
EMBEDDABLE_CHARTS = 'EMBEDDABLE_CHARTS', EMBEDDABLE_CHARTS = 'EMBEDDABLE_CHARTS',
EMBEDDED_SUPERSET = 'EMBEDDED_SUPERSET', EMBEDDED_SUPERSET = 'EMBEDDED_SUPERSET',
ENABLE_ADVANCED_DATA_TYPES = 'ENABLE_ADVANCED_DATA_TYPES', ENABLE_ADVANCED_DATA_TYPES = 'ENABLE_ADVANCED_DATA_TYPES',
ENABLE_BROAD_ACTIVITY_ACCESS = 'ENABLE_BROAD_ACTIVITY_ACCESS',
ENABLE_EXPLORE_DRAG_AND_DROP = 'ENABLE_EXPLORE_DRAG_AND_DROP', ENABLE_EXPLORE_DRAG_AND_DROP = 'ENABLE_EXPLORE_DRAG_AND_DROP',
ENABLE_JAVASCRIPT_CONTROLS = 'ENABLE_JAVASCRIPT_CONTROLS', ENABLE_JAVASCRIPT_CONTROLS = 'ENABLE_JAVASCRIPT_CONTROLS',
ENABLE_TEMPLATE_PROCESSING = 'ENABLE_TEMPLATE_PROCESSING', ENABLE_TEMPLATE_PROCESSING = 'ENABLE_TEMPLATE_PROCESSING',

View File

@ -710,7 +710,6 @@ export const testQuery: ISaveableDatasource = {
export const mockdatasets = [...new Array(3)].map((_, i) => ({ export const mockdatasets = [...new Array(3)].map((_, i) => ({
changed_by_name: 'user', changed_by_name: 'user',
kind: i === 0 ? 'virtual' : 'physical', // ensure there is 1 virtual kind: i === 0 ? 'virtual' : 'physical', // ensure there is 1 virtual
changed_by_url: 'changed_by_url',
changed_by: 'user', changed_by: 'user',
changed_on: new Date().toISOString(), changed_on: new Date().toISOString(),
database_name: `db ${i}`, database_name: `db ${i}`,

View File

@ -99,7 +99,6 @@ const dashboardInfo = {
certification_details: 'Sample certification', certification_details: 'Sample certification',
changed_by: null, changed_by: null,
changed_by_name: '', changed_by_name: '',
changed_by_url: '',
changed_on: '2021-03-30T19:30:14.020942', changed_on: '2021-03-30T19:30:14.020942',
charts: [ charts: [
'Vaccine Candidates per Country & Stage', 'Vaccine Candidates per Country & Stage',

View File

@ -231,9 +231,6 @@ function ChartList(props: ChartListProps) {
const canExport = const canExport =
hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
const enableBroadUserAccess = isFeatureEnabled(
FeatureFlag.ENABLE_BROAD_ACTIVITY_ACCESS,
);
const handleBulkChartExport = (chartsToExport: Chart[]) => { const handleBulkChartExport = (chartsToExport: Chart[]) => {
const ids = chartsToExport.map(({ id }) => id); const ids = chartsToExport.map(({ id }) => id);
handleResourceExport('chart', ids, () => { handleResourceExport('chart', ids, () => {
@ -415,17 +412,9 @@ function ChartList(props: ChartListProps) {
{ {
Cell: ({ Cell: ({
row: { row: {
original: { original: { last_saved_by: lastSavedBy },
last_saved_by: lastSavedBy,
changed_by_url: changedByUrl,
},
}, },
}: any) => }: any) => <>{changedByName(lastSavedBy)}</>,
enableBroadUserAccess ? (
<a href={changedByUrl}>{changedByName(lastSavedBy)}</a>
) : (
<>{changedByName(lastSavedBy)}</>
),
Header: t('Modified by'), Header: t('Modified by'),
accessor: 'last_saved_by.first_name', accessor: 'last_saved_by.first_name',
size: 'xl', size: 'xl',

View File

@ -58,7 +58,6 @@ const mockDashboards = [...new Array(3)].map((_, i) => ({
url: 'url', url: 'url',
dashboard_title: `title ${i}`, dashboard_title: `title ${i}`,
changed_by_name: 'user', changed_by_name: 'user',
changed_by_url: 'changed_by_url',
changed_by_fk: 1, changed_by_fk: 1,
published: true, published: true,
changed_on_utc: new Date().toISOString(), changed_on_utc: new Date().toISOString(),

View File

@ -83,7 +83,6 @@ interface DashboardListProps {
interface Dashboard { interface Dashboard {
changed_by_name: string; changed_by_name: string;
changed_by_url: string;
changed_on_delta_humanized: string; changed_on_delta_humanized: string;
changed_by: string; changed_by: string;
dashboard_title: string; dashboard_title: string;
@ -140,9 +139,6 @@ function DashboardList(props: DashboardListProps) {
const [importingDashboard, showImportModal] = useState<boolean>(false); const [importingDashboard, showImportModal] = useState<boolean>(false);
const [passwordFields, setPasswordFields] = useState<string[]>([]); const [passwordFields, setPasswordFields] = useState<string[]>([]);
const [preparingExport, setPreparingExport] = useState<boolean>(false); const [preparingExport, setPreparingExport] = useState<boolean>(false);
const enableBroadUserAccess = isFeatureEnabled(
FeatureFlag.ENABLE_BROAD_ACTIVITY_ACCESS,
);
const [sshTunnelPasswordFields, setSSHTunnelPasswordFields] = useState< const [sshTunnelPasswordFields, setSSHTunnelPasswordFields] = useState<
string[] string[]
>([]); >([]);
@ -193,7 +189,6 @@ function DashboardList(props: DashboardListProps) {
if (dashboard.id === json?.result?.id) { if (dashboard.id === json?.result?.id) {
const { const {
changed_by_name, changed_by_name,
changed_by_url,
changed_by, changed_by,
dashboard_title = '', dashboard_title = '',
slug = '', slug = '',
@ -208,7 +203,6 @@ function DashboardList(props: DashboardListProps) {
return { return {
...dashboard, ...dashboard,
changed_by_name, changed_by_name,
changed_by_url,
changed_by, changed_by,
dashboard_title, dashboard_title,
slug, slug,
@ -310,17 +304,9 @@ function DashboardList(props: DashboardListProps) {
{ {
Cell: ({ Cell: ({
row: { row: {
original: { original: { changed_by_name: changedByName },
changed_by_name: changedByName,
changed_by_url: changedByUrl,
},
}, },
}: any) => }: any) => <>{changedByName}</>,
enableBroadUserAccess ? (
<a href={changedByUrl}>{changedByName}</a>
) : (
<>{changedByName}</>
),
Header: t('Modified by'), Header: t('Modified by'),
accessor: 'changed_by.first_name', accessor: 'changed_by.first_name',
size: 'xl', size: 'xl',

View File

@ -50,7 +50,6 @@ const datasetsEndpoint = 'glob:*/api/v1/dataset/?*';
const mockdatasets = [...new Array(3)].map((_, i) => ({ const mockdatasets = [...new Array(3)].map((_, i) => ({
changed_by_name: 'user', changed_by_name: 'user',
kind: i === 0 ? 'virtual' : 'physical', // ensure there is 1 virtual kind: i === 0 ? 'virtual' : 'physical', // ensure there is 1 virtual
changed_by_url: 'changed_by_url',
changed_by: 'user', changed_by: 'user',
changed_on: new Date().toISOString(), changed_on: new Date().toISOString(),
database_name: `db ${i}`, database_name: `db ${i}`,

View File

@ -109,7 +109,6 @@ const Actions = styled.div`
type Dataset = { type Dataset = {
changed_by_name: string; changed_by_name: string;
changed_by_url: string;
changed_by: string; changed_by: string;
changed_on_delta_humanized: string; changed_on_delta_humanized: string;
database: { database: {

View File

@ -33,7 +33,7 @@ export default class Favorites extends React.PureComponent<FavoritesProps> {
const mutator = (payload: { result: Chart[] }) => const mutator = (payload: { result: Chart[] }) =>
payload.result.map(slice => ({ payload.result.map(slice => ({
slice: <a href={slice.slice_url}>{slice.slice_name}</a>, slice: <a href={slice.slice_url}>{slice.slice_name}</a>,
creator: <a href={slice.created_by_url}>{slice.created_by_name}</a>, creator: slice.created_by_name,
favorited: moment.utc(slice.changed_on_dttm).fromNow(), favorited: moment.utc(slice.changed_on_dttm).fromNow(),
_favorited: slice.changed_on_dttm, _favorited: slice.changed_on_dttm,
})); }));

View File

@ -31,7 +31,6 @@ export type Chart = {
slice_name: string; slice_name: string;
slice_url: string; slice_url: string;
created_by_name?: string; created_by_name?: string;
created_by_url?: string;
changed_on_dttm: number; changed_on_dttm: number;
}; };

View File

@ -20,7 +20,6 @@ import Owner from './Owner';
export default interface Dataset { export default interface Dataset {
changed_by_name: string; changed_by_name: string;
changed_by_url: string;
changed_by: string; changed_by: string;
changed_on_delta_humanized: string; changed_on_delta_humanized: string;
database: { database: {

View File

@ -55,7 +55,6 @@ export interface Dashboard {
certified_by?: string; certified_by?: string;
certification_details?: string; certification_details?: string;
changed_by_name: string; changed_by_name: string;
changed_by_url: string;
changed_on_delta_humanized?: string; changed_on_delta_humanized?: string;
changed_on_utc?: string; changed_on_utc?: string;
changed_by: string; changed_by: string;

View File

@ -157,7 +157,6 @@ class ChartRestApi(BaseSupersetModelRestApi):
"changed_by.first_name", "changed_by.first_name",
"changed_by.last_name", "changed_by.last_name",
"changed_by_name", "changed_by_name",
"changed_by_url",
"changed_on_delta_humanized", "changed_on_delta_humanized",
"changed_on_dttm", "changed_on_dttm",
"changed_on_utc", "changed_on_utc",
@ -165,7 +164,6 @@ class ChartRestApi(BaseSupersetModelRestApi):
"created_by.id", "created_by.id",
"created_by.last_name", "created_by.last_name",
"created_by_name", "created_by_name",
"created_by_url",
"created_on_delta_humanized", "created_on_delta_humanized",
"datasource_id", "datasource_id",
"datasource_name_text", "datasource_name_text",

View File

@ -476,7 +476,6 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
"AVOID_COLORS_COLLISION": True, "AVOID_COLORS_COLLISION": True,
# Set to False to only allow viewing own recent activity # Set to False to only allow viewing own recent activity
# or to disallow users from viewing other users profile page # or to disallow users from viewing other users profile page
"ENABLE_BROAD_ACTIVITY_ACCESS": False,
# Do not show user info or profile in the menu # Do not show user info or profile in the menu
"MENU_HIDE_USER_INFO": False, "MENU_HIDE_USER_INFO": False,
} }

View File

@ -599,14 +599,6 @@ class SqlaTable(
return "" return ""
return str(self.changed_by) return str(self.changed_by)
@property
def changed_by_url(self) -> str:
if not self.changed_by or not is_feature_enabled(
"ENABLE_BROAD_ACTIVITY_ACCESS"
):
return ""
return f"/superset/profile/{self.changed_by.username}"
@property @property
def connection(self) -> str: def connection(self) -> str:
return str(self.database) return str(self.database)

View File

@ -174,7 +174,6 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"changed_by.last_name", "changed_by.last_name",
"changed_by.id", "changed_by.id",
"changed_by_name", "changed_by_name",
"changed_by_url",
"changed_on_utc", "changed_on_utc",
"changed_on_delta_humanized", "changed_on_delta_humanized",
"created_on_delta_humanized", "created_on_delta_humanized",

View File

@ -194,7 +194,6 @@ class DashboardGetResponseSchema(Schema):
metadata={"description": certification_details_description} metadata={"description": certification_details_description}
) )
changed_by_name = fields.String() changed_by_name = fields.String()
changed_by_url = fields.String()
changed_by = fields.Nested(UserSchema(exclude=(["username"]))) changed_by = fields.Nested(UserSchema(exclude=(["username"])))
changed_on = fields.DateTime() changed_on = fields.DateTime()
charts = fields.List(fields.String(metadata={"description": charts_description})) charts = fields.List(fields.String(metadata={"description": charts_description}))

View File

@ -101,7 +101,6 @@ class DatasetRestApi(BaseSupersetModelRestApi):
"database.id", "database.id",
"database.database_name", "database.database_name",
"changed_by_name", "changed_by_name",
"changed_by_url",
"changed_by.first_name", "changed_by.first_name",
"changed_by.last_name", "changed_by.last_name",
"changed_on_utc", "changed_on_utc",

View File

@ -268,14 +268,6 @@ class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
return "" return ""
return str(self.changed_by) return str(self.changed_by)
@property
def changed_by_url(self) -> str:
if not self.changed_by or not is_feature_enabled(
"ENABLE_BROAD_ACTIVITY_ACCESS"
):
return ""
return f"/superset/profile/{self.changed_by.username}"
@property @property
def data(self) -> dict[str, Any]: def data(self) -> dict[str, Any]:
positions = self.position_json positions = self.position_json

View File

@ -25,7 +25,7 @@ from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, Text
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy_utils import generic_relationship from sqlalchemy_utils import generic_relationship
from superset import app, db, is_feature_enabled from superset import app, db
from superset.models.helpers import AuditMixinNullable from superset.models.helpers import AuditMixinNullable
metadata = Model.metadata # pylint: disable=no-member metadata = Model.metadata # pylint: disable=no-member
@ -65,14 +65,6 @@ class FilterSet(Model, AuditMixinNullable):
return "" return ""
return str(self.changed_by) return str(self.changed_by)
@property
def changed_by_url(self) -> str:
if not self.changed_by or not is_feature_enabled(
"ENABLE_BROAD_ACTIVITY_ACCESS"
):
return ""
return f"/superset/profile/{self.changed_by.username}"
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
return { return {
"id": self.id, "id": self.id,

View File

@ -331,20 +331,6 @@ class Slice( # pylint: disable=too-many-public-methods
name = escape(self.chart) name = escape(self.chart)
return Markup(f'<a href="{self.url}">{name}</a>') return Markup(f'<a href="{self.url}">{name}</a>')
@property
def created_by_url(self) -> str:
if not self.created_by:
return ""
return f"/superset/profile/{self.created_by.username}"
@property
def changed_by_url(self) -> str:
if not self.changed_by or not is_feature_enabled(
"ENABLE_BROAD_ACTIVITY_ACCESS"
):
return ""
return f"/superset/profile/{self.changed_by.username}"
@property @property
def icons(self) -> str: def icons(self) -> str:
return f""" return f"""

View File

@ -1991,23 +1991,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
guest_rls = self.get_guest_rls_filters_str(datasource) guest_rls = self.get_guest_rls_filters_str(datasource)
return guest_rls + rls_str return guest_rls + rls_str
@staticmethod
def raise_for_user_activity_access(user_id: int) -> None:
# pylint: disable=import-outside-toplevel
from superset.extensions import feature_flag_manager
if not get_user_id() or (
not feature_flag_manager.is_feature_enabled("ENABLE_BROAD_ACTIVITY_ACCESS")
and user_id != get_user_id()
):
raise SupersetSecurityException(
SupersetError(
error_type=SupersetErrorType.USER_ACTIVITY_SECURITY_ACCESS_ERROR,
message="Access to user's activity data is restricted",
level=ErrorLevel.ERROR,
)
)
def raise_for_dashboard_access(self, dashboard: "Dashboard") -> None: def raise_for_dashboard_access(self, dashboard: "Dashboard") -> None:
""" """
Raise an exception if the user cannot access the dashboard. Raise an exception if the user cannot access the dashboard.

View File

@ -90,7 +90,6 @@ FRONTEND_CONF_KEYS = (
"SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE", "SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE",
"DISABLE_DATASET_SOURCE_EDIT", "DISABLE_DATASET_SOURCE_EDIT",
"ENABLE_JAVASCRIPT_CONTROLS", "ENABLE_JAVASCRIPT_CONTROLS",
"ENABLE_BROAD_ACTIVITY_ACCESS",
"DEFAULT_SQLLAB_LIMIT", "DEFAULT_SQLLAB_LIMIT",
"DEFAULT_VIZ_TYPE", "DEFAULT_VIZ_TYPE",
"SQL_MAX_ROW", "SQL_MAX_ROW",
@ -391,7 +390,7 @@ def menu_data(user: User) -> dict[str, Any]:
"user_login_url": appbuilder.get_url_for_login, "user_login_url": appbuilder.get_url_for_login,
"user_profile_url": None "user_profile_url": None
if user.is_anonymous or is_feature_enabled("MENU_HIDE_USER_INFO") if user.is_anonymous or is_feature_enabled("MENU_HIDE_USER_INFO")
else f"/superset/profile/{user.username}", else "/superset/profile/",
"locale": session.get("locale", "en"), "locale": session.get("locale", "en"),
}, },
} }

View File

@ -30,9 +30,7 @@ from flask_appbuilder.security.decorators import (
has_access_api, has_access_api,
permission_name, permission_name,
) )
from flask_appbuilder.security.sqla import models as ab_models
from flask_babel import gettext as __, lazy_gettext as _ from flask_babel import gettext as __, lazy_gettext as _
from sqlalchemy import and_, or_
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from superset import ( from superset import (
@ -57,19 +55,14 @@ from superset.dashboards.permalink.exceptions import DashboardPermalinkGetFailed
from superset.databases.dao import DatabaseDAO from superset.databases.dao import DatabaseDAO
from superset.datasets.commands.exceptions import DatasetNotFoundError from superset.datasets.commands.exceptions import DatasetNotFoundError
from superset.datasource.dao import DatasourceDAO from superset.datasource.dao import DatasourceDAO
from superset.exceptions import ( from superset.exceptions import CacheLoadError, DatabaseNotFound, SupersetException
CacheLoadError,
DatabaseNotFound,
SupersetException,
SupersetSecurityException,
)
from superset.explore.form_data.commands.create import CreateFormDataCommand from superset.explore.form_data.commands.create import CreateFormDataCommand
from superset.explore.form_data.commands.get import GetFormDataCommand from superset.explore.form_data.commands.get import GetFormDataCommand
from superset.explore.form_data.commands.parameters import CommandParameters from superset.explore.form_data.commands.parameters import CommandParameters
from superset.explore.permalink.commands.get import GetExplorePermalinkCommand from superset.explore.permalink.commands.get import GetExplorePermalinkCommand
from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError
from superset.extensions import async_query_manager, cache_manager from superset.extensions import async_query_manager, cache_manager
from superset.models.core import Database, FavStar from superset.models.core import Database
from superset.models.dashboard import Dashboard from superset.models.dashboard import Dashboard
from superset.models.slice import Slice from superset.models.slice import Slice
from superset.models.sql_lab import Query, TabState from superset.models.sql_lab import Query, TabState
@ -79,7 +72,12 @@ from superset.tasks.async_queries import load_explore_json_into_cache
from superset.utils import core as utils from superset.utils import core as utils
from superset.utils.async_query_manager import AsyncQueryTokenException from superset.utils.async_query_manager import AsyncQueryTokenException
from superset.utils.cache import etag_cache from superset.utils.cache import etag_cache
from superset.utils.core import DatasourceType, get_user_id, ReservedUrlParameters from superset.utils.core import (
DatasourceType,
get_user_id,
get_username,
ReservedUrlParameters,
)
from superset.views.base import ( from superset.views.base import (
api, api,
BaseSupersetView, BaseSupersetView,
@ -93,7 +91,6 @@ from superset.views.base import (
json_error_response, json_error_response,
json_success, json_success,
) )
from superset.views.log.dao import LogDAO
from superset.views.utils import ( from superset.views.utils import (
bootstrap_user_data, bootstrap_user_data,
check_datasource_perms, check_datasource_perms,
@ -829,237 +826,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
return json_success(json.dumps(response)) return json_success(json.dumps(response))
@staticmethod
def get_user_activity_access_error(user_id: int) -> FlaskResponse | None:
try:
security_manager.raise_for_user_activity_access(user_id)
except SupersetSecurityException as ex:
return json_error_response(
ex.message,
status=403,
)
return None
@api
@has_access_api
@event_logger.log_this
@expose("/recent_activity/<int:user_id>/", methods=("GET",))
@deprecated(new_target="/api/v1/log/recent_activity/<user_id>/")
def recent_activity(self, user_id: int) -> FlaskResponse:
"""Recent activity (actions) for a given user"""
if error_obj := self.get_user_activity_access_error(user_id):
return error_obj
limit = request.args.get("limit")
limit = int(limit) if limit and limit.isdigit() else 100
actions = request.args.get("actions", "explore,dashboard").split(",")
# whether to get distinct subjects
distinct = request.args.get("distinct") != "false"
payload = LogDAO.get_recent_activity(user_id, actions, distinct, 0, limit)
return json_success(json.dumps(payload, default=utils.json_int_dttm_ser))
@api
@has_access_api
@event_logger.log_this
@expose("/fave_dashboards_by_username/<username>/", methods=("GET",))
@deprecated(new_target="api/v1/dashboard/favorite_status/")
def fave_dashboards_by_username(self, username: str) -> FlaskResponse:
"""This lets us use a user's username to pull favourite dashboards"""
user = security_manager.find_user(username=username)
return self.fave_dashboards(user.id)
@api
@has_access_api
@event_logger.log_this
@expose("/fave_dashboards/<int:user_id>/", methods=("GET",))
@deprecated(new_target="api/v1/dashboard/favorite_status/")
def fave_dashboards(self, user_id: int) -> FlaskResponse:
if error_obj := self.get_user_activity_access_error(user_id):
return error_obj
qry = (
db.session.query(Dashboard, FavStar.dttm)
.join(
FavStar,
and_(
FavStar.user_id == int(user_id),
FavStar.class_name == "Dashboard",
Dashboard.id == FavStar.obj_id,
),
)
.order_by(FavStar.dttm.desc())
)
payload = []
for o in qry.all():
dash = {
"id": o.Dashboard.id,
"dashboard": o.Dashboard.dashboard_link(),
"title": o.Dashboard.dashboard_title,
"url": o.Dashboard.url,
"dttm": o.dttm,
}
if o.Dashboard.created_by:
user = o.Dashboard.created_by
dash["creator"] = str(user)
dash["creator_url"] = f"/superset/profile/{user.username}/"
payload.append(dash)
return json_success(json.dumps(payload, default=utils.json_int_dttm_ser))
@api
@has_access_api
@event_logger.log_this
@expose("/created_dashboards/<int:user_id>/", methods=("GET",))
@deprecated(new_target="api/v1/dashboard/")
def created_dashboards(self, user_id: int) -> FlaskResponse:
if error_obj := self.get_user_activity_access_error(user_id):
return error_obj
qry = (
db.session.query(Dashboard)
.filter( # pylint: disable=comparison-with-callable
or_(
Dashboard.created_by_fk == user_id,
Dashboard.changed_by_fk == user_id,
)
)
.order_by(Dashboard.changed_on.desc())
)
payload = [
{
"id": o.id,
"dashboard": o.dashboard_link(),
"title": o.dashboard_title,
"url": o.url,
"dttm": o.changed_on,
}
for o in qry.all()
]
return json_success(json.dumps(payload, default=utils.json_int_dttm_ser))
@api
@has_access_api
@event_logger.log_this
@expose("/user_slices", methods=("GET",))
@expose("/user_slices/<int:user_id>/", methods=("GET",))
@deprecated(new_target="/api/v1/chart/")
def user_slices(self, user_id: int | None = None) -> FlaskResponse:
"""List of slices a user owns, created, modified or faved"""
if not user_id:
user_id = cast(int, get_user_id())
if error_obj := self.get_user_activity_access_error(user_id):
return error_obj
owner_ids_query = (
db.session.query(Slice.id)
.join(Slice.owners)
.filter(security_manager.user_model.id == user_id)
)
qry = (
db.session.query(Slice, FavStar.dttm)
.join(
FavStar,
and_(
FavStar.user_id == user_id,
FavStar.class_name == "slice",
Slice.id == FavStar.obj_id,
),
isouter=True,
)
.filter( # pylint: disable=comparison-with-callable
or_(
Slice.id.in_(owner_ids_query),
Slice.created_by_fk == user_id,
Slice.changed_by_fk == user_id,
FavStar.user_id == user_id,
)
)
.order_by(Slice.slice_name.asc())
)
payload = [
{
"id": o.Slice.id,
"title": o.Slice.slice_name,
"url": o.Slice.slice_url,
"data": o.Slice.form_data,
"dttm": o.dttm if o.dttm else o.Slice.changed_on,
"viz_type": o.Slice.viz_type,
}
for o in qry.all()
]
return json_success(json.dumps(payload, default=utils.json_int_dttm_ser))
@api
@has_access_api
@event_logger.log_this
@expose("/created_slices", methods=("GET",))
@expose("/created_slices/<int:user_id>/", methods=("GET",))
@deprecated(new_target="api/v1/chart/")
def created_slices(self, user_id: int | None = None) -> FlaskResponse:
"""List of slices created by this user"""
if not user_id:
user_id = cast(int, get_user_id())
if error_obj := self.get_user_activity_access_error(user_id):
return error_obj
qry = (
db.session.query(Slice)
.filter( # pylint: disable=comparison-with-callable
or_(Slice.created_by_fk == user_id, Slice.changed_by_fk == user_id)
)
.order_by(Slice.changed_on.desc())
)
payload = [
{
"id": o.id,
"title": o.slice_name,
"url": o.slice_url,
"dttm": o.changed_on,
"viz_type": o.viz_type,
}
for o in qry.all()
]
return json_success(json.dumps(payload, default=utils.json_int_dttm_ser))
@api
@has_access_api
@event_logger.log_this
@expose("/fave_slices", methods=("GET",))
@expose("/fave_slices/<int:user_id>/", methods=("GET",))
@deprecated(new_target="api/v1/chart/")
def fave_slices(self, user_id: int | None = None) -> FlaskResponse:
"""Favorite slices for a user"""
if user_id is None:
user_id = cast(int, get_user_id())
if error_obj := self.get_user_activity_access_error(user_id):
return error_obj
qry = (
db.session.query(Slice, FavStar.dttm)
.join(
FavStar,
and_(
FavStar.user_id == user_id,
FavStar.class_name == "slice",
Slice.id == FavStar.obj_id,
),
)
.order_by(FavStar.dttm.desc())
)
payload = []
for o in qry.all():
dash = {
"id": o.Slice.id,
"title": o.Slice.slice_name,
"url": o.Slice.slice_url,
"dttm": o.dttm,
"viz_type": o.Slice.viz_type,
}
if o.Slice.created_by:
user = o.Slice.created_by
dash["creator"] = str(user)
dash["creator_url"] = f"/superset/profile/{user.username}/"
payload.append(dash)
return json_success(json.dumps(payload, default=utils.json_int_dttm_ser))
@event_logger.log_this @event_logger.log_this
@api @api
@has_access_api @has_access_api
@ -1158,42 +924,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
return json_success(json.dumps(result)) return json_success(json.dumps(result))
@has_access_api
@event_logger.log_this
@expose("/favstar/<class_name>/<int:obj_id>/<action>/")
@deprecated(new_target="api/v1/dashboard|chart/<pk>/favorites/")
def favstar( # pylint: disable=no-self-use
self, class_name: str, obj_id: int, action: str
) -> FlaskResponse:
"""Toggle favorite stars on Slices and Dashboard"""
if not get_user_id():
return json_error_response("ERROR: Favstar toggling denied", status=403)
session = db.session()
count = 0
favs = (
session.query(FavStar)
.filter_by(class_name=class_name, obj_id=obj_id, user_id=get_user_id())
.all()
)
if action == "select":
if not favs:
session.add(
FavStar(
class_name=class_name,
obj_id=obj_id,
user_id=get_user_id(),
dttm=datetime.now(),
)
)
count = 1
elif action == "unselect":
for fav in favs:
session.delete(fav)
else:
count = len(favs)
session.commit()
return json_success(json.dumps({"count": count}))
@has_access @has_access
@expose("/dashboard/<dashboard_id_or_slug>/") @expose("/dashboard/<dashboard_id_or_slug>/")
@event_logger.log_this_with_extra_payload @event_logger.log_this_with_extra_payload
@ -1279,23 +1009,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
def log(self) -> FlaskResponse: # pylint: disable=no-self-use def log(self) -> FlaskResponse: # pylint: disable=no-self-use
return Response(status=200) return Response(status=200)
@has_access
@expose("/extra_table_metadata/<int:database_id>/<table_name>/<schema>/")
@event_logger.log_this
@deprecated(
new_target="api/v1/database/<int:pk>/table_extra/<table_name>/<schema_name>/"
)
def extra_table_metadata( # pylint: disable=no-self-use
self, database_id: int, table_name: str, schema: str
) -> FlaskResponse:
parsed_schema = utils.parse_js_uri_path_item(schema, eval_undefined=True)
table_name = utils.parse_js_uri_path_item(table_name) # type: ignore
mydb = db.session.query(Database).filter_by(id=database_id).one()
payload = mydb.db_engine_spec.extra_table_metadata(
mydb, table_name, parsed_schema
)
return json_success(json.dumps(payload))
@expose("/theme/") @expose("/theme/")
def theme(self) -> FlaskResponse: def theme(self) -> FlaskResponse:
return self.render_template("superset/theme.html") return self.render_template("superset/theme.html")
@ -1363,27 +1076,20 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
@has_access @has_access
@event_logger.log_this @event_logger.log_this
@expose("/profile/<username>/") @expose("/profile/")
def profile(self, username: str) -> FlaskResponse: def profile(self) -> FlaskResponse:
"""User profile page""" """User profile page"""
user = ( user = g.user if hasattr(g, "user") and g.user else None
db.session.query(ab_models.User).filter_by(username=username).one_or_none() if not user or security_manager.is_guest_user(user) or user.is_anonymous:
) abort(404)
# Prevent returning 404 when user is not found to prevent username scanning
user_id = -1 if not user else user.id
# Prevent unauthorized access to other user's profiles,
# unless configured to do so with ENABLE_BROAD_ACTIVITY_ACCESS
if error_obj := self.get_user_activity_access_error(user_id):
return error_obj
payload = { payload = {
"user": bootstrap_user_data(user, include_perms=True), "user": bootstrap_user_data(user, include_perms=True),
"common": common_bootstrap_payload(g.user), "common": common_bootstrap_payload(user),
} }
return self.render_template( return self.render_template(
"superset/basic.html", "superset/basic.html",
title=_("%(user)s's profile", user=username).__str__(), title=_("%(user)s's profile", user=get_username()).__str__(),
entry="profile", entry="profile",
bootstrap_data=json.dumps( bootstrap_data=json.dumps(
payload, default=utils.pessimistic_json_iso_dttm_ser payload, default=utils.pessimistic_json_iso_dttm_ser

View File

@ -23,9 +23,11 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
import superset.models.core as models import superset.models.core as models
from superset import event_logger, security_manager from superset import event_logger, security_manager
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
from superset.exceptions import SupersetSecurityException from superset.exceptions import SupersetSecurityException
from superset.superset_typing import FlaskResponse from superset.superset_typing import FlaskResponse
from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics
from superset.views.log import LogMixin
from superset.views.log.dao import LogDAO from superset.views.log.dao import LogDAO
from superset.views.log.schemas import ( from superset.views.log.schemas import (
get_recent_activity_schema, get_recent_activity_schema,
@ -33,9 +35,6 @@ from superset.views.log.schemas import (
RecentActivitySchema, RecentActivitySchema,
) )
from ...constants import MODEL_API_RW_METHOD_PERMISSION_MAP
from . import LogMixin
class LogRestApi(LogMixin, BaseSupersetModelRestApi): class LogRestApi(LogMixin, BaseSupersetModelRestApi):
datamodel = SQLAInterface(models.Log) datamodel = SQLAInterface(models.Log)
@ -82,7 +81,7 @@ class LogRestApi(LogMixin, BaseSupersetModelRestApi):
return self.response(403, message=ex.message) return self.response(403, message=ex.message)
return None return None
@expose("/recent_activity/<int:user_id>/", methods=("GET",)) @expose("/recent_activity/", methods=("GET",))
@protect() @protect()
@safe @safe
@statsd_metrics @statsd_metrics
@ -92,7 +91,7 @@ class LogRestApi(LogMixin, BaseSupersetModelRestApi):
f".recent_activity", f".recent_activity",
log_to_statsd=False, log_to_statsd=False,
) )
def recent_activity(self, user_id: int, **kwargs: Any) -> FlaskResponse: def recent_activity(self, **kwargs: Any) -> FlaskResponse:
"""Get recent activity data for a user """Get recent activity data for a user
--- ---
get: get:
@ -125,16 +124,11 @@ class LogRestApi(LogMixin, BaseSupersetModelRestApi):
500: 500:
$ref: '#/components/responses/500' $ref: '#/components/responses/500'
""" """
if error_obj := self.get_user_activity_access_error(user_id):
return error_obj
args = kwargs["rison"] args = kwargs["rison"]
page, page_size = self._sanitize_page_args(*self._handle_page_args(args)) page, page_size = self._sanitize_page_args(*self._handle_page_args(args))
actions = args.get("actions", ["explore", "dashboard"]) actions = args.get("actions", ["explore", "dashboard"])
distinct = args.get("distinct", True) distinct = args.get("distinct", True)
payload = LogDAO.get_recent_activity( payload = LogDAO.get_recent_activity(actions, distinct, page, page_size)
user_id, actions, distinct, page, page_size
)
return self.response(200, result=payload) return self.response(200, result=payload)

View File

@ -26,6 +26,7 @@ from superset.dao.base import BaseDAO
from superset.models.core import Log from superset.models.core import Log
from superset.models.dashboard import Dashboard from superset.models.dashboard import Dashboard
from superset.models.slice import Slice from superset.models.slice import Slice
from superset.utils.core import get_user_id
from superset.utils.dates import datetime_to_epoch from superset.utils.dates import datetime_to_epoch
@ -34,8 +35,12 @@ class LogDAO(BaseDAO):
@staticmethod @staticmethod
def get_recent_activity( def get_recent_activity(
user_id: int, actions: list[str], distinct: bool, page: int, page_size: int actions: list[str],
distinct: bool,
page: int,
page_size: int,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
user_id = get_user_id()
has_subject_title = or_( has_subject_title = or_(
and_( and_(
Dashboard.dashboard_title is not None, Dashboard.dashboard_title is not None,

View File

@ -605,54 +605,6 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixin):
db.session.delete(model) db.session.delete(model)
db.session.commit() db.session.commit()
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@with_feature_flags(ENABLE_BROAD_ACTIVITY_ACCESS=False)
def test_chart_activity_access_disabled(self):
"""
Chart API: Test ENABLE_BROAD_ACTIVITY_ACCESS = False
"""
admin = self.get_user("admin")
birth_names_table_id = SupersetTestCase.get_table(name="birth_names").id
chart_id = self.insert_chart("title", [admin.id], birth_names_table_id).id
chart_data = {
"slice_name": (new_name := "title1_changed"),
}
self.login(username="admin")
uri = f"api/v1/chart/{chart_id}"
rv = self.put_assert_metric(uri, chart_data, "put")
self.assertEqual(rv.status_code, 200)
model = db.session.query(Slice).get(chart_id)
self.assertEqual(model.slice_name, new_name)
self.assertEqual(model.changed_by_url, "")
db.session.delete(model)
db.session.commit()
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@with_feature_flags(ENABLE_BROAD_ACTIVITY_ACCESS=True)
def test_chart_activity_access_enabled(self):
"""
Chart API: Test ENABLE_BROAD_ACTIVITY_ACCESS = True
"""
admin = self.get_user("admin")
birth_names_table_id = SupersetTestCase.get_table(name="birth_names").id
chart_id = self.insert_chart("title", [admin.id], birth_names_table_id).id
chart_data = {
"slice_name": (new_name := "title1_changed"),
}
self.login(username="admin")
uri = f"api/v1/chart/{chart_id}"
rv = self.put_assert_metric(uri, chart_data, "put")
self.assertEqual(rv.status_code, 200)
model = db.session.query(Slice).get(chart_id)
self.assertEqual(model.slice_name, new_name)
self.assertEqual(model.changed_by_url, "/superset/profile/admin")
db.session.delete(model)
db.session.commit()
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_chart_get_list_no_username(self): def test_chart_get_list_no_username(self):
""" """

View File

@ -23,8 +23,10 @@ import json
import logging import logging
from urllib.parse import quote from urllib.parse import quote
import prison
import superset.utils.database import superset.utils.database
from superset.utils.core import backend from superset.utils.core import backend
from tests.integration_tests.fixtures.public_role import public_role_like_gamma
from tests.integration_tests.fixtures.birth_names_dashboard import ( from tests.integration_tests.fixtures.birth_names_dashboard import (
load_birth_names_dashboard_with_slices, load_birth_names_dashboard_with_slices,
load_birth_names_data, load_birth_names_data,
@ -47,6 +49,7 @@ from tests.integration_tests.fixtures.energy_dashboard import (
load_energy_table_with_slice, load_energy_table_with_slice,
load_energy_table_data, load_energy_table_data,
) )
from tests.integration_tests.insert_chart_mixin import InsertChartMixin
from tests.integration_tests.test_app import app from tests.integration_tests.test_app import app
import superset.views.utils import superset.views.utils
from superset import ( from superset import (
@ -89,7 +92,7 @@ def cleanup():
yield yield
class TestCore(SupersetTestCase): class TestCore(SupersetTestCase, InsertChartMixin):
def setUp(self): def setUp(self):
self.table_ids = { self.table_ids = {
tbl.table_name: tbl.id for tbl in (db.session.query(SqlaTable).all()) tbl.table_name: tbl.id for tbl in (db.session.query(SqlaTable).all())
@ -100,6 +103,50 @@ class TestCore(SupersetTestCase):
db.session.query(Query).delete() db.session.query(Query).delete()
app.config["PREVENT_UNSAFE_DB_CONNECTIONS"] = self.original_unsafe_db_setting app.config["PREVENT_UNSAFE_DB_CONNECTIONS"] = self.original_unsafe_db_setting
def insert_dashboard_created_by(self, username: str) -> Dashboard:
user = self.get_user(username)
dashboard = self.insert_dashboard(
f"create_title_test",
f"create_slug_test",
[user.id],
created_by=user,
)
return dashboard
def insert_chart_created_by(self, username: str) -> Slice:
user = self.get_user(username)
dataset = db.session.query(SqlaTable).first()
chart = self.insert_chart(
f"create_title_test",
[user.id],
dataset.id,
created_by=user,
)
return chart
@pytest.fixture()
def insert_dashboard_created_by_admin(self):
with self.create_app().app_context():
dashboard = self.insert_dashboard_created_by("admin")
yield dashboard
db.session.delete(dashboard)
db.session.commit()
@pytest.fixture()
def insert_dashboard_created_by_gamma(self):
dashboard = self.insert_dashboard_created_by("gamma")
yield dashboard
db.session.delete(dashboard)
db.session.commit()
@pytest.fixture()
def insert_chart_created_by_admin(self):
with self.create_app().app_context():
chart = self.insert_chart_created_by("admin")
yield chart
db.session.delete(chart)
db.session.commit()
def test_login(self): def test_login(self):
resp = self.get_resp("/login/", data=dict(username="admin", password="general")) resp = self.get_resp("/login/", data=dict(username="admin", password="general"))
self.assertNotIn("User confirmation needed", resp) self.assertNotIn("User confirmation needed", resp)
@ -262,43 +309,6 @@ class TestCore(SupersetTestCase):
resp = self.client.get(url) resp = self.client.get(url)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_get_user_slices_for_owners(self):
self.login(username="alpha")
user = security_manager.find_user("alpha")
slice_name = "Girls"
# ensure user is not owner of any slices
url = f"/superset/user_slices/{user.id}/"
resp = self.client.get(url)
data = json.loads(resp.data)
self.assertEqual(data, [])
# make user owner of slice and verify that endpoint returns said slice
slc = self.get_slice(
slice_name=slice_name, session=db.session, expunge_from_session=False
)
slc.owners = [user]
db.session.merge(slc)
db.session.commit()
url = f"/superset/user_slices/{user.id}/"
resp = self.client.get(url)
data = json.loads(resp.data)
self.assertEqual(len(data), 1)
self.assertEqual(data[0]["title"], slice_name)
# remove ownership and ensure user no longer gets slice
slc = self.get_slice(
slice_name=slice_name, session=db.session, expunge_from_session=False
)
slc.owners = []
db.session.merge(slc)
db.session.commit()
url = f"/superset/user_slices/{user.id}/"
resp = self.client.get(url)
data = json.loads(resp.data)
self.assertEqual(data, [])
def test_get_user_slices(self): def test_get_user_slices(self):
self.login(username="admin") self.login(username="admin")
userid = security_manager.find_user("admin").id userid = security_manager.find_user("admin").id
@ -483,71 +493,99 @@ class TestCore(SupersetTestCase):
for k in keys: for k in keys:
self.assertIn(k, resp.keys()) self.assertIn(k, resp.keys())
@staticmethod @pytest.mark.usefixtures("insert_dashboard_created_by_admin")
def _get_user_activity_endpoints(user: str): @pytest.mark.usefixtures("insert_chart_created_by_admin")
userid = security_manager.find_user(user).id
return (
f"/superset/recent_activity/{userid}/",
f"/superset/created_slices/{userid}/",
f"/superset/created_dashboards/{userid}/",
f"/superset/fave_slices/{userid}/",
f"/superset/fave_dashboards/{userid}/",
f"/superset/user_slices/{userid}/",
f"/superset/fave_dashboards_by_username/{user}/",
)
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_user_profile(self, username="admin"): def test_user_profile(self, username="admin"):
self.login(username=username) self.login(username=username)
slc = self.get_slice("Girls", db.session) slc = self.get_slice("Girls", db.session)
dashboard = db.session.query(Dashboard).filter_by(slug="births").first()
# Set a favorite dashboard
self.client.post(f"/api/v1/dashboard/{dashboard.id}/favorites/", json={})
# Set a favorite chart
self.client.post(f"/api/v1/chart/{slc.id}/favorites/", json={})
# Setting some faves # Get favorite dashboards:
url = f"/superset/favstar/Slice/{slc.id}/select/" request_query = {
resp = self.get_json_resp(url) "columns": ["created_on_delta_humanized", "dashboard_title", "url"],
self.assertEqual(resp["count"], 1) "filters": [{"col": "id", "opr": "dashboard_is_favorite", "value": True}],
"keys": ["none"],
"order_column": "changed_on",
"order_direction": "desc",
"page": 0,
"page_size": 100,
}
url = f"/api/v1/dashboard/?q={prison.dumps(request_query)}"
resp = self.client.get(url)
assert resp.json["count"] == 1
assert resp.json["result"][0]["dashboard_title"] == "USA Births Names"
dash = db.session.query(Dashboard).filter_by(slug="births").first() # Get Favorite Charts
url = f"/superset/favstar/Dashboard/{dash.id}/select/" request_query = {
resp = self.get_json_resp(url) "filters": [{"col": "id", "opr": "chart_is_favorite", "value": True}],
self.assertEqual(resp["count"], 1) "order_column": "slice_name",
"order_direction": "asc",
"page": 0,
"page_size": 25,
}
url = f"api/v1/chart/?q={prison.dumps(request_query)}"
resp = self.client.get(url)
assert resp.json["count"] == 1
assert resp.json["result"][0]["id"] == slc.id
resp = self.get_resp(f"/superset/profile/{username}/") # Get recent activity
url = "/api/v1/log/recent_activity/?q=(page_size:50)"
resp = self.client.get(url)
# TODO data for recent activity varies for sqlite, we should be able to assert
# the returned data
assert resp.status_code == 200
# Get dashboards created by the user
request_query = {
"columns": ["created_on_delta_humanized", "dashboard_title", "url"],
"filters": [
{"col": "created_by", "opr": "dashboard_created_by_me", "value": "me"}
],
"keys": ["none"],
"order_column": "changed_on",
"order_direction": "desc",
"page": 0,
"page_size": 100,
}
url = f"/api/v1/dashboard/?q={prison.dumps(request_query)}"
resp = self.client.get(url)
assert resp.json["result"][0]["dashboard_title"] == "create_title_test"
# Get charts created by the user
request_query = {
"columns": ["created_on_delta_humanized", "slice_name", "url"],
"filters": [
{"col": "created_by", "opr": "chart_created_by_me", "value": "me"}
],
"keys": ["none"],
"order_column": "changed_on_delta_humanized",
"order_direction": "desc",
"page": 0,
"page_size": 100,
}
url = f"/api/v1/chart/?q={prison.dumps(request_query)}"
resp = self.client.get(url)
assert resp.json["count"] == 1
assert resp.json["result"][0]["slice_name"] == "create_title_test"
resp = self.get_resp(f"/superset/profile/")
self.assertIn('"app"', resp) self.assertIn('"app"', resp)
for endpoint in self._get_user_activity_endpoints(username): def test_user_profile_gamma(self):
data = self.get_json_resp(endpoint)
self.assertNotIn("message", data)
def test_user_profile_default_access(self):
self.login(username="gamma") self.login(username="gamma")
resp = self.client.get(f"/superset/profile/admin/") resp = self.get_resp(f"/superset/profile/")
self.assertEqual(resp.status_code, 403) self.assertIn('"app"', resp)
@with_feature_flags(ENABLE_BROAD_ACTIVITY_ACCESS=True) @pytest.mark.usefixtures("public_role_like_gamma")
def test_user_profile_broad_access(self): def test_user_profile_anonymous(self):
self.login(username="gamma") self.logout()
resp = self.client.get(f"/superset/profile/admin/") resp = self.client.get("/superset/profile/")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 404
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_user_activity_default_access(self, username="gamma"):
self.login(username=username)
for user in ("admin", "gamma"):
for endpoint in self._get_user_activity_endpoints(user):
resp = self.client.get(endpoint)
expected_status_code = 200 if user == username else 403
assert resp.status_code == expected_status_code
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@with_feature_flags(ENABLE_BROAD_ACTIVITY_ACCESS=True)
def test_user_activity_broad_access(self, username="gamma"):
self.login(username=username)
for user in ("admin", "gamma"):
for endpoint in self._get_user_activity_endpoints(user):
resp = self.client.get(endpoint)
assert resp.status_code == 200
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_slice_id_is_always_logged_correctly_on_web_request(self): def test_slice_id_is_always_logged_correctly_on_web_request(self):
@ -1025,7 +1063,7 @@ class TestCore(SupersetTestCase):
"/superset/sqllab", "/superset/sqllab",
"/superset/welcome", "/superset/welcome",
f"/superset/dashboard/{dash_id}/", f"/superset/dashboard/{dash_id}/",
"/superset/profile/admin/", "/superset/profile/",
f"/explore/?datasource_type=table&datasource_id={tbl_id}", f"/explore/?datasource_type=table&datasource_id={tbl_id}",
] ]
for url in urls: for url in urls:

View File

@ -368,7 +368,6 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
"certification_details": None, "certification_details": None,
"changed_by": None, "changed_by": None,
"changed_by_name": "", "changed_by_name": "",
"changed_by_url": "",
"charts": [], "charts": [],
"created_by": { "created_by": {
"id": 1, "id": 1,
@ -1326,52 +1325,6 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
db.session.delete(model) db.session.delete(model)
db.session.commit() db.session.commit()
@with_feature_flags(ENABLE_BROAD_ACTIVITY_ACCESS=False)
def test_dashboard_activity_access_disabled(self):
"""
Dashboard API: Test ENABLE_BROAD_ACTIVITY_ACCESS = False
"""
admin = self.get_user("admin")
admin_role = self.get_role("Admin")
dashboard_id = self.insert_dashboard(
"title1", "slug1", [admin.id], roles=[admin_role.id]
).id
self.login(username="admin")
uri = f"api/v1/dashboard/{dashboard_id}"
dashboard_data = {"dashboard_title": "title2"}
rv = self.client.put(uri, json=dashboard_data)
self.assertEqual(rv.status_code, 200)
model = db.session.query(Dashboard).get(dashboard_id)
self.assertEqual(model.dashboard_title, "title2")
self.assertEqual(model.changed_by_url, "")
db.session.delete(model)
db.session.commit()
@with_feature_flags(ENABLE_BROAD_ACTIVITY_ACCESS=True)
def test_dashboard_activity_access_enabled(self):
"""
Dashboard API: Test ENABLE_BROAD_ACTIVITY_ACCESS = True
"""
admin = self.get_user("admin")
admin_role = self.get_role("Admin")
dashboard_id = self.insert_dashboard(
"title1", "slug1", [admin.id], roles=[admin_role.id]
).id
self.login(username="admin")
uri = f"api/v1/dashboard/{dashboard_id}"
dashboard_data = {"dashboard_title": "title2"}
rv = self.client.put(uri, json=dashboard_data)
self.assertEqual(rv.status_code, 200)
model = db.session.query(Dashboard).get(dashboard_id)
self.assertEqual(model.dashboard_title, "title2")
self.assertEqual(model.changed_by_url, "/superset/profile/admin")
db.session.delete(model)
db.session.commit()
def test_dashboard_get_list_no_username(self): def test_dashboard_get_list_no_username(self):
""" """
Dashboard API: Tests that no username is returned Dashboard API: Tests that no username is returned

View File

@ -207,7 +207,6 @@ class TestDatasetApi(SupersetTestCase):
expected_columns = [ expected_columns = [
"changed_by", "changed_by",
"changed_by_name", "changed_by_name",
"changed_by_url",
"changed_on_delta_humanized", "changed_on_delta_humanized",
"changed_on_utc", "changed_on_utc",
"database", "database",
@ -1358,56 +1357,6 @@ class TestDatasetApi(SupersetTestCase):
db.session.delete(dataset) db.session.delete(dataset)
db.session.commit() db.session.commit()
@with_feature_flags(ENABLE_BROAD_ACTIVITY_ACCESS=True)
def test_dataset_activity_access_enabled(self):
"""
Dataset API: Test ENABLE_BROAD_ACTIVITY_ACCESS = True
"""
if backend() == "sqlite":
return
dataset = self.insert_default_dataset()
self.login(username="admin")
table_data = {"description": "changed_description"}
uri = f"api/v1/dataset/{dataset.id}"
rv = self.client.put(uri, json=table_data)
self.assertEqual(rv.status_code, 200)
response = self.get_assert_metric("api/v1/dataset/", "get_list")
res = json.loads(response.data.decode("utf-8"))["result"]
current_dataset = [d for d in res if d["id"] == dataset.id][0]
self.assertEqual(current_dataset["description"], "changed_description")
self.assertEqual(current_dataset["changed_by_url"], "/superset/profile/admin")
db.session.delete(dataset)
db.session.commit()
@with_feature_flags(ENABLE_BROAD_ACTIVITY_ACCESS=False)
def test_dataset_activity_access_disabled(self):
"""
Dataset API: Test ENABLE_BROAD_ACTIVITY_ACCESS = Fase
"""
if backend() == "sqlite":
return
dataset = self.insert_default_dataset()
self.login(username="admin")
table_data = {"description": "changed_description"}
uri = f"api/v1/dataset/{dataset.id}"
rv = self.put_assert_metric(uri, table_data, "put")
self.assertEqual(rv.status_code, 200)
response = self.get_assert_metric("api/v1/dataset/", "get_list")
res = json.loads(response.data.decode("utf-8"))["result"]
current_dataset = [d for d in res if d["id"] == dataset.id][0]
self.assertEqual(current_dataset["description"], "changed_description")
self.assertEqual(current_dataset["changed_by_url"], "")
db.session.delete(dataset)
db.session.commit()
def test_update_dataset_item_not_owned(self): def test_update_dataset_item_not_owned(self):
""" """
Dataset API: Test update dataset item not owned Dataset API: Test update dataset item not owned

View File

@ -159,19 +159,6 @@ class TestLogApi(SupersetTestCase):
db.session.delete(log) db.session.delete(log)
db.session.commit() db.session.commit()
@with_feature_flags(ENABLE_BROAD_ACTIVITY_ACCESS=False)
def test_get_recent_activity_no_broad_access(self):
"""
Log API: Test recent activity not visible for other users without
ENABLE_BROAD_ACTIVITY_ACCESS flag on
"""
admin_user = self.get_user("admin")
self.login(username="admin")
uri = f"api/v1/log/recent_activity/{admin_user.id + 1}/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 403)
def test_get_recent_activity(self): def test_get_recent_activity(self):
""" """
Log API: Test recent activity endpoint Log API: Test recent activity endpoint
@ -182,7 +169,7 @@ class TestLogApi(SupersetTestCase):
log1 = self.insert_log("dashboard", admin_user, dashboard_id=dash.id) log1 = self.insert_log("dashboard", admin_user, dashboard_id=dash.id)
log2 = self.insert_log("dashboard", admin_user, dashboard_id=dash.id) log2 = self.insert_log("dashboard", admin_user, dashboard_id=dash.id)
uri = f"api/v1/log/recent_activity/{admin_user.id}/" uri = f"api/v1/log/recent_activity/"
rv = self.client.get(uri) rv = self.client.get(uri)
self.assertEqual(rv.status_code, 200) self.assertEqual(rv.status_code, 200)
response = json.loads(rv.data.decode("utf-8")) response = json.loads(rv.data.decode("utf-8"))
@ -219,7 +206,7 @@ class TestLogApi(SupersetTestCase):
log2 = self.insert_log("explore", admin_user, dashboard_id=dash.id) log2 = self.insert_log("explore", admin_user, dashboard_id=dash.id)
arguments = {"actions": ["dashboard"]} arguments = {"actions": ["dashboard"]}
uri = f"api/v1/log/recent_activity/{admin_user.id}/?q={prison.dumps(arguments)}" uri = f"api/v1/log/recent_activity/?q={prison.dumps(arguments)}"
rv = self.client.get(uri) rv = self.client.get(uri)
db.session.delete(log) db.session.delete(log)
@ -244,7 +231,7 @@ class TestLogApi(SupersetTestCase):
log2 = self.insert_log("dashboard", admin_user, dashboard_id=dash.id) log2 = self.insert_log("dashboard", admin_user, dashboard_id=dash.id)
arguments = {"distinct": False} arguments = {"distinct": False}
uri = f"api/v1/log/recent_activity/{admin_user.id}/?q={prison.dumps(arguments)}" uri = f"api/v1/log/recent_activity/?q={prison.dumps(arguments)}"
rv = self.client.get(uri) rv = self.client.get(uri)
db.session.delete(log) db.session.delete(log)
@ -274,7 +261,7 @@ class TestLogApi(SupersetTestCase):
log.dttm = now - timedelta(days=2) log.dttm = now - timedelta(days=2)
arguments = {"page": 0, "page_size": 2} arguments = {"page": 0, "page_size": 2}
uri = f"api/v1/log/recent_activity/{admin_user.id}/?q={prison.dumps(arguments)}" uri = f"api/v1/log/recent_activity/?q={prison.dumps(arguments)}"
rv = self.client.get(uri) rv = self.client.get(uri)
self.assertEqual(rv.status_code, 200) self.assertEqual(rv.status_code, 200)
@ -304,7 +291,7 @@ class TestLogApi(SupersetTestCase):
) )
arguments = {"page": 1, "page_size": 2} arguments = {"page": 1, "page_size": 2}
uri = f"api/v1/log/recent_activity/{admin_user.id}/?q={prison.dumps(arguments)}" uri = f"api/v1/log/recent_activity/?q={prison.dumps(arguments)}"
rv = self.client.get(uri) rv = self.client.get(uri)
db.session.delete(log) db.session.delete(log)

View File

@ -1350,16 +1350,12 @@ class TestRolePermission(SupersetTestCase):
# make sure that user can create slices and dashboards # make sure that user can create slices and dashboards
self.assert_can_all("Dashboard", perm_set) self.assert_can_all("Dashboard", perm_set)
self.assert_can_all("Chart", perm_set) self.assert_can_all("Chart", perm_set)
self.assertIn(("can_created_dashboards", "Superset"), perm_set)
self.assertIn(("can_created_slices", "Superset"), perm_set)
self.assertIn(("can_csv", "Superset"), perm_set) self.assertIn(("can_csv", "Superset"), perm_set)
self.assertIn(("can_dashboard", "Superset"), perm_set) self.assertIn(("can_dashboard", "Superset"), perm_set)
self.assertIn(("can_explore", "Superset"), perm_set) self.assertIn(("can_explore", "Superset"), perm_set)
self.assertIn(("can_share_chart", "Superset"), perm_set) self.assertIn(("can_share_chart", "Superset"), perm_set)
self.assertIn(("can_share_dashboard", "Superset"), perm_set) self.assertIn(("can_share_dashboard", "Superset"), perm_set)
self.assertIn(("can_explore_json", "Superset"), perm_set) self.assertIn(("can_explore_json", "Superset"), perm_set)
self.assertIn(("can_fave_dashboards", "Superset"), perm_set)
self.assertIn(("can_fave_slices", "Superset"), perm_set)
self.assertIn(("can_explore_json", "Superset"), perm_set) self.assertIn(("can_explore_json", "Superset"), perm_set)
self.assertIn(("can_userinfo", "UserDBModelView"), perm_set) self.assertIn(("can_userinfo", "UserDBModelView"), perm_set)
self.assert_can_menu("Databases", perm_set) self.assert_can_menu("Databases", perm_set)
@ -1525,16 +1521,12 @@ class TestRolePermission(SupersetTestCase):
self.assert_cannot_write("UserDBModelView", gamma_perm_set) self.assert_cannot_write("UserDBModelView", gamma_perm_set)
self.assert_cannot_write("RoleModelView", gamma_perm_set) self.assert_cannot_write("RoleModelView", gamma_perm_set)
self.assertIn(("can_created_dashboards", "Superset"), gamma_perm_set)
self.assertIn(("can_created_slices", "Superset"), gamma_perm_set)
self.assertIn(("can_csv", "Superset"), gamma_perm_set) self.assertIn(("can_csv", "Superset"), gamma_perm_set)
self.assertIn(("can_dashboard", "Superset"), gamma_perm_set) self.assertIn(("can_dashboard", "Superset"), gamma_perm_set)
self.assertIn(("can_explore", "Superset"), gamma_perm_set) self.assertIn(("can_explore", "Superset"), gamma_perm_set)
self.assertIn(("can_share_chart", "Superset"), gamma_perm_set) self.assertIn(("can_share_chart", "Superset"), gamma_perm_set)
self.assertIn(("can_share_dashboard", "Superset"), gamma_perm_set) self.assertIn(("can_share_dashboard", "Superset"), gamma_perm_set)
self.assertIn(("can_explore_json", "Superset"), gamma_perm_set) self.assertIn(("can_explore_json", "Superset"), gamma_perm_set)
self.assertIn(("can_fave_dashboards", "Superset"), gamma_perm_set)
self.assertIn(("can_fave_slices", "Superset"), gamma_perm_set)
self.assertIn(("can_userinfo", "UserDBModelView"), gamma_perm_set) self.assertIn(("can_userinfo", "UserDBModelView"), gamma_perm_set)
def test_views_are_secured(self): def test_views_are_secured(self):