fix(files): align adaptive tasks with live access

This commit is contained in:
2026-07-21 19:15:47 +02:00
parent 58af1a20a7
commit 2b34f6e305
3 changed files with 98 additions and 7 deletions

View File

@@ -13,6 +13,7 @@ from govoplan_core.core.modules import (
DocumentationTopic, DocumentationTopic,
) )
from govoplan_files.backend.storage.archives import ZIP_UPLOAD_MAX_FILES from govoplan_files.backend.storage.archives import ZIP_UPLOAD_MAX_FILES
from govoplan_files.backend.storage.access import user_group_ids
from govoplan_files.backend.storage.connector_visibility import ( from govoplan_files.backend.storage.connector_visibility import (
connector_profile_usable_for_import, connector_profile_usable_for_import,
visible_connector_profiles_for_actor, visible_connector_profiles_for_actor,
@@ -90,7 +91,7 @@ def _upload_topic(max_bytes: int) -> DocumentationTopic:
"help_contexts": ["files.list"], "help_contexts": ["files.list"],
"prerequisites": [ "prerequisites": [
"You may view and upload managed files.", "You may view and upload managed files.",
"You can access the destination personal or group space.", "The destination personal or group space grants this account write access; upload permission alone does not grant access to every space.",
], ],
"steps": [ "steps": [
"Open Files and choose My files or an accessible group space.", "Open Files and choose My files or an accessible group space.",
@@ -161,6 +162,7 @@ def _zip_topic(max_file_bytes: int, max_zip_bytes: int) -> DocumentationTopic:
"prerequisites": [ "prerequisites": [
"You may view and upload managed files.", "You may view and upload managed files.",
"The archive is not encrypted and fits the current configured limits.", "The archive is not encrypted and fits the current configured limits.",
"The destination personal or group space grants this account write access.",
], ],
"steps": [ "steps": [
"Open Files and choose the managed destination space and folder.", "Open Files and choose the managed destination space and folder.",
@@ -215,19 +217,36 @@ def _connector_import_topic(context: DocumentationContext) -> DocumentationTopic
) )
try: try:
group_ids = _principal_group_ids(principal) member_group_ids = _actor_group_ids(
session,
principal=principal,
tenant_id=tenant_id,
user_id=user_id,
include_admin_groups=False,
)
connector_group_ids = (
_actor_group_ids(
session,
principal=principal,
tenant_id=tenant_id,
user_id=user_id,
include_admin_groups=True,
)
if _has_all_scopes(principal, ("files:file:admin",))
else member_group_ids
)
profiles = visible_connector_profiles_for_actor( profiles = visible_connector_profiles_for_actor(
session, session,
tenant_id=tenant_id, tenant_id=tenant_id,
user_id=user_id, user_id=user_id,
group_ids=group_ids, group_ids=connector_group_ids,
settings=context.settings, settings=context.settings,
campaign_visible=_campaign_visibility( campaign_visible=_campaign_visibility(
context, context,
session=session, session=session,
tenant_id=tenant_id, tenant_id=tenant_id,
user_id=user_id, user_id=user_id,
group_ids=group_ids, group_ids=member_group_ids,
), ),
include_effective_policy=True, include_effective_policy=True,
) )
@@ -286,6 +305,7 @@ def _connector_import_topic(context: DocumentationContext) -> DocumentationTopic
"You may view and upload managed files.", "You may view and upload managed files.",
"At least one enabled, credential-ready, policy-allowed connection using a pinning-safe provider is visible to this account.", "At least one enabled, credential-ready, policy-allowed connection using a pinning-safe provider is visible to this account.",
"The selected remote path and item must pass their operation-time policy checks.", "The selected remote path and item must pass their operation-time policy checks.",
"The managed destination space grants this account write access.",
], ],
"steps": [ "steps": [
"Open Files and choose a managed destination space.", "Open Files and choose a managed destination space.",
@@ -382,6 +402,26 @@ def _principal_group_ids(principal: object) -> tuple[str, ...]:
return tuple(str(value) for value in values if str(value)) return tuple(str(value) for value in values if str(value))
def _actor_group_ids(
session: Session,
*,
principal: object,
tenant_id: str,
user_id: str,
include_admin_groups: bool,
) -> tuple[str, ...]:
try:
values = user_group_ids(
session,
tenant_id=tenant_id,
user_id=user_id,
include_admin_groups=include_admin_groups,
)
except Exception:
return _principal_group_ids(principal)
return tuple(str(value) for value in values if str(value))
def _campaign_visibility( def _campaign_visibility(
context: DocumentationContext, context: DocumentationContext,
*, *,

View File

@@ -226,7 +226,7 @@ manifest = ModuleManifest(
"help_contexts": ["files.list"], "help_contexts": ["files.list"],
"prerequisites": [ "prerequisites": [
"You may view and organize managed files.", "You may view and organize managed files.",
"You can access every source and destination space used by the operation.", "You have write or owner access to every source item and destination space used by the operation; the global organize permission alone does not grant resource access.",
], ],
"steps": [ "steps": [
"Open Files and select the personal or group space to organize.", "Open Files and select the personal or group space to organize.",
@@ -317,7 +317,7 @@ manifest = ModuleManifest(
"screen": "Files", "screen": "Files",
"help_contexts": ["files.list"], "help_contexts": ["files.list"],
"prerequisites": [ "prerequisites": [
"You may view and share the managed file.", "You may view and share the managed file and have write/manage access to that specific asset; the global share permission alone does not grant resource access.",
"A supporting workflow or API client is available; the Files page has no general share editor yet.", "A supporting workflow or API client is available; the Files page has no general share editor yet.",
"The process does not require share revocation, because no revocation route exists yet.", "The process does not require share revocation, because no revocation route exists yet.",
], ],
@@ -368,7 +368,7 @@ manifest = ModuleManifest(
"screen": "Files", "screen": "Files",
"help_contexts": ["files.list"], "help_contexts": ["files.list"],
"prerequisites": [ "prerequisites": [
"You may view and delete the selected managed content.", "You may view and delete the selected managed content and have write or owner access to every affected asset or folder.",
"You have reviewed the complete folder tree when deleting recursively.", "You have reviewed the complete folder tree when deleting recursively.",
], ],
"steps": [ "steps": [

View File

@@ -160,6 +160,57 @@ class FilesRuntimeDocumentationTests(unittest.TestCase):
): ):
self.assertNotIn(secret, payload) self.assertNotIn(secret, payload)
def test_files_admin_connector_groups_do_not_widen_campaign_membership(self) -> None:
usable = ConnectorProfile(
id="group-profile",
label="Group profile",
provider="webdav",
scope_type="group",
scope_id="admin-visible-group",
endpoint_url="https://files.example.invalid",
credential_mode="anonymous",
)
def groups_for_actor(_session, *, include_admin_groups, **_kwargs):
return ["member-group", "admin-visible-group"] if include_admin_groups else ["member-group"]
with (
patch(
"govoplan_files.backend.documentation.user_group_ids",
side_effect=groups_for_actor,
),
patch(
"govoplan_files.backend.documentation._campaign_visibility",
return_value=None,
) as campaign_visibility,
patch(
"govoplan_files.backend.documentation.visible_connector_profiles_for_actor",
return_value=[usable],
) as visible,
):
topics = {
topic.id: topic
for topic in documentation_topics(
self.context(
{
"files:file:read",
"files:file:upload",
"files:file:admin",
}
)
)
}
self.assertIn("files.workflow.import-managed-snapshot", topics)
self.assertEqual(
("member-group", "admin-visible-group"),
tuple(visible.call_args.kwargs["group_ids"]),
)
self.assertEqual(
("member-group",),
tuple(campaign_visibility.call_args.kwargs["group_ids"]),
)
def test_connector_evaluation_errors_fail_closed_without_error_details( def test_connector_evaluation_errors_fail_closed_without_error_details(
self, self,
) -> None: ) -> None: