Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
378 changes: 189 additions & 189 deletions RESOURCES/STANDARD_ROLES.md

Large diffs are not rendered by default.

32 changes: 27 additions & 5 deletions docs/docs/security/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,34 @@ to all databases by default, both **Alpha** and **Gamma** users need to be given

### Public

To allow logged-out users to access some Superset features, you can use the `PUBLIC_ROLE_LIKE` config setting and assign it to another role whose permissions you want passed to this role.
The **Public** role is the most restrictive built-in role, designed specifically for anonymous/unauthenticated
users who need to view dashboards. It provides minimal read-only access for:

For example, by setting `PUBLIC_ROLE_LIKE = "Gamma"` in your `superset_config.py` file, you grant
public role the same set of permissions as for the **Gamma** role. This is useful if one
wants to enable anonymous users to view dashboards. Explicit grant on specific datasets is
still required, meaning that you need to edit the **Public** role and add the public data sources to the role manually.
- Viewing dashboards and charts
- Using interactive dashboard filters
- Accessing dashboard permalinks
- Reading embedded dashboards

The Public role explicitly excludes:
- Any write permissions on dashboards, charts, or datasets
- SQL Lab access
- Share functionality
- User profile or admin features
- Menu access to most Superset features

To allow logged-out users to access Superset features, use the `PUBLIC_ROLE_LIKE` config setting
Comment thread
rusackas marked this conversation as resolved.
Outdated
to copy permissions from any built-in role to the actual public/anonymous role:

```python
# Recommended: Use the new Public role for minimal, secure public access
PUBLIC_ROLE_LIKE = "Public"

# Alternative: Use Gamma for broader access (includes create/edit permissions)
# PUBLIC_ROLE_LIKE = "Gamma"
```

**Important:** Explicit grants on specific datasets are still required. You need to edit the
public role in the Superset UI and add the public data sources to the role manually.
Comment thread
rusackas marked this conversation as resolved.
Outdated

### Managing Data Source Access for Gamma Roles

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ select = [

ignore = [
"S101",
"PT004", # Fixtures that don't return values - underscore prefix conflicts with pytest usage
"PT006",
"T201",
"N999",
Expand Down
90 changes: 87 additions & 3 deletions superset/security/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
}

GAMMA_READ_ONLY_MODEL_VIEWS = {
"CssTemplate",
"Dataset",
"Datasource",
} | READ_ONLY_MODEL_VIEWS
Expand Down Expand Up @@ -302,7 +303,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
"Annotation",
"CSS Templates",
"ColumnarToDatabaseView",
"CssTemplate",
"ExcelToDatabaseView",
"Import dashboards",
"ImportExportRestApi",
Expand Down Expand Up @@ -389,6 +389,55 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
("can_read", "Database"),
}

# Permissions for the Public role - minimal read-only access for viewing
# dashboards without authentication. This is more restrictive than Gamma.
# Users can set PUBLIC_ROLE_LIKE = "Public" to use these sensible defaults.
PUBLIC_ROLE_PERMISSIONS = {
# Core dashboard viewing
("can_read", "Dashboard"),
("can_read", "Chart"),
("can_dashboard", "Superset"),
("can_slice", "Superset"),
("can_explore_json", "Superset"),
("can_dashboard_permalink", "Superset"),
("can_read", "DashboardPermalinkRestApi"),
# Dashboard filter interactions
("can_read", "DashboardFilterStateRestApi"),
("can_write", "DashboardFilterStateRestApi"),
# API access for chart rendering
("can_time_range", "Api"),
("can_query_form_data", "Api"),
("can_query", "Api"),
# CSS for dashboard styling
("can_read", "CssTemplate"),
# Embedded dashboard support
("can_read", "EmbeddedDashboard"),
# Datasource metadata for chart rendering
("can_get", "Datasource"),
("can_external_metadata", "Datasource"),
}

Comment on lines +395 to +424

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public is given several permissions that Gamma does not have:

can_read → CssTemplate

can_read → SecurityRestApi

can_get → MenuApi

can_get → OpenApi

can_external_metadata_by_name → Datasource

Because of these, public_perm_set - gamma_perm_set is not empty, so test_public_role_more_restrictive_than_gamma will fail. Either remove these permissions from Public or update the test, but both cannot be true at once.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching that... I'll take 'em out.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I added CssTemplate can_read to Gamma :D

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a Public role in production. I set it up around 2022 so many versions ago. Here I compare this PR's list to the list I'm using, both ways.

An original artifact of the list I used was this gist: https://gist.github.com/byk0t/bd6e9c3839967b4ac28a8da30f468b2a its comments have a couple of useful clues.

Present here but not in my Public role

  1. ("can_read", "EmbeddedDashboard") - makes sense to add

It surprises me that my Public role works without having these:
# Datasource metadata for chart rendering
2. ("can_get", "Datasource"),
3. ("can_external_metadata", "Datasource")

