From ab6ae456daecd34e676273fa4d43c52875e00e94 Mon Sep 17 00:00:00 2001 From: altef Date: Wed, 27 Nov 2019 23:31:40 +0000 Subject: [PATCH] Support and apply filters. --- superset/connectors/sqla/models.py | 10 +++- ...36a828f_add_row_level_security_table_py.py | 56 +++++++++++++++++++ superset/models/core.py | 13 +++++ superset/security.py | 10 ++++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 superset/migrations/versions/415f336a828f_add_row_level_security_table_py.py diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 93b1f441864d9..ed49b75fe65a3 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -55,7 +55,7 @@ from superset.exceptions import DatabaseNotFound from superset.jinja_context import get_template_processor from superset.models.annotations import Annotation -from superset.models.core import Database +from superset.models.core import Database, RowLevelSecurityFilter from superset.models.helpers import QueryResult from superset.utils import core as utils, import_datasource @@ -363,6 +363,7 @@ class SqlaTable(Model, BaseDatasource): sql = Column(Text) is_sqllab_view = Column(Boolean, default=False) template_params = Column(Text) + row_level_security_filters = relationship(RowLevelSecurityFilter, backref="tables", lazy=True) baselink = "tablemodelview" @@ -977,6 +978,13 @@ def _get_top_groups( return or_(*groups) def query(self, query_obj: Dict) -> QueryResult: + filters = security_manager.get_row_level_security_filters(self) # Self is the Table model + if len(filters) > 0: + where = query_obj['extras']['where'] + if len(where) > 0: + where = "({}) AND ".format(where) + query_obj['extras']['where'] = "{} {}".format(where, " AND ".join(filters)) + qry_start_dttm = datetime.now() query_str_ext = self.get_query_str_extended(query_obj) sql = query_str_ext.sql diff --git a/superset/migrations/versions/415f336a828f_add_row_level_security_table_py.py b/superset/migrations/versions/415f336a828f_add_row_level_security_table_py.py new file mode 100644 index 0000000000000..ba058ce246b80 --- /dev/null +++ b/superset/migrations/versions/415f336a828f_add_row_level_security_table_py.py @@ -0,0 +1,56 @@ +# 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. +"""add_row_level_security_table.py + +Revision ID: 415f336a828f +Revises: db4b49eb0782 +Create Date: 2019-11-27 21:09:00.650139 + +""" + +# revision identifiers, used by Alembic. +revision = '415f336a828f' +down_revision = 'db4b49eb0782' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('row_level_security_filters', + 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('role_id', sa.Integer(), nullable=False), + sa.Column('table_id', sa.Integer(), nullable=False), + sa.Column('clause', sa.Text(), nullable=False), + sa.Column('created_by_fk', sa.Integer(), nullable=True), + sa.Column('changed_by_fk', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['ab_role.id'], ), + sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('row_level_security_filters') + # ### end Alembic commands ### diff --git a/superset/models/core.py b/superset/models/core.py index d378248edc0f6..ab080f3aaa751 100755 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -1353,6 +1353,19 @@ def user_roles(self) -> str: return "" +class RowLevelSecurityFilter(Model, AuditMixinNullable): + """ + Custom where clauses attached to Tables and Roles. + """ + + __tablename__ = "row_level_security_filters" + id = Column(Integer, primary_key=True) # pylint: disable=invalid-name + role_id = Column(Integer, ForeignKey("ab_role.id"), nullable=False) + table_id = Column(Integer, ForeignKey("tables.id"), nullable=False) + clause = Column(Text, nullable=False) + # Also look up the backref stuff to go into the table model?? + + # events for updating tags if is_feature_enabled("TAGGING_SYSTEM"): sqla.event.listen(Slice, "after_insert", ChartUpdater.after_insert) diff --git a/superset/security.py b/superset/security.py index d8dd5edcd5a50..2c05c40970520 100644 --- a/superset/security.py +++ b/superset/security.py @@ -813,3 +813,13 @@ def assert_viz_permission(self, viz: "BaseViz") -> None: """ self.assert_datasource_permission(viz.datasource) + + def get_row_level_security_filters(self, table): + """ + Retrieves the appropriate row level security filters for the current user and the passed table. + + :param table: The table to check against + :returns: A list of clause strings. + """ + roles = [role.id for role in g.user.roles] + return [filter.clause for filter in filter(lambda x: x.role_id in roles, table.row_level_security_filters)]