blob: eb2c13a0de473dfd486d79df3180358c1af870e9 [file]
# 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 unittest import mock
from urllib.parse import urljoin
import pytest
import requests
from pyhugegraph.api.auth import AuthManager
from pyhugegraph.utils.huge_requests import HGraphSession
pytestmark = pytest.mark.contract
# FIXME: cover real HGraphSession.resolve() with URL prefixes; this
# DummySession duplicates production routing behavior.
class DummyCfg:
def __init__(self, url, graphspace, gs_supported, graph_name):
self.url = url
self.graphspace = graphspace
self.gs_supported = gs_supported
self.graph_name = graph_name
class DummySession:
"""Minimal session mimic implementing resolve and request used by router."""
def __init__(self, cfg: DummyCfg):
self.cfg = cfg
self.last = None
def resolve(self, path: str) -> str:
base = f"{self.cfg.url.rstrip('/')}/"
if self.cfg.gs_supported:
base = urljoin(base, f"graphspaces/{self.cfg.graphspace}/graphs/{self.cfg.graph_name}/")
else:
base = urljoin(base, f"graphs/{self.cfg.graph_name}/")
return urljoin(base, path).strip("/")
def request(self, path: str, method: str = "GET", validator=None, **kwargs):
# mirror behavior of real session.request used by router: resolve path
self.last = self.resolve(path)
return {"url": self.last, "method": method}
@pytest.mark.parametrize(
"endpoint, method_call, args, expected_subpath",
[
("users", "list_users", (), "graphspaces/GS/auth/users"),
("users", "get_user", ("u1",), "graphspaces/GS/auth/users/u1"),
("accesses", "list_accesses", (), "graphspaces/GS/auth/accesses"),
(
"accesses",
"get_accesses",
("a1",),
"graphspaces/GS/auth/accesses/a1",
),
("targets", "list_targets", (), "graphspaces/GS/auth/targets"),
("belongs", "list_belongs", (), "graphspaces/GS/auth/belongs"),
],
)
def test_graphspace_scoped_endpoints_use_graphspace(endpoint, method_call, args, expected_subpath):
cfg = DummyCfg(url="http://127.0.0.1:8080", graphspace="GS", gs_supported=True, graph_name="g")
sess = DummySession(cfg)
auth = AuthManager(sess)
getattr(auth, method_call)(*args)
assert expected_subpath in sess.last
@pytest.mark.parametrize(
"endpoint, method_call, args",
[
("users", "list_users", ()),
("users", "get_user", ("u1",)),
("accesses", "list_accesses", ()),
("accesses", "get_accesses", ("a1",)),
("targets", "list_targets", ()),
("belongs", "list_belongs", ()),
],
)
def test_graphspace_scoped_endpoints_require_graphspace(endpoint, method_call, args):
# HugeGraph 1.7.0+ requires graphspace for these auth endpoints.
cfg = DummyCfg(url="http://127.0.0.1:8080", graphspace=None, gs_supported=False, graph_name="g")
sess = DummySession(cfg)
auth = AuthManager(sess)
with pytest.raises(ValueError, match="graphspace is required for auth endpoints"):
getattr(auth, method_call)(*args)
def test_groups_are_server_level():
# With graphspace support
cfg = DummyCfg(url="http://127.0.0.1:8080", graphspace="GS", gs_supported=True, graph_name="g")
sess = DummySession(cfg)
auth = AuthManager(sess)
auth.list_groups()
assert "auth/groups" in sess.last
# Without graphspace support
cfg2 = DummyCfg(url="http://127.0.0.1:8080", graphspace=None, gs_supported=False, graph_name="g")
sess2 = DummySession(cfg2)
auth2 = AuthManager(sess2)
auth2.list_groups()
assert "auth/groups" in sess2.last
def test_session_debug_log_redacts_sensitive_kwargs():
cfg = DummyCfg(url="http://127.0.0.1:8080", graphspace=None, gs_supported=False, graph_name="g")
cfg.username = "admin"
cfg.password = "admin-password"
cfg.timeout = 30
response = mock.Mock(spec=requests.Response)
response.status_code = 200
response.json.return_value = {"ok": True}
response.raise_for_status.return_value = None
raw_session = mock.Mock()
raw_session.post.return_value = response
session = HGraphSession(cfg, session=raw_session)
with (
mock.patch("pyhugegraph.utils.huge_requests.log.isEnabledFor", return_value=True),
mock.patch("pyhugegraph.utils.huge_requests.log.debug") as log_debug,
):
session.request(
"/auth/users",
method="POST",
data='{"user_name":"marko","user_password":"super-secret"}',
)
logged_args = str(log_debug.call_args)
assert "super-secret" not in logged_args
assert "***REDACTED***" in logged_args
def test_session_skips_debug_redaction_when_debug_disabled():
cfg = DummyCfg(url="http://127.0.0.1:8080", graphspace=None, gs_supported=False, graph_name="g")
cfg.username = "admin"
cfg.password = "admin-password"
cfg.timeout = 30
response = mock.Mock(spec=requests.Response)
response.status_code = 200
response.json.return_value = {"ok": True}
response.raise_for_status.return_value = None
raw_session = mock.Mock()
raw_session.post.return_value = response
session = HGraphSession(cfg, session=raw_session)
with (
mock.patch("pyhugegraph.utils.huge_requests.log.isEnabledFor", return_value=False),
mock.patch("pyhugegraph.utils.huge_requests.redact_sensitive_data") as redact,
):
session.request(
"/auth/users",
method="POST",
data='{"user_name":"marko","user_password":"super-secret"}',
)
redact.assert_not_called()