From 8bf7296bf5b251c8309a5f0d023ff6428b31a353 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Sat, 11 Jul 2026 16:49:02 +0200 Subject: [PATCH] Release v0.1.8 --- AGENTS.md | 4 +- README.md | 4 + package.json | 4 +- pyproject.toml | 5 +- src/govoplan_files/backend/manifest.py | 2 +- .../4f5a6b7c8d9e_file_connector_spaces.py | 0 .../5a6b7c8d9e0f_file_connector_profiles.py | 0 ...6b7c8d9e0f1a_file_connector_credentials.py | 0 .../migrations/dev_versions/__init__.py | 0 .../a7b8c9d0e1f3_file_connector_policies.py | 0 .../a7b8c9d0e1f3_v017_files_baseline.py | 158 ++++++++++++++++++ .../backend/storage/connector_browse.py | 5 +- webui/package.json | 4 +- 13 files changed, 175 insertions(+), 11 deletions(-) rename src/govoplan_files/backend/migrations/{versions => dev_versions}/4f5a6b7c8d9e_file_connector_spaces.py (100%) rename src/govoplan_files/backend/migrations/{versions => dev_versions}/5a6b7c8d9e0f_file_connector_profiles.py (100%) rename src/govoplan_files/backend/migrations/{versions => dev_versions}/6b7c8d9e0f1a_file_connector_credentials.py (100%) create mode 100644 src/govoplan_files/backend/migrations/dev_versions/__init__.py rename src/govoplan_files/backend/migrations/{versions => dev_versions}/a7b8c9d0e1f3_file_connector_policies.py (100%) create mode 100644 src/govoplan_files/backend/migrations/versions/a7b8c9d0e1f3_v017_files_baseline.py diff --git a/AGENTS.md b/AGENTS.md index 2c8bef9..c885548 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,8 +18,8 @@ cd /mnt/DATA/git/govoplan-core For combined checks, run: ```bash -cd /mnt/DATA/git/govoplan-core -./scripts/check-focused.sh +cd /mnt/DATA/git/govoplan +tools/checks/check-focused.sh ``` For WebUI permutation checks that include files: diff --git a/README.md b/README.md index e8320cd..f85e096 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # govoplan-files + +**Repository type:** module (domain). + + GovOPlaN Files is the managed file module. It bundles backend storage APIs and the Files WebUI package so file features can be installed as one module. ## Ownership diff --git a/package.json b/package.json index 891fbb0..18136d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/files-webui", - "version": "0.1.7", + "version": "0.1.8", "private": true, "type": "module", "main": "webui/src/index.ts", @@ -19,7 +19,7 @@ "LICENSE" ], "peerDependencies": { - "@govoplan/core-webui": "^0.1.7", + "@govoplan/core-webui": "^0.1.8", "lucide-react": "^1.23.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/pyproject.toml b/pyproject.toml index 9c6731e..008d97f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,15 @@ build-backend = "setuptools.build_meta" [project] name = "govoplan-files" -version = "0.1.7" +version = "0.1.8" description = "GovOPlaN files module with backend and WebUI integration." readme = "README.md" requires-python = ">=3.12" license = { file = "LICENSE" } authors = [{ name = "GovOPlaN" }] dependencies = [ - "govoplan-core>=0.1.7", + "govoplan-core>=0.1.8", + "defusedxml>=0.7,<1", "python-multipart>=0.0.31,<1", ] diff --git a/src/govoplan_files/backend/manifest.py b/src/govoplan_files/backend/manifest.py index 21ac229..239f04d 100644 --- a/src/govoplan_files/backend/manifest.py +++ b/src/govoplan_files/backend/manifest.py @@ -112,7 +112,7 @@ def _files_router(context: ModuleContext): manifest = ModuleManifest( id="files", name="Files", - version="0.1.7", + version="0.1.8", required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), optional_dependencies=("campaigns",), provides_interfaces=( diff --git a/src/govoplan_files/backend/migrations/versions/4f5a6b7c8d9e_file_connector_spaces.py b/src/govoplan_files/backend/migrations/dev_versions/4f5a6b7c8d9e_file_connector_spaces.py similarity index 100% rename from src/govoplan_files/backend/migrations/versions/4f5a6b7c8d9e_file_connector_spaces.py rename to src/govoplan_files/backend/migrations/dev_versions/4f5a6b7c8d9e_file_connector_spaces.py diff --git a/src/govoplan_files/backend/migrations/versions/5a6b7c8d9e0f_file_connector_profiles.py b/src/govoplan_files/backend/migrations/dev_versions/5a6b7c8d9e0f_file_connector_profiles.py similarity index 100% rename from src/govoplan_files/backend/migrations/versions/5a6b7c8d9e0f_file_connector_profiles.py rename to src/govoplan_files/backend/migrations/dev_versions/5a6b7c8d9e0f_file_connector_profiles.py diff --git a/src/govoplan_files/backend/migrations/versions/6b7c8d9e0f1a_file_connector_credentials.py b/src/govoplan_files/backend/migrations/dev_versions/6b7c8d9e0f1a_file_connector_credentials.py similarity index 100% rename from src/govoplan_files/backend/migrations/versions/6b7c8d9e0f1a_file_connector_credentials.py rename to src/govoplan_files/backend/migrations/dev_versions/6b7c8d9e0f1a_file_connector_credentials.py diff --git a/src/govoplan_files/backend/migrations/dev_versions/__init__.py b/src/govoplan_files/backend/migrations/dev_versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/govoplan_files/backend/migrations/versions/a7b8c9d0e1f3_file_connector_policies.py b/src/govoplan_files/backend/migrations/dev_versions/a7b8c9d0e1f3_file_connector_policies.py similarity index 100% rename from src/govoplan_files/backend/migrations/versions/a7b8c9d0e1f3_file_connector_policies.py rename to src/govoplan_files/backend/migrations/dev_versions/a7b8c9d0e1f3_file_connector_policies.py diff --git a/src/govoplan_files/backend/migrations/versions/a7b8c9d0e1f3_v017_files_baseline.py b/src/govoplan_files/backend/migrations/versions/a7b8c9d0e1f3_v017_files_baseline.py new file mode 100644 index 0000000..2cb09a2 --- /dev/null +++ b/src/govoplan_files/backend/migrations/versions/a7b8c9d0e1f3_v017_files_baseline.py @@ -0,0 +1,158 @@ +"""v0.1.7 files baseline + +Revision ID: a7b8c9d0e1f3 +Revises: None +Create Date: 2026-07-11 00:00:00.000000 +""" +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + + +revision = 'a7b8c9d0e1f3' +down_revision = None +branch_labels = None +depends_on = '4f2a9c8e7b6d' + + +def upgrade() -> None: + op.create_table('file_connector_credentials', + sa.Column('id', sa.String(length=255), nullable=False), + sa.Column('tenant_id', sa.String(length=36), nullable=True), + sa.Column('scope_type', sa.String(length=20), nullable=False), + sa.Column('scope_id', sa.String(length=36), nullable=True), + sa.Column('label', sa.String(length=255), nullable=False), + sa.Column('provider', sa.String(length=50), nullable=True), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('credential_mode', sa.String(length=30), nullable=False), + sa.Column('username', sa.String(length=320), nullable=True), + sa.Column('password_encrypted', sa.Text(), nullable=True), + sa.Column('token_encrypted', sa.Text(), nullable=True), + sa.Column('password_env', sa.String(length=255), nullable=True), + sa.Column('token_env', sa.String(length=255), nullable=True), + sa.Column('secret_ref', sa.String(length=1000), nullable=True), + sa.Column('policy', sa.JSON(), nullable=True), + sa.Column('metadata', sa.JSON(), nullable=True), + sa.Column('created_by_user_id', sa.String(length=36), nullable=True), + sa.Column('updated_by_user_id', sa.String(length=36), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['created_by_user_id'], ['access_users.id'], name=op.f('fk_file_connector_credentials_created_by_user_id_access_users'), ondelete='SET NULL'), + sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_file_connector_credentials_tenant_id_scopes'), ondelete='CASCADE'), + sa.ForeignKeyConstraint(['updated_by_user_id'], ['access_users.id'], name=op.f('fk_file_connector_credentials_updated_by_user_id_access_users'), ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_file_connector_credentials')) + ) + op.create_index(op.f('ix_file_connector_credentials_created_by_user_id'), 'file_connector_credentials', ['created_by_user_id'], unique=False) + op.create_index(op.f('ix_file_connector_credentials_enabled'), 'file_connector_credentials', ['enabled'], unique=False) + op.create_index(op.f('ix_file_connector_credentials_provider'), 'file_connector_credentials', ['provider'], unique=False) + op.create_index('ix_file_connector_credentials_scope', 'file_connector_credentials', ['scope_type', 'scope_id'], unique=False) + op.create_index(op.f('ix_file_connector_credentials_scope_id'), 'file_connector_credentials', ['scope_id'], unique=False) + op.create_index(op.f('ix_file_connector_credentials_scope_type'), 'file_connector_credentials', ['scope_type'], unique=False) + op.create_index(op.f('ix_file_connector_credentials_tenant_id'), 'file_connector_credentials', ['tenant_id'], unique=False) + op.create_index(op.f('ix_file_connector_credentials_updated_by_user_id'), 'file_connector_credentials', ['updated_by_user_id'], unique=False) + op.create_table('file_connector_policies', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('tenant_id', sa.String(length=36), nullable=True), + sa.Column('scope_type', sa.String(length=20), nullable=False), + sa.Column('scope_id', sa.String(length=36), nullable=True), + sa.Column('policy', sa.JSON(), nullable=False), + sa.Column('created_by_user_id', sa.String(length=36), nullable=True), + sa.Column('updated_by_user_id', sa.String(length=36), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['created_by_user_id'], ['access_users.id'], name=op.f('fk_file_connector_policies_created_by_user_id_access_users'), ondelete='SET NULL'), + sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_file_connector_policies_tenant_id_scopes'), ondelete='CASCADE'), + sa.ForeignKeyConstraint(['updated_by_user_id'], ['access_users.id'], name=op.f('fk_file_connector_policies_updated_by_user_id_access_users'), ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_file_connector_policies')), + sa.UniqueConstraint('tenant_id', 'scope_type', 'scope_id', name='uq_file_connector_policies_scope') + ) + op.create_index(op.f('ix_file_connector_policies_created_by_user_id'), 'file_connector_policies', ['created_by_user_id'], unique=False) + op.create_index('ix_file_connector_policies_scope', 'file_connector_policies', ['scope_type', 'scope_id'], unique=False) + op.create_index(op.f('ix_file_connector_policies_scope_id'), 'file_connector_policies', ['scope_id'], unique=False) + op.create_index(op.f('ix_file_connector_policies_scope_type'), 'file_connector_policies', ['scope_type'], unique=False) + op.create_index(op.f('ix_file_connector_policies_tenant_id'), 'file_connector_policies', ['tenant_id'], unique=False) + op.create_index(op.f('ix_file_connector_policies_updated_by_user_id'), 'file_connector_policies', ['updated_by_user_id'], unique=False) + op.create_table('file_connector_profiles', + sa.Column('id', sa.String(length=255), nullable=False), + sa.Column('tenant_id', sa.String(length=36), nullable=True), + sa.Column('scope_type', sa.String(length=20), nullable=False), + sa.Column('scope_id', sa.String(length=36), nullable=True), + sa.Column('label', sa.String(length=255), nullable=False), + sa.Column('provider', sa.String(length=50), nullable=False), + sa.Column('endpoint_url', sa.String(length=1000), nullable=True), + sa.Column('base_path', sa.String(length=1000), nullable=True), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('credential_profile_id', sa.String(length=255), nullable=True), + sa.Column('credential_mode', sa.String(length=30), nullable=False), + sa.Column('username', sa.String(length=320), nullable=True), + sa.Column('password_encrypted', sa.Text(), nullable=True), + sa.Column('token_encrypted', sa.Text(), nullable=True), + sa.Column('password_env', sa.String(length=255), nullable=True), + sa.Column('token_env', sa.String(length=255), nullable=True), + sa.Column('secret_ref', sa.String(length=1000), nullable=True), + sa.Column('capabilities', sa.JSON(), nullable=True), + sa.Column('policy', sa.JSON(), nullable=True), + sa.Column('metadata', sa.JSON(), nullable=True), + sa.Column('created_by_user_id', sa.String(length=36), nullable=True), + sa.Column('updated_by_user_id', sa.String(length=36), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['created_by_user_id'], ['access_users.id'], name=op.f('fk_file_connector_profiles_created_by_user_id_access_users'), ondelete='SET NULL'), + sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_file_connector_profiles_tenant_id_scopes'), ondelete='CASCADE'), + sa.ForeignKeyConstraint(['updated_by_user_id'], ['access_users.id'], name=op.f('fk_file_connector_profiles_updated_by_user_id_access_users'), ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_file_connector_profiles')) + ) + op.create_index(op.f('ix_file_connector_profiles_created_by_user_id'), 'file_connector_profiles', ['created_by_user_id'], unique=False) + op.create_index(op.f('ix_file_connector_profiles_credential_profile_id'), 'file_connector_profiles', ['credential_profile_id'], unique=False) + op.create_index(op.f('ix_file_connector_profiles_enabled'), 'file_connector_profiles', ['enabled'], unique=False) + op.create_index(op.f('ix_file_connector_profiles_provider'), 'file_connector_profiles', ['provider'], unique=False) + op.create_index('ix_file_connector_profiles_scope', 'file_connector_profiles', ['scope_type', 'scope_id'], unique=False) + op.create_index(op.f('ix_file_connector_profiles_scope_id'), 'file_connector_profiles', ['scope_id'], unique=False) + op.create_index(op.f('ix_file_connector_profiles_scope_type'), 'file_connector_profiles', ['scope_type'], unique=False) + op.create_index(op.f('ix_file_connector_profiles_tenant_id'), 'file_connector_profiles', ['tenant_id'], unique=False) + op.create_index(op.f('ix_file_connector_profiles_updated_by_user_id'), 'file_connector_profiles', ['updated_by_user_id'], unique=False) + op.create_table('file_connector_spaces', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('tenant_id', sa.String(length=36), nullable=False), + sa.Column('owner_type', sa.String(length=20), nullable=False), + sa.Column('owner_user_id', sa.String(length=36), nullable=True), + sa.Column('owner_group_id', sa.String(length=36), nullable=True), + sa.Column('label', sa.String(length=255), nullable=False), + sa.Column('connector_profile_id', sa.String(length=255), nullable=False), + sa.Column('provider', sa.String(length=50), nullable=False), + sa.Column('library_id', sa.String(length=255), nullable=True), + sa.Column('remote_path', sa.String(length=1000), nullable=False), + sa.Column('sync_mode', sa.String(length=30), nullable=False), + sa.Column('read_only', sa.Boolean(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_by_user_id', sa.String(length=36), nullable=True), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('metadata', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['created_by_user_id'], ['access_users.id'], name=op.f('fk_file_connector_spaces_created_by_user_id_access_users'), ondelete='SET NULL'), + sa.ForeignKeyConstraint(['owner_group_id'], ['access_groups.id'], name=op.f('fk_file_connector_spaces_owner_group_id_access_groups'), ondelete='SET NULL'), + sa.ForeignKeyConstraint(['owner_user_id'], ['access_users.id'], name=op.f('fk_file_connector_spaces_owner_user_id_access_users'), ondelete='SET NULL'), + sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_file_connector_spaces_tenant_id_scopes'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_file_connector_spaces')) + ) + op.create_index(op.f('ix_file_connector_spaces_connector_profile_id'), 'file_connector_spaces', ['connector_profile_id'], unique=False) + op.create_index(op.f('ix_file_connector_spaces_created_by_user_id'), 'file_connector_spaces', ['created_by_user_id'], unique=False) + op.create_index(op.f('ix_file_connector_spaces_deleted_at'), 'file_connector_spaces', ['deleted_at'], unique=False) + op.create_index(op.f('ix_file_connector_spaces_is_active'), 'file_connector_spaces', ['is_active'], unique=False) + op.create_index('ix_file_connector_spaces_owner', 'file_connector_spaces', ['tenant_id', 'owner_type', 'owner_user_id', 'owner_group_id'], unique=False) + op.create_index(op.f('ix_file_connector_spaces_owner_group_id'), 'file_connector_spaces', ['owner_group_id'], unique=False) + op.create_index(op.f('ix_file_connector_spaces_owner_type'), 'file_connector_spaces', ['owner_type'], unique=False) + op.create_index(op.f('ix_file_connector_spaces_owner_user_id'), 'file_connector_spaces', ['owner_user_id'], unique=False) + op.create_index(op.f('ix_file_connector_spaces_provider'), 'file_connector_spaces', ['provider'], unique=False) + op.create_index(op.f('ix_file_connector_spaces_tenant_id'), 'file_connector_spaces', ['tenant_id'], unique=False) + op.create_index('uq_file_connector_spaces_active_group_label', 'file_connector_spaces', ['tenant_id', 'owner_group_id', 'label'], unique=True, sqlite_where=sa.text("owner_type = 'group' AND deleted_at IS NULL"), postgresql_where=sa.text("owner_type = 'group' AND deleted_at IS NULL")) + op.create_index('uq_file_connector_spaces_active_user_label', 'file_connector_spaces', ['tenant_id', 'owner_user_id', 'label'], unique=True, sqlite_where=sa.text("owner_type = 'user' AND deleted_at IS NULL"), postgresql_where=sa.text("owner_type = 'user' AND deleted_at IS NULL")) + + +def downgrade() -> None: + op.drop_table('file_connector_spaces') + op.drop_table('file_connector_profiles') + op.drop_table('file_connector_policies') + op.drop_table('file_connector_credentials') diff --git a/src/govoplan_files/backend/storage/connector_browse.py b/src/govoplan_files/backend/storage/connector_browse.py index 0047940..55fee42 100644 --- a/src/govoplan_files/backend/storage/connector_browse.py +++ b/src/govoplan_files/backend/storage/connector_browse.py @@ -11,6 +11,7 @@ from typing import Any from urllib.parse import quote, unquote, urljoin, urlsplit import xml.etree.ElementTree as ET +from defusedxml import ElementTree as SafeElementTree import httpx from govoplan_files.backend.storage.connector_profiles import ConnectorProfile @@ -70,8 +71,8 @@ def browse_connector_profile(profile: ConnectorProfile, *, path: str | None = No def parse_webdav_multistatus(*, root_url: str, current_path: str, payload: str | bytes) -> list[ConnectorBrowseItem]: try: - root = ET.fromstring(payload) - except ET.ParseError as exc: + root = SafeElementTree.fromstring(payload) + except SafeElementTree.ParseError as exc: raise ConnectorBrowseError("Connector returned invalid WebDAV XML") from exc root_path = _url_path_root(root_url) current = normalize_connector_browse_path(current_path) diff --git a/webui/package.json b/webui/package.json index 8f0d1de..4f6042c 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/files-webui", - "version": "0.1.7", + "version": "0.1.8", "private": true, "type": "module", "main": "src/index.ts", @@ -21,7 +21,7 @@ "react-dom": "^19.0.0", "react-router-dom": "^7.1.1", "lucide-react": "^1.23.0", - "@govoplan/core-webui": "^0.1.7" + "@govoplan/core-webui": "^0.1.8" }, "peerDependenciesMeta": { "@govoplan/core-webui": {