mirror of
https://github.com/apache/superset.git
synced 2026-09-01 21:11:28 +00:00
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:
co-authored by
Yash Janoria
Evan
Claude Opus 4.8
Evan Rusackas
parent
8181917f79
commit
42e4030104
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user