Skip to content
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

fix: automatically add RLSF to all tables #707

Merged
merged 1 commit into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from superset.utils.database import get_or_create_db
from superset.models.embedded_dashboard import EmbeddedDashboard
from pythonpath.localization import get_translation

from pythonpath.create_row_level_security import create_rls_filters
BASE_DIR = "/app/assets/superset"

ASSET_FOLDER_MAPPING = {
Expand Down Expand Up @@ -94,6 +94,7 @@ def create_assets():
update_dashboard_roles(roles)
update_embeddable_uuids()
update_datasets()
create_rls_filters()


def import_databases():
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
from superset.app import create_app

app = create_app()
app.app_context().push()

from superset.connectors.sqla.models import (
RLSFilterRoles,
RowLevelSecurityFilter,
Expand All @@ -15,100 +10,34 @@

## https://docs.preset.io/docs/row-level-security-rls

VIRTUAL_TABLE_SCHEMA = "main"
XAPI_SCHEMA = "{{ ASPECTS_XAPI_DATABASE }}"
DBT_SCHEMA = "{{ DBT_PROFILE_TARGET_DATABASE }}"
EVENT_SINK_SCHEMA = "{{ ASPECTS_EVENT_SINK_DATABASE }}"


SECURITY_FILTERS = [
{
"schema": VIRTUAL_TABLE_SCHEMA,
"table_name": "{{ASPECTS_XAPI_TABLE}}",
"name": f"can_view_courses_{XAPI_SCHEMA}",
"schema": XAPI_SCHEMA,
"exclude": [],
"role_name": "{{SUPERSET_ROLES_MAPPING.instructor}}",
"group_key": "{{SUPERSET_ROW_LEVEL_SECURITY_XAPI_GROUP_KEY}}",
"clause": '{% raw %}{{can_view_courses(current_username(), "splitByChar(\'/\', course_id)[-1]")}}{% endraw %}',
"filter_type": "Regular",
},
{
"schema": VIRTUAL_TABLE_SCHEMA,
"table_name": "fact_enrollments_by_day",
"role_name": "{{SUPERSET_ROLES_MAPPING.instructor}}",
"group_key": "{{SUPERSET_ROW_LEVEL_SECURITY_XAPI_GROUP_KEY}}",
"clause": '{% raw %}{{can_view_courses(current_username(), "course_key")}}{% endraw %}',
"filter_type": "Regular"
},
{
"schema": VIRTUAL_TABLE_SCHEMA,
"table_name": "fact_enrollments",
"name": f"can_view_courses_{EVENT_SINK_SCHEMA}",
"schema": EVENT_SINK_SCHEMA,
"exclude": ["user_pii"],
"role_name": "{{SUPERSET_ROLES_MAPPING.instructor}}",
"group_key": "{{SUPERSET_ROW_LEVEL_SECURITY_XAPI_GROUP_KEY}}",
"clause": '{% raw %}{{can_view_courses(current_username(), "course_key")}}{% endraw %}',
"filter_type": "Regular"
},
{
"schema": VIRTUAL_TABLE_SCHEMA,
"table_name": "fact_learner_problem_summary",
"role_name": "{{SUPERSET_ROLES_MAPPING.instructor}}",
"group_key": "{{SUPERSET_ROW_LEVEL_SECURITY_XAPI_GROUP_KEY}}",
"clause": '{% raw %}{{can_view_courses(current_username(), "course_key")}}{% endraw %}',
"filter_type": "Regular"
},
{
"schema": VIRTUAL_TABLE_SCHEMA,
"table_name": "fact_problem_responses",
"role_name": "{{SUPERSET_ROLES_MAPPING.instructor}}",
"group_key": "{{SUPERSET_ROW_LEVEL_SECURITY_XAPI_GROUP_KEY}}",
"clause": '{% raw %}{{can_view_courses(current_username(), "course_key")}}{% endraw %}',
"filter_type": "Regular"
},
{
"schema": VIRTUAL_TABLE_SCHEMA,
"table_name": "fact_transcript_usage",
"role_name": "{{SUPERSET_ROLES_MAPPING.instructor}}",
"group_key": "{{SUPERSET_ROW_LEVEL_SECURITY_XAPI_GROUP_KEY}}",
"clause": '{% raw %}{{can_view_courses(current_username(), "course_key")}}{% endraw %}',
"filter_type": "Regular"
},
{
"schema": VIRTUAL_TABLE_SCHEMA,
"table_name": "fact_video_plays",
"role_name": "{{SUPERSET_ROLES_MAPPING.instructor}}",
"group_key": "{{SUPERSET_ROW_LEVEL_SECURITY_XAPI_GROUP_KEY}}",
"clause": '{% raw %}{{can_view_courses(current_username(), "course_key")}}{% endraw %}',
"filter_type": "Regular"
},
{
"schema": VIRTUAL_TABLE_SCHEMA,
"table_name": "fact_watched_video_segments",
"role_name": "{{SUPERSET_ROLES_MAPPING.instructor}}",
"group_key": "{{SUPERSET_ROW_LEVEL_SECURITY_XAPI_GROUP_KEY}}",
"clause": '{% raw %}{{can_view_courses(current_username(), "course_key")}}{% endraw %}',
"filter_type": "Regular"
},
{
"schema": VIRTUAL_TABLE_SCHEMA,
"table_name": "hints_per_success",
"role_name": "{{SUPERSET_ROLES_MAPPING.instructor}}",
"group_key": "{{SUPERSET_ROW_LEVEL_SECURITY_XAPI_GROUP_KEY}}",
"clause": '{% raw %}{{can_view_courses(current_username(), "course_key")}}{% endraw %}',
"filter_type": "Regular"
},
{
"schema": VIRTUAL_TABLE_SCHEMA,
"table_name": "course_names",
"role_name": "{{SUPERSET_ROLES_MAPPING.instructor}}",
"group_key": "{{SUPERSET_ROW_LEVEL_SECURITY_XAPI_GROUP_KEY}}",
"clause": '{% raw %}{{can_view_courses(current_username(), "course_key")}}{% endraw %}',
"filter_type": "Regular"
},
{
"schema": VIRTUAL_TABLE_SCHEMA,
"table_name": "course_overviews",
"role_name": "{{SUPERSET_ROLES_MAPPING.instructor}}",
"group_key": "{{SUPERSET_ROW_LEVEL_SECURITY_XAPI_GROUP_KEY}}",
"clause": '{% raw %}{{can_view_courses(current_username(), "course_key")}}{% endraw %}',
"filter_type": "Regular"
},
{
"schema": VIRTUAL_TABLE_SCHEMA,
"table_name": "course_blocks",
"name": f"can_view_courses_{DBT_SCHEMA}",
"schema": DBT_SCHEMA,
"exclude": [],
"role_name": "{{SUPERSET_ROLES_MAPPING.instructor}}",
"group_key": "{{SUPERSET_ROW_LEVEL_SECURITY_XAPI_GROUP_KEY}}",
"clause": '{% raw %}{{can_view_courses(current_username(), "course_key")}}{% endraw %}',
Expand All @@ -118,67 +47,57 @@

{{patch("superset-row-level-security") | indent(4)}}

def create_rls_filters():
for security_filter in SECURITY_FILTERS:
# Fetch the table we want to restrict access to
(
name,
schema,
exclude,
role_name,
group_key,
clause,
filter_type,
) = security_filter.values()
tables = (
session.query(SqlaTable)
.filter(SqlaTable.schema == schema)
.filter(SqlaTable.table_name.not_in(exclude))
.all()
)
print(f"Creating RLS filter {name} for {schema} schema")

for security_filter in SECURITY_FILTERS:
# Fetch the table we want to restrict access to
(
schema,
table_name,
role_name,
group_key,
clause,
filter_type,
) = security_filter.values()
table = (
session.query(SqlaTable)
.filter(SqlaTable.schema == schema)
.filter(SqlaTable.table_name == table_name)
.first()
)

assert table, (f"{schema}.{table_name} table doesn't exist. If you have changed "
"your database (schema) name, you will need to update the database "
"connection and dataset schema entries in the Superset UI or "
"database. You may also need to rebuild your aspects-superset "
"image after changes.")
role = session.query(Role).filter(Role.name == role_name).first()
assert role, f"{role_name} role doesn't exist yet?"
# See if the Row Level Security Filter already exists
rlsf = (
session.query(RowLevelSecurityFilter)
.filter(RowLevelSecurityFilter.group_key == group_key)
.filter(RowLevelSecurityFilter.name == name)
).first()
# If it doesn't already exist, create one
if not rlsf:
rlsf = RowLevelSecurityFilter()
# Sync the fields to our expectations
rlsf.filter_type = filter_type
rlsf.group_key = group_key
rlsf.tables = tables
rlsf.clause = clause
rlsf.name = name

role = session.query(Role).filter(Role.name == role_name).first()
assert role, f"{role_name} role doesn't exist yet?"
# See if the Row Level Security Filter already exists
rlsf = (
session.query(RowLevelSecurityFilter)
.filter(RLSFilterRoles.c.role_id.in_((role.id,)))
.filter(RowLevelSecurityFilter.group_key == group_key)
.filter(RowLevelSecurityFilter.tables.any(id=table.id))
).first()
# If it doesn't already exist, create one
if rlsf:
create = False
else:
create = True
rlsf = RowLevelSecurityFilter()
# Sync the fields to our expectations
rlsf.filter_type = filter_type
rlsf.group_key = group_key
rlsf.tables = [table]
rlsf.clause = clause
rlsf.name = f"{table.table_name} - {role.name}"
# Create if needed
if create:
session.add(rlsf)
# ...and commit, so we are sure to have an rlsf.id
session.commit()
# Add the filter role if needed
rls_filter_roles = (
session.query(RLSFilterRoles)
.filter(RLSFilterRoles.c.role_id == role.id)
.filter(RLSFilterRoles.c.rls_filter_id == rlsf.id)
)

if not rls_filter_roles.count():
session.execute(
RLSFilterRoles.insert(), [dict(role_id=role.id, rls_filter_id=rlsf.id)]
# Add the filter role if needed
rls_filter_roles = (
session.query(RLSFilterRoles)
.filter(RLSFilterRoles.c.role_id == role.id)
.filter(RLSFilterRoles.c.rls_filter_id == rlsf.id)
)
session.commit()

print("Successfully create row-level security filters.")
if not rls_filter_roles.count():
session.execute(
RLSFilterRoles.insert(), [dict(role_id=role.id, rls_filter_id=rlsf.id)]
)
session.commit()

print("Successfully create row-level security filters.")
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,3 @@ echo_step "4" "Starting" "Importing assets"
bash /app/scripts/import-assets.sh

echo_step "4" "Complete" "Importing assets"

# Set up a Row-Level Security filter to enforce course-based access restrictions.
# Note: there are no cli commands or REST API endpoints to help us with this,
# so we have to pipe python code directly into the superset shell. Yuck!
echo_step "5" "Starting" "Setup row level security filters"
python /app/pythonpath/create_row_level_security.py
echo_step "5" "Complete" "Setup row level security filters"