chore: Remove deprecated ENABLE_ACCESS_REQUEST workflow (#24266)

This commit is contained in:
John Bodley 2023-06-09 18:54:31 -07:00 committed by GitHub
parent 4e47771df1
commit 0e3f1f638c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 62 additions and 1061 deletions

View File

@ -191,12 +191,6 @@
|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 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 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|

View File

@ -33,6 +33,7 @@ assists people when migrating to a new version.
### 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`.
- [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.

View File

@ -58,11 +58,6 @@ fetchMock.get(
value: 4,
extra: {},
},
{
text: 'granter',
value: 5,
extra: {},
},
{
text: 'Public',
value: 2,
@ -393,7 +388,7 @@ test('should show all roles', async () => {
const options = await findAllSelectOptions();
expect(options).toHaveLength(6);
expect(options).toHaveLength(5);
expect(options[0]).toHaveTextContent('Admin');
});

View File

@ -50,7 +50,7 @@ const mockRules = [
},
{
id: 5,
name: 'granter',
name: 'Gamma',
},
],
tables: [
@ -79,7 +79,7 @@ const mockRules = [
},
{
id: 5,
name: 'granter',
name: 'Gamma',
},
],
tables: [

View File

@ -1071,10 +1071,6 @@ CONFIG_PATH_ENV_VAR = "SUPERSET_CONFIG_PATH"
# example: FLASK_APP_MUTATOR = lambda x: x.before_request = f
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
EMAIL_NOTIFICATIONS = False # all the emails are sent using dryrun
SMTP_HOST = "localhost"

View File

@ -19,11 +19,11 @@ from abc import ABC
from typing import Any, cast, Optional
import simplejson as json
from flask import current_app, request
from flask_babel import gettext as __, lazy_gettext as _
from flask import request
from flask_babel import lazy_gettext as _
from sqlalchemy.exc import SQLAlchemyError
from superset import db, security_manager
from superset import db
from superset.commands.base import BaseCommand
from superset.connectors.base.models import BaseDatasource
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.exceptions import SupersetException
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.parameters import (
CommandParameters as FormDataCommandParameters,
@ -119,20 +119,6 @@ class GetExploreCommand(BaseCommand, ABC):
except DatasourceNotFound:
pass
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")
if not viz_type and datasource and datasource.default_endpoint:
raise WrongEndpointError(redirect=datasource.default_endpoint)

View File

@ -154,7 +154,6 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
from superset.security.api import SecurityRestApi
from superset.sqllab.api import SqlLabRestApi
from superset.tags.api import TagRestApi
from superset.views.access_requests import AccessRequestsModelView
from superset.views.alerts import AlertView, ReportView
from superset.views.all_entities import TaggedObjectsModelView, TaggedObjectView
from superset.views.annotations import AnnotationLayerView
@ -419,16 +418,6 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
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(
RowLevelSecurityView,
"Row Level Security",

View File

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

View File

@ -14,4 +14,4 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from . import core, datasource_access_request, dynamic_plugins, sql_lab, user_attributes
from . import core, dynamic_plugins, sql_lab, user_attributes

View File

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

View File

@ -164,7 +164,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
ADMIN_ONLY_VIEW_MENUS = {
"Access Requests",
"AccessRequestsModelView",
"Action Log",
"Log",
"List Users",
@ -195,8 +194,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
}
ADMIN_ONLY_PERMISSIONS = {
"can_override_role_permissions",
"can_approve",
"can_update_role",
"all_query_access",
"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("Alpha", self._is_alpha_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)
# Configure public role
@ -981,19 +977,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
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(
self,
mapper: Mapper,

View File

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

View File

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

View File

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

View File

@ -59,9 +59,9 @@ import pandas as pd
import sqlalchemy as sa
from cryptography.hazmat.backends import default_backend
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.security.sqla.models import Role, User
from flask_appbuilder.security.sqla.models import User
from flask_babel import gettext as __
from flask_babel.speaklater import LazyString
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
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
to: str,
subject: str,

View File

@ -15,7 +15,6 @@
# specific language governing permissions and limitations
# under the License.
from . import (
access_requests,
alerts,
api,
base,

View File

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

View File

@ -39,7 +39,6 @@ from flask_appbuilder.security.sqla import models as ab_models
from flask_babel import gettext as __, lazy_gettext as _
from sqlalchemy import and_, or_
from sqlalchemy.exc import DBAPIError, NoSuchModuleError, SQLAlchemyError
from sqlalchemy.orm.session import Session
from superset import (
app,
@ -99,7 +98,6 @@ from superset.extensions import async_query_manager, cache_manager
from superset.jinja_context import get_template_processor
from superset.models.core import Database, FavStar
from superset.models.dashboard import Dashboard
from superset.models.datasource_access_request import DatasourceAccessRequest
from superset.models.slice import Slice
from superset.models.sql_lab import Query, TabState
from superset.models.user_attributes import UserAttribute
@ -175,7 +173,6 @@ from superset.viz import BaseViz
config = app.config
SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT = config["SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT"]
stats_logger = config["STATS_LOGGER"]
DAR = DatasourceAccessRequest
logger = logging.getLogger(__name__)
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
@event_logger.log_this
@expose("/slice/<int:slice_id>/")
@ -888,21 +684,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
except DatasetNotFoundError:
pass
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")
if not viz_type and datasource and datasource.default_endpoint:
return redirect(datasource.default_endpoint)
@ -1893,15 +1674,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
):
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_:
break

View File

@ -16,10 +16,8 @@
# under the License.
# isort:skip_file
"""Unit tests for Superset"""
import json
import unittest
from typing import Optional
from unittest import mock
import pytest
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.connectors.sqla.models import SqlaTable
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.database import get_example_database
@ -84,29 +81,6 @@ DB_ACCESS_ROLE = "db_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):
@classmethod
def setUpClass(cls):
@ -139,386 +113,6 @@ class TestRequestAccess(SupersetTestCase):
db.session.commit()
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(
"username,user_id",

View File

@ -42,7 +42,6 @@ from superset.models import core as models
from superset.models.slice import Slice
from superset.models.core import Database
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.database import get_example_database
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_)
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):
self.client.get("/logout/", follow_redirects=True)

View File

@ -66,7 +66,6 @@ from superset.extensions import async_query_manager, cache_manager
from superset.models import core as models
from superset.models.annotations import Annotation, AnnotationLayer
from superset.models.dashboard import Dashboard
from superset.models.datasource_access_request import DatasourceAccessRequest
from superset.models.slice import Slice
from superset.models.sql_lab import Query
from superset.result_set import SupersetResultSet
@ -87,7 +86,6 @@ logger = logging.getLogger(__name__)
@pytest.fixture(scope="module")
def cleanup():
db.session.query(Query).delete()
db.session.query(DatasourceAccessRequest).delete()
db.session.query(models.Log).delete()
db.session.commit()
yield
@ -232,16 +230,6 @@ class TestCore(SupersetTestCase):
rv = self.client.get(uri)
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 assert_admin_view_menus_in(role_name, assert_func):
role = security_manager.find_role(role_name)

View File

@ -348,7 +348,7 @@ class TestDatasetApi(SupersetTestCase):
"sql": None,
"table_name": "energy_usage",
"template_params": None,
"uid": "2__table",
"uid": ANY,
"datasource_name": "energy_usage",
"name": f"{get_example_default_schema()}.energy_usage",
"column_formats": {},

View File

@ -45,7 +45,6 @@ from superset.utils.core import (
)
from superset.utils.database import get_example_database
from superset.utils.urls import get_url_host
from superset.views.access_requests import AccessRequestsModelView
from .base_tests import SupersetTestCase
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)
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("RoleModelView", 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("RoleModelView", perm_set)
self.assert_can_all("UserDBModelView", 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("List Users", 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(
security_manager._is_admin_only(
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(
SupersetTestCase.is_module_installed("pydruid"), "pydruid not installed"
@ -1534,13 +1512,6 @@ class TestRolePermission(SupersetTestCase):
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):
gamma_perm_set = set()
for perm in security_manager.find_role("Gamma").permissions:
@ -1752,22 +1723,6 @@ class TestSecurityManager(SupersetTestCase):
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):
@patch("superset.security.manager.g")
@patch("superset.security.SupersetSecurityManager.can_access_database")