This is an automated email from the ASF dual-hosted git repository.

amitmiran pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 84f7614  feat(filter-set): Add filterset resource (#14015)
84f7614 is described below

commit 84f7614e97b71bd8952f78d82cabf766afc0b98b
Author: ofekisr <[email protected]>
AuthorDate: Thu Sep 23 11:27:59 2021 +0300

    feat(filter-set): Add filterset resource (#14015)
    
    * Add filterset resource
    
    * fix: fix pre-commit
    
    * add tests
    
    * add tests and fixes based of failures
    
    * Fix pre-commit errors
    
    * chore init filterset resource under ff constraint
    
    * Fix migration conflicts
    
    * Fix pylint and migrations issues
    
    * Fix pylint and migrations issues
    
    * Fix pylint and migrations issues
    
    * Fix pylint and migrations issues
    
    * Fix pylint and migrations issues
    
    * Fix pylint and migrations issues
    
    * Fix pylint and migrations issues
    
    * Fix pylint and migrations issues
    
    * Fix pylint and migrations issues
    
    * Fix pylint and migrations issues
    
    * Fix pylint and migrations issues
    
    * add tests and fixes based of failures
    
    * Fix missing license
    
    * fix down revision
    
    * update down_revision
    
    * fix: update down_revision
    
    * chore: add description to migration
    
    * fix: type
    
    * refactor: is_user_admin
    
    * fix: use get_public_role
    
    * fix: move import to the relevant location
    
    * chore: add openSpec api schema
    
    * chore: cover all openspec API
    
    * fix: pre-commit and lint
    
    * fix: put and post schemas
    
    * fix: undo superset_test_config.py
    
    * fix: limit filterSetsApi to include_route_methods = {"get_list", "put", 
"post", "delete"}
    
    * renaming some params
    
    * chore: add debug in test config
    
    * fix: rename database to different name
    
    * fix: try to make conftest.py harmless
    
    * fix: pre-commit
    
    * fix: new down_revision ref
    
    * fix: bad ref
    
    * fix: bad ref 2
    
    * fix: bad ref 3
    
    * fix: add api in initiatior
    
    * fix: open spec
    
    * fix: convert name to str to include int usecases
    
    * fix: pylint
    
    * fix: pylint
    
    * Update superset/common/request_contexed_based.py
    
    Co-authored-by: Ville Brofeldt <[email protected]>
    
    * chore: resolve PR comments
    
    * chore: resolve PR comments
    
    * chore: resolve PR comments
    
    * fix failed tests
    
    * fix pylint
    
    * Update conftest.py
    
    * chore remove BaseCommand to remove abstraction
    
    * chore remove BaseCommand to remove abstraction
    
    * chore remove BaseCommand to remove abstraction
    
    * chore remove BaseCommand to remove abstraction
    
    * chore fix migration
    
    Co-authored-by: Ofeknielsen <[email protected]>
    Co-authored-by: amitmiran137 <[email protected]>
    Co-authored-by: Amit Miran <[email protected]>
    Co-authored-by: Ville Brofeldt <[email protected]>
---
 superset/commands/exceptions.py                    |  22 +-
 superset/commands/export.py                        |   6 +-
 superset/common/not_authrized_object.py            |  39 ++
 superset/common/request_contexed_based.py          |  39 ++
 superset/dashboards/commands/exceptions.py         |  11 +-
 superset/dashboards/filter_sets/__init__.py        |  16 +
 superset/dashboards/filter_sets/api.py             | 374 ++++++++++++
 .../dashboards/filter_sets/commands/__init__.py    |  16 +
 superset/dashboards/filter_sets/commands/base.py   |  91 +++
 superset/dashboards/filter_sets/commands/create.py |  78 +++
 superset/dashboards/filter_sets/commands/delete.py |  56 ++
 .../dashboards/filter_sets/commands/exceptions.py  |  94 +++
 superset/dashboards/filter_sets/commands/update.py |  56 ++
 superset/dashboards/filter_sets/consts.py          |  30 +
 superset/dashboards/filter_sets/dao.py             |  64 +++
 superset/dashboards/filter_sets/filters.py         |  58 ++
 superset/dashboards/filter_sets/schemas.py         |  93 +++
 superset/initialization/__init__.py                |   2 +
 .../versions/3ebe0993c770_filterset_table.py       |  56 ++
 superset/models/dashboard.py                       |  35 ++
 superset/models/filter_set.py                      | 106 ++++
 .../dashboards/filter_sets/__init__.py             |  16 +
 .../dashboards/filter_sets/conftest.py             | 321 +++++++++++
 .../dashboards/filter_sets/consts.py               |  22 +
 .../dashboards/filter_sets/create_api_tests.py     | 630 +++++++++++++++++++++
 .../dashboards/filter_sets/delete_api_tests.py     | 209 +++++++
 .../dashboards/filter_sets/get_api_tests.py        | 129 +++++
 .../dashboards/filter_sets/update_api_tests.py     | 519 +++++++++++++++++
 .../dashboards/filter_sets/utils.py                | 102 ++++
 .../dashboards/superset_factory_util.py            |  48 +-
 tests/integration_tests/superset_test_config.py    |   2 +-
 31 files changed, 3317 insertions(+), 23 deletions(-)

diff --git a/superset/commands/exceptions.py b/superset/commands/exceptions.py
index bb8992a..40c0597 100644
--- a/superset/commands/exceptions.py
+++ b/superset/commands/exceptions.py
@@ -14,7 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Optional
 
 from flask_babel import lazy_gettext as _
 from marshmallow import ValidationError
@@ -31,6 +31,26 @@ class CommandException(SupersetException):
         return repr(self)
 
 
+class ObjectNotFoundError(CommandException):
+    status = 404
+    message_format = "{} {}not found."
+
+    def __init__(
+        self,
+        object_type: str,
+        object_id: Optional[str] = None,
+        exception: Optional[Exception] = None,
+    ) -> None:
+        super().__init__(
+            _(
+                self.message_format.format(
+                    object_type, '"%s" ' % object_id if object_id else ""
+                )
+            ),
+            exception,
+        )
+
+
 class CommandInvalidError(CommandException):
     """ Common base class for Command Invalid errors. """
 
diff --git a/superset/commands/export.py b/superset/commands/export.py
index 5bf117c..2b54de8 100644
--- a/superset/commands/export.py
+++ b/superset/commands/export.py
@@ -18,7 +18,7 @@
 
 from datetime import datetime
 from datetime import timezone
-from typing import Iterator, List, Tuple
+from typing import Iterator, List, Tuple, Type
 
 import yaml
 from flask_appbuilder import Model
@@ -33,8 +33,8 @@ METADATA_FILE_NAME = "metadata.yaml"
 
 class ExportModelsCommand(BaseCommand):
 
-    dao = BaseDAO
-    not_found = CommandException
+    dao: Type[BaseDAO] = BaseDAO
+    not_found: Type[CommandException] = CommandException
 
     def __init__(self, model_ids: List[int]):
         self.model_ids = model_ids
diff --git a/superset/common/not_authrized_object.py 
b/superset/common/not_authrized_object.py
new file mode 100644
index 0000000..7295da9
--- /dev/null
+++ b/superset/common/not_authrized_object.py
@@ -0,0 +1,39 @@
+# 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.
+from typing import Any, Optional
+
+from superset.exceptions import SupersetException
+
+
+class NotAuthorizedObject:
+    def __init__(self, what_not_authorized: str):
+        self._what_not_authorized = what_not_authorized
+
+    def __getattr__(self, item: Any) -> None:
+        raise NotAuthorizedException(self._what_not_authorized)
+
+    def __getitem__(self, item: Any) -> None:
+        raise NotAuthorizedException(self._what_not_authorized)
+
+
+class NotAuthorizedException(SupersetException):
+    def __init__(
+        self, what_not_authorized: str = "", exception: Optional[Exception] = 
None
+    ) -> None:
+        super().__init__(
+            "The user is not authorized to " + what_not_authorized, exception
+        )
diff --git a/superset/common/request_contexed_based.py 
b/superset/common/request_contexed_based.py
new file mode 100644
index 0000000..0b06a0c
--- /dev/null
+++ b/superset/common/request_contexed_based.py
@@ -0,0 +1,39 @@
+# 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.
+from __future__ import annotations
+
+from typing import List, TYPE_CHECKING
+
+from flask import g
+
+from superset import conf, security_manager
+
+if TYPE_CHECKING:
+    from flask_appbuilder.security.sqla.models import Role
+
+
+def get_user_roles() -> List[Role]:
+    if g.user.is_anonymous:
+        public_role = conf.get("AUTH_ROLE_PUBLIC")
+        return [security_manager.get_public_role()] if public_role else []
+    return g.user.roles
+
+
+def is_user_admin() -> bool:
+    user_roles = [role.name.lower() for role in get_user_roles()]
+    admin_role = conf.get("AUTH_ROLE_ADMIN").lower()
+    return admin_role in user_roles
diff --git a/superset/dashboards/commands/exceptions.py 
b/superset/dashboards/commands/exceptions.py
index ee85c1f..1a5bdaf 100644
--- a/superset/dashboards/commands/exceptions.py
+++ b/superset/dashboards/commands/exceptions.py
@@ -14,16 +14,18 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+from typing import Optional
+
 from flask_babel import lazy_gettext as _
 from marshmallow.validate import ValidationError
 
 from superset.commands.exceptions import (
-    CommandException,
     CommandInvalidError,
     CreateFailedError,
     DeleteFailedError,
     ForbiddenError,
     ImportFailedError,
+    ObjectNotFoundError,
     UpdateFailedError,
 )
 
@@ -41,8 +43,11 @@ class DashboardInvalidError(CommandInvalidError):
     message = _("Dashboard parameters are invalid.")
 
 
-class DashboardNotFoundError(CommandException):
-    message = _("Dashboard not found.")
+class DashboardNotFoundError(ObjectNotFoundError):
+    def __init__(
+        self, dashboard_id: Optional[str] = None, exception: 
Optional[Exception] = None
+    ) -> None:
+        super().__init__("Dashboard", dashboard_id, exception)
 
 
 class DashboardCreateFailedError(CreateFailedError):
diff --git a/superset/dashboards/filter_sets/__init__.py 
b/superset/dashboards/filter_sets/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/superset/dashboards/filter_sets/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/superset/dashboards/filter_sets/api.py 
b/superset/dashboards/filter_sets/api.py
new file mode 100644
index 0000000..24c4e86
--- /dev/null
+++ b/superset/dashboards/filter_sets/api.py
@@ -0,0 +1,374 @@
+# 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.
+import logging
+from typing import Any, cast
+
+from flask import g, request, Response
+from flask_appbuilder.api import (
+    expose,
+    get_list_schema,
+    permission_name,
+    protect,
+    rison,
+    safe,
+)
+from flask_appbuilder.models.sqla.interface import SQLAInterface
+from marshmallow import ValidationError
+
+from superset.commands.exceptions import ObjectNotFoundError
+from superset.dashboards.commands.exceptions import DashboardNotFoundError
+from superset.dashboards.dao import DashboardDAO
+from superset.dashboards.filter_sets.commands.create import 
CreateFilterSetCommand
+from superset.dashboards.filter_sets.commands.delete import 
DeleteFilterSetCommand
+from superset.dashboards.filter_sets.commands.exceptions import (
+    FilterSetCreateFailedError,
+    FilterSetDeleteFailedError,
+    FilterSetForbiddenError,
+    FilterSetNotFoundError,
+    FilterSetUpdateFailedError,
+    UserIsNotDashboardOwnerError,
+)
+from superset.dashboards.filter_sets.commands.update import 
UpdateFilterSetCommand
+from superset.dashboards.filter_sets.consts import (
+    DASHBOARD_FIELD,
+    DASHBOARD_ID_FIELD,
+    DESCRIPTION_FIELD,
+    FILTER_SET_API_PERMISSIONS_NAME,
+    JSON_METADATA_FIELD,
+    NAME_FIELD,
+    OWNER_ID_FIELD,
+    OWNER_OBJECT_FIELD,
+    OWNER_TYPE_FIELD,
+    PARAMS_PROPERTY,
+)
+from superset.dashboards.filter_sets.filters import FilterSetFilter
+from superset.dashboards.filter_sets.schemas import (
+    FilterSetPostSchema,
+    FilterSetPutSchema,
+)
+from superset.extensions import event_logger
+from superset.models.filter_set import FilterSet
+from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class FilterSetRestApi(BaseSupersetModelRestApi):
+    # pylint: disable=arguments-differ
+    include_route_methods = {"get_list", "put", "post", "delete"}
+    datamodel = SQLAInterface(FilterSet)
+    resource_name = "dashboard"
+    class_permission_name = FILTER_SET_API_PERMISSIONS_NAME
+    allow_browser_login = True
+    csrf_exempt = False
+    add_exclude_columns = [
+        "id",
+        OWNER_OBJECT_FIELD,
+        DASHBOARD_FIELD,
+        JSON_METADATA_FIELD,
+    ]
+    add_model_schema = FilterSetPostSchema()
+    edit_model_schema = FilterSetPutSchema()
+    edit_exclude_columns = [
+        "id",
+        OWNER_OBJECT_FIELD,
+        DASHBOARD_FIELD,
+        JSON_METADATA_FIELD,
+    ]
+    list_columns = [
+        "created_on",
+        "changed_on",
+        "created_by_fk",
+        "changed_by_fk",
+        NAME_FIELD,
+        DESCRIPTION_FIELD,
+        OWNER_TYPE_FIELD,
+        OWNER_ID_FIELD,
+        DASHBOARD_ID_FIELD,
+        PARAMS_PROPERTY,
+    ]
+    show_exclude_columns = [OWNER_OBJECT_FIELD, DASHBOARD_FIELD, 
JSON_METADATA_FIELD]
+    search_columns = ["id", NAME_FIELD, OWNER_ID_FIELD, DASHBOARD_ID_FIELD]
+    base_filters = [[OWNER_ID_FIELD, FilterSetFilter, ""]]
+
+    def __init__(self) -> None:
+        self.datamodel.get_search_columns_list = lambda: []
+        super().__init__()
+
+    def _init_properties(self) -> None:
+        # pylint: disable=bad-super-call
+        super(BaseSupersetModelRestApi, self)._init_properties()
+
+    @expose("/<int:dashboard_id>/filtersets", methods=["GET"])
+    @protect()
+    @safe
+    @permission_name("get")
+    @rison(get_list_schema)
+    def get_list(self, dashboard_id: int, **kwargs: Any) -> Response:
+        """
+            Gets a dashboard's Filter sets
+         ---
+        get:
+          description: >-
+            Get a dashboard's list of filter sets
+          parameters:
+          - in: path
+            schema:
+              type: integer
+            name: dashboard_id
+            description: The id of the dashboard
+          responses:
+            200:
+              description: FilterSets
+              content:
+                application/json:
+                  schema:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          description: Name of the Filter set
+                          type: string
+                        json_metadata:
+                          description: metadata of the filter set
+                          type: string
+                        description:
+                          description: A description field of the filter set
+                          type: string
+                        owner_id:
+                          description: A description field of the filter set
+                          type: integer
+                        owner_type:
+                          description: the Type of the owner ( Dashboard/User)
+                          type: integer
+                        parameters:
+                          description: JSON schema defining the needed 
parameters
+            302:
+              description: Redirects to the current digest
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+        """
+        if not DashboardDAO.find_by_id(cast(int, dashboard_id)):
+            return self.response(404, message="dashboard '%s' not found" % 
dashboard_id)
+        rison_data = kwargs.setdefault("rison", {})
+        rison_data.setdefault("filters", [])
+        rison_data["filters"].append(
+            {"col": "dashboard_id", "opr": "eq", "value": str(dashboard_id)}
+        )
+        return self.get_list_headless(**kwargs)
+
+    @expose("/<int:dashboard_id>/filtersets", methods=["POST"])
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+        log_to_statsd=False,
+    )
+    def post(self, dashboard_id: int) -> Response:
+        """
+            Creates a new Dashboard's Filter Set
+        ---
+        post:
+          description: >-
+            Create a new Dashboard's Filter Set.
+          parameters:
+          - in: path
+            schema:
+              type: integer
+            name: dashboard_id
+            description: The id of the dashboard
+          requestBody:
+            description: Filter set schema
+            required: true
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/{{self.__class__.__name__}}.post'
+          responses:
+            201:
+              description: Filter set added
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      id:
+                        type: number
+                      result:
+                        $ref: 
'#/components/schemas/{{self.__class__.__name__}}.post'
+            302:
+              description: Redirects to the current digest
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        if not request.is_json:
+            return self.response_400(message="Request is not JSON")
+        try:
+            item = self.add_model_schema.load(request.json)
+            new_model = CreateFilterSetCommand(g.user, dashboard_id, 
item).run()
+            return self.response(201, id=new_model.id, result=item)
+        except ValidationError as error:
+            return self.response_400(message=error.messages)
+        except UserIsNotDashboardOwnerError:
+            return self.response_403()
+        except FilterSetCreateFailedError as error:
+            return self.response_400(message=error.message)
+        except DashboardNotFoundError:
+            return self.response_404()
+
+    @expose("/<int:dashboard_id>/filtersets/<int:pk>", methods=["PUT"])
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.put",
+        log_to_statsd=False,
+    )
+    def put(self, dashboard_id: int, pk: int) -> Response:
+        """Changes a Dashboard's Filter set
+        ---
+        put:
+          description: >-
+            Changes a Dashboard's Filter set.
+          parameters:
+          - in: path
+            schema:
+              type: integer
+            name: dashboard_id
+          - in: path
+            schema:
+              type: integer
+            name: pk
+          requestBody:
+            description: Filter set schema
+            required: true
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/{{self.__class__.__name__}}.put'
+          responses:
+            200:
+              description: Filter set changed
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      id:
+                        type: number
+                      result:
+                        $ref: 
'#/components/schemas/{{self.__class__.__name__}}.put'
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            403:
+              $ref: '#/components/responses/403'
+            404:
+              $ref: '#/components/responses/404'
+            422:
+              $ref: '#/components/responses/422'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        if not request.is_json:
+            return self.response_400(message="Request is not JSON")
+        try:
+            item = self.edit_model_schema.load(request.json)
+            changed_model = UpdateFilterSetCommand(g.user, dashboard_id, pk, 
item).run()
+            return self.response(200, id=changed_model.id, result=item)
+        except ValidationError as error:
+            return self.response_400(message=error.messages)
+        except (
+            ObjectNotFoundError,
+            FilterSetForbiddenError,
+            FilterSetUpdateFailedError,
+        ) as err:
+            logger.error(err)
+            return self.response(err.status)
+
+    @expose("/<int:dashboard_id>/filtersets/<int:pk>", methods=["DELETE"])
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: 
f"{self.__class__.__name__}.delete",
+        log_to_statsd=False,
+    )
+    def delete(self, dashboard_id: int, pk: int) -> Response:
+        """
+            Deletes a Dashboard's FilterSet
+        ---
+        delete:
+          description: >-
+            Deletes a Dashboard.
+          parameters:
+          - in: path
+            schema:
+              type: integer
+            name: dashboard_id
+          - in: path
+            schema:
+              type: integer
+            name: pk
+          responses:
+            200:
+              description: Filter set deleted
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      message:
+                        type: string
+            401:
+              $ref: '#/components/responses/401'
+            403:
+              $ref: '#/components/responses/403'
+            404:
+              $ref: '#/components/responses/404'
+            422:
+              $ref: '#/components/responses/422'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            changed_model = DeleteFilterSetCommand(g.user, dashboard_id, 
pk).run()
+            return self.response(200, id=changed_model.id)
+        except ValidationError as error:
+            return self.response_400(message=error.messages)
+        except FilterSetNotFoundError:
+            return self.response(200)
+        except (
+            ObjectNotFoundError,
+            FilterSetForbiddenError,
+            FilterSetDeleteFailedError,
+        ) as err:
+            logger.error(err)
+            return self.response(err.status)
diff --git a/superset/dashboards/filter_sets/commands/__init__.py 
b/superset/dashboards/filter_sets/commands/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/superset/dashboards/filter_sets/commands/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/superset/dashboards/filter_sets/commands/base.py 
b/superset/dashboards/filter_sets/commands/base.py
new file mode 100644
index 0000000..af31bbd
--- /dev/null
+++ b/superset/dashboards/filter_sets/commands/base.py
@@ -0,0 +1,91 @@
+# 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.
+import logging
+from typing import cast, Optional
+
+from flask_appbuilder.models.sqla import Model
+from flask_appbuilder.security.sqla.models import User
+
+from superset.common.not_authrized_object import NotAuthorizedException
+from superset.common.request_contexed_based import is_user_admin
+from superset.dashboards.commands.exceptions import DashboardNotFoundError
+from superset.dashboards.dao import DashboardDAO
+from superset.dashboards.filter_sets.commands.exceptions import (
+    FilterSetForbiddenError,
+    FilterSetNotFoundError,
+)
+from superset.dashboards.filter_sets.consts import USER_OWNER_TYPE
+from superset.models.dashboard import Dashboard
+from superset.models.filter_set import FilterSet
+
+logger = logging.getLogger(__name__)
+
+
+class BaseFilterSetCommand:
+    # pylint: disable=C0103
+    _dashboard: Dashboard
+    _filter_set_id: Optional[int]
+    _filter_set: Optional[FilterSet]
+
+    def __init__(self, user: User, dashboard_id: int):
+        self._actor = user
+        self._is_actor_admin = is_user_admin()
+        self._dashboard_id = dashboard_id
+
+    def run(self) -> Model:
+        pass
+
+    def _validate_filterset_dashboard_exists(self) -> None:
+        self._dashboard = 
DashboardDAO.get_by_id_or_slug(str(self._dashboard_id))
+        if not self._dashboard:
+            raise DashboardNotFoundError()
+
+    def is_user_dashboard_owner(self) -> bool:
+        return self._is_actor_admin or self._dashboard.is_actor_owner()
+
+    def validate_exist_filter_use_cases_set(self) -> None:  # pylint: 
disable=C0103
+        self._validate_filter_set_exists_and_set_when_exists()
+        self.check_ownership()
+
+    def _validate_filter_set_exists_and_set_when_exists(self) -> None:
+        self._filter_set = self._dashboard.filter_sets.get(
+            cast(int, self._filter_set_id), None
+        )
+        if not self._filter_set:
+            raise FilterSetNotFoundError(str(self._filter_set_id))
+
+    def check_ownership(self) -> None:
+        try:
+            if not self._is_actor_admin:
+                filter_set: FilterSet = cast(FilterSet, self._filter_set)
+                if filter_set.owner_type == USER_OWNER_TYPE:
+                    if self._actor.id != filter_set.owner_id:
+                        raise FilterSetForbiddenError(
+                            str(self._filter_set_id),
+                            "The user is not the owner of the filter_set",
+                        )
+                elif not self.is_user_dashboard_owner():
+                    raise FilterSetForbiddenError(
+                        str(self._filter_set_id),
+                        "The user is not an owner of the filter_set's 
dashboard",
+                    )
+        except NotAuthorizedException as err:
+            raise FilterSetForbiddenError(
+                str(self._filter_set_id), "user not authorized to access the 
filterset",
+            ) from err
+        except FilterSetForbiddenError as err:
+            raise err
diff --git a/superset/dashboards/filter_sets/commands/create.py 
b/superset/dashboards/filter_sets/commands/create.py
new file mode 100644
index 0000000..b74e6d3
--- /dev/null
+++ b/superset/dashboards/filter_sets/commands/create.py
@@ -0,0 +1,78 @@
+# 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.
+import logging
+from typing import Any, Dict
+
+from flask import g
+from flask_appbuilder.models.sqla import Model
+from flask_appbuilder.security.sqla.models import User
+
+from superset import security_manager
+from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
+from superset.dashboards.filter_sets.commands.exceptions import (
+    DashboardIdInconsistencyError,
+    FilterSetCreateFailedError,
+    UserIsNotDashboardOwnerError,
+)
+from superset.dashboards.filter_sets.consts import (
+    DASHBOARD_ID_FIELD,
+    DASHBOARD_OWNER_TYPE,
+    OWNER_ID_FIELD,
+    OWNER_TYPE_FIELD,
+)
+from superset.dashboards.filter_sets.dao import FilterSetDAO
+
+logger = logging.getLogger(__name__)
+
+
+class CreateFilterSetCommand(BaseFilterSetCommand):
+    # pylint: disable=C0103
+    def __init__(self, user: User, dashboard_id: int, data: Dict[str, Any]):
+        super().__init__(user, dashboard_id)
+        self._properties = data.copy()
+
+    def run(self) -> Model:
+        self.validate()
+        self._properties[DASHBOARD_ID_FIELD] = self._dashboard.id
+        filter_set = FilterSetDAO.create(self._properties, commit=True)
+        return filter_set
+
+    def validate(self) -> None:
+        self._validate_filterset_dashboard_exists()
+        if self._properties[OWNER_TYPE_FIELD] == DASHBOARD_OWNER_TYPE:
+            self._validate_owner_id_is_dashboard_id()
+            self._validate_user_is_the_dashboard_owner()
+        else:
+            self._validate_owner_id_exists()
+
+    def _validate_owner_id_exists(self) -> None:
+        owner_id = self._properties[OWNER_ID_FIELD]
+        if not (g.user.id == owner_id or 
security_manager.get_user_by_id(owner_id)):
+            raise FilterSetCreateFailedError(
+                str(self._dashboard_id), "owner_id does not exists"
+            )
+
+    def _validate_user_is_the_dashboard_owner(self) -> None:
+        if not self.is_user_dashboard_owner():
+            raise UserIsNotDashboardOwnerError(str(self._dashboard_id))
+
+    def _validate_owner_id_is_dashboard_id(self) -> None:
+        if (
+            self._properties.get(OWNER_ID_FIELD, self._dashboard_id)
+            != self._dashboard_id
+        ):
+            raise DashboardIdInconsistencyError(str(self._dashboard_id))
diff --git a/superset/dashboards/filter_sets/commands/delete.py 
b/superset/dashboards/filter_sets/commands/delete.py
new file mode 100644
index 0000000..18d7fed
--- /dev/null
+++ b/superset/dashboards/filter_sets/commands/delete.py
@@ -0,0 +1,56 @@
+# 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.
+import logging
+
+from flask_appbuilder.models.sqla import Model
+from flask_appbuilder.security.sqla.models import User
+
+from superset.dao.exceptions import DAODeleteFailedError
+from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
+from superset.dashboards.filter_sets.commands.exceptions import (
+    FilterSetDeleteFailedError,
+    FilterSetForbiddenError,
+    FilterSetNotFoundError,
+)
+from superset.dashboards.filter_sets.dao import FilterSetDAO
+
+logger = logging.getLogger(__name__)
+
+
+class DeleteFilterSetCommand(BaseFilterSetCommand):
+    def __init__(self, user: User, dashboard_id: int, filter_set_id: int):
+        super().__init__(user, dashboard_id)
+        self._filter_set_id = filter_set_id
+
+    def run(self) -> Model:
+        try:
+            self.validate()
+            return FilterSetDAO.delete(self._filter_set, commit=True)
+        except DAODeleteFailedError as err:
+            raise FilterSetDeleteFailedError(str(self._filter_set_id), "") 
from err
+
+    def validate(self) -> None:
+        self._validate_filterset_dashboard_exists()
+        try:
+            self.validate_exist_filter_use_cases_set()
+        except FilterSetNotFoundError as err:
+            if FilterSetDAO.find_by_id(self._filter_set_id):  # type: ignore
+                raise FilterSetForbiddenError(
+                    'the filter-set does not related to dashboard "%s"'
+                    % str(self._dashboard_id)
+                ) from err
+            raise err
diff --git a/superset/dashboards/filter_sets/commands/exceptions.py 
b/superset/dashboards/filter_sets/commands/exceptions.py
new file mode 100644
index 0000000..ade0bbb
--- /dev/null
+++ b/superset/dashboards/filter_sets/commands/exceptions.py
@@ -0,0 +1,94 @@
+# 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.
+from typing import Optional
+
+from flask_babel import lazy_gettext as _
+
+from superset.commands.exceptions import (
+    CreateFailedError,
+    DeleteFailedError,
+    ForbiddenError,
+    ObjectNotFoundError,
+    UpdateFailedError,
+)
+
+
+class FilterSetNotFoundError(ObjectNotFoundError):
+    def __init__(
+        self, filterset_id: Optional[str] = None, exception: 
Optional[Exception] = None
+    ) -> None:
+        super().__init__("FilterSet", filterset_id, exception)
+
+
+class FilterSetCreateFailedError(CreateFailedError):
+    base_message = 'CreateFilterSetCommand of dashboard "%s" failed: '
+
+    def __init__(
+        self, dashboard_id: str, reason: str = "", exception: 
Optional[Exception] = None
+    ) -> None:
+        super().__init__((self.base_message % dashboard_id) + reason, 
exception)
+
+
+class FilterSetUpdateFailedError(UpdateFailedError):
+    base_message = 'UpdateFilterSetCommand of filter_set "%s" failed: '
+
+    def __init__(
+        self, filterset_id: str, reason: str = "", exception: 
Optional[Exception] = None
+    ) -> None:
+        super().__init__((self.base_message % filterset_id) + reason, 
exception)
+
+
+class FilterSetDeleteFailedError(DeleteFailedError):
+    base_message = 'DeleteFilterSetCommand of filter_set "%s" failed: '
+
+    def __init__(
+        self, filterset_id: str, reason: str = "", exception: 
Optional[Exception] = None
+    ) -> None:
+        super().__init__((self.base_message % filterset_id) + reason, 
exception)
+
+
+class UserIsNotDashboardOwnerError(FilterSetCreateFailedError):
+    reason = (
+        "cannot create dashboard owner filterset based when"
+        " the user is not the dashboard owner"
+    )
+
+    def __init__(
+        self, dashboard_id: str, exception: Optional[Exception] = None
+    ) -> None:
+        super().__init__(dashboard_id, self.reason, exception)
+
+
+class DashboardIdInconsistencyError(FilterSetCreateFailedError):
+    reason = (
+        "cannot create dashboard owner filterset based when the"
+        " ownerid is not the dashboard id"
+    )
+
+    def __init__(
+        self, dashboard_id: str, exception: Optional[Exception] = None
+    ) -> None:
+        super().__init__(dashboard_id, self.reason, exception)
+
+
+class FilterSetForbiddenError(ForbiddenError):
+    message_format = 'Changing FilterSet "{}" is forbidden: {}'
+
+    def __init__(
+        self, filterset_id: str, reason: str = "", exception: 
Optional[Exception] = None
+    ) -> None:
+        super().__init__(_(self.message_format.format(filterset_id, reason)), 
exception)
diff --git a/superset/dashboards/filter_sets/commands/update.py 
b/superset/dashboards/filter_sets/commands/update.py
new file mode 100644
index 0000000..d2c43f0
--- /dev/null
+++ b/superset/dashboards/filter_sets/commands/update.py
@@ -0,0 +1,56 @@
+# 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.
+import logging
+from typing import Any, Dict
+
+from flask_appbuilder.models.sqla import Model
+from flask_appbuilder.security.sqla.models import User
+
+from superset.dao.exceptions import DAOUpdateFailedError
+from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
+from superset.dashboards.filter_sets.commands.exceptions import (
+    FilterSetUpdateFailedError,
+)
+from superset.dashboards.filter_sets.consts import OWNER_ID_FIELD, 
OWNER_TYPE_FIELD
+from superset.dashboards.filter_sets.dao import FilterSetDAO
+
+logger = logging.getLogger(__name__)
+
+
+class UpdateFilterSetCommand(BaseFilterSetCommand):
+    def __init__(
+        self, user: User, dashboard_id: int, filter_set_id: int, data: 
Dict[str, Any]
+    ):
+        super().__init__(user, dashboard_id)
+        self._filter_set_id = filter_set_id
+        self._properties = data.copy()
+
+    def run(self) -> Model:
+        try:
+            self.validate()
+            if (
+                OWNER_TYPE_FIELD in self._properties
+                and self._properties[OWNER_TYPE_FIELD] == "Dashboard"
+            ):
+                self._properties[OWNER_ID_FIELD] = self._dashboard_id
+            return FilterSetDAO.update(self._filter_set, self._properties, 
commit=True)
+        except DAOUpdateFailedError as err:
+            raise FilterSetUpdateFailedError(str(self._filter_set_id), "") 
from err
+
+    def validate(self) -> None:
+        self._validate_filterset_dashboard_exists()
+        self.validate_exist_filter_use_cases_set()
diff --git a/superset/dashboards/filter_sets/consts.py 
b/superset/dashboards/filter_sets/consts.py
new file mode 100644
index 0000000..ff60a4f
--- /dev/null
+++ b/superset/dashboards/filter_sets/consts.py
@@ -0,0 +1,30 @@
+# 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.
+USER_OWNER_TYPE = "User"
+DASHBOARD_OWNER_TYPE = "Dashboard"
+
+NAME_FIELD = "name"
+DESCRIPTION_FIELD = "description"
+JSON_METADATA_FIELD = "json_metadata"
+OWNER_ID_FIELD = "owner_id"
+OWNER_TYPE_FIELD = "owner_type"
+DASHBOARD_ID_FIELD = "dashboard_id"
+OWNER_OBJECT_FIELD = "owner_object"
+DASHBOARD_FIELD = "dashboard"
+PARAMS_PROPERTY = "params"
+
+FILTER_SET_API_PERMISSIONS_NAME = "FilterSets"
diff --git a/superset/dashboards/filter_sets/dao.py 
b/superset/dashboards/filter_sets/dao.py
new file mode 100644
index 0000000..949aa6d
--- /dev/null
+++ b/superset/dashboards/filter_sets/dao.py
@@ -0,0 +1,64 @@
+# 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.
+import logging
+from typing import Any, Dict
+
+from flask_appbuilder.models.sqla import Model
+from sqlalchemy.exc import SQLAlchemyError
+
+from superset.dao.base import BaseDAO
+from superset.dao.exceptions import DAOConfigError, DAOCreateFailedError
+from superset.dashboards.filter_sets.consts import (
+    DASHBOARD_ID_FIELD,
+    DESCRIPTION_FIELD,
+    JSON_METADATA_FIELD,
+    NAME_FIELD,
+    OWNER_ID_FIELD,
+    OWNER_TYPE_FIELD,
+)
+from superset.extensions import db
+from superset.models.filter_set import FilterSet
+
+logger = logging.getLogger(__name__)
+
+
+class FilterSetDAO(BaseDAO):
+    model_cls = FilterSet
+
+    @classmethod
+    def create(cls, properties: Dict[str, Any], commit: bool = True) -> Model:
+        if cls.model_cls is None:
+            raise DAOConfigError()
+        model = FilterSet()
+        setattr(model, NAME_FIELD, properties[NAME_FIELD])
+        setattr(model, JSON_METADATA_FIELD, properties[JSON_METADATA_FIELD])
+        setattr(model, DESCRIPTION_FIELD, properties.get(DESCRIPTION_FIELD, 
None))
+        setattr(
+            model,
+            OWNER_ID_FIELD,
+            properties.get(OWNER_ID_FIELD, properties[DASHBOARD_ID_FIELD]),
+        )
+        setattr(model, OWNER_TYPE_FIELD, properties[OWNER_TYPE_FIELD])
+        setattr(model, DASHBOARD_ID_FIELD, properties[DASHBOARD_ID_FIELD])
+        try:
+            db.session.add(model)
+            if commit:
+                db.session.commit()
+        except SQLAlchemyError as ex:  # pragma: no cover
+            db.session.rollback()
+            raise DAOCreateFailedError() from ex
+        return model
diff --git a/superset/dashboards/filter_sets/filters.py 
b/superset/dashboards/filter_sets/filters.py
new file mode 100644
index 0000000..0083f40
--- /dev/null
+++ b/superset/dashboards/filter_sets/filters.py
@@ -0,0 +1,58 @@
+# 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.
+from __future__ import annotations
+
+from typing import Any, TYPE_CHECKING
+
+from flask import g
+from sqlalchemy import and_, or_
+
+from superset.dashboards.filter_sets.consts import DASHBOARD_OWNER_TYPE, 
USER_OWNER_TYPE
+from superset.models.dashboard import dashboard_user
+from superset.models.filter_set import FilterSet
+from superset.views.base import BaseFilter, is_user_admin
+
+if TYPE_CHECKING:
+    from sqlalchemy.orm.query import Query
+
+
+class FilterSetFilter(BaseFilter):  # pylint: disable=too-few-public-methods)
+    def apply(self, query: Query, value: Any) -> Query:
+        if is_user_admin():
+            return query
+        current_user_id = g.user.id
+
+        filter_set_ids_by_dashboard_owners = (  # pylint: disable=C0103
+            query.from_self(FilterSet.id)
+            .join(dashboard_user, FilterSet.owner_id == 
dashboard_user.c.dashboard_id)
+            .filter(
+                and_(
+                    FilterSet.owner_type == DASHBOARD_OWNER_TYPE,
+                    dashboard_user.c.user_id == current_user_id,
+                )
+            )
+        )
+
+        return query.filter(
+            or_(
+                and_(
+                    FilterSet.owner_type == USER_OWNER_TYPE,
+                    FilterSet.owner_id == current_user_id,
+                ),
+                FilterSet.id.in_(filter_set_ids_by_dashboard_owners),
+            )
+        )
diff --git a/superset/dashboards/filter_sets/schemas.py 
b/superset/dashboards/filter_sets/schemas.py
new file mode 100644
index 0000000..3c0436d
--- /dev/null
+++ b/superset/dashboards/filter_sets/schemas.py
@@ -0,0 +1,93 @@
+# 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.
+from typing import Any, cast, Dict, Mapping
+
+from marshmallow import fields, post_load, Schema, ValidationError
+from marshmallow.validate import Length, OneOf
+
+from superset.dashboards.filter_sets.consts import (
+    DASHBOARD_OWNER_TYPE,
+    JSON_METADATA_FIELD,
+    OWNER_ID_FIELD,
+    OWNER_TYPE_FIELD,
+    USER_OWNER_TYPE,
+)
+
+
+class JsonMetadataSchema(Schema):
+    nativeFilters = fields.Mapping(required=True, allow_none=False)
+    dataMask = fields.Mapping(required=False, allow_none=False)
+
+
+class FilterSetSchema(Schema):
+    json_metadata_schema: JsonMetadataSchema = JsonMetadataSchema()
+
+    def _validate_json_meta_data(self, json_meta_data: str) -> None:
+        try:
+            self.json_metadata_schema.loads(json_meta_data)
+        except Exception as ex:
+            raise ValidationError("failed to parse json_metadata to json") 
from ex
+
+
+class FilterSetPostSchema(FilterSetSchema):
+    json_metadata_schema: JsonMetadataSchema = JsonMetadataSchema()
+    # pylint: disable=W0613
+    name = fields.String(required=True, allow_none=False, validate=Length(0, 
500),)
+    description = fields.String(
+        required=False, allow_none=True, validate=[Length(1, 1000)]
+    )
+    json_metadata = fields.String(allow_none=False, required=True)
+
+    owner_type = fields.String(
+        required=True, validate=OneOf([USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE])
+    )
+    owner_id = fields.Int(required=False)
+
+    @post_load
+    def validate(
+        self, data: Mapping[Any, Any], *, many: Any, partial: Any
+    ) -> Dict[str, Any]:
+        self._validate_json_meta_data(data[JSON_METADATA_FIELD])
+        if data[OWNER_TYPE_FIELD] == USER_OWNER_TYPE and OWNER_ID_FIELD not in 
data:
+            raise ValidationError("owner_id is mandatory when owner_type is 
User")
+        return cast(Dict[str, Any], data)
+
+
+class FilterSetPutSchema(FilterSetSchema):
+    name = fields.String(required=False, allow_none=False, validate=Length(0, 
500))
+    description = fields.String(
+        required=False, allow_none=False, validate=[Length(1, 1000)]
+    )
+    json_metadata = fields.String(required=False, allow_none=False)
+    owner_type = fields.String(
+        allow_none=False, required=False, 
validate=OneOf([DASHBOARD_OWNER_TYPE])
+    )
+
+    @post_load
+    def validate(  # pylint: disable=unused-argument
+        self, data: Mapping[Any, Any], *, many: Any, partial: Any
+    ) -> Dict[str, Any]:
+        if JSON_METADATA_FIELD in data:
+            self._validate_json_meta_data(data[JSON_METADATA_FIELD])
+        return cast(Dict[str, Any], data)
+
+
+def validate_pair(first_field: str, second_field: str, data: Dict[str, Any]) 
-> None:
+    if first_field in data and second_field not in data:
+        raise ValidationError(
+            "{} must be included alongside {}".format(first_field, 
second_field)
+        )
diff --git a/superset/initialization/__init__.py 
b/superset/initialization/__init__.py
index d19c1d1..b8a1098 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -141,6 +141,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         from superset.queries.saved_queries.api import SavedQueryRestApi
         from superset.reports.api import ReportScheduleRestApi
         from superset.reports.logs.api import ReportExecutionLogRestApi
+        from superset.dashboards.filter_sets.api import FilterSetRestApi
         from superset.views.access_requests import AccessRequestsModelView
         from superset.views.alerts import (
             AlertLogModelView,
@@ -208,6 +209,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         appbuilder.add_api(SavedQueryRestApi)
         appbuilder.add_api(ReportScheduleRestApi)
         appbuilder.add_api(ReportExecutionLogRestApi)
+        appbuilder.add_api(FilterSetRestApi)
         #
         # Setup regular views
         #
diff --git a/superset/migrations/versions/3ebe0993c770_filterset_table.py 
b/superset/migrations/versions/3ebe0993c770_filterset_table.py
new file mode 100644
index 0000000..b509f89
--- /dev/null
+++ b/superset/migrations/versions/3ebe0993c770_filterset_table.py
@@ -0,0 +1,56 @@
+# 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 filter set model
+
+Revision ID: 3ebe0993c770
+Revises: 07071313dd52
+Create Date: 2021-03-29 11:15:48.831225
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = "3ebe0993c770"
+down_revision = "181091c0ef16"
+
+import sqlalchemy as sa
+from alembic import op
+
+
+def upgrade():
+    op.create_table(
+        "filter_sets",
+        sa.Column("created_on", sa.DateTime(), nullable=True),
+        sa.Column("changed_on", sa.DateTime(), nullable=True),
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("name", sa.VARCHAR(500), nullable=False),
+        sa.Column("description", sa.Text(), nullable=True),
+        sa.Column("json_metadata", sa.Text(), nullable=False),
+        sa.Column("owner_id", sa.Integer(), nullable=False),
+        sa.Column("owner_type", sa.VARCHAR(255), nullable=False),
+        sa.Column(
+            "dashboard_id", sa.Integer(), sa.ForeignKey("dashboards.id"), 
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.PrimaryKeyConstraint("id"),
+    )
+
+
+def downgrade():
+    op.drop_table("filter_sets")
diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py
index 3b15841..0db1ddf 100644
--- a/superset/models/dashboard.py
+++ b/superset/models/dashboard.py
@@ -23,6 +23,7 @@ from functools import partial
 from typing import Any, Callable, Dict, List, Set, Tuple, Type, Union
 
 import sqlalchemy as sqla
+from flask import g
 from flask_appbuilder import Model
 from flask_appbuilder.models.decorators import renders
 from flask_appbuilder.security.sqla.models import User
@@ -46,10 +47,12 @@ from sqlalchemy.sql import join, select
 from sqlalchemy.sql.elements import BinaryExpression
 
 from superset import app, ConnectorRegistry, db, is_feature_enabled, 
security_manager
+from superset.common.request_contexed_based import is_user_admin
 from superset.connectors.base.models import BaseDatasource
 from superset.connectors.druid.models import DruidColumn, DruidMetric
 from superset.connectors.sqla.models import SqlMetric, TableColumn
 from superset.extensions import cache_manager
+from superset.models.filter_set import FilterSet
 from superset.models.helpers import AuditMixinNullable, ImportExportMixin
 from superset.models.slice import Slice
 from superset.models.tags import DashboardUpdater
@@ -129,6 +132,7 @@ DashboardRoles = Table(
 )
 
 
+# pylint: disable=too-many-public-methods
 class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
     """The dashboard object!"""
 
@@ -144,6 +148,9 @@ class Dashboard(Model, AuditMixinNullable, 
ImportExportMixin):
     owners = relationship(security_manager.user_model, 
secondary=dashboard_user)
     published = Column(Boolean, default=False)
     roles = relationship(security_manager.role_model, secondary=DashboardRoles)
+    _filter_sets = relationship(
+        "FilterSet", back_populates="dashboard", cascade="all, delete"
+    )
     export_fields = [
         "dashboard_title",
         "position_json",
@@ -179,6 +186,29 @@ class Dashboard(Model, AuditMixinNullable, 
ImportExportMixin):
         }
 
     @property
+    def filter_sets(self) -> Dict[int, FilterSet]:
+        return {fs.id: fs for fs in self._filter_sets}
+
+    @property
+    def filter_sets_lst(self) -> Dict[int, FilterSet]:
+        if is_user_admin():
+            return self._filter_sets
+        current_user = g.user.id
+        filter_sets_by_owner_type: Dict[str, List[Any]] = {"Dashboard": [], 
"User": []}
+        for fs in self._filter_sets:
+            filter_sets_by_owner_type[fs.owner_type].append(fs)
+        user_filter_sets = list(
+            filter(
+                lambda filter_set: filter_set.owner_id == current_user,
+                filter_sets_by_owner_type["User"],
+            )
+        )
+        return {
+            fs.id: fs
+            for fs in user_filter_sets + filter_sets_by_owner_type["Dashboard"]
+        }
+
+    @property
     def charts(self) -> List[BaseDatasource]:
         return [slc.chart for slc in self.slices]
 
@@ -397,6 +427,11 @@ class Dashboard(Model, AuditMixinNullable, 
ImportExportMixin):
         qry = session.query(Dashboard).filter(id_or_slug_filter(id_or_slug))
         return qry.one_or_none()
 
+    def is_actor_owner(self) -> bool:
+        if g.user is None or g.user.is_anonymous or not 
g.user.is_authenticated:
+            return False
+        return g.user.id in set(map(lambda user: user.id, self.owners))
+
 
 def id_or_slug_filter(id_or_slug: str) -> BinaryExpression:
     if id_or_slug.isdigit():
diff --git a/superset/models/filter_set.py b/superset/models/filter_set.py
new file mode 100644
index 0000000..2d3b218
--- /dev/null
+++ b/superset/models/filter_set.py
@@ -0,0 +1,106 @@
+# 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.
+from __future__ import annotations
+
+import json
+import logging
+from typing import Any, Dict
+
+from flask_appbuilder import Model
+from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, Text
+from sqlalchemy.orm import relationship
+from sqlalchemy_utils import generic_relationship
+
+from superset import app, db
+from superset.models.helpers import AuditMixinNullable
+
+metadata = Model.metadata  # pylint: disable=no-member
+config = app.config
+logger = logging.getLogger(__name__)
+
+
+class FilterSet(Model, AuditMixinNullable):
+    __tablename__ = "filter_sets"
+    id = Column(Integer, primary_key=True)
+    name = Column(String(500), nullable=False, unique=True)
+    description = Column(Text, nullable=True)
+    json_metadata = Column(Text, nullable=False)
+    dashboard_id = Column(Integer, ForeignKey("dashboards.id"))
+    dashboard = relationship("Dashboard", back_populates="_filter_sets")
+    owner_id = Column(Integer, nullable=False)
+    owner_type = Column(String(255), nullable=False)
+    owner_object = generic_relationship(owner_type, owner_id)
+
+    def __repr__(self) -> str:
+        return f"FilterSet<{self.name or self.id}>"
+
+    @property
+    def url(self) -> str:
+        return f"/api/filtersets/{self.id}/"
+
+    @property
+    def sqla_metadata(self) -> None:
+        # pylint: disable=no-member
+        meta = MetaData(bind=self.get_sqla_engine())
+        meta.reflect()
+
+    @property
+    def changed_by_name(self) -> str:
+        if not self.changed_by:
+            return ""
+        return str(self.changed_by)
+
+    @property
+    def changed_by_url(self) -> str:
+        if not self.changed_by:
+            return ""
+        return f"/superset/profile/{self.changed_by.username}"
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "id": self.id,
+            "name": self.name,
+            "description": self.description,
+            "params": self.params,
+            "dashboard_id": self.dashboard_id,
+            "owner_type": self.owner_type,
+            "owner_id": self.owner_id,
+        }
+
+    @classmethod
+    def get(cls, _id: int) -> FilterSet:
+        session = db.session()
+        qry = session.query(FilterSet).filter(_id)
+        return qry.one_or_none()
+
+    @classmethod
+    def get_by_name(cls, name: str) -> FilterSet:
+        session = db.session()
+        qry = session.query(FilterSet).filter(FilterSet.name == name)
+        return qry.one_or_none()
+
+    @classmethod
+    def get_by_dashboard_id(cls, dashboard_id: int) -> FilterSet:
+        session = db.session()
+        qry = session.query(FilterSet).filter(FilterSet.dashboard_id == 
dashboard_id)
+        return qry.all()
+
+    @property
+    def params(self) -> Dict[str, Any]:
+        if self.json_metadata:
+            return json.loads(self.json_metadata)
+        return {}
diff --git a/tests/integration_tests/dashboards/filter_sets/__init__.py 
b/tests/integration_tests/dashboards/filter_sets/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/tests/integration_tests/dashboards/filter_sets/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/tests/integration_tests/dashboards/filter_sets/conftest.py 
b/tests/integration_tests/dashboards/filter_sets/conftest.py
new file mode 100644
index 0000000..36642b1
--- /dev/null
+++ b/tests/integration_tests/dashboards/filter_sets/conftest.py
@@ -0,0 +1,321 @@
+# 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.
+from __future__ import annotations
+
+import json
+from typing import Any, Dict, Generator, List, TYPE_CHECKING
+
+import pytest
+
+from superset import security_manager as sm
+from superset.dashboards.filter_sets.consts import (
+    DESCRIPTION_FIELD,
+    JSON_METADATA_FIELD,
+    NAME_FIELD,
+    OWNER_ID_FIELD,
+    OWNER_TYPE_FIELD,
+    USER_OWNER_TYPE,
+)
+from superset.models.dashboard import Dashboard
+from superset.models.filter_set import FilterSet
+from tests.integration_tests.dashboards.filter_sets.consts import (
+    ADMIN_USERNAME_FOR_TEST,
+    DASHBOARD_OWNER_USERNAME,
+    FILTER_SET_OWNER_USERNAME,
+    REGULAR_USER,
+)
+from tests.integration_tests.dashboards.superset_factory_util import (
+    create_dashboard,
+    create_database,
+    create_datasource_table,
+    create_slice,
+)
+from tests.integration_tests.test_app import app
+
+if TYPE_CHECKING:
+    from flask.ctx import AppContext
+    from flask.testing import FlaskClient
+    from flask_appbuilder.security.sqla.models import (
+        Role,
+        User,
+        ViewMenu,
+        PermissionView,
+    )
+    from flask_appbuilder.security.manager import BaseSecurityManager
+    from sqlalchemy.orm import Session
+    from superset.models.slice import Slice
+    from superset.connectors.sqla.models import SqlaTable
+    from superset.models.core import Database
+
+
+security_manager: BaseSecurityManager = sm
+
+
+# @pytest.fixture(autouse=True, scope="session")
+# def setup_sample_data() -> Any:
+#     pass
+
+
[email protected](autouse=True)
+def expire_on_commit_true() -> Generator[None, None, None]:
+    ctx: AppContext
+    with app.app_context() as ctx:
+        ctx.app.appbuilder.get_session.configure(expire_on_commit=False)
+    yield
+    ctx.app.appbuilder.get_session.configure(expire_on_commit=True)
+
+
[email protected](autouse=True, scope="module")
+def test_users() -> Generator[Dict[str, int], None, None]:
+    usernames = [
+        ADMIN_USERNAME_FOR_TEST,
+        DASHBOARD_OWNER_USERNAME,
+        FILTER_SET_OWNER_USERNAME,
+        REGULAR_USER,
+    ]
+    with app.app_context():
+        filter_set_role = build_filter_set_role()
+        admin_role: Role = security_manager.find_role("Admin")
+        usernames_to_ids = create_test_users(admin_role, filter_set_role, 
usernames)
+    yield usernames_to_ids
+    ctx: AppContext
+    delete_users(usernames_to_ids)
+
+
+def delete_users(usernames_to_ids: Dict[str, int]) -> None:
+    with app.app_context() as ctx:
+        session: Session = ctx.app.appbuilder.get_session
+        for username in usernames_to_ids.keys():
+            session.delete(security_manager.find_user(username))
+        session.commit()
+
+
+def create_test_users(
+    admin_role: Role, filter_set_role: Role, usernames: List[str]
+) -> Dict[str, int]:
+    users: List[User] = []
+    for username in usernames:
+        user = build_user(username, filter_set_role, admin_role)
+        users.append(user)
+    return {user.username: user.id for user in users}
+
+
+def build_user(username: str, filter_set_role: Role, admin_role: Role) -> User:
+    roles_to_add = (
+        [admin_role] if username == ADMIN_USERNAME_FOR_TEST else 
[filter_set_role]
+    )
+    user: User = security_manager.add_user(
+        username, "test", "test", username, roles_to_add, password="general"
+    )
+    if not user:
+        user = security_manager.find_user(username)
+        if user is None:
+            raise Exception("Failed to build the user {}".format(username))
+    return user
+
+
+def build_filter_set_role() -> Role:
+    filter_set_role: Role = security_manager.add_role("filter_set_role")
+    filterset_view_name: ViewMenu = 
security_manager.find_view_menu("FilterSets")
+    all_datasource_view_name: ViewMenu = security_manager.find_view_menu(
+        "all_datasource_access"
+    )
+    pvms: List[PermissionView] = security_manager.find_permissions_view_menu(
+        filterset_view_name
+    ) + security_manager.find_permissions_view_menu(all_datasource_view_name)
+    for pvm in pvms:
+        security_manager.add_permission_role(filter_set_role, pvm)
+    return filter_set_role
+
+
[email protected]
+def client() -> Generator[FlaskClient[Any], None, None]:
+    with app.test_client() as client:
+        yield client
+
+
[email protected]
+def dashboard() -> Generator[Dashboard, None, None]:
+    dashboard: Dashboard
+    slice_: Slice
+    datasource: SqlaTable
+    database: Database
+    session: Session
+    try:
+        with app.app_context() as ctx:
+            dashboard_owner_user = 
security_manager.find_user(DASHBOARD_OWNER_USERNAME)
+            database = create_database("test_database_filter_sets")
+            datasource = create_datasource_table(
+                name="test_datasource", database=database, 
owners=[dashboard_owner_user]
+            )
+            slice_ = create_slice(
+                datasource=datasource, name="test_slice", 
owners=[dashboard_owner_user]
+            )
+            dashboard = create_dashboard(
+                dashboard_title="test_dashboard",
+                published=True,
+                slices=[slice_],
+                owners=[dashboard_owner_user],
+            )
+            session = ctx.app.appbuilder.get_session
+            session.add(dashboard)
+            session.commit()
+        yield dashboard
+    except Exception as ex:
+        print(str(ex))
+    finally:
+        with app.app_context() as ctx:
+            session = ctx.app.appbuilder.get_session
+            try:
+                dashboard.owners = []
+                slice_.owners = []
+                datasource.owners = []
+                session.merge(dashboard)
+                session.merge(slice_)
+                session.merge(datasource)
+                session.commit()
+                session.delete(dashboard)
+                session.delete(slice_)
+                session.delete(datasource)
+                session.delete(database)
+                session.commit()
+            except Exception as ex:
+                print(str(ex))
+
+
[email protected]
+def dashboard_id(dashboard) -> int:
+    return dashboard.id
+
+
[email protected]
+def filtersets(
+    dashboard_id: int, test_users: Dict[str, int], dumped_valid_json_metadata: 
str
+) -> Generator[Dict[str, List[FilterSet]], None, None]:
+    try:
+        with app.app_context() as ctx:
+            session: Session = ctx.app.appbuilder.get_session
+            first_filter_set = FilterSet(
+                name="filter_set_1_of_" + str(dashboard_id),
+                dashboard_id=dashboard_id,
+                json_metadata=dumped_valid_json_metadata,
+                owner_id=dashboard_id,
+                owner_type="Dashboard",
+            )
+            second_filter_set = FilterSet(
+                name="filter_set_2_of_" + str(dashboard_id),
+                json_metadata=dumped_valid_json_metadata,
+                dashboard_id=dashboard_id,
+                owner_id=dashboard_id,
+                owner_type="Dashboard",
+            )
+            third_filter_set = FilterSet(
+                name="filter_set_3_of_" + str(dashboard_id),
+                json_metadata=dumped_valid_json_metadata,
+                dashboard_id=dashboard_id,
+                owner_id=test_users[FILTER_SET_OWNER_USERNAME],
+                owner_type="User",
+            )
+            forth_filter_set = FilterSet(
+                name="filter_set_4_of_" + str(dashboard_id),
+                json_metadata=dumped_valid_json_metadata,
+                dashboard_id=dashboard_id,
+                owner_id=test_users[FILTER_SET_OWNER_USERNAME],
+                owner_type="User",
+            )
+            session.add(first_filter_set)
+            session.add(second_filter_set)
+            session.add(third_filter_set)
+            session.add(forth_filter_set)
+            session.commit()
+            yv = {
+                "Dashboard": [first_filter_set, second_filter_set],
+                FILTER_SET_OWNER_USERNAME: [third_filter_set, 
forth_filter_set],
+            }
+        yield yv
+    except Exception as ex:
+        print(str(ex))
+
+
[email protected]
+def filterset_id(filtersets: Dict[str, List[FilterSet]]) -> int:
+    return filtersets["Dashboard"][0].id
+
+
[email protected]
+def valid_json_metadata() -> Dict[str, Any]:
+    return {"nativeFilters": {}}
+
+
[email protected]
+def dumped_valid_json_metadata(valid_json_metadata: Dict[str, Any]) -> str:
+    return json.dumps(valid_json_metadata)
+
+
[email protected]
+def exists_user_id() -> int:
+    return 1
+
+
[email protected]
+def valid_filter_set_data_for_create(
+    dashboard_id: int, dumped_valid_json_metadata: str, exists_user_id: int
+) -> Dict[str, Any]:
+    name = "test_filter_set_of_dashboard_" + str(dashboard_id)
+    return {
+        NAME_FIELD: name,
+        DESCRIPTION_FIELD: "description of " + name,
+        JSON_METADATA_FIELD: dumped_valid_json_metadata,
+        OWNER_TYPE_FIELD: USER_OWNER_TYPE,
+        OWNER_ID_FIELD: exists_user_id,
+    }
+
+
[email protected]
+def valid_filter_set_data_for_update(
+    dashboard_id: int, dumped_valid_json_metadata: str, exists_user_id: int
+) -> Dict[str, Any]:
+    name = "name_changed_test_filter_set_of_dashboard_" + str(dashboard_id)
+    return {
+        NAME_FIELD: name,
+        DESCRIPTION_FIELD: "changed description of " + name,
+        JSON_METADATA_FIELD: dumped_valid_json_metadata,
+    }
+
+
[email protected]
+def not_exists_dashboard(dashboard_id: int) -> int:
+    return dashboard_id + 1
+
+
[email protected]
+def not_exists_user_id() -> int:
+    return 99999
+
+
[email protected]()
+def dashboard_based_filter_set_dict(
+    filtersets: Dict[str, List[FilterSet]]
+) -> Dict[str, Any]:
+    return filtersets["Dashboard"][0].to_dict()
+
+
[email protected]()
+def user_based_filter_set_dict(
+    filtersets: Dict[str, List[FilterSet]]
+) -> Dict[str, Any]:
+    return filtersets[FILTER_SET_OWNER_USERNAME][0].to_dict()
diff --git a/tests/integration_tests/dashboards/filter_sets/consts.py 
b/tests/integration_tests/dashboards/filter_sets/consts.py
new file mode 100644
index 0000000..f54f00f
--- /dev/null
+++ b/tests/integration_tests/dashboards/filter_sets/consts.py
@@ -0,0 +1,22 @@
+# 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.
+FILTER_SET_URI = "api/v1/dashboard/{dashboard_id}/filtersets"
+
+ADMIN_USERNAME_FOR_TEST = "[email protected]"
+DASHBOARD_OWNER_USERNAME = "[email protected]"
+FILTER_SET_OWNER_USERNAME = "[email protected]"
+REGULAR_USER = "[email protected]"
diff --git a/tests/integration_tests/dashboards/filter_sets/create_api_tests.py 
b/tests/integration_tests/dashboards/filter_sets/create_api_tests.py
new file mode 100644
index 0000000..cbdaef9
--- /dev/null
+++ b/tests/integration_tests/dashboards/filter_sets/create_api_tests.py
@@ -0,0 +1,630 @@
+# 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.
+from __future__ import annotations
+
+from typing import Any, Dict, TYPE_CHECKING
+
+from superset.dashboards.filter_sets.consts import (
+    DASHBOARD_OWNER_TYPE,
+    DESCRIPTION_FIELD,
+    JSON_METADATA_FIELD,
+    NAME_FIELD,
+    OWNER_ID_FIELD,
+    OWNER_TYPE_FIELD,
+    USER_OWNER_TYPE,
+)
+from tests.integration_tests.base_tests import login
+from tests.integration_tests.dashboards.filter_sets.consts import (
+    ADMIN_USERNAME_FOR_TEST,
+    DASHBOARD_OWNER_USERNAME,
+    FILTER_SET_OWNER_USERNAME,
+)
+from tests.integration_tests.dashboards.filter_sets.utils import (
+    call_create_filter_set,
+    get_filter_set_by_dashboard_id,
+    get_filter_set_by_name,
+)
+
+if TYPE_CHECKING:
+    from flask.testing import FlaskClient
+
+
+def assert_filterset_was_not_created(filter_set_data: Dict[str, Any]) -> None:
+    assert get_filter_set_by_name(str(filter_set_data["name"])) is None
+
+
+def assert_filterset_was_created(filter_set_data: Dict[str, Any]) -> None:
+    assert get_filter_set_by_name(filter_set_data["name"]) is not None
+
+
+class TestCreateFilterSetsApi:
+    def test_with_extra_field__400(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create["extra"] = "val"
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert response.json["message"]["extra"][0] == "Unknown field."
+        assert_filterset_was_not_created(valid_filter_set_data_for_create)
+
+    def test_with_id_field__400(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create["id"] = 1
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert response.json["message"]["id"][0] == "Unknown field."
+        assert_filterset_was_not_created(valid_filter_set_data_for_create)
+
+    def test_with_dashboard_not_exists__404(
+        self,
+        not_exists_dashboard: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # act
+        login(client, "admin")
+        response = call_create_filter_set(
+            client, not_exists_dashboard, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 404
+        assert_filterset_was_not_created(valid_filter_set_data_for_create)
+
+    def test_without_name__400(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create.pop(NAME_FIELD, None)
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert get_filter_set_by_dashboard_id(dashboard_id) == []
+
+    def test_with_none_name__400(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create[NAME_FIELD] = None
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_created(valid_filter_set_data_for_create)
+
+    def test_with_int_as_name__400(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create[NAME_FIELD] = 4
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_created(valid_filter_set_data_for_create)
+
+    def test_without_description__201(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create.pop(DESCRIPTION_FIELD, None)
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_with_none_description__201(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create[DESCRIPTION_FIELD] = None
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_with_int_as_description__400(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create[DESCRIPTION_FIELD] = 1
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_created(valid_filter_set_data_for_create)
+
+    def test_without_json_metadata__400(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create.pop(JSON_METADATA_FIELD, None)
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_created(valid_filter_set_data_for_create)
+
+    def test_with_invalid_json_metadata__400(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create[DESCRIPTION_FIELD] = {}
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_created(valid_filter_set_data_for_create)
+
+    def test_without_owner_type__400(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create.pop(OWNER_TYPE_FIELD, None)
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_created(valid_filter_set_data_for_create)
+
+    def test_with_invalid_owner_type__400(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = "OTHER_TYPE"
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_created(valid_filter_set_data_for_create)
+
+    def test_without_owner_id_when_owner_type_is_user__400(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
+        valid_filter_set_data_for_create.pop(OWNER_ID_FIELD, None)
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_created(valid_filter_set_data_for_create)
+
+    def test_without_owner_id_when_owner_type_is_dashboard__201(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = 
DASHBOARD_OWNER_TYPE
+        valid_filter_set_data_for_create.pop(OWNER_ID_FIELD, None)
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_with_not_exists_owner__400(
+        self,
+        dashboard_id: int,
+        valid_filter_set_data_for_create: Dict[str, Any],
+        not_exists_user_id: int,
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
+        valid_filter_set_data_for_create[OWNER_ID_FIELD] = not_exists_user_id
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_created(valid_filter_set_data_for_create)
+
+    def test_when_caller_is_admin_and_owner_is_admin__201(
+        self,
+        dashboard_id: int,
+        test_users: Dict[str, int],
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
+        valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
+            ADMIN_USERNAME_FOR_TEST
+        ]
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_when_caller_is_admin_and_owner_is_dashboard_owner__201(
+        self,
+        dashboard_id: int,
+        test_users: Dict[str, int],
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
+        valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
+            DASHBOARD_OWNER_USERNAME
+        ]
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_when_caller_is_admin_and_owner_is_regular_user__201(
+        self,
+        dashboard_id: int,
+        test_users: Dict[str, int],
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
+        valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
+            FILTER_SET_OWNER_USERNAME
+        ]
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_when_caller_is_admin_and_owner_type_is_dashboard__201(
+        self,
+        dashboard_id: int,
+        test_users: Dict[str, int],
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = 
DASHBOARD_OWNER_TYPE
+        valid_filter_set_data_for_create[OWNER_ID_FIELD] = dashboard_id
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_when_caller_is_dashboard_owner_and_owner_is_admin__201(
+        self,
+        dashboard_id: int,
+        test_users: Dict[str, int],
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, DASHBOARD_OWNER_USERNAME)
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
+        valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
+            ADMIN_USERNAME_FOR_TEST
+        ]
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_when_caller_is_dashboard_owner_and_owner_is_dashboard_owner__201(
+        self,
+        dashboard_id: int,
+        test_users: Dict[str, int],
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, DASHBOARD_OWNER_USERNAME)
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
+        valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
+            DASHBOARD_OWNER_USERNAME
+        ]
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_when_caller_is_dashboard_owner_and_owner_is_regular_user__201(
+        self,
+        dashboard_id: int,
+        test_users: Dict[str, int],
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, DASHBOARD_OWNER_USERNAME)
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
+        valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
+            FILTER_SET_OWNER_USERNAME
+        ]
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__201(
+        self,
+        dashboard_id: int,
+        test_users: Dict[str, int],
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, DASHBOARD_OWNER_USERNAME)
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = 
DASHBOARD_OWNER_TYPE
+        valid_filter_set_data_for_create[OWNER_ID_FIELD] = dashboard_id
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_when_caller_is_regular_user_and_owner_is_admin__201(
+        self,
+        dashboard_id: int,
+        test_users: Dict[str, int],
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, FILTER_SET_OWNER_USERNAME)
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
+        valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
+            ADMIN_USERNAME_FOR_TEST
+        ]
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_when_caller_is_regular_user_and_owner_is_dashboard_owner__201(
+        self,
+        dashboard_id: int,
+        test_users: Dict[str, int],
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, FILTER_SET_OWNER_USERNAME)
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
+        valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
+            DASHBOARD_OWNER_USERNAME
+        ]
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_when_caller_is_regular_user_and_owner_is_regular_user__201(
+        self,
+        dashboard_id: int,
+        test_users: Dict[str, int],
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, FILTER_SET_OWNER_USERNAME)
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
+        valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
+            FILTER_SET_OWNER_USERNAME
+        ]
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 201
+        assert_filterset_was_created(valid_filter_set_data_for_create)
+
+    def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403(
+        self,
+        dashboard_id: int,
+        test_users: Dict[str, int],
+        valid_filter_set_data_for_create: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, FILTER_SET_OWNER_USERNAME)
+        valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = 
DASHBOARD_OWNER_TYPE
+        valid_filter_set_data_for_create[OWNER_ID_FIELD] = dashboard_id
+
+        # act
+        response = call_create_filter_set(
+            client, dashboard_id, valid_filter_set_data_for_create
+        )
+
+        # assert
+        assert response.status_code == 403
+        assert_filterset_was_not_created(valid_filter_set_data_for_create)
diff --git a/tests/integration_tests/dashboards/filter_sets/delete_api_tests.py 
b/tests/integration_tests/dashboards/filter_sets/delete_api_tests.py
new file mode 100644
index 0000000..12a9bff
--- /dev/null
+++ b/tests/integration_tests/dashboards/filter_sets/delete_api_tests.py
@@ -0,0 +1,209 @@
+# 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.
+from __future__ import annotations
+
+from typing import Any, Dict, List, TYPE_CHECKING
+
+from tests.integration_tests.base_tests import login
+from tests.integration_tests.dashboards.filter_sets.consts import (
+    DASHBOARD_OWNER_USERNAME,
+    FILTER_SET_OWNER_USERNAME,
+    REGULAR_USER,
+)
+from tests.integration_tests.dashboards.filter_sets.utils import (
+    call_delete_filter_set,
+    collect_all_ids,
+    get_filter_set_by_name,
+)
+
+if TYPE_CHECKING:
+    from flask.testing import FlaskClient
+    from superset.models.filter_set import FilterSet
+
+
+def assert_filterset_was_not_deleted(filter_set_dict: Dict[str, Any]) -> None:
+    assert get_filter_set_by_name(filter_set_dict["name"]) is not None
+
+
+def assert_filterset_deleted(filter_set_dict: Dict[str, Any]) -> None:
+    assert get_filter_set_by_name(filter_set_dict["name"]) is None
+
+
+class TestDeleteFilterSet:
+    def test_with_dashboard_exists_filterset_not_exists__200(
+        self,
+        dashboard_id: int,
+        filtersets: Dict[str, List[FilterSet]],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        filter_set_id = max(collect_all_ids(filtersets)) + 1
+
+        response = call_delete_filter_set(client, {"id": filter_set_id}, 
dashboard_id)
+        # assert
+        assert response.status_code == 200
+
+    def test_with_dashboard_not_exists_filterset_not_exists__404(
+        self,
+        not_exists_dashboard: int,
+        filtersets: Dict[str, List[FilterSet]],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        filter_set_id = max(collect_all_ids(filtersets)) + 1
+
+        response = call_delete_filter_set(
+            client, {"id": filter_set_id}, not_exists_dashboard
+        )
+        # assert
+        assert response.status_code == 404
+
+    def test_with_dashboard_not_exists_filterset_exists__404(
+        self,
+        not_exists_dashboard: int,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+
+        # act
+        response = call_delete_filter_set(
+            client, dashboard_based_filter_set_dict, not_exists_dashboard
+        )
+        # assert
+        assert response.status_code == 404
+        assert_filterset_was_not_deleted(dashboard_based_filter_set_dict)
+
+    def test_when_caller_is_admin_and_owner_type_is_user__200(
+        self,
+        test_users: Dict[str, int],
+        user_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        # act
+        response = call_delete_filter_set(client, user_based_filter_set_dict)
+
+        # assert
+        assert response.status_code == 200
+        assert_filterset_deleted(user_based_filter_set_dict)
+
+    def test_when_caller_is_admin_and_owner_type_is_dashboard__200(
+        self,
+        test_users: Dict[str, int],
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        # act
+        response = call_delete_filter_set(client, 
dashboard_based_filter_set_dict)
+
+        # assert
+        assert response.status_code == 200
+        assert_filterset_deleted(dashboard_based_filter_set_dict)
+
+    def test_when_caller_is_dashboard_owner_and_owner_is_other_user_403(
+        self,
+        test_users: Dict[str, int],
+        user_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, DASHBOARD_OWNER_USERNAME)
+
+        # act
+        response = call_delete_filter_set(client, user_based_filter_set_dict)
+
+        # assert
+        assert response.status_code == 403
+        assert_filterset_was_not_deleted(user_based_filter_set_dict)
+
+    def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__200(
+        self,
+        test_users: Dict[str, int],
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, DASHBOARD_OWNER_USERNAME)
+
+        # act
+        response = call_delete_filter_set(client, 
dashboard_based_filter_set_dict)
+
+        # assert
+        assert response.status_code == 200
+        assert_filterset_deleted(dashboard_based_filter_set_dict)
+
+    def test_when_caller_is_filterset_owner__200(
+        self,
+        test_users: Dict[str, int],
+        user_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, FILTER_SET_OWNER_USERNAME)
+
+        # act
+        response = call_delete_filter_set(client, user_based_filter_set_dict)
+
+        # assert
+        assert response.status_code == 200
+        assert_filterset_deleted(user_based_filter_set_dict)
+
+    def test_when_caller_is_regular_user_and_owner_type_is_user__403(
+        self,
+        test_users: Dict[str, int],
+        user_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, REGULAR_USER)
+
+        # act
+        response = call_delete_filter_set(client, user_based_filter_set_dict)
+
+        # assert
+        assert response.status_code == 403
+        assert_filterset_was_not_deleted(user_based_filter_set_dict)
+
+    def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403(
+        self,
+        test_users: Dict[str, int],
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, REGULAR_USER)
+
+        # act
+        response = call_delete_filter_set(client, 
dashboard_based_filter_set_dict)
+
+        # assert
+        assert response.status_code == 403
+        assert_filterset_was_not_deleted(dashboard_based_filter_set_dict)
diff --git a/tests/integration_tests/dashboards/filter_sets/get_api_tests.py 
b/tests/integration_tests/dashboards/filter_sets/get_api_tests.py
new file mode 100644
index 0000000..3db2d47
--- /dev/null
+++ b/tests/integration_tests/dashboards/filter_sets/get_api_tests.py
@@ -0,0 +1,129 @@
+# 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.
+from __future__ import annotations
+
+from typing import Any, Dict, List, Set, TYPE_CHECKING
+
+from tests.integration_tests.base_tests import login
+from tests.integration_tests.dashboards.filter_sets.consts import (
+    DASHBOARD_OWNER_USERNAME,
+    FILTER_SET_OWNER_USERNAME,
+    REGULAR_USER,
+)
+from tests.integration_tests.dashboards.filter_sets.utils import (
+    call_get_filter_sets,
+    collect_all_ids,
+)
+
+if TYPE_CHECKING:
+    from flask.testing import FlaskClient
+    from superset.models.filter_set import FilterSet
+
+
+class TestGetFilterSetsApi:
+    def test_with_dashboard_not_exists__404(
+        self, not_exists_dashboard: int, client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+
+        # act
+        response = call_get_filter_sets(client, not_exists_dashboard)
+
+        # assert
+        assert response.status_code == 404
+
+    def test_dashboards_without_filtersets__200(
+        self, dashboard_id: int, client: FlaskClient[Any]
+    ):
+        # arrange
+        login(client, "admin")
+
+        # act
+        response = call_get_filter_sets(client, dashboard_id)
+
+        # assert
+        assert response.status_code == 200
+        assert response.is_json and response.json["count"] == 0
+
+    def test_when_caller_admin__200(
+        self,
+        dashboard_id: int,
+        filtersets: Dict[str, List[FilterSet]],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        expected_ids: Set[int] = collect_all_ids(filtersets)
+
+        # act
+        response = call_get_filter_sets(client, dashboard_id)
+
+        # assert
+        assert response.status_code == 200
+        assert response.is_json and set(response.json["ids"]) == expected_ids
+
+    def test_when_caller_dashboard_owner__200(
+        self,
+        dashboard_id: int,
+        filtersets: Dict[str, List[FilterSet]],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, DASHBOARD_OWNER_USERNAME)
+        expected_ids = collect_all_ids(filtersets["Dashboard"])
+
+        # act
+        response = call_get_filter_sets(client, dashboard_id)
+
+        # assert
+        assert response.status_code == 200
+        assert response.is_json and set(response.json["ids"]) == expected_ids
+
+    def test_when_caller_filterset_owner__200(
+        self,
+        dashboard_id: int,
+        filtersets: Dict[str, List[FilterSet]],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, FILTER_SET_OWNER_USERNAME)
+        expected_ids = collect_all_ids(filtersets[FILTER_SET_OWNER_USERNAME])
+
+        # act
+        response = call_get_filter_sets(client, dashboard_id)
+
+        # assert
+        assert response.status_code == 200
+        assert response.is_json and set(response.json["ids"]) == expected_ids
+
+    def test_when_caller_regular_user__200(
+        self,
+        dashboard_id: int,
+        filtersets: Dict[str, List[int]],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, REGULAR_USER)
+        expected_ids: Set[int] = set()
+
+        # act
+        response = call_get_filter_sets(client, dashboard_id)
+
+        # assert
+        assert response.status_code == 200
+        assert response.is_json and set(response.json["ids"]) == expected_ids
diff --git a/tests/integration_tests/dashboards/filter_sets/update_api_tests.py 
b/tests/integration_tests/dashboards/filter_sets/update_api_tests.py
new file mode 100644
index 0000000..fcaeaab
--- /dev/null
+++ b/tests/integration_tests/dashboards/filter_sets/update_api_tests.py
@@ -0,0 +1,519 @@
+# 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.
+from __future__ import annotations
+
+import json
+from typing import Any, Dict, List, TYPE_CHECKING
+
+from superset.dashboards.filter_sets.consts import (
+    DESCRIPTION_FIELD,
+    JSON_METADATA_FIELD,
+    NAME_FIELD,
+    OWNER_TYPE_FIELD,
+    PARAMS_PROPERTY,
+)
+from tests.integration_tests.base_tests import login
+from tests.integration_tests.dashboards.filter_sets.consts import (
+    DASHBOARD_OWNER_USERNAME,
+    FILTER_SET_OWNER_USERNAME,
+    REGULAR_USER,
+)
+from tests.integration_tests.dashboards.filter_sets.utils import (
+    call_update_filter_set,
+    collect_all_ids,
+    get_filter_set_by_name,
+)
+
+if TYPE_CHECKING:
+    from flask.testing import FlaskClient
+    from superset.models.filter_set import FilterSet
+
+
+def merge_two_filter_set_dict(
+    first: Dict[Any, Any], second: Dict[Any, Any]
+) -> Dict[Any, Any]:
+    for d in [first, second]:
+        if JSON_METADATA_FIELD in d:
+            if PARAMS_PROPERTY not in d:
+                d.setdefault(PARAMS_PROPERTY, 
json.loads(d[JSON_METADATA_FIELD]))
+            d.pop(JSON_METADATA_FIELD)
+    return {**first, **second}
+
+
+def assert_filterset_was_not_updated(filter_set_dict: Dict[str, Any]) -> None:
+    assert filter_set_dict == 
get_filter_set_by_name(filter_set_dict["name"]).to_dict()
+
+
+def assert_filterset_updated(
+    filter_set_dict_before: Dict[str, Any], data_updated: Dict[str, Any]
+) -> None:
+    expected_data = merge_two_filter_set_dict(filter_set_dict_before, 
data_updated)
+    assert expected_data == 
get_filter_set_by_name(expected_data["name"]).to_dict()
+
+
+class TestUpdateFilterSet:
+    def test_with_dashboard_exists_filterset_not_exists__404(
+        self,
+        dashboard_id: int,
+        filtersets: Dict[str, List[FilterSet]],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        filter_set_id = max(collect_all_ids(filtersets)) + 1
+
+        response = call_update_filter_set(
+            client, {"id": filter_set_id}, {}, dashboard_id
+        )
+        # assert
+        assert response.status_code == 404
+
+    def test_with_dashboard_not_exists_filterset_not_exists__404(
+        self,
+        not_exists_dashboard: int,
+        filtersets: Dict[str, List[FilterSet]],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        filter_set_id = max(collect_all_ids(filtersets)) + 1
+
+        response = call_update_filter_set(
+            client, {"id": filter_set_id}, {}, not_exists_dashboard
+        )
+        # assert
+        assert response.status_code == 404
+
+    def test_with_dashboard_not_exists_filterset_exists__404(
+        self,
+        not_exists_dashboard: int,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, {}, not_exists_dashboard
+        )
+        # assert
+        assert response.status_code == 404
+        assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
+
+    def test_with_extra_field__400(
+        self,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_update["extra"] = "val"
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert response.json["message"]["extra"][0] == "Unknown field."
+        assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
+
+    def test_with_id_field__400(
+        self,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_update["id"] = 1
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert response.json["message"]["id"][0] == "Unknown field."
+        assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
+
+    def test_with_none_name__400(
+        self,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_update[NAME_FIELD] = None
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
+
+    def test_with_int_as_name__400(
+        self,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_update[NAME_FIELD] = 4
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
+
+    def test_without_name__200(
+        self,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_update.pop(NAME_FIELD, None)
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 200
+        assert_filterset_updated(
+            dashboard_based_filter_set_dict, valid_filter_set_data_for_update
+        )
+
+    def test_with_none_description__400(
+        self,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_update[DESCRIPTION_FIELD] = None
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
+
+    def test_with_int_as_description__400(
+        self,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_update[DESCRIPTION_FIELD] = 1
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
+
+    def test_without_description__200(
+        self,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_update.pop(DESCRIPTION_FIELD, None)
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 200
+        assert_filterset_updated(
+            dashboard_based_filter_set_dict, valid_filter_set_data_for_update
+        )
+
+    def test_with_invalid_json_metadata__400(
+        self,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_update[DESCRIPTION_FIELD] = {}
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
+
+    def test_with_json_metadata__200(
+        self,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        valid_json_metadata: Dict[Any, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_json_metadata["nativeFilters"] = {"changed": "changed"}
+        valid_filter_set_data_for_update[JSON_METADATA_FIELD] = json.dumps(
+            valid_json_metadata
+        )
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 200
+        assert_filterset_updated(
+            dashboard_based_filter_set_dict, valid_filter_set_data_for_update
+        )
+
+    def test_with_invalid_owner_type__400(
+        self,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_update[OWNER_TYPE_FIELD] = "OTHER_TYPE"
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
+
+    def test_with_user_owner_type__400(
+        self,
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_update[OWNER_TYPE_FIELD] = "User"
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 400
+        assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
+
+    def test_with_dashboard_owner_type__200(
+        self,
+        user_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        valid_filter_set_data_for_update[OWNER_TYPE_FIELD] = "Dashboard"
+
+        # act
+        response = call_update_filter_set(
+            client, user_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 200
+        user_based_filter_set_dict["owner_id"] = user_based_filter_set_dict[
+            "dashboard_id"
+        ]
+        assert_filterset_updated(
+            user_based_filter_set_dict, valid_filter_set_data_for_update
+        )
+
+    def test_when_caller_is_admin_and_owner_type_is_user__200(
+        self,
+        test_users: Dict[str, int],
+        user_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        # act
+        response = call_update_filter_set(
+            client, user_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 200
+        assert_filterset_updated(
+            user_based_filter_set_dict, valid_filter_set_data_for_update
+        )
+
+    def test_when_caller_is_admin_and_owner_type_is_dashboard__200(
+        self,
+        test_users: Dict[str, int],
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, "admin")
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 200
+        assert_filterset_updated(
+            dashboard_based_filter_set_dict, valid_filter_set_data_for_update
+        )
+
+    def test_when_caller_is_dashboard_owner_and_owner_is_other_user_403(
+        self,
+        test_users: Dict[str, int],
+        user_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, DASHBOARD_OWNER_USERNAME)
+
+        # act
+        response = call_update_filter_set(
+            client, user_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 403
+        assert_filterset_was_not_updated(user_based_filter_set_dict)
+
+    def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__200(
+        self,
+        test_users: Dict[str, int],
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, DASHBOARD_OWNER_USERNAME)
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 200
+        assert_filterset_updated(
+            dashboard_based_filter_set_dict, valid_filter_set_data_for_update
+        )
+
+    def test_when_caller_is_filterset_owner__200(
+        self,
+        test_users: Dict[str, int],
+        user_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, FILTER_SET_OWNER_USERNAME)
+
+        # act
+        response = call_update_filter_set(
+            client, user_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 200
+        assert_filterset_updated(
+            user_based_filter_set_dict, valid_filter_set_data_for_update
+        )
+
+    def test_when_caller_is_regular_user_and_owner_type_is_user__403(
+        self,
+        test_users: Dict[str, int],
+        user_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, REGULAR_USER)
+
+        # act
+        response = call_update_filter_set(
+            client, user_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 403
+        assert_filterset_was_not_updated(user_based_filter_set_dict)
+
+    def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403(
+        self,
+        test_users: Dict[str, int],
+        dashboard_based_filter_set_dict: Dict[str, Any],
+        valid_filter_set_data_for_update: Dict[str, Any],
+        client: FlaskClient[Any],
+    ):
+        # arrange
+        login(client, REGULAR_USER)
+
+        # act
+        response = call_update_filter_set(
+            client, dashboard_based_filter_set_dict, 
valid_filter_set_data_for_update
+        )
+
+        # assert
+        assert response.status_code == 403
+        assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
diff --git a/tests/integration_tests/dashboards/filter_sets/utils.py 
b/tests/integration_tests/dashboards/filter_sets/utils.py
new file mode 100644
index 0000000..a63e416
--- /dev/null
+++ b/tests/integration_tests/dashboards/filter_sets/utils.py
@@ -0,0 +1,102 @@
+# 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.
+from __future__ import annotations
+
+from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING, Union
+
+from superset.models.filter_set import FilterSet
+from tests.integration_tests.dashboards.filter_sets.consts import 
FILTER_SET_URI
+from tests.integration_tests.test_app import app
+
+if TYPE_CHECKING:
+    from flask import Response
+    from flask.testing import FlaskClient
+
+
+def call_create_filter_set(
+    client: FlaskClient[Any], dashboard_id: int, data: Dict[str, Any]
+) -> Response:
+    uri = FILTER_SET_URI.format(dashboard_id=dashboard_id)
+    return client.post(uri, json=data)
+
+
+def call_get_filter_sets(client: FlaskClient[Any], dashboard_id: int) -> 
Response:
+    uri = FILTER_SET_URI.format(dashboard_id=dashboard_id)
+    return client.get(uri)
+
+
+def call_delete_filter_set(
+    client: FlaskClient[Any],
+    filter_set_dict_to_update: Dict[str, Any],
+    dashboard_id: Optional[int] = None,
+) -> Response:
+    dashboard_id = (
+        dashboard_id
+        if dashboard_id is not None
+        else filter_set_dict_to_update["dashboard_id"]
+    )
+    uri = "{}/{}".format(
+        FILTER_SET_URI.format(dashboard_id=dashboard_id),
+        filter_set_dict_to_update["id"],
+    )
+    return client.delete(uri)
+
+
+def call_update_filter_set(
+    client: FlaskClient[Any],
+    filter_set_dict_to_update: Dict[str, Any],
+    data: Dict[str, Any],
+    dashboard_id: Optional[int] = None,
+) -> Response:
+    dashboard_id = (
+        dashboard_id
+        if dashboard_id is not None
+        else filter_set_dict_to_update["dashboard_id"]
+    )
+    uri = "{}/{}".format(
+        FILTER_SET_URI.format(dashboard_id=dashboard_id),
+        filter_set_dict_to_update["id"],
+    )
+    return client.put(uri, json=data)
+
+
+def get_filter_set_by_name(name: str) -> FilterSet:
+    with app.app_context():
+        return FilterSet.get_by_name(name)
+
+
+def get_filter_set_by_id(id_: int) -> FilterSet:
+    with app.app_context():
+        return FilterSet.get(id_)
+
+
+def get_filter_set_by_dashboard_id(dashboard_id: int) -> FilterSet:
+    with app.app_context():
+        return FilterSet.get_by_dashboard_id(dashboard_id)
+
+
+def collect_all_ids(
+    filtersets: Union[Dict[str, List[FilterSet]], List[FilterSet]]
+) -> Set[int]:
+    if isinstance(filtersets, dict):
+        filtersets_lists: List[List[FilterSet]] = list(filtersets.values())
+        ids: Set[int] = set()
+        lst: List[FilterSet]
+        for lst in filtersets_lists:
+            ids.update(set(map(lambda fs: fs.id, lst)))
+        return ids
+    return set(map(lambda fs: fs.id, filtersets))
diff --git a/tests/integration_tests/dashboards/superset_factory_util.py 
b/tests/integration_tests/dashboards/superset_factory_util.py
index dfd2f67..b67c60c 100644
--- a/tests/integration_tests/dashboards/superset_factory_util.py
+++ b/tests/integration_tests/dashboards/superset_factory_util.py
@@ -82,10 +82,10 @@ def create_dashboard(
     json_metadata: str = "",
     position_json: str = "",
 ) -> Dashboard:
-    dashboard_title = dashboard_title or random_title()
-    slug = slug or random_slug()
-    owners = owners or []
-    slices = slices or []
+    dashboard_title = dashboard_title if dashboard_title is not None else 
random_title()
+    slug = slug if slug is not None else random_slug()
+    owners = owners if owners is not None else []
+    slices = slices if slices is not None else []
     return Dashboard(
         dashboard_title=dashboard_title,
         slug=slug,
@@ -109,25 +109,40 @@ def create_slice_to_db(
     datasource_id: Optional[int] = None,
     owners: Optional[List[User]] = None,
 ) -> Slice:
-    slice_ = create_slice(datasource_id, name, owners)
+    slice_ = create_slice(datasource_id, name=name, owners=owners)
     insert_model(slice_)
     inserted_slices_ids.append(slice_.id)
     return slice_
 
 
 def create_slice(
-    datasource_id: Optional[int], name: Optional[str], owners: 
Optional[List[User]]
+    datasource_id: Optional[int] = None,
+    datasource: Optional[SqlaTable] = None,
+    name: Optional[str] = None,
+    owners: Optional[List[User]] = None,
 ) -> Slice:
-    name = name or random_str()
-    owners = owners or []
+    name = name if name is not None else random_str()
+    owners = owners if owners is not None else []
+    datasource_type = "table"
+    if datasource:
+        return Slice(
+            slice_name=name,
+            table=datasource,
+            owners=owners,
+            datasource_type=datasource_type,
+        )
+
     datasource_id = (
-        datasource_id or create_datasource_table_to_db(name=name + "_table").id
+        datasource_id
+        if datasource_id is not None
+        else create_datasource_table_to_db(name=name + "_table").id
     )
+
     return Slice(
         slice_name=name,
         datasource_id=datasource_id,
         owners=owners,
-        datasource_type="table",
+        datasource_type=datasource_type,
     )
 
 
@@ -136,7 +151,7 @@ def create_datasource_table_to_db(
     db_id: Optional[int] = None,
     owners: Optional[List[User]] = None,
 ) -> SqlaTable:
-    sqltable = create_datasource_table(name, db_id, owners)
+    sqltable = create_datasource_table(name, db_id, owners=owners)
     insert_model(sqltable)
     inserted_sqltables_ids.append(sqltable.id)
     return sqltable
@@ -145,11 +160,14 @@ def create_datasource_table_to_db(
 def create_datasource_table(
     name: Optional[str] = None,
     db_id: Optional[int] = None,
+    database: Optional[Database] = None,
     owners: Optional[List[User]] = None,
 ) -> SqlaTable:
-    name = name or random_str()
-    owners = owners or []
-    db_id = db_id or create_database_to_db(name=name + "_db").id
+    name = name if name is not None else random_str()
+    owners = owners if owners is not None else []
+    if database:
+        return SqlaTable(table_name=name, database=database, owners=owners)
+    db_id = db_id if db_id is not None else create_database_to_db(name=name + 
"_db").id
     return SqlaTable(table_name=name, database_id=db_id, owners=owners)
 
 
@@ -161,7 +179,7 @@ def create_database_to_db(name: Optional[str] = None) -> 
Database:
 
 
 def create_database(name: Optional[str] = None) -> Database:
-    name = name or random_str()
+    name = name if name is not None else random_str()
     return Database(database_name=name, sqlalchemy_uri="sqlite:///:memory:")
 
 
diff --git a/tests/integration_tests/superset_test_config.py 
b/tests/integration_tests/superset_test_config.py
index a83758b..698440c 100644
--- a/tests/integration_tests/superset_test_config.py
+++ b/tests/integration_tests/superset_test_config.py
@@ -29,7 +29,7 @@ SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(
 )
 DEBUG = False
 SUPERSET_WEBSERVER_PORT = 8081
-
+SILENCE_FAB = False
 # Allowing SQLALCHEMY_DATABASE_URI and SQLALCHEMY_EXAMPLES_URI to be defined 
as an env vars for
 # continuous integration
 if "SUPERSET__SQLALCHEMY_DATABASE_URI" in os.environ:

Reply via email to