mirror of
https://github.com/apache/superset.git
synced 2026-09-01 13:01:33 +00:00
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
5.8 KiB
Python
141 lines
5.8 KiB
Python
# 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.
|
|
"""
|
|
Traces how ``sync_database_permissions_task`` binds an acting identity.
|
|
|
|
The Celery task receives the immutable ``id`` of the user who enqueued it
|
|
(captured at enqueue time by ``SyncPermissionsCommand.validate``), not a
|
|
mutable username string. At execution time it resolves that id to a user
|
|
record via ``security_manager.get_user_by_id`` and binds the result to
|
|
``flask.g.user`` for the duration of the sync. Because resolution is by id,
|
|
a username change between enqueue and execution has no effect on which user
|
|
record the task acts as.
|
|
|
|
That identity is not just used for logging: ``Database._get_sqla_engine``
|
|
reads ``g.user.id`` to look up a per-user OAuth2 access token, and, for
|
|
databases with ``impersonate_user`` enabled, ``Database.get_effective_user``
|
|
reads ``g.user.username`` (via ``get_username()``) to pick the identity the
|
|
outgoing connection impersonates at the external database. These tests pin
|
|
down both halves of that chain: the id-based resolution in the task, and the
|
|
fact that the resolved user is what a privileged, identity-sensitive
|
|
codepath consumes downstream.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
from flask import g
|
|
from pytest_mock import MockerFixture
|
|
|
|
from superset.commands.database.sync_permissions import (
|
|
sync_database_permissions_task,
|
|
)
|
|
from superset.models.core import Database
|
|
|
|
|
|
def test_task_binds_g_user_to_whoever_held_the_id_at_enqueue_time(
|
|
mocker: MockerFixture,
|
|
) -> None:
|
|
"""
|
|
The task resolves its acting identity from the user id captured at
|
|
enqueue time, via ``security_manager.get_user_by_id``. A username change
|
|
that happens between enqueue and execution has no effect on which user
|
|
record the task binds ``g.user`` to, because the id -- not the mutable
|
|
username -- is what crosses the enqueue/execute boundary.
|
|
"""
|
|
# Whoever enqueued the task saw this identity at enqueue time. Its id is
|
|
# what's passed to the task.
|
|
enqueuing_user = MagicMock()
|
|
enqueuing_user.id = 101
|
|
enqueuing_user.username = "alice"
|
|
|
|
get_user_mock = mocker.patch(
|
|
"superset.commands.database.sync_permissions.security_manager.get_user_by_id",
|
|
return_value=enqueuing_user,
|
|
)
|
|
|
|
mock_db_connection = MagicMock()
|
|
mocker.patch(
|
|
"superset.commands.database.sync_permissions.DatabaseDAO.find_by_id",
|
|
return_value=mock_db_connection,
|
|
)
|
|
|
|
observed_g_user: list[MagicMock] = []
|
|
|
|
def capture_g_user(self: object) -> None:
|
|
# Read g.user at the moment the sync logic actually runs, the same
|
|
# way privileged downstream code (e.g. _get_sqla_engine) would.
|
|
observed_g_user.append(g.user)
|
|
|
|
mocker.patch(
|
|
"superset.commands.database.sync_permissions.SyncPermissionsCommand"
|
|
".sync_database_permissions",
|
|
autospec=True,
|
|
side_effect=capture_g_user,
|
|
)
|
|
|
|
# By the time the task executes, "alice" has been renamed (and the
|
|
# username could even have been reassigned to someone else) -- but the
|
|
# task was enqueued with id 101, so the rename doesn't affect resolution.
|
|
enqueuing_user.username = "alice_renamed"
|
|
|
|
sync_database_permissions_task(1, 101, "old_db_name")
|
|
|
|
# Resolution happened purely off the immutable id...
|
|
get_user_mock.assert_called_once_with(101)
|
|
# ...and the sync ran under the same user captured at enqueue time,
|
|
# regardless of the username change in between.
|
|
assert observed_g_user == [enqueuing_user]
|
|
assert observed_g_user[0].id == 101
|
|
|
|
|
|
def test_g_user_bound_by_the_task_drives_external_db_impersonation_identity(
|
|
mocker: MockerFixture,
|
|
) -> None:
|
|
"""
|
|
``Database.get_effective_user`` -- consulted by ``_get_sqla_engine`` to
|
|
decide which identity an outgoing, ``impersonate_user``-enabled
|
|
connection impersonates at the external database -- reads
|
|
``g.user.username``. Whatever user object the task bound to ``g.user``
|
|
(per the previous test, the user resolved from the id captured at
|
|
enqueue time) is therefore the identity used to connect to the external
|
|
database.
|
|
"""
|
|
database = MagicMock(spec=Database)
|
|
database.impersonate_user = True
|
|
|
|
object_url = MagicMock()
|
|
object_url.username = "url-embedded-user"
|
|
|
|
# ``get_effective_user`` calls ``get_username()``, which reads
|
|
# ``g.user.username`` using the ``g`` imported into
|
|
# ``superset.utils.core`` (where ``get_username`` is defined) -- patch
|
|
# that module's ``g``, matching what the running task actually touches.
|
|
user_a = MagicMock()
|
|
user_a.username = "user_a"
|
|
mocker.patch("superset.utils.core.g", MagicMock(user=user_a))
|
|
assert Database.get_effective_user(database, object_url) == "user_a"
|
|
|
|
# A different user bound to g.user (as would happen if a different id
|
|
# had been captured at enqueue time) changes the impersonated identity
|
|
# for the exact same database configuration and target URL.
|
|
user_b = MagicMock()
|
|
user_b.username = "user_b"
|
|
mocker.patch("superset.utils.core.g", MagicMock(user=user_b))
|
|
assert Database.get_effective_user(database, object_url) == "user_b"
|