feat(security): add guest user attributes and get_guest_user_attribute() macro (#33924)

Co-authored-by: Yash Janoria <yash.janoria@314ecorp.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
This commit is contained in:
Yash Janoria
2026-08-08 16:56:07 -07:00
committed by GitHub
co-authored by Yash Janoria Evan Claude Opus 4.8 Evan Rusackas
parent 8181917f79
commit 42e4030104
10 changed files with 1743 additions and 17 deletions
+182
View File
@@ -37,6 +37,12 @@ from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import SupersetSecurityException
from superset.models.core import Database
from superset.models.slice import Slice
from superset.security.guest_token import (
GuestTokenResource,
GuestTokenResourceType,
GuestTokenRlsRule,
GuestTokenUser,
)
from superset.sql.parse import Table
from superset.utils.core import (
DatasourceType,
@@ -2331,6 +2337,182 @@ class TestGuestTokens(SupersetTestCase):
assert guest_user is not None
assert "test_guest" == guest_user.username
def create_guest_token_with_attributes(self) -> bytes:
user: GuestTokenUser = {
"username": "test_guest_with_attrs",
"first_name": "Test",
"last_name": "Guest",
"attributes": {
"department": "Engineering",
"region": "US",
"role": "developer",
"team": "data-platform",
},
}
resources: list[GuestTokenResource] = [
{"type": GuestTokenResourceType.DASHBOARD, "id": "test-dashboard"}
]
rls: list[GuestTokenRlsRule] = [{"dataset": "1", "clause": "access = 1"}]
return security_manager.create_guest_access_token(user, resources, rls)
def test_create_guest_access_token_with_attributes(self) -> None:
"""Test creating guest access token with user attributes."""
user_with_attributes: GuestTokenUser = {
"username": "test_guest_attrs",
"first_name": "Test",
"last_name": "Guest",
"attributes": {
"department": "Engineering",
"region": "US",
"clearance_level": "standard",
"projects": ["analytics", "ml-platform"],
"team_lead": True,
},
}
resources: list[GuestTokenResource] = [
{"type": GuestTokenResourceType.DASHBOARD, "id": "test-dashboard"}
]
rls: list[GuestTokenRlsRule] = [{"dataset": "1", "clause": "id = 1"}]
token = security_manager.create_guest_access_token(
user_with_attributes, resources, rls
)
# Decode and verify the token contains attributes
aud = get_url_host()
decoded_token = jwt.decode(
token,
self.app.config["GUEST_TOKEN_JWT_SECRET"],
algorithms=[self.app.config["GUEST_TOKEN_JWT_ALGO"]],
audience=aud,
)
assert "user" in decoded_token
user = decoded_token["user"]
assert "attributes" in user
assert user["attributes"]["department"] == "Engineering"
assert user["attributes"]["region"] == "US"
assert user["attributes"]["clearance_level"] == "standard"
assert user["attributes"]["projects"] == ["analytics", "ml-platform"]
assert user["attributes"]["team_lead"] is True
def test_get_guest_user_with_attributes(self) -> None:
"""Test that guest user properly retains attributes from token."""
token = self.create_guest_token_with_attributes()
fake_request = FakeRequest()
fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = token
guest_user = security_manager.get_guest_user_from_request(fake_request)
assert guest_user is not None
assert "test_guest_with_attrs" == guest_user.username
# Verify attributes are accessible through guest_token
assert hasattr(guest_user, "guest_token")
token_user = guest_user.guest_token["user"]
assert "attributes" in token_user
token_attributes = token_user["attributes"]
assert token_attributes is not None
assert token_attributes["department"] == "Engineering"
assert token_attributes["region"] == "US"
assert token_attributes["role"] == "developer"
assert token_attributes["team"] == "data-platform"
def test_create_guest_access_token_without_attributes(self) -> None:
"""Test creating guest access token without user attributes.
This test ensures backward compatibility.
"""
user_without_attributes: GuestTokenUser = {
"username": "test_guest_no_attrs",
"first_name": "Test",
"last_name": "Guest",
}
resources: list[GuestTokenResource] = [
{"type": GuestTokenResourceType.DASHBOARD, "id": "test-dashboard"}
]
rls: list[GuestTokenRlsRule] = [{"dataset": "1", "clause": "id = 1"}]
token = security_manager.create_guest_access_token(
user_without_attributes, resources, rls
)
# Decode and verify the token works without attributes
aud = get_url_host()
decoded_token = jwt.decode(
token,
self.app.config["GUEST_TOKEN_JWT_SECRET"],
algorithms=[self.app.config["GUEST_TOKEN_JWT_ALGO"]],
audience=aud,
)
assert "user" in decoded_token
user = decoded_token["user"]
assert "attributes" not in user
assert user["username"] == "test_guest_no_attrs"
def test_create_guest_access_token_with_empty_attributes(self) -> None:
"""Test creating guest access token with empty attributes."""
user_with_empty_attributes: GuestTokenUser = {
"username": "test_guest_empty_attrs",
"first_name": "Test",
"last_name": "Guest",
"attributes": {},
}
resources: list[GuestTokenResource] = [
{"type": GuestTokenResourceType.DASHBOARD, "id": "test-dashboard"}
]
rls: list[GuestTokenRlsRule] = [{"dataset": "1", "clause": "id = 1"}]
token = security_manager.create_guest_access_token(
user_with_empty_attributes, resources, rls
)
# Decode and verify the token contains empty attributes
aud = get_url_host()
decoded_token = jwt.decode(
token,
self.app.config["GUEST_TOKEN_JWT_SECRET"],
algorithms=[self.app.config["GUEST_TOKEN_JWT_ALGO"]],
audience=aud,
)
assert "user" in decoded_token
user = decoded_token["user"]
assert "attributes" in user
assert user["attributes"] == {}
def test_create_guest_access_token_with_null_attributes(self) -> None:
"""Test creating guest access token with null attributes."""
user_with_null_attributes: GuestTokenUser = {
"username": "test_guest_null_attrs",
"first_name": "Test",
"last_name": "Guest",
"attributes": None,
}
resources: list[GuestTokenResource] = [
{"type": GuestTokenResourceType.DASHBOARD, "id": "test-dashboard"}
]
rls: list[GuestTokenRlsRule] = [{"dataset": "1", "clause": "id = 1"}]
token = security_manager.create_guest_access_token(
user_with_null_attributes, resources, rls
)
# Decode and verify the token contains null attributes
aud = get_url_host()
decoded_token = jwt.decode(
token,
self.app.config["GUEST_TOKEN_JWT_SECRET"],
algorithms=[self.app.config["GUEST_TOKEN_JWT_ALGO"]],
audience=aud,
)
assert "user" in decoded_token
user = decoded_token["user"]
assert "attributes" in user
assert user["attributes"] is None
def test_get_guest_user_with_request_form(self):
token = self.create_guest_token()
fake_request = FakeRequest()