def check_datasource_perms( _self: Any, datasource_type: Optional[str] = None, datasource_id: Optional[int] = None, **kwargs: Any ) -> None: """ Check if user can access a cached response from explore_json. This function takes `self` since it must have the same signature as the the decorated method. :param datasource_type: The datasource type, i.e., 'druid' or 'table' :param datasource_id: The datasource ID :raises SupersetSecurityException: If the user cannot access the resource """ form_data = kwargs["form_data"] if "form_data" in kwargs else get_form_data()[0] try: datasource_id, datasource_type = get_datasource_info( datasource_id, datasource_type, form_data ) except SupersetException as ex: raise SupersetSecurityException( SupersetError( error_type=SupersetErrorType.FAILED_FETCHING_DATASOURCE_INFO_ERROR, level=ErrorLevel.ERROR, message=str(ex), ) ) if datasource_type is None: raise SupersetSecurityException( SupersetError( error_type=SupersetErrorType.UNKNOWN_DATASOURCE_TYPE_ERROR, level=ErrorLevel.ERROR, message=_("Could not determine datasource type"), ) ) try: viz_obj = get_viz( datasource_type=datasource_type, datasource_id=datasource_id, form_data=form_data, force=False, ) except NoResultFound: raise SupersetSecurityException( SupersetError( error_type=SupersetErrorType.UNKNOWN_DATASOURCE_TYPE_ERROR, level=ErrorLevel.ERROR, message=_("Could not find viz object"), ) ) viz_obj.raise_for_access()
def check_slice_perms(_self: Any, slice_id: int) -> None: """ Check if user can access a cached response from slice_json. This function takes `self` since it must have the same signature as the the decorated method. :param slice_id: The slice ID :raises SupersetSecurityException: If the user cannot access the resource """ form_data, slc = get_form_data(slice_id, use_slice_data=True) if slc: try: viz_obj = get_viz( datasource_type=slc.datasource.type, datasource_id=slc.datasource.id, form_data=form_data, force=False, ) except NoResultFound: raise SupersetSecurityException( SupersetError( error_type=SupersetErrorType.UNKNOWN_DATASOURCE_TYPE_ERROR, level=ErrorLevel.ERROR, message="Could not find viz object", )) viz_obj.raise_for_access()
def validate_adhoc_subquery( sql: str, database_id: int, default_schema: str, ) -> str: """ Check if adhoc SQL contains sub-queries or nested sub-queries with table. If sub-queries are allowed, the adhoc SQL is modified to insert any applicable RLS predicates to it. :param sql: adhoc sql expression :raise SupersetSecurityException if sql contains sub-queries or nested sub-queries with table """ # pylint: disable=import-outside-toplevel from superset import is_feature_enabled statements = [] for statement in sqlparse.parse(sql): if has_table_query(statement): if not is_feature_enabled("ALLOW_ADHOC_SUBQUERY"): raise SupersetSecurityException( SupersetError( error_type=SupersetErrorType. ADHOC_SUBQUERY_NOT_ALLOWED_ERROR, message=_( "Custom SQL fields cannot contain sub-queries."), level=ErrorLevel.ERROR, )) statement = insert_rls(statement, database_id, default_schema) statements.append(statement) return ";\n".join(str(statement) for statement in statements)
def get_virtual_table_metadata(dataset: "SqlaTable") -> List[Dict[str, str]]: """Use SQLparser to get virtual dataset metadata""" if not dataset.sql: raise SupersetGenericDBErrorException( message=_("Virtual dataset query cannot be empty"), ) db_engine_spec = dataset.database.db_engine_spec engine = dataset.database.get_sqla_engine(schema=dataset.schema) sql = dataset.get_template_processor().process_template( dataset.sql, **dataset.template_params_dict ) parsed_query = ParsedQuery(sql) if not db_engine_spec.is_readonly_query(parsed_query): raise SupersetSecurityException( SupersetError( error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR, message=_("Only `SELECT` statements are allowed"), level=ErrorLevel.ERROR, ) ) statements = parsed_query.get_statements() if len(statements) > 1: raise SupersetSecurityException( SupersetError( error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR, message=_("Only single queries supported"), level=ErrorLevel.ERROR, ) ) # TODO(villebro): refactor to use same code that's used by # sql_lab.py:execute_sql_statements try: with closing(engine.raw_connection()) as conn: cursor = conn.cursor() query = dataset.database.apply_limit_to_sql(statements[0]) db_engine_spec.execute(cursor, query) result = db_engine_spec.fetch_data(cursor, limit=1) result_set = SupersetResultSet(result, cursor.description, db_engine_spec) cols = result_set.columns except Exception as exc: raise SupersetGenericDBErrorException(message=str(exc)) return cols
def raise_for_user_activity_access(user_id: int) -> None: user = g.user if g.user and g.user.get_id() else None if not user or (not current_app.config["ENABLE_BROAD_ACTIVITY_ACCESS"] and user_id != user.id): raise SupersetSecurityException( SupersetError( error_type=SupersetErrorType. USER_ACTIVITY_SECURITY_ACCESS_ERROR, message="Access to user's activity data is restricted", level=ErrorLevel.ERROR, ))
def assert_datasource_permission(self, datasource: "BaseDatasource") -> None: """ Assert the the user has permission to access the Superset datasource. :param datasource: The Superset datasource :raises SupersetSecurityException: If the user does not have permission """ if not self.datasource_access(datasource): raise SupersetSecurityException( self.get_datasource_access_error_object(datasource), )
def test_can_access_table(self, mock_raise_for_access): database = get_example_database() table = Table("bar", "foo") mock_raise_for_access.return_value = None self.assertTrue(security_manager.can_access_table(database, table)) mock_raise_for_access.side_effect = SupersetSecurityException( SupersetError("dummy", SupersetErrorType.TABLE_SECURITY_ACCESS_ERROR, ErrorLevel.ERROR)) self.assertFalse(security_manager.can_access_table(database, table))
def test_can_access_datasource(self, mock_raise_for_access): datasource = self.get_datasource_mock() mock_raise_for_access.return_value = None self.assertTrue(security_manager.can_access_datasource(datasource=datasource)) mock_raise_for_access.side_effect = SupersetSecurityException( SupersetError( "dummy", SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR, ErrorLevel.ERROR, ) ) self.assertFalse(security_manager.can_access_datasource(datasource=datasource))
def check_sqlalchemy_uri(uri: URL) -> None: if uri.drivername in BLOCKLIST: try: dialect = uri.get_dialect().__name__ except NoSuchModuleError: dialect = uri.drivername raise SupersetSecurityException( SupersetError( error_type=SupersetErrorType.DATABASE_SECURITY_ACCESS_ERROR, message=_( "%(dialect)s cannot be used as a data source for security reasons.", dialect=dialect, ), level=ErrorLevel.ERROR, ))
def check_ownership(obj: Any, raise_if_false: bool = True) -> bool: """Meant to be used in `pre_update` hooks on models to enforce ownership Admin have all access, and other users need to be referenced on either the created_by field that comes with the ``AuditMixin``, or in a field named ``owners`` which is expected to be a one-to-many with the User model. It is meant to be used in the ModelView's pre_update hook in which raising will abort the update. """ if not obj: return False security_exception = SupersetSecurityException( SupersetError( error_type=SupersetErrorType.MISSING_OWNERSHIP_ERROR, message="You don't have the rights to alter [{}]".format(obj), level=ErrorLevel.ERROR, )) if g.user.is_anonymous: if raise_if_false: raise security_exception return False roles = [r.name for r in get_user_roles()] if "Admin" in roles: return True scoped_session = db.create_scoped_session() orig_obj = scoped_session.query(obj.__class__).filter_by(id=obj.id).first() # Making a list of owners that works across ORM models owners: List[User] = [] if hasattr(orig_obj, "owners"): owners += orig_obj.owners if hasattr(orig_obj, "owner"): owners += [orig_obj.owner] if hasattr(orig_obj, "created_by"): owners += [orig_obj.created_by] owner_names = [o.username for o in owners if o] if g.user and hasattr(g.user, "username") and g.user.username in owner_names: return True if raise_if_false: raise security_exception else: return False
def test_connection_superset_security_connection(self, mock_event_logger, mock_get_sqla_engine): """Test to make sure event_logger is called when security connection exc is raised""" database = get_example_database() mock_get_sqla_engine.side_effect = SupersetSecurityException( SupersetError(error_type=500, message="test", level="info")) db_uri = database.sqlalchemy_uri_decrypted json_payload = {"sqlalchemy_uri": db_uri} command_without_db_name = TestConnectionDatabaseCommand( security_manager.find_user("admin"), json_payload) with pytest.raises(DatabaseSecurityUnsafeError) as excinfo: command_without_db_name.run() assert str( excinfo.value) == ("Stopped an unsafe database connection") mock_event_logger.assert_called()
def check_ownership(obj, raise_if_false=True): """Meant to be used in `pre_update` hooks on models to enforce ownership Admin have all access, and other users need to be referenced on either the created_by field that comes with the ``AuditMixin``, or in a field named ``owners`` which is expected to be a one-to-many with the User model. It is meant to be used in the ModelView's pre_update hook in which raising will abort the update. """ if not obj: return False security_exception = SupersetSecurityException( "You don't have the rights to alter [{}]".format(obj)) if g.user.is_anonymous: if raise_if_false: raise security_exception return False roles = [r.name for r in get_user_roles()] if 'Admin' in roles: return True session = db.create_scoped_session() orig_obj = session.query(obj.__class__).filter_by(id=obj.id).first() # Making a list of owners that works across ORM models owners = [] if hasattr(orig_obj, 'owners'): owners += orig_obj.owners if hasattr(orig_obj, 'owner'): owners += [orig_obj.owner] if hasattr(orig_obj, 'created_by'): owners += [orig_obj.created_by] owner_names = [o.username for o in owners if o] if ( g.user and hasattr(g.user, 'username') and g.user.username in owner_names): return True if raise_if_false: raise security_exception else: return False
def validate_adhoc_subquery(raw_sql: str) -> None: """ Check if adhoc SQL contains sub-queries or nested sub-queries with table :param raw_sql: adhoc sql expression :raise SupersetSecurityException if sql contains sub-queries or nested sub-queries with table """ # pylint: disable=import-outside-toplevel from superset import is_feature_enabled if is_feature_enabled("ALLOW_ADHOC_SUBQUERY"): return for statement in sqlparse.parse(raw_sql): if has_table_query(statement): raise SupersetSecurityException( SupersetError( error_type=SupersetErrorType. ADHOC_SUBQUERY_NOT_ALLOWED_ERROR, message=_("Custom SQL fields cannot contain sub-queries."), level=ErrorLevel.ERROR, )) return
def raise_for_access( # pylint: disable=too-many-arguments,too-many-locals self, database: Optional["Database"] = None, datasource: Optional["BaseDatasource"] = None, query: Optional["Query"] = None, query_context: Optional["QueryContext"] = None, table: Optional["Table"] = None, viz: Optional["BaseViz"] = None, ) -> None: """ Raise an exception if the user cannot access the resource. :param database: The Superset database :param datasource: The Superset datasource :param query: The SQL Lab query :param query_context: The query context :param table: The Superset table (requires database) :param viz: The visualization :raises SupersetSecurityException: If the user cannot access the resource """ # pylint: disable=import-outside-toplevel from superset.connectors.sqla.models import SqlaTable from superset.extensions import feature_flag_manager from superset.sql_parse import Table if database and table or query: if query: database = query.database database = cast("Database", database) if self.can_access_database(database): return if query: tables = { Table(table_.table, table_.schema or query.schema) for table_ in sql_parse.ParsedQuery(query.sql).tables } elif table: tables = {table} denied = set() for table_ in tables: schema_perm = self.get_schema_perm(database, schema=table_.schema) if not (schema_perm and self.can_access("schema_access", schema_perm)): datasources = SqlaTable.query_datasources_by_name( self.get_session, database, table_.table, schema=table_.schema) # Access to any datasource is suffice. for datasource_ in datasources: if self.can_access("datasource_access", datasource_.perm): break else: denied.add(table_) if denied: raise SupersetSecurityException( self.get_table_access_error_object(denied)) if datasource or query_context or viz: if query_context: datasource = query_context.datasource elif viz: datasource = viz.datasource assert datasource should_check_dashboard_access = ( feature_flag_manager.is_feature_enabled("DASHBOARD_RBAC") or self.is_guest_user()) if not (self.can_access_schema(datasource) or self.can_access( "datasource_access", datasource.perm or "") or (should_check_dashboard_access and self.can_access_based_on_dashboard(datasource))): raise SupersetSecurityException( self.get_datasource_access_error_object(datasource))
def assert_datasource_permission(self, datasource): if not self.datasource_access(datasource): raise SupersetSecurityException( self.get_datasource_access_error_msg(datasource), self.get_datasource_access_link(datasource), )
def raise_for_access( self, database: Optional["Database"] = None, datasource: Optional["BaseDatasource"] = None, query: Optional["Query"] = None, query_context: Optional["QueryContext"] = None, table: Optional["Table"] = None, viz: Optional["BaseViz"] = None, ) -> None: """ Raise an exception if the user cannot access the resource. :param database: The Superset database :param datasource: The Superset datasource :param query: The SQL Lab query :param query_context: The query context :param table: The Superset table (requires database) :param viz: The visualization :raises SupersetSecurityException: If the user cannot access the resource """ from superset.connectors.sqla.models import SqlaTable from superset.sql_parse import Table if database and table or query: if query: database = query.database database = cast("Database", database) if self.can_access_database(database): return if query: tables = { Table(table_.table, table_.schema or query.schema) for table_ in sql_parse.ParsedQuery(query.sql).tables } elif table: tables = {table} denied = set() for table_ in tables: schema_perm = self.get_schema_perm(database, schema=table_.schema) if not (schema_perm and self.can_access("schema_access", schema_perm)): datasources = SqlaTable.query_datasources_by_name( self.get_session, database, table_.table, schema=table_.schema) # Access to any datasource is suffice. for datasource in datasources: if self.can_access("datasource_access", datasource.perm): break else: denied.add(table_) if denied: raise SupersetSecurityException( self.get_table_access_error_object(denied), ) if datasource or query_context or viz: if query_context: datasource = query_context.datasource elif viz: datasource = viz.datasource assert datasource if not (self.can_access_schema(datasource) or self.can_access( "datasource_access", datasource.perm or "")): raise SupersetSecurityException( self.get_datasource_access_error_object(datasource), )