mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-30 20:35:22 +08:00
135 lines
6.0 KiB
Python
135 lines
6.0 KiB
Python
"""Narrow write scopes retain HTTP audit snapshots without acquiring read access."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
import quart
|
|
import sqlalchemy as sa
|
|
|
|
from langbot.pkg.api.http.controller.groups.knowledge.base import KnowledgeBaseRouterGroup
|
|
from langbot.pkg.api.http.controller.groups.pipelines.pipelines import PipelinesRouterGroup
|
|
from langbot.pkg.api.http.controller.groups.provider.models import LLMModelsRouterGroup
|
|
from langbot.pkg.api.http.controller.groups.provider.providers import ModelProvidersRouterGroup
|
|
from langbot.pkg.api.http.controller.groups.skills import SkillsRouterGroup
|
|
from langbot.pkg.api.http.service.skill import SkillService
|
|
from langbot.pkg.entity.persistence.operation_log import WorkspaceOperationLog
|
|
from langbot.pkg.operation_trace import service as tracing
|
|
from langbot.pkg.skill.repository import SkillRepository
|
|
from tests.integration.api.test_application_foundation import context, foundation # noqa: F401
|
|
from tests.integration.api.test_application_resources import resources # noqa: F401
|
|
|
|
|
|
pytestmark = [pytest.mark.integration, pytest.mark.asyncio]
|
|
|
|
|
|
@pytest.mark.parametrize('kind', ['pipeline', 'pipeline_extensions', 'provider', 'model', 'knowledge', 'skill'])
|
|
async def test_write_only_keys_preserve_before_values_and_one_redacted_audit(resources, tmp_path, monkeypatch, kind): # noqa: F811
|
|
api, ap, engine = resources
|
|
ap.application_api = api
|
|
scope = 'provider_secret.manage' if kind in {'provider', 'model'} else 'resource.manage'
|
|
key = (await ap.apikey_service.create_api_key(context(), 'writer', scopes=[scope]))['key']
|
|
empty_key = (await ap.apikey_service.create_api_key(context(), 'empty', scopes=[]))['key']
|
|
body = {'name': 'after'}
|
|
expected_change = {'field': 'name', 'before': 'before', 'after': 'after'}
|
|
if kind.startswith('pipeline'):
|
|
identity = await api.execute(
|
|
'pipeline.create',
|
|
context(),
|
|
{
|
|
'pipeline_data': {
|
|
'name': 'before',
|
|
'config': {'ai': {'api_key': 'old-secret'}},
|
|
'extensions_preferences': {'enable_all_plugins': False, 'plugins': []},
|
|
}
|
|
},
|
|
)
|
|
endpoint = '/api/v1/pipelines/' + identity
|
|
router = PipelinesRouterGroup
|
|
if kind == 'pipeline_extensions':
|
|
endpoint += '/extensions'
|
|
body = {'enable_all_plugins': True}
|
|
expected_change = {'field': 'enable_all_plugins', 'before': False, 'after': True}
|
|
else:
|
|
body['config'] = {'ai': {'api_key': 'new-secret'}}
|
|
elif kind in {'provider', 'model'}:
|
|
provider = await api.execute(
|
|
'provider.create',
|
|
context(),
|
|
{
|
|
'provider_data': {
|
|
'name': 'before',
|
|
'requester': 'test-requester',
|
|
'base_url': 'https://example.test/v1',
|
|
'api_keys': ['old-secret'],
|
|
}
|
|
},
|
|
)
|
|
if kind == 'provider':
|
|
endpoint = '/api/v1/provider/providers/' + provider
|
|
router = ModelProvidersRouterGroup
|
|
body['api_keys'] = ['new-secret']
|
|
else:
|
|
identity = await api.execute(
|
|
'model.llm.create',
|
|
context(),
|
|
{'model_data': {'name': 'before', 'provider_uuid': provider, 'extra_args': {'api_key': 'old-secret'}}},
|
|
)
|
|
endpoint = '/api/v1/provider/models/llm/' + identity
|
|
router = LLMModelsRouterGroup
|
|
body['extra_args'] = {'api_key': 'new-secret'}
|
|
elif kind == 'knowledge':
|
|
identity = await api.execute(
|
|
'knowledge.base.create',
|
|
context(),
|
|
{
|
|
'kb_data': {
|
|
'name': 'before',
|
|
'knowledge_engine_plugin_id': 'test/engine',
|
|
'defer_initialization': True,
|
|
'creation_settings': {'api_key': 'old-secret'},
|
|
}
|
|
},
|
|
)
|
|
endpoint = '/api/v1/knowledge/bases/' + identity
|
|
router = KnowledgeBaseRouterGroup
|
|
else:
|
|
ap.instance_config.data['skills'] = {'root': str(tmp_path / 'skills')}
|
|
ap.skill_repository = SkillRepository(ap)
|
|
ap.skill_service = SkillService(ap)
|
|
skill = await api.execute(
|
|
'skill.create',
|
|
context(),
|
|
{'data': {'name': 'audit-skill', 'description': 'before', 'instructions': 'Read me'}},
|
|
)
|
|
endpoint = '/api/v1/skills/audit-skill'
|
|
router = SkillsRouterGroup
|
|
body = {'description': 'after', 'base_revision': skill['revision']}
|
|
expected_change = {'field': 'description', 'before': 'before', 'after': 'after'}
|
|
monkeypatch.setattr(tracing, '_global_enabled', False)
|
|
audit = ap.workspace_settings_service = tracing.WorkspaceSettingsService(ap)
|
|
await audit.set_operation_level('workspace-a', tracing.OPERATION_LEVEL_MUTATION, dedupe_window_seconds=0)
|
|
app = quart.Quart(__name__)
|
|
await router(ap, app).initialize()
|
|
client = app.test_client()
|
|
headers = {'X-API-Key': key}
|
|
assert (await client.get(endpoint, headers=headers)).status_code == 403
|
|
assert (await client.put(endpoint, headers={'X-API-Key': empty_key}, json=body)).status_code == 403
|
|
response = await client.put(endpoint, headers={**headers, 'X-Request-Id': 'write-audit'}, json=body)
|
|
assert response.status_code == 200, await response.get_data(as_text=True)
|
|
await audit.flush_pending()
|
|
async with engine.connect() as connection:
|
|
rows = (
|
|
(
|
|
await connection.execute(
|
|
sa.select(WorkspaceOperationLog).where(WorkspaceOperationLog.request_id == 'write-audit')
|
|
)
|
|
)
|
|
.mappings()
|
|
.all()
|
|
)
|
|
assert len(rows) == 1
|
|
assert rows[0]['status_code'] == 200 and rows[0]['outcome'] == 'ok'
|
|
assert expected_change in json.loads(rows[0]['changes'])
|
|
assert 'old-secret' not in str(rows[0]) and 'new-secret' not in str(rows[0])
|
|
assert (await client.get(endpoint, headers=headers)).status_code == 403
|