Skip to content

Commit

Permalink
Merge pull request #707 from openedx/cag/rlsf
Browse files Browse the repository at this point in the history
fix: automatically add RLSF to all tables
  • Loading branch information
Cristhian Garcia authored Apr 9, 2024
2 parents 8ebe9ae + 43731da commit 5caf08e
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 150 deletions.
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"

0 comments on commit 5caf08e

Please sign in to comment.