-
Notifications
You must be signed in to change notification settings - Fork 14k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SIP-29] Add support for row-level security #8699
Changes from 43 commits
ab6ae45
024912e
4d6789f
4152dc2
13f1381
1100a60
76a314a
86234fa
81c2bdb
ff80add
aafce2f
fd0eaf6
8b6ce72
5f69386
032efcb
2c252d9
7223da6
84a5009
7e0e3c8
253e992
1b3b8f6
c0ac8e9
bdfaff6
041038e
8f0d0d1
b402fc2
e403e9b
9c71f05
78ebc1c
5a0257c
9d4bcad
790f395
8adfc89
2b0ccfc
c68d6e6
b82acf6
d94e6aa
f76e953
3d0822d
db9f20a
524c109
a2e6b67
f581cf7
085a501
a625111
a131a72
2ea460b
1451c05
5eeb296
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -225,6 +225,38 @@ class SqlMetricInlineView(CompactCRUDMixin, SupersetModelView): | |
edit_form_extra_fields = add_form_extra_fields | ||
|
||
|
||
class RowLevelSecurityFiltersModelView(SupersetModelView, DeleteMixin): | ||
datamodel = SQLAInterface(models.RowLevelSecurityFilter) | ||
|
||
list_title = _("Row level security filter") | ||
show_title = _("Show Row level security filter") | ||
add_title = _("Add Row level security filter") | ||
edit_title = _("Edit Row level security filter") | ||
|
||
list_columns = ["table", "roles", "clause", "creator", "modified"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. small improvement: list_columns = ["table.table_name", "roles", "clause", "creator", "modified"] |
||
order_columns = ["modified"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. small improvement: order_columns = ["table.table_name", "clause", "modified"] |
||
edit_columns = ["table", "roles", "clause"] | ||
show_columns = edit_columns | ||
search_columns = ("table", "roles", "clause") | ||
add_columns = edit_columns | ||
base_order = ("changed_on", "desc") | ||
description_columns = { | ||
"table": _("This is the table this filter will be applied to."), | ||
"roles": _("These are the roles this filter will be applied to."), | ||
"clause": _( | ||
"This is the condition that will be added to the WHERE clause. " | ||
"For example, to only return rows for a particular client, you might put in: client_id = 9" | ||
), | ||
} | ||
label_columns = { | ||
"table": _("Table"), | ||
"roles": _("Roles"), | ||
"clause": _("Clause"), | ||
"creator": _("Creator"), | ||
"modified": _("Modified"), | ||
} | ||
|
||
|
||
class TableModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): | ||
datamodel = SQLAInterface(models.SqlaTable) | ||
include_route_methods = RouteMethod.CRUD_SET | ||
|
@@ -255,7 +287,11 @@ class TableModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): | |
] | ||
base_filters = [["id", DatasourceFilter, lambda: []]] | ||
show_columns = edit_columns + ["perm", "slices"] | ||
related_views = [TableColumnInlineView, SqlMetricInlineView] | ||
related_views = [ | ||
TableColumnInlineView, | ||
SqlMetricInlineView, | ||
RowLevelSecurityFiltersModelView, | ||
] | ||
base_order = ("changed_on", "desc") | ||
search_columns = ("database", "schema", "table_name", "owners", "is_sqllab_view") | ||
description_columns = { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
# 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_role_level_security | ||
|
||
Revision ID: 0a6f12f60c73 | ||
Revises: e96dbf2cfef0 | ||
Create Date: 2019-12-04 17:07:54.390805 | ||
|
||
""" | ||
|
||
# revision identifiers, used by Alembic. | ||
revision = "0a6f12f60c73" | ||
down_revision = "e96dbf2cfef0" | ||
|
||
import sqlalchemy as sa | ||
from alembic import op | ||
|
||
|
||
def upgrade(): | ||
op.create_table( | ||
"row_level_security_filters", | ||
sa.Column("created_on", sa.DateTime(), nullable=True), | ||
altef marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sa.Column("changed_on", sa.DateTime(), nullable=True), | ||
sa.Column("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(["table_id"], ["tables.id"]), | ||
sa.PrimaryKeyConstraint("id"), | ||
) | ||
op.create_table( | ||
"rls_filter_roles", | ||
sa.Column("id", sa.Integer(), nullable=False), | ||
sa.Column("role_id", sa.Integer(), nullable=False), | ||
sa.Column("rls_filter_id", sa.Integer(), nullable=True), | ||
sa.ForeignKeyConstraint(["rls_filter_id"], ["row_level_security_filters.id"]), | ||
sa.ForeignKeyConstraint(["role_id"], ["ab_role.id"]), | ||
sa.PrimaryKeyConstraint("id"), | ||
) | ||
|
||
|
||
def downgrade(): | ||
op.drop_table("rls_filter_roles") | ||
op.drop_table("row_level_security_filters") |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -116,6 +116,7 @@ class SupersetSecurityManager(SecurityManager): | |
"ResetPasswordView", | ||
"RoleModelView", | ||
"Security", | ||
"RowLevelSecurityFiltersModelView", | ||
} | USER_MODEL_VIEWS | ||
|
||
ALPHA_ONLY_VIEW_MENUS = {"Upload a CSV"} | ||
|
@@ -887,3 +888,48 @@ def assert_viz_permission(self, viz: "BaseViz") -> None: | |
""" | ||
|
||
self.assert_datasource_permission(viz.datasource) | ||
|
||
def get_rls_filters(self, table: "BaseDatasource"): | ||
""" | ||
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 filters. | ||
""" | ||
if hasattr(g, "user") and hasattr(g.user, "id"): | ||
from superset import db | ||
from superset.connectors.sqla.models import ( | ||
RLSFilterRoles, | ||
RowLevelSecurityFilter, | ||
) | ||
|
||
user_roles = ( | ||
db.session.query(assoc_user_role.c.role_id) | ||
.filter(assoc_user_role.c.user_id == g.user.id) | ||
.subquery() | ||
) | ||
filter_roles = ( | ||
db.session.query(RLSFilterRoles.c.id) | ||
.filter(RLSFilterRoles.c.role_id.in_(user_roles)) | ||
.subquery() | ||
) | ||
query = ( | ||
db.session.query( | ||
RowLevelSecurityFilter.id, RowLevelSecurityFilter.clause | ||
) | ||
.filter(RowLevelSecurityFilter.table_id == table.id) | ||
.filter(RowLevelSecurityFilter.id.in_(filter_roles)) | ||
) | ||
return query.all() | ||
return [] | ||
|
||
def get_rls_ids(self, table: "BaseDatasource") -> List[int]: | ||
""" | ||
Retrieves the appropriate row level security filters IDs for the current user and the passed table. | ||
|
||
:param table: The table to check against | ||
:returns: A list of IDs. | ||
""" | ||
ids = [f.id for f in self.get_rls_filters(table)] | ||
ids.sort() # Combinations rather than permutations | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems these IDs are sorted to satisfy the cache consistency. Rather than storing these as an ordered list why not return these as a set? |
||
return ids |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we use an
f
string here as opposed to.format()
?