diff --git a/tutoraspects/templates/aspects/apps/superset/pythonpath/create_assets.py b/tutoraspects/templates/aspects/apps/superset/pythonpath/create_assets.py index 0564de7d0..6faed15db 100644 --- a/tutoraspects/templates/aspects/apps/superset/pythonpath/create_assets.py +++ b/tutoraspects/templates/aspects/apps/superset/pythonpath/create_assets.py @@ -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 = { @@ -93,6 +93,7 @@ def create_assets(): update_dashboard_roles(roles) update_embeddable_uuids() update_datasets() + create_rls_filters() def import_databases(): diff --git a/tutoraspects/templates/aspects/apps/superset/pythonpath/create_row_level_security.py b/tutoraspects/templates/aspects/apps/superset/pythonpath/create_row_level_security.py index 5c53a2f27..95f482551 100644 --- a/tutoraspects/templates/aspects/apps/superset/pythonpath/create_row_level_security.py +++ b/tutoraspects/templates/aspects/apps/superset/pythonpath/create_row_level_security.py @@ -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, @@ -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 %}', @@ -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.") diff --git a/tutoraspects/templates/aspects/jobs/init/superset/init-superset.sh b/tutoraspects/templates/aspects/jobs/init/superset/init-superset.sh index 747c8fdcb..d6c2f3186 100755 --- a/tutoraspects/templates/aspects/jobs/init/superset/init-superset.sh +++ b/tutoraspects/templates/aspects/jobs/init/superset/init-superset.sh @@ -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"