mirror of https://github.com/apache/superset.git
chore: Remove deprecated ENABLE_ACCESS_REQUEST workflow (#24266)
This commit is contained in:
parent
4e47771df1
commit
0e3f1f638c
|
@ -191,12 +191,6 @@
|
||||||
|can show on AlertLogModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can show on AlertLogModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can list on AlertObservationModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can list on AlertObservationModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can show on AlertObservationModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can show on AlertObservationModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can edit on AccessRequestsModelView|:heavy_check_mark:|O|O|O|
|
|
||||||
|can list on AccessRequestsModelView|:heavy_check_mark:|O|O|O|
|
|
||||||
|can show on AccessRequestsModelView|:heavy_check_mark:|O|O|O|
|
|
||||||
|can add on AccessRequestsModelView|:heavy_check_mark:|O|O|O|
|
|
||||||
|can delete on AccessRequestsModelView|:heavy_check_mark:|O|O|O|
|
|
||||||
|muldelete on AccessRequestsModelView|:heavy_check_mark:|O|O|O|
|
|
||||||
|menu access on Row Level Security|:heavy_check_mark:|O|O|O|
|
|menu access on Row Level Security|:heavy_check_mark:|O|O|O|
|
||||||
|menu access on Access requests|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|menu access on Access requests|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|menu access on Home|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|menu access on Home|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|
|
|
@ -33,6 +33,7 @@ assists people when migrating to a new version.
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
|
- [24266](https://github.com/apache/superset/pull/24266) Remove the `ENABLE_ACCESS_REQUEST` config parameter and the associated request/approval workflows.
|
||||||
- [24330](https://github.com/apache/superset/pull/24330) Removes `getUiOverrideRegistry` from `ExtensionsRegistry`.
|
- [24330](https://github.com/apache/superset/pull/24330) Removes `getUiOverrideRegistry` from `ExtensionsRegistry`.
|
||||||
- [23933](https://github.com/apache/superset/pull/23933) Removes the deprecated Multiple Line Charts.
|
- [23933](https://github.com/apache/superset/pull/23933) Removes the deprecated Multiple Line Charts.
|
||||||
- [23741](https://github.com/apache/superset/pull/23741) Migrates the TreeMap chart and removes the legacy Treemap code.
|
- [23741](https://github.com/apache/superset/pull/23741) Migrates the TreeMap chart and removes the legacy Treemap code.
|
||||||
|
|
|
@ -58,11 +58,6 @@ fetchMock.get(
|
||||||
value: 4,
|
value: 4,
|
||||||
extra: {},
|
extra: {},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: 'granter',
|
|
||||||
value: 5,
|
|
||||||
extra: {},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: 'Public',
|
text: 'Public',
|
||||||
value: 2,
|
value: 2,
|
||||||
|
@ -393,7 +388,7 @@ test('should show all roles', async () => {
|
||||||
|
|
||||||
const options = await findAllSelectOptions();
|
const options = await findAllSelectOptions();
|
||||||
|
|
||||||
expect(options).toHaveLength(6);
|
expect(options).toHaveLength(5);
|
||||||
expect(options[0]).toHaveTextContent('Admin');
|
expect(options[0]).toHaveTextContent('Admin');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ const mockRules = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
name: 'granter',
|
name: 'Gamma',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tables: [
|
tables: [
|
||||||
|
@ -79,7 +79,7 @@ const mockRules = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
name: 'granter',
|
name: 'Gamma',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tables: [
|
tables: [
|
||||||
|
|
|
@ -1071,10 +1071,6 @@ CONFIG_PATH_ENV_VAR = "SUPERSET_CONFIG_PATH"
|
||||||
# example: FLASK_APP_MUTATOR = lambda x: x.before_request = f
|
# example: FLASK_APP_MUTATOR = lambda x: x.before_request = f
|
||||||
FLASK_APP_MUTATOR = None
|
FLASK_APP_MUTATOR = None
|
||||||
|
|
||||||
# Set this to false if you don't want users to be able to request/grant
|
|
||||||
# datasource access requests from/to other users.
|
|
||||||
ENABLE_ACCESS_REQUEST = False
|
|
||||||
|
|
||||||
# smtp server configuration
|
# smtp server configuration
|
||||||
EMAIL_NOTIFICATIONS = False # all the emails are sent using dryrun
|
EMAIL_NOTIFICATIONS = False # all the emails are sent using dryrun
|
||||||
SMTP_HOST = "localhost"
|
SMTP_HOST = "localhost"
|
||||||
|
|
|
@ -19,11 +19,11 @@ from abc import ABC
|
||||||
from typing import Any, cast, Optional
|
from typing import Any, cast, Optional
|
||||||
|
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
from flask import current_app, request
|
from flask import request
|
||||||
from flask_babel import gettext as __, lazy_gettext as _
|
from flask_babel import lazy_gettext as _
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
from superset import db, security_manager
|
from superset import db
|
||||||
from superset.commands.base import BaseCommand
|
from superset.commands.base import BaseCommand
|
||||||
from superset.connectors.base.models import BaseDatasource
|
from superset.connectors.base.models import BaseDatasource
|
||||||
from superset.connectors.sqla.models import SqlaTable
|
from superset.connectors.sqla.models import SqlaTable
|
||||||
|
@ -31,7 +31,7 @@ from superset.dao.exceptions import DatasourceNotFound
|
||||||
from superset.datasource.dao import DatasourceDAO
|
from superset.datasource.dao import DatasourceDAO
|
||||||
from superset.exceptions import SupersetException
|
from superset.exceptions import SupersetException
|
||||||
from superset.explore.commands.parameters import CommandParameters
|
from superset.explore.commands.parameters import CommandParameters
|
||||||
from superset.explore.exceptions import DatasetAccessDeniedError, WrongEndpointError
|
from superset.explore.exceptions import WrongEndpointError
|
||||||
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 (
|
from superset.explore.form_data.commands.parameters import (
|
||||||
CommandParameters as FormDataCommandParameters,
|
CommandParameters as FormDataCommandParameters,
|
||||||
|
@ -119,20 +119,6 @@ class GetExploreCommand(BaseCommand, ABC):
|
||||||
except DatasourceNotFound:
|
except DatasourceNotFound:
|
||||||
pass
|
pass
|
||||||
datasource_name = datasource.name if datasource else _("[Missing Dataset]")
|
datasource_name = datasource.name if datasource else _("[Missing Dataset]")
|
||||||
|
|
||||||
if datasource:
|
|
||||||
if current_app.config["ENABLE_ACCESS_REQUEST"] and (
|
|
||||||
not security_manager.can_access_datasource(datasource)
|
|
||||||
):
|
|
||||||
message = __(
|
|
||||||
security_manager.get_datasource_access_error_msg(datasource)
|
|
||||||
)
|
|
||||||
raise DatasetAccessDeniedError(
|
|
||||||
message=message,
|
|
||||||
datasource_type=self._datasource_type,
|
|
||||||
datasource_id=self._datasource_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
viz_type = form_data.get("viz_type")
|
viz_type = form_data.get("viz_type")
|
||||||
if not viz_type and datasource and datasource.default_endpoint:
|
if not viz_type and datasource and datasource.default_endpoint:
|
||||||
raise WrongEndpointError(redirect=datasource.default_endpoint)
|
raise WrongEndpointError(redirect=datasource.default_endpoint)
|
||||||
|
|
|
@ -154,7 +154,6 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
||||||
from superset.security.api import SecurityRestApi
|
from superset.security.api import SecurityRestApi
|
||||||
from superset.sqllab.api import SqlLabRestApi
|
from superset.sqllab.api import SqlLabRestApi
|
||||||
from superset.tags.api import TagRestApi
|
from superset.tags.api import TagRestApi
|
||||||
from superset.views.access_requests import AccessRequestsModelView
|
|
||||||
from superset.views.alerts import AlertView, ReportView
|
from superset.views.alerts import AlertView, ReportView
|
||||||
from superset.views.all_entities import TaggedObjectsModelView, TaggedObjectView
|
from superset.views.all_entities import TaggedObjectsModelView, TaggedObjectView
|
||||||
from superset.views.annotations import AnnotationLayerView
|
from superset.views.annotations import AnnotationLayerView
|
||||||
|
@ -419,16 +418,6 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
||||||
category_label=__("Manage"),
|
category_label=__("Manage"),
|
||||||
)
|
)
|
||||||
|
|
||||||
appbuilder.add_view(
|
|
||||||
AccessRequestsModelView,
|
|
||||||
"Access requests",
|
|
||||||
label=__("Access requests"),
|
|
||||||
category="Security",
|
|
||||||
category_label=__("Security"),
|
|
||||||
icon="fa-table",
|
|
||||||
menu_cond=lambda: bool(self.config["ENABLE_ACCESS_REQUEST"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
appbuilder.add_view(
|
appbuilder.add_view(
|
||||||
RowLevelSecurityView,
|
RowLevelSecurityView,
|
||||||
"Row Level Security",
|
"Row Level Security",
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""drop access_request
|
||||||
|
|
||||||
|
Revision ID: 83e1abbe777f
|
||||||
|
Revises: ae58e1e58e5c
|
||||||
|
Create Date: 2023-06-01 13:13:18.147362
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "83e1abbe777f"
|
||||||
|
down_revision = "ae58e1e58e5c"
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.drop_table("access_request")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.create_table(
|
||||||
|
"access_request",
|
||||||
|
sa.Column("created_on", sa.DateTime(), nullable=True),
|
||||||
|
sa.Column("changed_on", sa.DateTime(), nullable=True),
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("datasource_type", sa.String(length=200), nullable=True),
|
||||||
|
sa.Column("datasource_id", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("changed_by_fk", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("created_by_fk", sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(["changed_by_fk"], ["ab_user.id"]),
|
||||||
|
sa.ForeignKeyConstraint(["created_by_fk"], ["ab_user.id"]),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
|
@ -14,4 +14,4 @@
|
||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
from . import core, datasource_access_request, dynamic_plugins, sql_lab, user_attributes
|
from . import core, dynamic_plugins, sql_lab, user_attributes
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
|
||||||
|
|
||||||
from flask import Markup
|
|
||||||
from flask_appbuilder import Model
|
|
||||||
from sqlalchemy import Column, Integer, String
|
|
||||||
|
|
||||||
from superset import app, db, security_manager
|
|
||||||
from superset.models.helpers import AuditMixinNullable
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from superset.connectors.base.models import BaseDatasource
|
|
||||||
|
|
||||||
config = app.config
|
|
||||||
|
|
||||||
|
|
||||||
class DatasourceAccessRequest(Model, AuditMixinNullable):
|
|
||||||
"""ORM model for the access requests for datasources and dbs."""
|
|
||||||
|
|
||||||
__tablename__ = "access_request"
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
|
|
||||||
datasource_id = Column(Integer)
|
|
||||||
datasource_type = Column(String(200))
|
|
||||||
|
|
||||||
ROLES_DENYLIST = set(config["ROBOT_PERMISSION_ROLES"])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cls_model(self) -> type["BaseDatasource"]:
|
|
||||||
# pylint: disable=import-outside-toplevel
|
|
||||||
from superset.datasource.dao import DatasourceDAO
|
|
||||||
|
|
||||||
return DatasourceDAO.sources[self.datasource_type]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def username(self) -> Markup:
|
|
||||||
return self.creator()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def datasource(self) -> "BaseDatasource":
|
|
||||||
return self.get_datasource
|
|
||||||
|
|
||||||
@datasource.getter # type: ignore
|
|
||||||
def get_datasource(self) -> "BaseDatasource":
|
|
||||||
ds = db.session.query(self.cls_model).filter_by(id=self.datasource_id).first()
|
|
||||||
return ds
|
|
||||||
|
|
||||||
@property
|
|
||||||
def datasource_link(self) -> Optional[Markup]:
|
|
||||||
return self.datasource.link # pylint: disable=no-member
|
|
||||||
|
|
||||||
@property
|
|
||||||
def roles_with_datasource(self) -> str:
|
|
||||||
action_list = ""
|
|
||||||
perm = self.datasource.perm # pylint: disable=no-member
|
|
||||||
pv = security_manager.find_permission_view_menu("datasource_access", perm)
|
|
||||||
for role in pv.role:
|
|
||||||
if role.name in self.ROLES_DENYLIST:
|
|
||||||
continue
|
|
||||||
href = (
|
|
||||||
f"/superset/approve?datasource_type={self.datasource_type}&"
|
|
||||||
f"datasource_id={self.datasource_id}&"
|
|
||||||
f"created_by={self.created_by.username}&role_to_grant={role.name}"
|
|
||||||
)
|
|
||||||
link = f'<a href="{href}">Grant {role.name} Role</a>'
|
|
||||||
action_list = action_list + "<li>" + link + "</li>"
|
|
||||||
return "<ul>" + action_list + "</ul>"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user_roles(self) -> str:
|
|
||||||
action_list = ""
|
|
||||||
for role in self.created_by.roles:
|
|
||||||
href = (
|
|
||||||
f"/superset/approve?datasource_type={self.datasource_type}&"
|
|
||||||
f"datasource_id={self.datasource_id}&"
|
|
||||||
f"created_by={self.created_by.username}&role_to_extend={role.name}"
|
|
||||||
)
|
|
||||||
link = f'<a href="{href}">Extend {role.name} Role</a>'
|
|
||||||
if role.name in self.ROLES_DENYLIST:
|
|
||||||
link = f"{role.name} Role"
|
|
||||||
action_list = action_list + "<li>" + link + "</li>"
|
|
||||||
return "<ul>" + action_list + "</ul>"
|
|
|
@ -164,7 +164,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
ADMIN_ONLY_VIEW_MENUS = {
|
ADMIN_ONLY_VIEW_MENUS = {
|
||||||
"Access Requests",
|
"Access Requests",
|
||||||
"AccessRequestsModelView",
|
|
||||||
"Action Log",
|
"Action Log",
|
||||||
"Log",
|
"Log",
|
||||||
"List Users",
|
"List Users",
|
||||||
|
@ -195,8 +194,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||||
}
|
}
|
||||||
|
|
||||||
ADMIN_ONLY_PERMISSIONS = {
|
ADMIN_ONLY_PERMISSIONS = {
|
||||||
"can_override_role_permissions",
|
|
||||||
"can_approve",
|
|
||||||
"can_update_role",
|
"can_update_role",
|
||||||
"all_query_access",
|
"all_query_access",
|
||||||
"can_grant_guest_token",
|
"can_grant_guest_token",
|
||||||
|
@ -767,7 +764,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||||
self.set_role("Admin", self._is_admin_pvm)
|
self.set_role("Admin", self._is_admin_pvm)
|
||||||
self.set_role("Alpha", self._is_alpha_pvm)
|
self.set_role("Alpha", self._is_alpha_pvm)
|
||||||
self.set_role("Gamma", self._is_gamma_pvm)
|
self.set_role("Gamma", self._is_gamma_pvm)
|
||||||
self.set_role("granter", self._is_granter_pvm)
|
|
||||||
self.set_role("sql_lab", self._is_sql_lab_pvm)
|
self.set_role("sql_lab", self._is_sql_lab_pvm)
|
||||||
|
|
||||||
# Configure public role
|
# Configure public role
|
||||||
|
@ -981,19 +977,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||||
in self.SQLLAB_EXTRA_PERMISSION_VIEWS
|
in self.SQLLAB_EXTRA_PERMISSION_VIEWS
|
||||||
)
|
)
|
||||||
|
|
||||||
def _is_granter_pvm( # pylint: disable=no-self-use
|
|
||||||
self, pvm: PermissionView
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Return True if the user can grant the FAB permission/view, False
|
|
||||||
otherwise.
|
|
||||||
|
|
||||||
:param pvm: The FAB permission/view
|
|
||||||
:returns: Whether the user can grant the FAB permission/view
|
|
||||||
"""
|
|
||||||
|
|
||||||
return pvm.permission.name in {"can_override_role_permissions", "can_approve"}
|
|
||||||
|
|
||||||
def database_after_insert(
|
def database_after_insert(
|
||||||
self,
|
self,
|
||||||
mapper: Mapper,
|
mapper: Mapper,
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
<!--
|
|
||||||
Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
or more contributor license agreements. See the NOTICE file
|
|
||||||
distributed with this work for additional information
|
|
||||||
regarding copyright ownership. The ASF licenses this file
|
|
||||||
to you under the Apache License, Version 2.0 (the
|
|
||||||
"License"); you may not use this file except in compliance
|
|
||||||
with the License. You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing,
|
|
||||||
software distributed under the License is distributed on an
|
|
||||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
KIND, either express or implied. See the License for the
|
|
||||||
specific language governing permissions and limitations
|
|
||||||
under the License.
|
|
||||||
-->
|
|
||||||
Dear {{ user.username }},
|
|
||||||
<br>
|
|
||||||
<a href={{ url_for('Superset.profile', username=granter.username, _external=True) }}>
|
|
||||||
{{ granter.username }}</a> has extended the role {{ role.name }} to include
|
|
||||||
<a href={{ url_for('ExploreView.root', datasource_type=datasource.type, datasource_id=datasource.id, _external=True) }}>
|
|
||||||
{{datasource.full_name}}</a> and granted you access to it.
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
To see all your permissions please visit your
|
|
||||||
<a href={{ url_for('Superset.profile', username=user.username, _external=True) }}>
|
|
||||||
profile page</a>.
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
Regards, Superset Admin.
|
|
|
@ -1,36 +0,0 @@
|
||||||
<!--
|
|
||||||
Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
or more contributor license agreements. See the NOTICE file
|
|
||||||
distributed with this work for additional information
|
|
||||||
regarding copyright ownership. The ASF licenses this file
|
|
||||||
to you under the Apache License, Version 2.0 (the
|
|
||||||
"License"); you may not use this file except in compliance
|
|
||||||
with the License. You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing,
|
|
||||||
software distributed under the License is distributed on an
|
|
||||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
KIND, either express or implied. See the License for the
|
|
||||||
specific language governing permissions and limitations
|
|
||||||
under the License.
|
|
||||||
-->
|
|
||||||
Dear {{ user.username }},
|
|
||||||
<br>
|
|
||||||
<a href={{ url_for('Superset.profile', username=granter.username, _external=True) }}>
|
|
||||||
{{ granter.username }}</a> has granted you the role {{ role.name }}
|
|
||||||
that gives access to the
|
|
||||||
<a href={{ url_for('ExploreView.root', datasource_type=datasource.type, datasource_id=datasource.id, _external=True) }}>
|
|
||||||
{{datasource.full_name}}</a>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
In addition to that role grants you access to the: {{ role.permissions }}.
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
To see all your permissions please visit your
|
|
||||||
<a href={{ url_for('Superset.profile', username=user.username, _external=True) }}>
|
|
||||||
profile page</a>.
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
Regards, Superset Admin.
|
|
|
@ -1,38 +0,0 @@
|
||||||
{#
|
|
||||||
Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
or more contributor license agreements. See the NOTICE file
|
|
||||||
distributed with this work for additional information
|
|
||||||
regarding copyright ownership. The ASF licenses this file
|
|
||||||
to you under the Apache License, Version 2.0 (the
|
|
||||||
"License"); you may not use this file except in compliance
|
|
||||||
with the License. You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing,
|
|
||||||
software distributed under the License is distributed on an
|
|
||||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
KIND, either express or implied. See the License for the
|
|
||||||
specific language governing permissions and limitations
|
|
||||||
under the License.
|
|
||||||
#}
|
|
||||||
{% extends "superset/basic.html" %}
|
|
||||||
{% block title %}{{ _("No Access!") }}{% endblock %}
|
|
||||||
{% block body %}
|
|
||||||
<div class="container">
|
|
||||||
{% include "superset/flash_wrapper.html" %}
|
|
||||||
<h4>
|
|
||||||
{{ _("You do not have permissions to access the datasource(s): %(name)s.",
|
|
||||||
name=datasource_names)
|
|
||||||
}}
|
|
||||||
</h4>
|
|
||||||
<div>
|
|
||||||
<button onclick="window.location += '&action=go';">
|
|
||||||
{{ _("Request Permissions") }}
|
|
||||||
</button>
|
|
||||||
<button onclick="window.location.href = '/chart/list/';">
|
|
||||||
{{ _("Cancel") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
|
@ -59,9 +59,9 @@ import pandas as pd
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.x509 import Certificate, load_pem_x509_certificate
|
from cryptography.x509 import Certificate, load_pem_x509_certificate
|
||||||
from flask import current_app, flash, g, Markup, render_template, request
|
from flask import current_app, flash, g, Markup, request
|
||||||
from flask_appbuilder import SQLA
|
from flask_appbuilder import SQLA
|
||||||
from flask_appbuilder.security.sqla.models import Role, User
|
from flask_appbuilder.security.sqla.models import User
|
||||||
from flask_babel import gettext as __
|
from flask_babel import gettext as __
|
||||||
from flask_babel.speaklater import LazyString
|
from flask_babel.speaklater import LazyString
|
||||||
from pandas.api.types import infer_dtype
|
from pandas.api.types import infer_dtype
|
||||||
|
@ -852,32 +852,6 @@ def pessimistic_connection_handling(some_engine: Engine) -> None:
|
||||||
connection.should_close_with_result = save_should_close_with_result
|
connection.should_close_with_result = save_should_close_with_result
|
||||||
|
|
||||||
|
|
||||||
def notify_user_about_perm_udate( # pylint: disable=too-many-arguments
|
|
||||||
granter: User,
|
|
||||||
user: User,
|
|
||||||
role: Role,
|
|
||||||
datasource: BaseDatasource,
|
|
||||||
tpl_name: str,
|
|
||||||
config: dict[str, Any],
|
|
||||||
) -> None:
|
|
||||||
msg = render_template(
|
|
||||||
tpl_name, granter=granter, user=user, role=role, datasource=datasource
|
|
||||||
)
|
|
||||||
logger.info(msg)
|
|
||||||
subject = __(
|
|
||||||
"[Superset] Access to the datasource %(name)s was granted",
|
|
||||||
name=datasource.full_name,
|
|
||||||
)
|
|
||||||
send_email_smtp(
|
|
||||||
user.email,
|
|
||||||
subject,
|
|
||||||
msg,
|
|
||||||
config,
|
|
||||||
bcc=granter.email,
|
|
||||||
dryrun=not config["EMAIL_NOTIFICATIONS"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def send_email_smtp( # pylint: disable=invalid-name,too-many-arguments,too-many-locals
|
def send_email_smtp( # pylint: disable=invalid-name,too-many-arguments,too-many-locals
|
||||||
to: str,
|
to: str,
|
||||||
subject: str,
|
subject: str,
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
from . import (
|
from . import (
|
||||||
access_requests,
|
|
||||||
alerts,
|
alerts,
|
||||||
api,
|
api,
|
||||||
base,
|
base,
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
from flask import current_app as app
|
|
||||||
from flask_appbuilder.hooks import before_request
|
|
||||||
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
|
||||||
from flask_babel import lazy_gettext as _
|
|
||||||
from werkzeug.exceptions import NotFound
|
|
||||||
|
|
||||||
from superset.constants import RouteMethod
|
|
||||||
from superset.views.base import DeleteMixin, SupersetModelView
|
|
||||||
from superset.views.core import DAR
|
|
||||||
|
|
||||||
|
|
||||||
class AccessRequestsModelView( # pylint: disable=too-many-ancestors
|
|
||||||
SupersetModelView,
|
|
||||||
DeleteMixin,
|
|
||||||
):
|
|
||||||
datamodel = SQLAInterface(DAR)
|
|
||||||
include_route_methods = RouteMethod.CRUD_SET
|
|
||||||
list_columns = [
|
|
||||||
"username",
|
|
||||||
"user_roles",
|
|
||||||
"datasource_link",
|
|
||||||
"roles_with_datasource",
|
|
||||||
"created_on",
|
|
||||||
]
|
|
||||||
order_columns = ["created_on"]
|
|
||||||
base_order = ("changed_on", "desc")
|
|
||||||
label_columns = {
|
|
||||||
"username": _("User"),
|
|
||||||
"user_roles": _("User Roles"),
|
|
||||||
"database": _("Database URL"),
|
|
||||||
"datasource_link": _("Datasource"),
|
|
||||||
"roles_with_datasource": _("Roles to grant"),
|
|
||||||
"created_on": _("Created On"),
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_enabled() -> bool:
|
|
||||||
return bool(app.config["ENABLE_ACCESS_REQUEST"])
|
|
||||||
|
|
||||||
@before_request
|
|
||||||
def ensure_enabled(self) -> None:
|
|
||||||
if not self.is_enabled():
|
|
||||||
raise NotFound()
|
|
|
@ -39,7 +39,6 @@ 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 import and_, or_
|
||||||
from sqlalchemy.exc import DBAPIError, NoSuchModuleError, SQLAlchemyError
|
from sqlalchemy.exc import DBAPIError, NoSuchModuleError, SQLAlchemyError
|
||||||
from sqlalchemy.orm.session import Session
|
|
||||||
|
|
||||||
from superset import (
|
from superset import (
|
||||||
app,
|
app,
|
||||||
|
@ -99,7 +98,6 @@ from superset.extensions import async_query_manager, cache_manager
|
||||||
from superset.jinja_context import get_template_processor
|
from superset.jinja_context import get_template_processor
|
||||||
from superset.models.core import Database, FavStar
|
from superset.models.core import Database, FavStar
|
||||||
from superset.models.dashboard import Dashboard
|
from superset.models.dashboard import Dashboard
|
||||||
from superset.models.datasource_access_request import DatasourceAccessRequest
|
|
||||||
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
|
||||||
from superset.models.user_attributes import UserAttribute
|
from superset.models.user_attributes import UserAttribute
|
||||||
|
@ -175,7 +173,6 @@ from superset.viz import BaseViz
|
||||||
config = app.config
|
config = app.config
|
||||||
SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT = config["SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT"]
|
SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT = config["SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT"]
|
||||||
stats_logger = config["STATS_LOGGER"]
|
stats_logger = config["STATS_LOGGER"]
|
||||||
DAR = DatasourceAccessRequest
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATABASE_KEYS = [
|
DATABASE_KEYS = [
|
||||||
|
@ -226,207 +223,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@has_access_api
|
|
||||||
@event_logger.log_this
|
|
||||||
@expose("/override_role_permissions/", methods=("POST",))
|
|
||||||
@deprecated()
|
|
||||||
def override_role_permissions(self) -> FlaskResponse:
|
|
||||||
"""Updates the role with the give datasource permissions.
|
|
||||||
|
|
||||||
Permissions not in the request will be revoked. This endpoint should
|
|
||||||
be available to admins only. Expects JSON in the format:
|
|
||||||
{
|
|
||||||
'role_name': '{role_name}',
|
|
||||||
'database': [{
|
|
||||||
'datasource_type': '{table}',
|
|
||||||
'name': '{database_name}',
|
|
||||||
'schema': [{
|
|
||||||
'name': '{schema_name}',
|
|
||||||
'datasources': ['{datasource name}, {datasource name}']
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
data = request.get_json(force=True)
|
|
||||||
role_name = data["role_name"]
|
|
||||||
databases = data["database"]
|
|
||||||
|
|
||||||
db_ds_names = set()
|
|
||||||
for dbs in databases:
|
|
||||||
for schema in dbs["schema"]:
|
|
||||||
for ds_name in schema["datasources"]:
|
|
||||||
fullname = utils.get_datasource_full_name(
|
|
||||||
dbs["name"], ds_name, schema=schema["name"]
|
|
||||||
)
|
|
||||||
db_ds_names.add(fullname)
|
|
||||||
|
|
||||||
existing_datasources = SqlaTable.get_all_datasources(db.session)
|
|
||||||
datasources = [d for d in existing_datasources if d.full_name in db_ds_names]
|
|
||||||
role = security_manager.find_role(role_name)
|
|
||||||
# remove all permissions
|
|
||||||
role.permissions = []
|
|
||||||
# grant permissions to the list of datasources
|
|
||||||
granted_perms = []
|
|
||||||
for datasource in datasources:
|
|
||||||
view_menu_perm = security_manager.find_permission_view_menu(
|
|
||||||
view_menu_name=datasource.perm, permission_name="datasource_access"
|
|
||||||
)
|
|
||||||
# prevent creating empty permissions
|
|
||||||
if view_menu_perm and view_menu_perm.view_menu:
|
|
||||||
role.permissions.append(view_menu_perm)
|
|
||||||
granted_perms.append(view_menu_perm.view_menu.name)
|
|
||||||
db.session.commit()
|
|
||||||
return self.json_response(
|
|
||||||
{"granted": granted_perms, "requested": list(db_ds_names)}, status=201
|
|
||||||
)
|
|
||||||
|
|
||||||
@has_access
|
|
||||||
@event_logger.log_this
|
|
||||||
@expose("/request_access/", methods=("POST",))
|
|
||||||
@deprecated()
|
|
||||||
def request_access(self) -> FlaskResponse:
|
|
||||||
datasources = set()
|
|
||||||
dashboard_id = request.args.get("dashboard_id")
|
|
||||||
if dashboard_id:
|
|
||||||
dash = db.session.query(Dashboard).filter_by(id=int(dashboard_id)).one()
|
|
||||||
datasources |= dash.datasources
|
|
||||||
datasource_id = request.args.get("datasource_id")
|
|
||||||
datasource_type = request.args.get("datasource_type")
|
|
||||||
if datasource_id and datasource_type:
|
|
||||||
ds_class = DatasourceDAO.sources.get(datasource_type)
|
|
||||||
datasource = (
|
|
||||||
db.session.query(ds_class).filter_by(id=int(datasource_id)).one()
|
|
||||||
)
|
|
||||||
datasources.add(datasource)
|
|
||||||
|
|
||||||
has_access_ = all(
|
|
||||||
datasource and security_manager.can_access_datasource(datasource)
|
|
||||||
for datasource in datasources
|
|
||||||
)
|
|
||||||
if has_access_:
|
|
||||||
return redirect(f"/superset/dashboard/{dashboard_id}")
|
|
||||||
|
|
||||||
if request.args.get("action") == "go":
|
|
||||||
for datasource in datasources:
|
|
||||||
access_request = DAR(
|
|
||||||
datasource_id=datasource.id, datasource_type=datasource.type
|
|
||||||
)
|
|
||||||
db.session.add(access_request)
|
|
||||||
db.session.commit()
|
|
||||||
flash(__("Access was requested"), "info")
|
|
||||||
return redirect("/")
|
|
||||||
|
|
||||||
return self.render_template(
|
|
||||||
"superset/request_access.html",
|
|
||||||
datasources=datasources,
|
|
||||||
datasource_names=", ".join([o.name for o in datasources]),
|
|
||||||
)
|
|
||||||
|
|
||||||
@has_access
|
|
||||||
@event_logger.log_this
|
|
||||||
@expose("/approve", methods=("POST",))
|
|
||||||
@deprecated()
|
|
||||||
def approve(self) -> FlaskResponse: # pylint: disable=too-many-locals,no-self-use
|
|
||||||
def clean_fulfilled_requests(session: Session) -> None:
|
|
||||||
for dar in session.query(DAR).all():
|
|
||||||
datasource = DatasourceDAO.get_datasource(
|
|
||||||
session, DatasourceType(dar.datasource_type), dar.datasource_id
|
|
||||||
)
|
|
||||||
if not datasource or security_manager.can_access_datasource(datasource):
|
|
||||||
# Dataset does not exist anymore
|
|
||||||
session.delete(dar)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
datasource_type = request.args["datasource_type"]
|
|
||||||
datasource_id = request.args["datasource_id"]
|
|
||||||
created_by_username = request.args.get("created_by")
|
|
||||||
role_to_grant = request.args.get("role_to_grant")
|
|
||||||
role_to_extend = request.args.get("role_to_extend")
|
|
||||||
|
|
||||||
session = db.session
|
|
||||||
datasource = DatasourceDAO.get_datasource(
|
|
||||||
session, DatasourceType(datasource_type), int(datasource_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not datasource:
|
|
||||||
flash(DATASOURCE_MISSING_ERR, "alert")
|
|
||||||
return json_error_response(DATASOURCE_MISSING_ERR)
|
|
||||||
|
|
||||||
requested_by = security_manager.find_user(username=created_by_username)
|
|
||||||
if not requested_by:
|
|
||||||
flash(USER_MISSING_ERR, "alert")
|
|
||||||
return json_error_response(USER_MISSING_ERR)
|
|
||||||
|
|
||||||
requests = (
|
|
||||||
session.query(DAR)
|
|
||||||
.filter( # pylint: disable=comparison-with-callable
|
|
||||||
DAR.datasource_id == datasource_id,
|
|
||||||
DAR.datasource_type == datasource_type,
|
|
||||||
DAR.created_by_fk == requested_by.id,
|
|
||||||
)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not requests:
|
|
||||||
err = __("The access requests seem to have been deleted")
|
|
||||||
flash(err, "alert")
|
|
||||||
return json_error_response(err)
|
|
||||||
|
|
||||||
# check if you can approve
|
|
||||||
if security_manager.can_access_all_datasources() or security_manager.is_owner(
|
|
||||||
datasource
|
|
||||||
):
|
|
||||||
# can by done by admin only
|
|
||||||
if role_to_grant:
|
|
||||||
role = security_manager.find_role(role_to_grant)
|
|
||||||
requested_by.roles.append(role)
|
|
||||||
msg = __(
|
|
||||||
"%(user)s was granted the role %(role)s that gives access "
|
|
||||||
"to the %(datasource)s",
|
|
||||||
user=requested_by.username,
|
|
||||||
role=role_to_grant,
|
|
||||||
datasource=datasource.full_name,
|
|
||||||
)
|
|
||||||
utils.notify_user_about_perm_udate(
|
|
||||||
g.user,
|
|
||||||
requested_by,
|
|
||||||
role,
|
|
||||||
datasource,
|
|
||||||
"email/role_granted.txt",
|
|
||||||
app.config,
|
|
||||||
)
|
|
||||||
flash(msg, "info")
|
|
||||||
|
|
||||||
if role_to_extend:
|
|
||||||
perm_view = security_manager.find_permission_view_menu(
|
|
||||||
"email/datasource_access", datasource.perm
|
|
||||||
)
|
|
||||||
role = security_manager.find_role(role_to_extend)
|
|
||||||
security_manager.add_permission_role(role, perm_view)
|
|
||||||
msg = __(
|
|
||||||
"Role %(r)s was extended to provide the access to "
|
|
||||||
"the datasource %(ds)s",
|
|
||||||
r=role_to_extend,
|
|
||||||
ds=datasource.full_name,
|
|
||||||
)
|
|
||||||
utils.notify_user_about_perm_udate(
|
|
||||||
g.user,
|
|
||||||
requested_by,
|
|
||||||
role,
|
|
||||||
datasource,
|
|
||||||
"email/role_extended.txt",
|
|
||||||
app.config,
|
|
||||||
)
|
|
||||||
flash(msg, "info")
|
|
||||||
clean_fulfilled_requests(session)
|
|
||||||
else:
|
|
||||||
flash(__("You have no permission to approve this request"), "danger")
|
|
||||||
return redirect("/accessrequestsmodelview/list/")
|
|
||||||
for request_ in requests:
|
|
||||||
session.delete(request_)
|
|
||||||
session.commit()
|
|
||||||
return redirect("/accessrequestsmodelview/list/")
|
|
||||||
|
|
||||||
@has_access
|
@has_access
|
||||||
@event_logger.log_this
|
@event_logger.log_this
|
||||||
@expose("/slice/<int:slice_id>/")
|
@expose("/slice/<int:slice_id>/")
|
||||||
|
@ -888,21 +684,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
||||||
except DatasetNotFoundError:
|
except DatasetNotFoundError:
|
||||||
pass
|
pass
|
||||||
datasource_name = datasource.name if datasource else _("[Missing Dataset]")
|
datasource_name = datasource.name if datasource else _("[Missing Dataset]")
|
||||||
|
|
||||||
if datasource:
|
|
||||||
if config["ENABLE_ACCESS_REQUEST"] and (
|
|
||||||
not security_manager.can_access_datasource(datasource)
|
|
||||||
):
|
|
||||||
flash(
|
|
||||||
__(security_manager.get_datasource_access_error_msg(datasource)),
|
|
||||||
"danger",
|
|
||||||
)
|
|
||||||
return redirect(
|
|
||||||
"superset/request_access/?"
|
|
||||||
f"datasource_type={datasource_type}&"
|
|
||||||
f"datasource_id={datasource_id}&"
|
|
||||||
)
|
|
||||||
|
|
||||||
viz_type = form_data.get("viz_type")
|
viz_type = form_data.get("viz_type")
|
||||||
if not viz_type and datasource and datasource.default_endpoint:
|
if not viz_type and datasource and datasource.default_endpoint:
|
||||||
return redirect(datasource.default_endpoint)
|
return redirect(datasource.default_endpoint)
|
||||||
|
@ -1893,15 +1674,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
||||||
):
|
):
|
||||||
has_access_ = True
|
has_access_ = True
|
||||||
|
|
||||||
if has_access_ is False and config["ENABLE_ACCESS_REQUEST"]:
|
|
||||||
flash(
|
|
||||||
__(security_manager.get_datasource_access_error_msg(datasource)),
|
|
||||||
"danger",
|
|
||||||
)
|
|
||||||
return redirect(
|
|
||||||
f"/superset/request_access/?dashboard_id={dashboard.id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if has_access_:
|
if has_access_:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
@ -16,10 +16,8 @@
|
||||||
# under the License.
|
# under the License.
|
||||||
# isort:skip_file
|
# isort:skip_file
|
||||||
"""Unit tests for Superset"""
|
"""Unit tests for Superset"""
|
||||||
import json
|
|
||||||
import unittest
|
import unittest
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from flask.ctx import AppContext
|
from flask.ctx import AppContext
|
||||||
|
@ -42,7 +40,6 @@ from tests.integration_tests.test_app import app # isort:skip
|
||||||
from superset import db, security_manager
|
from superset import db, security_manager
|
||||||
from superset.connectors.sqla.models import SqlaTable
|
from superset.connectors.sqla.models import SqlaTable
|
||||||
from superset.models import core as models
|
from superset.models import core as models
|
||||||
from superset.models.datasource_access_request import DatasourceAccessRequest
|
|
||||||
from superset.utils.core import get_user_id, get_username, override_user
|
from superset.utils.core import get_user_id, get_username, override_user
|
||||||
from superset.utils.database import get_example_database
|
from superset.utils.database import get_example_database
|
||||||
|
|
||||||
|
@ -84,29 +81,6 @@ DB_ACCESS_ROLE = "db_access_role"
|
||||||
SCHEMA_ACCESS_ROLE = "schema_access_role"
|
SCHEMA_ACCESS_ROLE = "schema_access_role"
|
||||||
|
|
||||||
|
|
||||||
def create_access_request(session, ds_type, ds_name, role_name, username):
|
|
||||||
# TODO: generalize datasource names
|
|
||||||
if ds_type == "table":
|
|
||||||
ds = session.query(SqlaTable).filter(SqlaTable.table_name == ds_name).first()
|
|
||||||
else:
|
|
||||||
# This function will only work for ds_type == "table"
|
|
||||||
raise NotImplementedError()
|
|
||||||
ds_perm_view = security_manager.find_permission_view_menu(
|
|
||||||
"datasource_access", ds.perm
|
|
||||||
)
|
|
||||||
security_manager.add_permission_role(
|
|
||||||
security_manager.find_role(role_name), ds_perm_view
|
|
||||||
)
|
|
||||||
access_request = DatasourceAccessRequest(
|
|
||||||
datasource_id=ds.id,
|
|
||||||
datasource_type=ds_type,
|
|
||||||
created_by_fk=security_manager.find_user(username=username).id,
|
|
||||||
)
|
|
||||||
session.add(access_request)
|
|
||||||
session.commit()
|
|
||||||
return access_request
|
|
||||||
|
|
||||||
|
|
||||||
class TestRequestAccess(SupersetTestCase):
|
class TestRequestAccess(SupersetTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
@ -139,386 +113,6 @@ class TestRequestAccess(SupersetTestCase):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
def test_override_role_permissions_is_admin_only(self):
|
|
||||||
self.logout()
|
|
||||||
self.login("alpha")
|
|
||||||
response = self.client.post(
|
|
||||||
"/superset/override_role_permissions/",
|
|
||||||
data=json.dumps(ROLE_TABLES_PERM_DATA),
|
|
||||||
content_type="application/json",
|
|
||||||
follow_redirects=True,
|
|
||||||
)
|
|
||||||
self.assertNotEqual(405, response.status_code)
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
|
|
||||||
def test_override_role_permissions_1_table(self):
|
|
||||||
database = get_example_database()
|
|
||||||
with database.get_sqla_engine_with_context() as engine:
|
|
||||||
schema = inspect(engine).default_schema_name
|
|
||||||
|
|
||||||
perm_data = ROLE_TABLES_PERM_DATA.copy()
|
|
||||||
perm_data["database"][0]["schema"][0]["name"] = schema
|
|
||||||
|
|
||||||
response = self.client.post(
|
|
||||||
"/superset/override_role_permissions/",
|
|
||||||
data=json.dumps(perm_data),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
self.assertEqual(201, response.status_code)
|
|
||||||
|
|
||||||
updated_override_me = security_manager.find_role("override_me")
|
|
||||||
self.assertEqual(1, len(updated_override_me.permissions))
|
|
||||||
birth_names = self.get_table(name="birth_names")
|
|
||||||
self.assertEqual(
|
|
||||||
birth_names.perm, updated_override_me.permissions[0].view_menu.name
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
"datasource_access", updated_override_me.permissions[0].permission.name
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures(
|
|
||||||
"load_energy_table_with_slice", "load_birth_names_dashboard_with_slices"
|
|
||||||
)
|
|
||||||
def test_override_role_permissions_drops_absent_perms(self):
|
|
||||||
database = get_example_database()
|
|
||||||
with database.get_sqla_engine_with_context() as engine:
|
|
||||||
schema = inspect(engine).default_schema_name
|
|
||||||
|
|
||||||
override_me = security_manager.find_role("override_me")
|
|
||||||
override_me.permissions.append(
|
|
||||||
security_manager.find_permission_view_menu(
|
|
||||||
view_menu_name=self.get_table(name="energy_usage").perm,
|
|
||||||
permission_name="datasource_access",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
db.session.flush()
|
|
||||||
|
|
||||||
perm_data = ROLE_TABLES_PERM_DATA.copy()
|
|
||||||
perm_data["database"][0]["schema"][0]["name"] = schema
|
|
||||||
|
|
||||||
response = self.client.post(
|
|
||||||
"/superset/override_role_permissions/",
|
|
||||||
data=json.dumps(perm_data),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
self.assertEqual(201, response.status_code)
|
|
||||||
updated_override_me = security_manager.find_role("override_me")
|
|
||||||
self.assertEqual(1, len(updated_override_me.permissions))
|
|
||||||
birth_names = self.get_table(name="birth_names")
|
|
||||||
self.assertEqual(
|
|
||||||
birth_names.perm, updated_override_me.permissions[0].view_menu.name
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
"datasource_access", updated_override_me.permissions[0].permission.name
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_clean_requests_after_role_extend(self):
|
|
||||||
session = db.session
|
|
||||||
|
|
||||||
# Case 1. Gamma and gamma2 requested test_role1 on energy_usage access
|
|
||||||
# Gamma already has role test_role1
|
|
||||||
# Extend test_role1 with energy_usage access for gamma2
|
|
||||||
# Check if access request for gamma at energy_usage was deleted
|
|
||||||
|
|
||||||
# gamma2 and gamma request table_role on energy usage
|
|
||||||
if app.config["ENABLE_ACCESS_REQUEST"]:
|
|
||||||
access_request1 = create_access_request(
|
|
||||||
session, "table", "random_time_series", TEST_ROLE_1, "gamma2"
|
|
||||||
)
|
|
||||||
ds_1_id = access_request1.datasource_id
|
|
||||||
create_access_request(
|
|
||||||
session, "table", "random_time_series", TEST_ROLE_1, "gamma"
|
|
||||||
)
|
|
||||||
access_requests = self.get_access_requests("gamma", "table", ds_1_id)
|
|
||||||
self.assertTrue(access_requests)
|
|
||||||
# gamma gets test_role1
|
|
||||||
self.get_resp(
|
|
||||||
GRANT_ROLE_REQUEST.format("table", ds_1_id, "gamma", TEST_ROLE_1)
|
|
||||||
)
|
|
||||||
# extend test_role1 with access on energy usage
|
|
||||||
self.client.get(
|
|
||||||
EXTEND_ROLE_REQUEST.format("table", ds_1_id, "gamma2", TEST_ROLE_1)
|
|
||||||
)
|
|
||||||
access_requests = self.get_access_requests("gamma", "table", ds_1_id)
|
|
||||||
self.assertFalse(access_requests)
|
|
||||||
|
|
||||||
gamma_user = security_manager.find_user(username="gamma")
|
|
||||||
gamma_user.roles.remove(security_manager.find_role("test_role1"))
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
|
|
||||||
def test_clean_requests_after_alpha_grant(self):
|
|
||||||
session = db.session
|
|
||||||
|
|
||||||
# Case 2. Two access requests from gamma and gamma2
|
|
||||||
# Gamma becomes alpha, gamma2 gets granted
|
|
||||||
# Check if request by gamma has been deleted
|
|
||||||
|
|
||||||
access_request1 = create_access_request(
|
|
||||||
session, "table", "birth_names", TEST_ROLE_1, "gamma"
|
|
||||||
)
|
|
||||||
create_access_request(session, "table", "birth_names", TEST_ROLE_2, "gamma2")
|
|
||||||
ds_1_id = access_request1.datasource_id
|
|
||||||
# gamma becomes alpha
|
|
||||||
alpha_role = security_manager.find_role("Alpha")
|
|
||||||
gamma_user = security_manager.find_user(username="gamma")
|
|
||||||
gamma_user.roles.append(alpha_role)
|
|
||||||
session.commit()
|
|
||||||
access_requests = self.get_access_requests("gamma", "table", ds_1_id)
|
|
||||||
self.assertTrue(access_requests)
|
|
||||||
self.client.post(
|
|
||||||
EXTEND_ROLE_REQUEST.format("table", ds_1_id, "gamma2", TEST_ROLE_2)
|
|
||||||
)
|
|
||||||
access_requests = self.get_access_requests("gamma", "table", ds_1_id)
|
|
||||||
self.assertFalse(access_requests)
|
|
||||||
|
|
||||||
gamma_user = security_manager.find_user(username="gamma")
|
|
||||||
gamma_user.roles.remove(security_manager.find_role("Alpha"))
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("load_energy_table_with_slice")
|
|
||||||
def test_clean_requests_after_db_grant(self):
|
|
||||||
session = db.session
|
|
||||||
|
|
||||||
# Case 3. Two access requests from gamma and gamma2
|
|
||||||
# Gamma gets database access, gamma2 access request granted
|
|
||||||
# Check if request by gamma has been deleted
|
|
||||||
|
|
||||||
gamma_user = security_manager.find_user(username="gamma")
|
|
||||||
access_request1 = create_access_request(
|
|
||||||
session, "table", "energy_usage", TEST_ROLE_1, "gamma"
|
|
||||||
)
|
|
||||||
create_access_request(session, "table", "energy_usage", TEST_ROLE_2, "gamma2")
|
|
||||||
ds_1_id = access_request1.datasource_id
|
|
||||||
# gamma gets granted database access
|
|
||||||
database = session.query(models.Database).first()
|
|
||||||
|
|
||||||
security_manager.add_permission_view_menu("database_access", database.perm)
|
|
||||||
ds_perm_view = security_manager.find_permission_view_menu(
|
|
||||||
"database_access", database.perm
|
|
||||||
)
|
|
||||||
security_manager.add_permission_role(
|
|
||||||
security_manager.find_role(DB_ACCESS_ROLE), ds_perm_view
|
|
||||||
)
|
|
||||||
gamma_user.roles.append(security_manager.find_role(DB_ACCESS_ROLE))
|
|
||||||
session.commit()
|
|
||||||
access_requests = self.get_access_requests("gamma", "table", ds_1_id)
|
|
||||||
self.assertTrue(access_requests)
|
|
||||||
# gamma2 request gets fulfilled
|
|
||||||
self.client.post(
|
|
||||||
EXTEND_ROLE_REQUEST.format("table", ds_1_id, "gamma2", TEST_ROLE_2)
|
|
||||||
)
|
|
||||||
access_requests = self.get_access_requests("gamma", "table", ds_1_id)
|
|
||||||
|
|
||||||
self.assertFalse(access_requests)
|
|
||||||
gamma_user = security_manager.find_user(username="gamma")
|
|
||||||
gamma_user.roles.remove(security_manager.find_role(DB_ACCESS_ROLE))
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
|
|
||||||
def test_clean_requests_after_schema_grant(self):
|
|
||||||
session = db.session
|
|
||||||
|
|
||||||
# Case 4. Two access requests from gamma and gamma2
|
|
||||||
# Gamma gets schema access, gamma2 access request granted
|
|
||||||
# Check if request by gamma has been deleted
|
|
||||||
|
|
||||||
gamma_user = security_manager.find_user(username="gamma")
|
|
||||||
access_request1 = create_access_request(
|
|
||||||
session, "table", "wb_health_population", TEST_ROLE_1, "gamma"
|
|
||||||
)
|
|
||||||
create_access_request(
|
|
||||||
session, "table", "wb_health_population", TEST_ROLE_2, "gamma2"
|
|
||||||
)
|
|
||||||
ds_1_id = access_request1.datasource_id
|
|
||||||
ds = (
|
|
||||||
session.query(SqlaTable)
|
|
||||||
.filter_by(table_name="wb_health_population")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
original_schema = ds.schema
|
|
||||||
|
|
||||||
ds.schema = "temp_schema"
|
|
||||||
security_manager.add_permission_view_menu("schema_access", ds.schema_perm)
|
|
||||||
schema_perm_view = security_manager.find_permission_view_menu(
|
|
||||||
"schema_access", ds.schema_perm
|
|
||||||
)
|
|
||||||
security_manager.add_permission_role(
|
|
||||||
security_manager.find_role(SCHEMA_ACCESS_ROLE), schema_perm_view
|
|
||||||
)
|
|
||||||
gamma_user.roles.append(security_manager.find_role(SCHEMA_ACCESS_ROLE))
|
|
||||||
session.commit()
|
|
||||||
# gamma2 request gets fulfilled
|
|
||||||
self.client.post(
|
|
||||||
EXTEND_ROLE_REQUEST.format("table", ds_1_id, "gamma2", TEST_ROLE_2)
|
|
||||||
)
|
|
||||||
access_requests = self.get_access_requests("gamma", "table", ds_1_id)
|
|
||||||
self.assertFalse(access_requests)
|
|
||||||
gamma_user = security_manager.find_user(username="gamma")
|
|
||||||
gamma_user.roles.remove(security_manager.find_role(SCHEMA_ACCESS_ROLE))
|
|
||||||
|
|
||||||
ds.schema = original_schema
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@mock.patch("superset.utils.core.send_mime_email")
|
|
||||||
def test_approve(self, mock_send_mime):
|
|
||||||
if app.config["ENABLE_ACCESS_REQUEST"]:
|
|
||||||
session = db.session
|
|
||||||
TEST_ROLE_NAME = "table_role"
|
|
||||||
security_manager.add_role(TEST_ROLE_NAME)
|
|
||||||
|
|
||||||
# Case 1. Grant new role to the user.
|
|
||||||
|
|
||||||
access_request1 = create_access_request(
|
|
||||||
session, "table", "unicode_test", TEST_ROLE_NAME, "gamma"
|
|
||||||
)
|
|
||||||
ds_1_id = access_request1.datasource_id
|
|
||||||
self.get_resp(
|
|
||||||
GRANT_ROLE_REQUEST.format("table", ds_1_id, "gamma", TEST_ROLE_NAME)
|
|
||||||
)
|
|
||||||
# Test email content.
|
|
||||||
self.assertTrue(mock_send_mime.called)
|
|
||||||
call_args = mock_send_mime.call_args[0]
|
|
||||||
self.assertEqual(
|
|
||||||
[
|
|
||||||
security_manager.find_user(username="gamma").email,
|
|
||||||
security_manager.find_user(username="admin").email,
|
|
||||||
],
|
|
||||||
call_args[1],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
"[Superset] Access to the datasource {} was granted".format(
|
|
||||||
self.get_table_by_id(ds_1_id).full_name
|
|
||||||
),
|
|
||||||
call_args[2]["Subject"],
|
|
||||||
)
|
|
||||||
self.assertIn(TEST_ROLE_NAME, call_args[2].as_string())
|
|
||||||
self.assertIn("unicode_test", call_args[2].as_string())
|
|
||||||
|
|
||||||
access_requests = self.get_access_requests("gamma", "table", ds_1_id)
|
|
||||||
# request was removed
|
|
||||||
self.assertFalse(access_requests)
|
|
||||||
# user was granted table_role
|
|
||||||
user_roles = [r.name for r in security_manager.find_user("gamma").roles]
|
|
||||||
self.assertIn(TEST_ROLE_NAME, user_roles)
|
|
||||||
|
|
||||||
# Case 2. Extend the role to have access to the table
|
|
||||||
|
|
||||||
access_request2 = create_access_request(
|
|
||||||
session, "table", "energy_usage", TEST_ROLE_NAME, "gamma"
|
|
||||||
)
|
|
||||||
ds_2_id = access_request2.datasource_id
|
|
||||||
energy_usage_perm = access_request2.datasource.perm
|
|
||||||
|
|
||||||
self.client.get(
|
|
||||||
EXTEND_ROLE_REQUEST.format(
|
|
||||||
"table", access_request2.datasource_id, "gamma", TEST_ROLE_NAME
|
|
||||||
)
|
|
||||||
)
|
|
||||||
access_requests = self.get_access_requests("gamma", "table", ds_2_id)
|
|
||||||
|
|
||||||
# Test email content.
|
|
||||||
self.assertTrue(mock_send_mime.called)
|
|
||||||
call_args = mock_send_mime.call_args[0]
|
|
||||||
self.assertEqual(
|
|
||||||
[
|
|
||||||
security_manager.find_user(username="gamma").email,
|
|
||||||
security_manager.find_user(username="admin").email,
|
|
||||||
],
|
|
||||||
call_args[1],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
"[Superset] Access to the datasource {} was granted".format(
|
|
||||||
self.get_table_by_id(ds_2_id).full_name
|
|
||||||
),
|
|
||||||
call_args[2]["Subject"],
|
|
||||||
)
|
|
||||||
self.assertIn(TEST_ROLE_NAME, call_args[2].as_string())
|
|
||||||
self.assertIn("energy_usage", call_args[2].as_string())
|
|
||||||
|
|
||||||
# request was removed
|
|
||||||
self.assertFalse(access_requests)
|
|
||||||
# table_role was extended to grant access to the energy_usage table/
|
|
||||||
perm_view = security_manager.find_permission_view_menu(
|
|
||||||
"datasource_access", energy_usage_perm
|
|
||||||
)
|
|
||||||
TEST_ROLE = security_manager.find_role(TEST_ROLE_NAME)
|
|
||||||
self.assertIn(perm_view, TEST_ROLE.permissions)
|
|
||||||
|
|
||||||
def test_request_access(self):
|
|
||||||
if app.config["ENABLE_ACCESS_REQUEST"]:
|
|
||||||
session = db.session
|
|
||||||
self.logout()
|
|
||||||
self.login(username="gamma")
|
|
||||||
gamma_user = security_manager.find_user(username="gamma")
|
|
||||||
security_manager.add_role("dummy_role")
|
|
||||||
gamma_user.roles.append(security_manager.find_role("dummy_role"))
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
ACCESS_REQUEST = (
|
|
||||||
"/superset/request_access?"
|
|
||||||
"datasource_type={}&"
|
|
||||||
"datasource_id={}&"
|
|
||||||
"action={}&"
|
|
||||||
)
|
|
||||||
ROLE_GRANT_LINK = (
|
|
||||||
'<a href="/superset/approve?datasource_type={}&datasource_id={}&'
|
|
||||||
'created_by={}&role_to_grant={}">Grant {} Role</a>'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Request table access, there are no roles have this table.
|
|
||||||
|
|
||||||
table1 = (
|
|
||||||
session.query(SqlaTable)
|
|
||||||
.filter_by(table_name="random_time_series")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
table_1_id = table1.id
|
|
||||||
|
|
||||||
# request access to the table
|
|
||||||
resp = self.get_resp(ACCESS_REQUEST.format("table", table_1_id, "go"))
|
|
||||||
assert "Access was requested" in resp
|
|
||||||
access_request1 = self.get_access_requests("gamma", "table", table_1_id)
|
|
||||||
assert access_request1 is not None
|
|
||||||
|
|
||||||
# Request access, roles exist that contains the table.
|
|
||||||
# add table to the existing roles
|
|
||||||
table3 = (
|
|
||||||
session.query(SqlaTable).filter_by(table_name="energy_usage").first()
|
|
||||||
)
|
|
||||||
table_3_id = table3.id
|
|
||||||
table3_perm = table3.perm
|
|
||||||
|
|
||||||
security_manager.add_role("energy_usage_role")
|
|
||||||
alpha_role = security_manager.find_role("Alpha")
|
|
||||||
security_manager.add_permission_role(
|
|
||||||
alpha_role,
|
|
||||||
security_manager.find_permission_view_menu(
|
|
||||||
"datasource_access", table3_perm
|
|
||||||
),
|
|
||||||
)
|
|
||||||
security_manager.add_permission_role(
|
|
||||||
security_manager.find_role("energy_usage_role"),
|
|
||||||
security_manager.find_permission_view_menu(
|
|
||||||
"datasource_access", table3_perm
|
|
||||||
),
|
|
||||||
)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
self.get_resp(ACCESS_REQUEST.format("table", table_3_id, "go"))
|
|
||||||
access_request3 = self.get_access_requests("gamma", "table", table_3_id)
|
|
||||||
approve_link_3 = ROLE_GRANT_LINK.format(
|
|
||||||
"table", table_3_id, "gamma", "energy_usage_role", "energy_usage_role"
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
access_request3.roles_with_datasource,
|
|
||||||
f"<ul><li>{approve_link_3}</li></ul>",
|
|
||||||
)
|
|
||||||
|
|
||||||
# cleanup
|
|
||||||
gamma_user = security_manager.find_user(username="gamma")
|
|
||||||
gamma_user.roles.remove(security_manager.find_role("dummy_role"))
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"username,user_id",
|
"username,user_id",
|
||||||
|
|
|
@ -42,7 +42,6 @@ from superset.models import core as models
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
from superset.models.core import Database
|
from superset.models.core import Database
|
||||||
from superset.models.dashboard import Dashboard
|
from superset.models.dashboard import Dashboard
|
||||||
from superset.models.datasource_access_request import DatasourceAccessRequest
|
|
||||||
from superset.utils.core import get_example_default_schema
|
from superset.utils.core import get_example_default_schema
|
||||||
from superset.utils.database import get_example_database
|
from superset.utils.database import get_example_database
|
||||||
from superset.views.base_api import BaseSupersetModelRestApi
|
from superset.views.base_api import BaseSupersetModelRestApi
|
||||||
|
@ -268,18 +267,6 @@ class SupersetTestCase(TestCase):
|
||||||
resp = self.get_resp(url, data, follow_redirects, raise_on_error, json_)
|
resp = self.get_resp(url, data, follow_redirects, raise_on_error, json_)
|
||||||
return json.loads(resp)
|
return json.loads(resp)
|
||||||
|
|
||||||
def get_access_requests(self, username, ds_type, ds_id):
|
|
||||||
DAR = DatasourceAccessRequest
|
|
||||||
return (
|
|
||||||
db.session.query(DAR)
|
|
||||||
.filter(
|
|
||||||
DAR.created_by == security_manager.find_user(username=username),
|
|
||||||
DAR.datasource_type == ds_type,
|
|
||||||
DAR.datasource_id == ds_id,
|
|
||||||
)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
def logout(self):
|
def logout(self):
|
||||||
self.client.get("/logout/", follow_redirects=True)
|
self.client.get("/logout/", follow_redirects=True)
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,6 @@ from superset.extensions import async_query_manager, cache_manager
|
||||||
from superset.models import core as models
|
from superset.models import core as models
|
||||||
from superset.models.annotations import Annotation, AnnotationLayer
|
from superset.models.annotations import Annotation, AnnotationLayer
|
||||||
from superset.models.dashboard import Dashboard
|
from superset.models.dashboard import Dashboard
|
||||||
from superset.models.datasource_access_request import DatasourceAccessRequest
|
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
from superset.models.sql_lab import Query
|
from superset.models.sql_lab import Query
|
||||||
from superset.result_set import SupersetResultSet
|
from superset.result_set import SupersetResultSet
|
||||||
|
@ -87,7 +86,6 @@ logger = logging.getLogger(__name__)
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def cleanup():
|
def cleanup():
|
||||||
db.session.query(Query).delete()
|
db.session.query(Query).delete()
|
||||||
db.session.query(DatasourceAccessRequest).delete()
|
|
||||||
db.session.query(models.Log).delete()
|
db.session.query(models.Log).delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
yield
|
yield
|
||||||
|
@ -232,16 +230,6 @@ class TestCore(SupersetTestCase):
|
||||||
rv = self.client.get(uri)
|
rv = self.client.get(uri)
|
||||||
self.assertEqual(rv.status_code, 422)
|
self.assertEqual(rv.status_code, 422)
|
||||||
|
|
||||||
def test_admin_only_permissions(self):
|
|
||||||
def assert_admin_permission_in(role_name, assert_func):
|
|
||||||
role = security_manager.find_role(role_name)
|
|
||||||
permissions = [p.permission.name for p in role.permissions]
|
|
||||||
assert_func("can_approve", permissions)
|
|
||||||
|
|
||||||
assert_admin_permission_in("Admin", self.assertIn)
|
|
||||||
assert_admin_permission_in("Alpha", self.assertNotIn)
|
|
||||||
assert_admin_permission_in("Gamma", self.assertNotIn)
|
|
||||||
|
|
||||||
def test_admin_only_menu_views(self):
|
def test_admin_only_menu_views(self):
|
||||||
def assert_admin_view_menus_in(role_name, assert_func):
|
def assert_admin_view_menus_in(role_name, assert_func):
|
||||||
role = security_manager.find_role(role_name)
|
role = security_manager.find_role(role_name)
|
||||||
|
|
|
@ -348,7 +348,7 @@ class TestDatasetApi(SupersetTestCase):
|
||||||
"sql": None,
|
"sql": None,
|
||||||
"table_name": "energy_usage",
|
"table_name": "energy_usage",
|
||||||
"template_params": None,
|
"template_params": None,
|
||||||
"uid": "2__table",
|
"uid": ANY,
|
||||||
"datasource_name": "energy_usage",
|
"datasource_name": "energy_usage",
|
||||||
"name": f"{get_example_default_schema()}.energy_usage",
|
"name": f"{get_example_default_schema()}.energy_usage",
|
||||||
"column_formats": {},
|
"column_formats": {},
|
||||||
|
|
|
@ -45,7 +45,6 @@ from superset.utils.core import (
|
||||||
)
|
)
|
||||||
from superset.utils.database import get_example_database
|
from superset.utils.database import get_example_database
|
||||||
from superset.utils.urls import get_url_host
|
from superset.utils.urls import get_url_host
|
||||||
from superset.views.access_requests import AccessRequestsModelView
|
|
||||||
|
|
||||||
from .base_tests import SupersetTestCase
|
from .base_tests import SupersetTestCase
|
||||||
from tests.integration_tests.fixtures.public_role import (
|
from tests.integration_tests.fixtures.public_role import (
|
||||||
|
@ -1386,9 +1385,6 @@ class TestRolePermission(SupersetTestCase):
|
||||||
self.assertIn(("all_datasource_access", "all_datasource_access"), perm_set)
|
self.assertIn(("all_datasource_access", "all_datasource_access"), perm_set)
|
||||||
|
|
||||||
def assert_cannot_alpha(self, perm_set):
|
def assert_cannot_alpha(self, perm_set):
|
||||||
if app.config["ENABLE_ACCESS_REQUEST"]:
|
|
||||||
self.assert_cannot_write("AccessRequestsModelView", perm_set)
|
|
||||||
self.assert_can_all("AccessRequestsModelView", perm_set)
|
|
||||||
self.assert_cannot_write("Queries", perm_set)
|
self.assert_cannot_write("Queries", perm_set)
|
||||||
self.assert_cannot_write("RoleModelView", perm_set)
|
self.assert_cannot_write("RoleModelView", perm_set)
|
||||||
self.assert_cannot_write("UserDBModelView", perm_set)
|
self.assert_cannot_write("UserDBModelView", perm_set)
|
||||||
|
@ -1398,12 +1394,7 @@ class TestRolePermission(SupersetTestCase):
|
||||||
self.assert_can_all("Database", perm_set)
|
self.assert_can_all("Database", perm_set)
|
||||||
self.assert_can_all("RoleModelView", perm_set)
|
self.assert_can_all("RoleModelView", perm_set)
|
||||||
self.assert_can_all("UserDBModelView", perm_set)
|
self.assert_can_all("UserDBModelView", perm_set)
|
||||||
|
|
||||||
self.assertIn(("all_database_access", "all_database_access"), perm_set)
|
self.assertIn(("all_database_access", "all_database_access"), perm_set)
|
||||||
self.assertIn(("can_override_role_permissions", "Superset"), perm_set)
|
|
||||||
self.assertIn(("can_override_role_permissions", "Superset"), perm_set)
|
|
||||||
self.assertIn(("can_approve", "Superset"), perm_set)
|
|
||||||
|
|
||||||
self.assert_can_menu("Security", perm_set)
|
self.assert_can_menu("Security", perm_set)
|
||||||
self.assert_can_menu("List Users", perm_set)
|
self.assert_can_menu("List Users", perm_set)
|
||||||
self.assert_can_menu("List Roles", perm_set)
|
self.assert_can_menu("List Roles", perm_set)
|
||||||
|
@ -1430,14 +1421,6 @@ class TestRolePermission(SupersetTestCase):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if app.config["ENABLE_ACCESS_REQUEST"]:
|
|
||||||
self.assertTrue(
|
|
||||||
security_manager._is_admin_only(
|
|
||||||
security_manager.find_permission_view_menu(
|
|
||||||
"can_list", "AccessRequestsModelView"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
security_manager._is_admin_only(
|
security_manager._is_admin_only(
|
||||||
security_manager.find_permission_view_menu(
|
security_manager.find_permission_view_menu(
|
||||||
|
@ -1445,11 +1428,6 @@ class TestRolePermission(SupersetTestCase):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
|
||||||
security_manager._is_admin_only(
|
|
||||||
security_manager.find_permission_view_menu("can_approve", "Superset")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@unittest.skipUnless(
|
@unittest.skipUnless(
|
||||||
SupersetTestCase.is_module_installed("pydruid"), "pydruid not installed"
|
SupersetTestCase.is_module_installed("pydruid"), "pydruid not installed"
|
||||||
|
@ -1534,13 +1512,6 @@ class TestRolePermission(SupersetTestCase):
|
||||||
|
|
||||||
self.assert_cannot_alpha(sql_lab_set)
|
self.assert_cannot_alpha(sql_lab_set)
|
||||||
|
|
||||||
def test_granter_permissions(self):
|
|
||||||
granter_set = get_perm_tuples("granter")
|
|
||||||
self.assertIn(("can_override_role_permissions", "Superset"), granter_set)
|
|
||||||
self.assertIn(("can_approve", "Superset"), granter_set)
|
|
||||||
|
|
||||||
self.assert_cannot_alpha(granter_set)
|
|
||||||
|
|
||||||
def test_gamma_permissions(self):
|
def test_gamma_permissions(self):
|
||||||
gamma_perm_set = set()
|
gamma_perm_set = set()
|
||||||
for perm in security_manager.find_role("Gamma").permissions:
|
for perm in security_manager.find_role("Gamma").permissions:
|
||||||
|
@ -1752,22 +1723,6 @@ class TestSecurityManager(SupersetTestCase):
|
||||||
self.assertEqual([security_manager.get_public_role()], roles)
|
self.assertEqual([security_manager.get_public_role()], roles)
|
||||||
|
|
||||||
|
|
||||||
class TestAccessRequestEndpoints(SupersetTestCase):
|
|
||||||
def test_access_request_disabled(self):
|
|
||||||
with patch.object(AccessRequestsModelView, "is_enabled", return_value=False):
|
|
||||||
self.login("admin")
|
|
||||||
uri = "/accessrequestsmodelview/list/"
|
|
||||||
rv = self.client.get(uri)
|
|
||||||
self.assertEqual(rv.status_code, 404)
|
|
||||||
|
|
||||||
def test_access_request_enabled(self):
|
|
||||||
with patch.object(AccessRequestsModelView, "is_enabled", return_value=True):
|
|
||||||
self.login("admin")
|
|
||||||
uri = "/accessrequestsmodelview/list/"
|
|
||||||
rv = self.client.get(uri)
|
|
||||||
self.assertLess(rv.status_code, 400)
|
|
||||||
|
|
||||||
|
|
||||||
class TestDatasources(SupersetTestCase):
|
class TestDatasources(SupersetTestCase):
|
||||||
@patch("superset.security.manager.g")
|
@patch("superset.security.manager.g")
|
||||||
@patch("superset.security.SupersetSecurityManager.can_access_database")
|
@patch("superset.security.SupersetSecurityManager.can_access_database")
|
||||||
|
|
Loading…
Reference in New Issue