Present in my Public role but not this PR

I think we might want to add:

  • can annotation json Superset - I would think this is needed for annotations to render on a Public-facing chart?
  • can read Annotation - same
  • can filter Superset - I don't know how this is different from the filter permissions you have above
  • can read ExplorePermalinkRestApi - I think this was for permalinks to charts? I was trying to embed charts in webpages when I started using Superset for public facing.

I'm not sure about these - I have them but it's not obvious to me what they do and I don't remember why I added each:

  • can favstar Superset - at least in Superset 1.5.0 this was needed to avoid the error message: "There was an issue fetching the favorite status of this dashboard"
  • can get OpenApi
  • can list FilterSets
  • can queries Superset
  • can read AdvancedDataType
  • can read ExploreFormDataRestApi,
  • can share dashboard Superset
  • can slice json Superset
  • can sql json Superset
  • can validate sql json Superset
  • can write DashboardPermalinkRestApi - maybe I had this because of Without "can write on DashboardPermalinkRestApi", a click on a anchor (tabs or header) redirect to login page #30004 which has been fixed?
  • can write ExploreFormDataRestApi
  • can write ExplorePermalinkRestApi

Probably only a fit for my use case:

  • can csv Superset - maybe we note in the docs that if you want the public user to be able to download the data behind a chart, add this?

# View menus that Public role should NOT have access to
PUBLIC_EXCLUDED_VIEW_MENUS = {
"SQL Lab",
"SQL Editor",
"Saved Queries",
"Query Search",
"Queries",
"Security",
"List Users",
"List Roles",
"Row Level Security",
"Row Level Security Filters",
"Access Requests",
"Action Log",
"Manage",
"Import dashboards",
"Annotation Layers",
"CSS Templates",
"Alerts & Report",
}

data_access_permissions = (
"database_access",
"schema_access",
Expand Down Expand Up @@ -1205,9 +1254,16 @@ def sync_role_definitions(self) -> None:
self.set_role("sql_lab", self._is_sql_lab_pvm, pvms)

# Configure public role
if get_conf()["PUBLIC_ROLE_LIKE"]:
# If PUBLIC_ROLE_LIKE is not set or is "Public", use the built-in Public role
Comment thread
rusackas marked this conversation as resolved.
Outdated
# Otherwise, copy permissions from the specified role (legacy behavior)
public_role_like = get_conf()["PUBLIC_ROLE_LIKE"]
if not public_role_like or public_role_like == "Public":
# Use the built-in Public role with minimal read-only permissions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Role Name Inconsistency

The set_role call uses a hardcoded "Public" string, but to match the AUTH_ROLE_PUBLIC config (which defaults to "Public" but can be customized), it should use self.auth_role_public. This ensures consistency, as the copy_role in the elif branch correctly uses self.auth_role_public.

Citations

Code Review Run #8f0e3a


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

self.set_role("Public", self._is_public_pvm, pvms)
elif public_role_like:
# Copy permissions from another role (e.g., "Gamma") to Public
self.copy_role(
get_conf()["PUBLIC_ROLE_LIKE"],
public_role_like,
self.auth_role_public,
merge=True,
)
Expand Down Expand Up @@ -1427,6 +1483,34 @@ def _is_sql_lab_pvm(self, pvm: PermissionView) -> bool:
in self.SQLLAB_EXTRA_PERMISSION_VIEWS
)

def _is_public_pvm(self, pvm: PermissionView) -> bool:
"""
Return True if the FAB permission/view is appropriate for the Public role,
False otherwise.

The Public role is designed for anonymous/unauthenticated users who need
to view dashboards. It provides minimal read-only access - more restrictive
than Gamma - suitable for public-facing dashboard deployments.

:param pvm: The FAB permission/view
:returns: Whether the FAB object is appropriate for Public role
"""
# Explicitly allow permissions in the PUBLIC_ROLE_PERMISSIONS set
if (pvm.permission.name, pvm.view_menu.name) in self.PUBLIC_ROLE_PERMISSIONS:
return True

# Exclude any view menus in the excluded list
if pvm.view_menu.name in self.PUBLIC_EXCLUDED_VIEW_MENUS:
return False

# Exclude user-defined permissions (datasource_access, schema_access, etc.)
# These must be explicitly granted to the Public role
if self._is_user_defined_permission(pvm):
return False

# Exclude all other permissions not explicitly allowed
return False

def database_after_insert(
self,
mapper: Mapper,
Expand Down
1 change: 1 addition & 0 deletions tests/integration_tests/fixtures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
load_energy_table_with_slice,
)
from .public_role import ( # noqa: F401
public_role_builtin,
public_role_like_gamma,
public_role_like_test_role,
)
Expand Down
18 changes: 18 additions & 0 deletions tests/integration_tests/fixtures/public_role.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,21 @@ def public_role_like_test_role(app_context: AppContext):

security_manager.get_public_role().permissions = []
db.session.commit()


@pytest.fixture
def public_role_builtin(app_context: AppContext):
"""
Fixture that uses the built-in Public role with minimal read-only permissions.
This sets PUBLIC_ROLE_LIKE to "Public" to use the new sensible defaults.
"""
original_value = app.config.get("PUBLIC_ROLE_LIKE")
app.config["PUBLIC_ROLE_LIKE"] = "Public"
security_manager.sync_role_definitions()

yield

# Restore original config and clean up
app.config["PUBLIC_ROLE_LIKE"] = original_value
security_manager.get_public_role().permissions = []
db.session.commit()
91 changes: 91 additions & 0 deletions tests/integration_tests/security_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from tests.integration_tests.constants import GAMMA_USERNAME
from tests.integration_tests.conftest import with_feature_flags
from tests.integration_tests.fixtures.public_role import (
public_role_builtin, # noqa: F401
public_role_like_gamma, # noqa: F401
public_role_like_test_role, # noqa: F401
)
Expand Down Expand Up @@ -1438,6 +1439,96 @@ def test_is_gamma_pvm(self):
security_manager.find_permission_view_menu("can_read", "Dataset")
)

def test_is_public_pvm(self):
"""Test that _is_public_pvm correctly identifies Public role permissions."""
# Should include core dashboard viewing permissions
assert security_manager._is_public_pvm(
security_manager.find_permission_view_menu("can_read", "Dashboard")
)
assert security_manager._is_public_pvm(
security_manager.find_permission_view_menu("can_read", "Chart")
)
assert security_manager._is_public_pvm(
security_manager.find_permission_view_menu("can_dashboard", "Superset")
)
assert security_manager._is_public_pvm(
security_manager.find_permission_view_menu("can_explore_json", "Superset")
)

# Should NOT include write permissions on core objects
assert not security_manager._is_public_pvm(
security_manager.find_permission_view_menu("can_write", "Dashboard")
)
assert not security_manager._is_public_pvm(
security_manager.find_permission_view_menu("can_write", "Chart")
)

# Should NOT include admin/alpha permissions
assert not security_manager._is_public_pvm(
security_manager.find_permission_view_menu(
"all_datasource_access", "all_datasource_access"
)
)

@pytest.mark.usefixtures("public_role_builtin")
def test_public_role_permissions(self):
"""Test that Public role has the expected minimal permissions."""
public_perm_set = get_perm_tuples("Public")

# Core dashboard viewing - should be present
assert ("can_read", "Dashboard") in public_perm_set
assert ("can_read", "Chart") in public_perm_set
assert ("can_dashboard", "Superset") in public_perm_set
assert ("can_slice", "Superset") in public_perm_set
assert ("can_explore_json", "Superset") in public_perm_set
assert ("can_dashboard_permalink", "Superset") in public_perm_set

# Filter state for interactive dashboards
assert ("can_read", "DashboardFilterStateRestApi") in public_perm_set
assert ("can_write", "DashboardFilterStateRestApi") in public_perm_set

# Should NOT have write permissions on core objects
assert ("can_write", "Dashboard") not in public_perm_set
assert ("can_write", "Chart") not in public_perm_set
assert ("can_write", "Dataset") not in public_perm_set

# Should NOT have share permissions
assert ("can_share_dashboard", "Superset") not in public_perm_set
assert ("can_share_chart", "Superset") not in public_perm_set

# Should NOT have SQL Lab access
assert ("can_sqllab", "Superset") not in public_perm_set
assert ("menu_access", "SQL Lab") not in public_perm_set

# Should NOT have admin permissions
assert (
"all_datasource_access",
"all_datasource_access",
) not in public_perm_set
assert ("all_database_access", "all_database_access") not in public_perm_set

# Should NOT have user management access
assert ("can_userinfo", "UserDBModelView") not in public_perm_set

@pytest.mark.usefixtures("public_role_builtin")
def test_public_role_more_restrictive_than_gamma(self):
"""Test that Public role is more restrictive than Gamma."""
public_perm_set = get_perm_tuples("Public")
gamma_perm_set = get_perm_tuples("Gamma")

# Public should be a subset of Gamma (more restrictive)
# Note: Public has filter state write which Gamma also has
public_only = public_perm_set - gamma_perm_set

# Any permissions Public has that Gamma doesn't should be intentional
# (there shouldn't be any in the current design)
assert len(public_only) == 0, (
f"Public has permissions Gamma doesn't: {public_only}"
)

# Public should have significantly fewer permissions than Gamma
assert len(public_perm_set) < len(gamma_perm_set)

def test_gamma_permissions_basic(self):
self.assert_can_gamma(get_perm_tuples("Gamma"))
self.assert_cannot_alpha(get_perm_tuples("Gamma"))
Expand Down
Loading