howler-api 4.1.0.dev1588__tar.gz → 4.1.0.dev1616__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/PKG-INFO +2 -2
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/action.py +13 -2
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/dossier.py +3 -1
- howler_api-4.1.0.dev1616/howler/api/v1/utils/permission_helper.py +83 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/view.py +30 -24
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/collection.py +173 -69
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/howler_store.py +6 -0
- howler_api-4.1.0.dev1616/howler/datastore/migrations/__init__.py +4 -0
- howler_api-4.1.0.dev1616/howler/datastore/migrations/action_owner.py +34 -0
- howler_api-4.1.0.dev1616/howler/datastore/migrations/base.py +20 -0
- howler_api-4.1.0.dev1616/howler/datastore/migrations/runner.py +313 -0
- howler_api-4.1.0.dev1616/howler/external/README.md +84 -0
- howler_api-4.1.0.dev1616/howler/external/run_migrations.py +139 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/base.py +6 -3
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/action.py +2 -2
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/dossier.py +3 -2
- howler_api-4.1.0.dev1616/howler/odm/models/migration.py +28 -0
- howler_api-4.1.0.dev1616/howler/odm/models/ownership.py +17 -0
- howler_api-4.1.0.dev1616/howler/odm/models/permission_request.py +17 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/view.py +3 -5
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/random_data.py +28 -11
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/dossier_service.py +7 -10
- howler_api-4.1.0.dev1616/howler/services/permission_service.py +150 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/pyproject.toml +3 -2
- howler_api-4.1.0.dev1588/howler/external/README.md +0 -30
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/README.md +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/actions/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/actions/add_label.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/actions/add_to_case.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/actions/change_field.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/actions/demote.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/actions/prioritization.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/actions/promote.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/actions/remove_label.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/actions/transition.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/base.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/socket.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/auth.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/clue.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/configs.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/help.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/hit.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/overview.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/search.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/template.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/tool.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/user.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v1/utils/params.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v2/case.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v2/fuzzy.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v2/ingest.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/api/v2/search.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/app.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/common/README.md +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/common/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/common/classification.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/common/classification.yml +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/common/exceptions.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/common/loader.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/common/logging/audit.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/common/logging/format.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/common/net.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/common/net_static.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/common/random_user.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/common/swagger.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/config.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/cronjobs/action_queue_worker.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/cronjobs/correlation.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/README.md +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/bulk.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/constants.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/operations.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/schemas.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/store.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/support/build.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/types.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/datastore/utils.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/error.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/external/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/external/check_duplicates.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/external/reindex_data.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/gunicorn_config.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/healthz.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/helper/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/helper/azure.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/helper/discover.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/helper/hit.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/helper/oauth.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/helper/search.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/helper/workflow.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/helper/ws.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/README.md +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/charter.txt +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/constants.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/helper.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/mixins.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/aws.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/azure.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/case.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/clue.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/config.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/event.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/hit.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/howler_data.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/lead.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/overview.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/record.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/template.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/models/user.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/odm/randomizer.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/patched.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/plugins/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/plugins/config.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/security/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/security/login.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/security/socket.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/security/utils.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/action_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/analytic_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/auth_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/bundle_compat_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/case_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/comms_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/config_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/correlation_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/docs_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/event_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/fuzzy_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/hit_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/jwt_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/lucene_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/notebook_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/overview_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/search_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/template_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/user_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/services/viewer_service.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/telemetry.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/__init__.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/annotations.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/chunk.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/compat.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/constants.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/indexes.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/isotime.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/list_utils.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/lucene.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/net_utils.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/path.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/str_utils.py +0 -0
- {howler_api-4.1.0.dev1588 → howler_api-4.1.0.dev1616}/howler/utils/uid.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: howler-api
|
|
3
|
-
Version: 4.1.0.
|
|
3
|
+
Version: 4.1.0.dev1616
|
|
4
4
|
Summary: Howler - API server
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
|
|
@@ -42,7 +42,7 @@ Requires-Dist: prometheus-client (==0.26.0)
|
|
|
42
42
|
Requires-Dist: pydantic (>=2.11.4,<3.0.0)
|
|
43
43
|
Requires-Dist: pydantic-settings[yaml] (>=2.9.1,<3.0.0)
|
|
44
44
|
Requires-Dist: pydash (>=8.0.5,<9.0.0)
|
|
45
|
-
Requires-Dist: pyjwt (==2.
|
|
45
|
+
Requires-Dist: pyjwt (==2.14.0)
|
|
46
46
|
Requires-Dist: pysigma (==0.11.23)
|
|
47
47
|
Requires-Dist: pysigma-backend-elasticsearch (>=1.1.2,<2.0.0)
|
|
48
48
|
Requires-Dist: python-baseconv (==1.2.2)
|
|
@@ -4,6 +4,7 @@ from flask import Response, request
|
|
|
4
4
|
|
|
5
5
|
import howler.actions as actions
|
|
6
6
|
from howler.api import bad_request, created, forbidden, internal_error, make_subapi_blueprint, no_content, not_found, ok
|
|
7
|
+
from howler.api.v1.utils import permission_helper
|
|
7
8
|
from howler.api.v1.utils.etag import add_etag
|
|
8
9
|
from howler.api.v1.utils.params import parse_parameters, parse_refresh
|
|
9
10
|
from howler.common.exceptions import HowlerException
|
|
@@ -21,6 +22,7 @@ classification_definition = CLASSIFICATION.get_parsed_classification_definition(
|
|
|
21
22
|
|
|
22
23
|
action_api = make_subapi_blueprint(SUB_API, api_version=1)
|
|
23
24
|
action_api._doc = "Endpoints relating to bulk actions and automation" # type: ignore
|
|
25
|
+
permission_helper.add_access_control_endpoints(action_api, Action)
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
@generate_swagger_docs()
|
|
@@ -117,7 +119,7 @@ def add_action(user: User, **kwargs) -> Response:
|
|
|
117
119
|
return error
|
|
118
120
|
|
|
119
121
|
try:
|
|
120
|
-
new_action["
|
|
122
|
+
new_action["owner"] = user.uname
|
|
121
123
|
|
|
122
124
|
action_obj = Action(new_action)
|
|
123
125
|
|
|
@@ -168,6 +170,9 @@ def update_action(id: str, user: User, **kwargs) -> Response:
|
|
|
168
170
|
if not isinstance(updated_action, dict):
|
|
169
171
|
return bad_request(err="Incorrect data structure!")
|
|
170
172
|
|
|
173
|
+
if set(updated_action) & {"owner", "admins", "members"}:
|
|
174
|
+
return bad_request(err="You cannot change permissions using this endpoint.")
|
|
175
|
+
|
|
171
176
|
refresh = kwargs.get("refresh")
|
|
172
177
|
|
|
173
178
|
ds = datastore()
|
|
@@ -182,6 +187,12 @@ def update_action(id: str, user: User, **kwargs) -> Response:
|
|
|
182
187
|
):
|
|
183
188
|
return forbidden(err="Updating triggers requires the role 'automation_advanced'.")
|
|
184
189
|
|
|
190
|
+
is_action_admin = user.uname == existing_action.get("owner") or user.uname in existing_action.get("admins", [])
|
|
191
|
+
if not is_action_admin and "admin" not in user.type:
|
|
192
|
+
return forbidden(
|
|
193
|
+
err="You cannot update an action that is not owned by you, or you are not an administrator of."
|
|
194
|
+
)
|
|
195
|
+
|
|
185
196
|
updated_action = {
|
|
186
197
|
**existing_action,
|
|
187
198
|
**updated_action,
|
|
@@ -230,7 +241,7 @@ def delete_action(id: str, user: User, **kwargs) -> Response:
|
|
|
230
241
|
|
|
231
242
|
action: Action = result["items"][0]
|
|
232
243
|
|
|
233
|
-
if action.
|
|
244
|
+
if action.owner != user.uname and "admin" not in user.type:
|
|
234
245
|
return forbidden(err="You do not have the permissions necessary to delete this action.")
|
|
235
246
|
|
|
236
247
|
try:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from flask import request
|
|
2
2
|
|
|
3
3
|
from howler.api import bad_request, created, forbidden, internal_error, make_subapi_blueprint, no_content, not_found, ok
|
|
4
|
+
from howler.api.v1.utils import permission_helper
|
|
4
5
|
from howler.api.v1.utils.params import parse_parameters, parse_refresh
|
|
5
6
|
from howler.common.exceptions import (
|
|
6
7
|
ForbiddenException,
|
|
@@ -19,6 +20,7 @@ from howler.services import dossier_service
|
|
|
19
20
|
SUB_API = "dossier"
|
|
20
21
|
dossier_api = make_subapi_blueprint(SUB_API, api_version=1)
|
|
21
22
|
dossier_api._doc = "Manage the different dossiers created for filtering hits" # type: ignore
|
|
23
|
+
permission_helper.add_access_control_endpoints(dossier_api, Dossier)
|
|
22
24
|
|
|
23
25
|
logger = get_logger(__file__)
|
|
24
26
|
|
|
@@ -185,7 +187,7 @@ def delete_dossier(id: str, user: User, **kwargs):
|
|
|
185
187
|
return not_found(err="This dossier does not exist")
|
|
186
188
|
|
|
187
189
|
if existing_dossier.owner != user.uname and "admin" not in user.type:
|
|
188
|
-
return forbidden(err="You cannot delete a dossier unless you are
|
|
190
|
+
return forbidden(err="You cannot delete a dossier unless you are a global administrator or the owner.")
|
|
189
191
|
|
|
190
192
|
success = storage.dossier.delete(id, refresh=refresh)
|
|
191
193
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from flask import Blueprint, Response
|
|
2
|
+
|
|
3
|
+
from howler.api import bad_request, conflict, forbidden, ok
|
|
4
|
+
from howler.api.v1.utils.params import parse_parameters, parse_refresh
|
|
5
|
+
from howler.common.exceptions import ForbiddenException, InvalidDataException
|
|
6
|
+
from howler.common.swagger import generate_swagger_docs
|
|
7
|
+
from howler.datastore.exceptions import VersionConflictException
|
|
8
|
+
from howler.odm.models.ownership import Ownership
|
|
9
|
+
from howler.odm.models.user import User
|
|
10
|
+
from howler.security.login import api_login
|
|
11
|
+
from howler.services import permission_service
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def add_access_control_endpoints(api: Blueprint, odm: type[Ownership]):
|
|
15
|
+
"""Add access control routes for managing user permissions."""
|
|
16
|
+
|
|
17
|
+
@generate_swagger_docs()
|
|
18
|
+
@api.route("/<id>/permission", methods=["PUT"])
|
|
19
|
+
@api_login(required_priv=["R", "W"])
|
|
20
|
+
@parse_parameters(refresh=parse_refresh)
|
|
21
|
+
def give_privilege(id: str, user: User, **kwargs) -> Response:
|
|
22
|
+
"""Grant a permission on an action, dossier, or view.
|
|
23
|
+
|
|
24
|
+
Variables:
|
|
25
|
+
id => The identifier of the object whose permissions should be updated.
|
|
26
|
+
|
|
27
|
+
Arguments:
|
|
28
|
+
None
|
|
29
|
+
|
|
30
|
+
Optional Arguments:
|
|
31
|
+
refresh => ('true' | 'false' | 'wait_for') Whether to refresh the datastore before returning.
|
|
32
|
+
|
|
33
|
+
Data Block:
|
|
34
|
+
{
|
|
35
|
+
"privilege": "members", # The permission level: 'owner', 'admins', or 'members'
|
|
36
|
+
"user_ids": ["username"] # The users receiving the permission
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Result Example:
|
|
40
|
+
{
|
|
41
|
+
...object # The updated action, dossier, or view
|
|
42
|
+
}
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
result = permission_service.give_privilege(id, user, odm, refresh=kwargs.get("refresh"))
|
|
46
|
+
except VersionConflictException:
|
|
47
|
+
return conflict(err="Someone has updated this item. Please try again")
|
|
48
|
+
except ForbiddenException as e:
|
|
49
|
+
return forbidden(err=e.message)
|
|
50
|
+
except (ValueError, InvalidDataException) as e:
|
|
51
|
+
return bad_request(err=str(e))
|
|
52
|
+
return ok(result)
|
|
53
|
+
|
|
54
|
+
@generate_swagger_docs()
|
|
55
|
+
@api.route("/<id>/permission", methods=["DELETE"])
|
|
56
|
+
@api_login(required_priv=["R", "W"])
|
|
57
|
+
@parse_parameters(refresh=parse_refresh)
|
|
58
|
+
def remove_privilege(id: str, user: User, **kwargs) -> Response:
|
|
59
|
+
"""Revoke a permission on an action, dossier, or view.
|
|
60
|
+
|
|
61
|
+
Variables:
|
|
62
|
+
id => The identifier of the object whose permissions should be updated.
|
|
63
|
+
|
|
64
|
+
Arguments:
|
|
65
|
+
None
|
|
66
|
+
|
|
67
|
+
Optional Arguments:
|
|
68
|
+
refresh => ('true' | 'false' | 'wait_for') Whether to refresh the datastore before returning.
|
|
69
|
+
|
|
70
|
+
Result Example:
|
|
71
|
+
{
|
|
72
|
+
...object # The updated action, dossier, or view
|
|
73
|
+
}
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
result = permission_service.remove_privilege(id, user, odm, refresh=kwargs.get("refresh"))
|
|
77
|
+
except VersionConflictException:
|
|
78
|
+
return conflict(err="Someone has updated this item. Please try again")
|
|
79
|
+
except ForbiddenException as e:
|
|
80
|
+
return forbidden(err=e.message)
|
|
81
|
+
except (ValueError, InvalidDataException) as e:
|
|
82
|
+
return bad_request(err=str(e))
|
|
83
|
+
return ok(result)
|
|
@@ -4,6 +4,7 @@ from flask import request
|
|
|
4
4
|
from mergedeep.mergedeep import merge
|
|
5
5
|
|
|
6
6
|
from howler.api import bad_request, created, forbidden, make_subapi_blueprint, no_content, not_found, ok
|
|
7
|
+
from howler.api.v1.utils import permission_helper
|
|
7
8
|
from howler.api.v1.utils.params import parse_parameters, parse_refresh
|
|
8
9
|
from howler.common.exceptions import HowlerException
|
|
9
10
|
from howler.common.loader import datastore
|
|
@@ -17,6 +18,7 @@ from howler.security.login import api_login
|
|
|
17
18
|
SUB_API = "view"
|
|
18
19
|
view_api = make_subapi_blueprint(SUB_API, api_version=1)
|
|
19
20
|
view_api._doc = "Manage the different views created for filtering hits" # type: ignore
|
|
21
|
+
permission_helper.add_access_control_endpoints(view_api, View)
|
|
20
22
|
|
|
21
23
|
logger = get_logger(__file__)
|
|
22
24
|
|
|
@@ -41,7 +43,10 @@ def get_views(user: User, **kwargs):
|
|
|
41
43
|
try:
|
|
42
44
|
return ok(
|
|
43
45
|
datastore().view.search(
|
|
44
|
-
f"type:global OR owner:({user['uname']} OR none)
|
|
46
|
+
f"type:global OR owner:({user['uname']} OR none) OR admins:{user['uname']} OR members:{user['uname']}",
|
|
47
|
+
as_obj=False,
|
|
48
|
+
rows=1000,
|
|
49
|
+
sort="title asc",
|
|
45
50
|
)["items"]
|
|
46
51
|
)
|
|
47
52
|
except ValueError as e:
|
|
@@ -52,7 +57,7 @@ def get_views(user: User, **kwargs):
|
|
|
52
57
|
@view_api.route("/", methods=["POST"])
|
|
53
58
|
@api_login(required_priv=["R", "W"])
|
|
54
59
|
@parse_parameters(refresh=parse_refresh)
|
|
55
|
-
def create_view(**kwargs):
|
|
60
|
+
def create_view(user: User, **kwargs):
|
|
56
61
|
"""Create a new view
|
|
57
62
|
|
|
58
63
|
Variables:
|
|
@@ -95,12 +100,12 @@ def create_view(**kwargs):
|
|
|
95
100
|
# Make sure the query is valid
|
|
96
101
|
storage.hit.search(view_data["query"])
|
|
97
102
|
|
|
98
|
-
|
|
103
|
+
view_data["owner"] = user.uname
|
|
99
104
|
|
|
100
|
-
view
|
|
105
|
+
view = View(view_data)
|
|
101
106
|
|
|
102
107
|
if view.type == "personal":
|
|
103
|
-
current_user = storage.user.get_if_exists(
|
|
108
|
+
current_user = storage.user.get_if_exists(user.uname)
|
|
104
109
|
|
|
105
110
|
current_user.favourite_views.append(view.view_id)
|
|
106
111
|
|
|
@@ -145,7 +150,7 @@ def delete_view(view_id: str, user: User, **kwargs):
|
|
|
145
150
|
return not_found(err="This view does not exist")
|
|
146
151
|
|
|
147
152
|
if existing_view.owner != user.uname and "admin" not in user.type:
|
|
148
|
-
return forbidden(err="You cannot delete a view unless you are an
|
|
153
|
+
return forbidden(err="You cannot delete a view unless you are an owner or a global admin.")
|
|
149
154
|
|
|
150
155
|
if existing_view.type == "readonly":
|
|
151
156
|
return forbidden(err="You cannot delete built-in views.")
|
|
@@ -159,7 +164,7 @@ def delete_view(view_id: str, user: User, **kwargs):
|
|
|
159
164
|
@view_api.route("/<view_id>", methods=["PUT"])
|
|
160
165
|
@api_login(required_priv=["R", "W"])
|
|
161
166
|
@parse_parameters(refresh=parse_refresh)
|
|
162
|
-
def update_view(view_id: str, user: User, **kwargs):
|
|
167
|
+
def update_view(view_id: str, user: User, **kwargs): # noqa: C901
|
|
163
168
|
"""Update a view
|
|
164
169
|
|
|
165
170
|
Variables:
|
|
@@ -188,8 +193,11 @@ def update_view(view_id: str, user: User, **kwargs):
|
|
|
188
193
|
if not isinstance(new_data, dict):
|
|
189
194
|
return bad_request(err="Invalid data format")
|
|
190
195
|
|
|
191
|
-
if
|
|
192
|
-
return bad_request(err="You cannot change the
|
|
196
|
+
if "view_id" in new_data:
|
|
197
|
+
return bad_request(err="You cannot change the view ID.")
|
|
198
|
+
|
|
199
|
+
if set(new_data) & {"owner", "admins", "members"}:
|
|
200
|
+
return bad_request(err="You cannot change permissions using this endpoint.")
|
|
193
201
|
|
|
194
202
|
existing_view: View = storage.view.get_if_exists(view_id)
|
|
195
203
|
if not existing_view:
|
|
@@ -198,27 +206,23 @@ def update_view(view_id: str, user: User, **kwargs):
|
|
|
198
206
|
if existing_view.type == "readonly":
|
|
199
207
|
return forbidden(err="You cannot edit a built-in view.")
|
|
200
208
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if existing_view.type == "global" and existing_view.owner != user.uname and "admin" not in user.type:
|
|
205
|
-
return forbidden(err="Only the owner of a view and administrators can edit a global view.")
|
|
206
|
-
|
|
207
|
-
new_view = View(cast(dict, merge({}, existing_view.as_primitives(), new_data)))
|
|
208
|
-
|
|
209
|
-
storage.view.save(new_view.view_id, new_view, refresh=refresh)
|
|
209
|
+
is_view_admin = user.uname == existing_view.owner or user.uname in existing_view.admins
|
|
210
|
+
if not is_view_admin and "admin" not in user.type:
|
|
211
|
+
return forbidden(err="You cannot update a view that is not owned by you, or you are not an administrator of.")
|
|
210
212
|
|
|
211
213
|
try:
|
|
212
214
|
if "query" in new_data:
|
|
213
|
-
# Make sure the query is valid
|
|
214
215
|
storage.hit.search(new_data["query"])
|
|
215
|
-
|
|
216
|
-
return ok(storage.view.get_if_exists(existing_view.view_id, as_obj=False))
|
|
217
216
|
except SearchException:
|
|
218
217
|
return bad_request(err="You must use a valid query when updating a view.")
|
|
219
218
|
except HowlerException as e:
|
|
220
219
|
return bad_request(err=str(e))
|
|
221
220
|
|
|
221
|
+
new_view = View(cast(dict, merge({}, existing_view.as_primitives(), new_data)))
|
|
222
|
+
storage.view.save(new_view.view_id, new_view, refresh=refresh)
|
|
223
|
+
|
|
224
|
+
return ok(new_view.as_primitives())
|
|
225
|
+
|
|
222
226
|
|
|
223
227
|
@generate_swagger_docs()
|
|
224
228
|
@view_api.route("/<view_id>/favourite", methods=["POST"])
|
|
@@ -246,10 +250,12 @@ def set_as_favourite(view_id: str, **kwargs):
|
|
|
246
250
|
if not existing_view:
|
|
247
251
|
return not_found(err="This view does not exist")
|
|
248
252
|
|
|
249
|
-
if
|
|
250
|
-
existing_view.
|
|
253
|
+
if (
|
|
254
|
+
existing_view.type == "personal"
|
|
255
|
+
and kwargs["user"]["uname"] not in [existing_view.owner, *existing_view.admins, *existing_view.members]
|
|
256
|
+
and existing_view.owner != "none"
|
|
251
257
|
):
|
|
252
|
-
return forbidden(err="You can only favourite global views, or views
|
|
258
|
+
return forbidden(err="You can only favourite global views, or views you have permission to use.")
|
|
253
259
|
|
|
254
260
|
try:
|
|
255
261
|
current_user = storage.user.get_if_exists(kwargs["user"]["uname"])
|
|
@@ -2,11 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
+
import math
|
|
5
6
|
import re
|
|
6
7
|
import sys
|
|
7
8
|
import time
|
|
8
9
|
import typing
|
|
9
10
|
import warnings
|
|
11
|
+
from collections.abc import Mapping as ABCMapping
|
|
10
12
|
from copy import deepcopy
|
|
11
13
|
from datetime import datetime
|
|
12
14
|
from os import environ
|
|
@@ -170,6 +172,9 @@ class ESCollection(Generic[ModelType]):
|
|
|
170
172
|
RETRY_NONE = 0
|
|
171
173
|
RETRY_INFINITY = -1
|
|
172
174
|
SCROLL_TIMEOUT = "5m"
|
|
175
|
+
TASK_POLL_TIMEOUT = 3600.0
|
|
176
|
+
TASK_POLL_INTERVAL = 0.5
|
|
177
|
+
MAX_VERSION_CONFLICT_ATTEMPTS = 3
|
|
173
178
|
UPDATE_SET = "SET"
|
|
174
179
|
UPDATE_INC = "INC"
|
|
175
180
|
UPDATE_DEC = "DEC"
|
|
@@ -458,37 +463,138 @@ class ESCollection(Generic[ModelType]):
|
|
|
458
463
|
else:
|
|
459
464
|
raise
|
|
460
465
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
466
|
+
@staticmethod
|
|
467
|
+
def _task_response_mapping(response, description: str) -> ABCMapping:
|
|
468
|
+
"""Return an Elasticsearch response body as a mapping."""
|
|
469
|
+
if isinstance(response, ABCMapping):
|
|
470
|
+
return response
|
|
471
|
+
|
|
472
|
+
body = getattr(response, "body", None)
|
|
473
|
+
if isinstance(body, ABCMapping):
|
|
474
|
+
return body
|
|
475
|
+
|
|
476
|
+
raise DataStoreException(f"Elasticsearch {description} returned a malformed response.")
|
|
477
|
+
|
|
478
|
+
@staticmethod
|
|
479
|
+
def _task_duration(name: str, default: float) -> float:
|
|
480
|
+
value = environ.get(name, str(default))
|
|
481
|
+
try:
|
|
482
|
+
duration = float(value)
|
|
483
|
+
except ValueError as error:
|
|
484
|
+
raise DataStoreException(f"{name} must be a non-negative number, got {value!r}") from error
|
|
485
|
+
if not math.isfinite(duration) or duration < 0:
|
|
486
|
+
raise DataStoreException(f"{name} must be a non-negative number, got {value!r}")
|
|
487
|
+
return duration
|
|
488
|
+
|
|
489
|
+
def _get_task_results(self: Self, task, strict=True):
|
|
490
|
+
"""Wait for an asynchronous Elasticsearch task to return a final response."""
|
|
491
|
+
task_response = self._task_response_mapping(task, "task submission")
|
|
492
|
+
if not isinstance(task_response.get("task"), str) or not task_response["task"]:
|
|
493
|
+
raise DataStoreException("Elasticsearch did not return a valid task identifier.")
|
|
494
|
+
|
|
495
|
+
task_id = task_response["task"]
|
|
496
|
+
poll_timeout = self._task_duration("HWL_DATASTORE_TASK_POLL_TIMEOUT", self.TASK_POLL_TIMEOUT)
|
|
497
|
+
poll_interval = self._task_duration("HWL_DATASTORE_TASK_POLL_INTERVAL", self.TASK_POLL_INTERVAL)
|
|
498
|
+
deadline = time.monotonic() + poll_timeout
|
|
499
|
+
while True:
|
|
500
|
+
if time.monotonic() >= deadline:
|
|
501
|
+
raise HowlerRuntimeError(f"Timed out waiting for Elasticsearch task {task_id} to complete.")
|
|
502
|
+
|
|
503
|
+
result = self._task_response_mapping(
|
|
504
|
+
self.with_retries(
|
|
473
505
|
self.datastore.client.tasks.get,
|
|
474
|
-
task_id=
|
|
475
|
-
wait_for_completion=
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
506
|
+
task_id=task_id,
|
|
507
|
+
wait_for_completion=False,
|
|
508
|
+
),
|
|
509
|
+
f"task {task_id}",
|
|
510
|
+
)
|
|
511
|
+
if "error" in result and result["error"] is not None:
|
|
512
|
+
raise DataStoreException(f"Elasticsearch task {task_id} failed: {result['error']!r}")
|
|
513
|
+
if result.get("completed") is False:
|
|
514
|
+
remaining = deadline - time.monotonic()
|
|
515
|
+
if remaining <= 0:
|
|
516
|
+
raise HowlerRuntimeError(f"Timed out waiting for Elasticsearch task {task_id} to complete.")
|
|
517
|
+
time.sleep(min(poll_interval, remaining))
|
|
518
|
+
continue
|
|
519
|
+
if result.get("completed") is not True:
|
|
520
|
+
raise DataStoreException(f"Elasticsearch task {task_id} did not report a completion state.")
|
|
521
|
+
|
|
522
|
+
response = result.get("response")
|
|
523
|
+
if not isinstance(response, ABCMapping):
|
|
524
|
+
raise DataStoreException(f"Elasticsearch task {task_id} completed without a valid response.")
|
|
525
|
+
response = dict(response)
|
|
526
|
+
if strict:
|
|
527
|
+
if response.get("timed_out") is not False:
|
|
528
|
+
raise DataStoreException(f"Elasticsearch task {task_id} returned a timeout result.")
|
|
529
|
+
failures = response.get("failures")
|
|
530
|
+
if not isinstance(failures, list):
|
|
531
|
+
raise DataStoreException(f"Elasticsearch task {task_id} returned an invalid failures list.")
|
|
532
|
+
if failures:
|
|
533
|
+
raise DataStoreException(f"Elasticsearch task {task_id} reported failures: {failures!r}")
|
|
534
|
+
return response
|
|
535
|
+
|
|
536
|
+
@staticmethod
|
|
537
|
+
def _normalize_query_refresh(refresh):
|
|
538
|
+
if refresh is None or isinstance(refresh, bool):
|
|
539
|
+
return refresh
|
|
540
|
+
if isinstance(refresh, str):
|
|
541
|
+
normalized = refresh.lower()
|
|
542
|
+
if normalized == "true":
|
|
543
|
+
return True
|
|
544
|
+
if normalized == "false":
|
|
545
|
+
return False
|
|
546
|
+
if normalized == "wait_for":
|
|
547
|
+
return True
|
|
548
|
+
raise DataStoreException(f"Invalid refresh value for an Elasticsearch query operation: {refresh!r}")
|
|
488
549
|
|
|
489
|
-
|
|
550
|
+
@staticmethod
|
|
551
|
+
def _validate_async_query_result(result, count_field: str) -> dict[str, Any]:
|
|
552
|
+
result = ESCollection._task_response_mapping(result, "query task")
|
|
553
|
+
if result.get("error") is not None:
|
|
554
|
+
raise DataStoreException(f"Elasticsearch query task failed: {result['error']!r}")
|
|
555
|
+
if result.get("timed_out") is not False:
|
|
556
|
+
if result.get("timed_out") is True:
|
|
557
|
+
raise DataStoreException("Elasticsearch query task timed out.")
|
|
558
|
+
raise DataStoreException("Elasticsearch query task did not report whether it timed out.")
|
|
559
|
+
|
|
560
|
+
failures = result.get("failures")
|
|
561
|
+
if not isinstance(failures, list):
|
|
562
|
+
raise DataStoreException("Elasticsearch query task did not return a valid failures list.")
|
|
563
|
+
if failures:
|
|
564
|
+
raise DataStoreException(f"Elasticsearch query task reported failures: {failures!r}")
|
|
565
|
+
|
|
566
|
+
validated_result = dict(result)
|
|
567
|
+
for field_name in (count_field, "version_conflicts"):
|
|
568
|
+
value = validated_result.get(field_name)
|
|
569
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
570
|
+
raise DataStoreException(f"Elasticsearch query task returned an invalid {field_name} value.")
|
|
571
|
+
|
|
572
|
+
return validated_result
|
|
573
|
+
|
|
574
|
+
def _run_async_query(self, operation, operation_name: str, count_field: str, **kwargs):
|
|
575
|
+
refresh = self._normalize_query_refresh(kwargs.pop("refresh", None))
|
|
576
|
+
if self.MAX_VERSION_CONFLICT_ATTEMPTS < 1:
|
|
577
|
+
raise DataStoreException("MAX_VERSION_CONFLICT_ATTEMPTS must be at least one.")
|
|
578
|
+
|
|
579
|
+
affected_documents = 0
|
|
580
|
+
for _ in range(self.MAX_VERSION_CONFLICT_ATTEMPTS):
|
|
581
|
+
task = self.with_retries(
|
|
582
|
+
operation,
|
|
583
|
+
**kwargs,
|
|
584
|
+
wait_for_completion=False,
|
|
585
|
+
conflicts="proceed",
|
|
586
|
+
refresh=refresh,
|
|
587
|
+
)
|
|
588
|
+
result = self._validate_async_query_result(self._get_task_results(task, strict=True), count_field)
|
|
589
|
+
affected_documents += result[count_field]
|
|
590
|
+
if result["version_conflicts"] == 0:
|
|
591
|
+
result[count_field] = affected_documents
|
|
592
|
+
return result
|
|
490
593
|
|
|
491
|
-
|
|
594
|
+
raise DataStoreException(
|
|
595
|
+
f"Elasticsearch {operation_name} did not complete after "
|
|
596
|
+
f"{self.MAX_VERSION_CONFLICT_ATTEMPTS} version-conflict attempt(s)."
|
|
597
|
+
)
|
|
492
598
|
|
|
493
599
|
def _get_current_alias(self: Self, index: str) -> typing.Optional[str]:
|
|
494
600
|
if self.with_retries(self.datastore.client.indices.exists_alias, name=index):
|
|
@@ -524,46 +630,28 @@ class ESCollection(Generic[ModelType]):
|
|
|
524
630
|
self._wait_for_status(target, min_status=min_status)
|
|
525
631
|
|
|
526
632
|
def _delete_async(self: Self, index, query, max_docs=None, sort=None, refresh=None):
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
refresh=refresh,
|
|
538
|
-
)
|
|
539
|
-
res = self._get_task_results(task)
|
|
540
|
-
|
|
541
|
-
if res["version_conflicts"] == 0:
|
|
542
|
-
res["deleted"] += deleted
|
|
543
|
-
return res
|
|
544
|
-
else:
|
|
545
|
-
deleted += res["deleted"]
|
|
633
|
+
return self._run_async_query(
|
|
634
|
+
self.datastore.client.delete_by_query,
|
|
635
|
+
"delete-by-query",
|
|
636
|
+
"deleted",
|
|
637
|
+
index=index,
|
|
638
|
+
query=query,
|
|
639
|
+
sort=sort,
|
|
640
|
+
max_docs=max_docs,
|
|
641
|
+
refresh=refresh,
|
|
642
|
+
)
|
|
546
643
|
|
|
547
644
|
def _update_async(self: Self, index, script, query, max_docs=None, refresh=None):
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
refresh=refresh,
|
|
559
|
-
)
|
|
560
|
-
res = self._get_task_results(task)
|
|
561
|
-
|
|
562
|
-
if res["version_conflicts"] == 0:
|
|
563
|
-
res["updated"] += updated
|
|
564
|
-
return res
|
|
565
|
-
else:
|
|
566
|
-
updated += res["updated"]
|
|
645
|
+
return self._run_async_query(
|
|
646
|
+
self.datastore.client.update_by_query,
|
|
647
|
+
"update-by-query",
|
|
648
|
+
"updated",
|
|
649
|
+
index=index,
|
|
650
|
+
script=script,
|
|
651
|
+
query=query,
|
|
652
|
+
max_docs=max_docs,
|
|
653
|
+
refresh=refresh,
|
|
654
|
+
)
|
|
567
655
|
|
|
568
656
|
def bulk(self: Self, operations: ElasticBulkPlan, refresh: str | None = None):
|
|
569
657
|
"""
|
|
@@ -859,7 +947,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
859
947
|
wait_for_completion=False,
|
|
860
948
|
)
|
|
861
949
|
logger.warning("Reindex taskId: %s", r_task["task"])
|
|
862
|
-
reindex_result = self._get_task_results(r_task)
|
|
950
|
+
reindex_result = self._get_task_results(r_task, strict=False)
|
|
863
951
|
|
|
864
952
|
# Validate the reindex did not silently drop or conflict on documents before
|
|
865
953
|
# we commit to deleting the source index further down.
|
|
@@ -1447,15 +1535,28 @@ class ESCollection(Generic[ModelType]):
|
|
|
1447
1535
|
|
|
1448
1536
|
return True
|
|
1449
1537
|
|
|
1450
|
-
def delete(self: Self, key, refresh=None):
|
|
1538
|
+
def delete(self: Self, key: str, refresh=None, version=None):
|
|
1451
1539
|
"""This function should delete the underlying document referenced by the key.
|
|
1452
1540
|
It should return true if the document was in fact properly deleted.
|
|
1453
1541
|
|
|
1454
1542
|
:param key: id of the document to delete
|
|
1543
|
+
:param version: version token returned by ``get_if_exists(..., version=True)`` for an atomic delete
|
|
1455
1544
|
:return: True is delete successful
|
|
1456
1545
|
"""
|
|
1546
|
+
index = self.name
|
|
1547
|
+
kwargs = {"id": key, "index": index, "refresh": refresh}
|
|
1548
|
+
if version:
|
|
1549
|
+
if version == CREATE_TOKEN:
|
|
1550
|
+
raise DataStoreException("Cannot delete a document using the create version token.")
|
|
1551
|
+
index, seq_no, primary_term = self._get_version_write_target(version)
|
|
1552
|
+
kwargs.update({"index": index, "if_seq_no": seq_no, "if_primary_term": primary_term})
|
|
1553
|
+
|
|
1457
1554
|
try:
|
|
1458
|
-
info = self.with_retries(
|
|
1555
|
+
info = self.with_retries(
|
|
1556
|
+
self.datastore.client.delete,
|
|
1557
|
+
raise_conflicts=bool(version),
|
|
1558
|
+
**kwargs,
|
|
1559
|
+
)
|
|
1459
1560
|
return info["result"] == "deleted"
|
|
1460
1561
|
except elasticsearch.NotFoundError:
|
|
1461
1562
|
return False
|
|
@@ -1802,6 +1903,9 @@ class ESCollection(Generic[ModelType]):
|
|
|
1802
1903
|
if isinstance(fields, str):
|
|
1803
1904
|
fields = fields.split(",")
|
|
1804
1905
|
|
|
1906
|
+
if source_data is None:
|
|
1907
|
+
source_data = {}
|
|
1908
|
+
|
|
1805
1909
|
if source_data is not None and (fields is None or "*" in fields or "id" in fields):
|
|
1806
1910
|
source_data["id"] = [item_id]
|
|
1807
1911
|
|
|
@@ -12,6 +12,7 @@ from howler.odm.models.config import ILMIndexConfig
|
|
|
12
12
|
from howler.odm.models.dossier import Dossier
|
|
13
13
|
from howler.odm.models.event import Event
|
|
14
14
|
from howler.odm.models.hit import Hit
|
|
15
|
+
from howler.odm.models.migration import MigrationRecord
|
|
15
16
|
from howler.odm.models.overview import Overview
|
|
16
17
|
from howler.odm.models.template import Template
|
|
17
18
|
from howler.odm.models.user import User
|
|
@@ -29,6 +30,7 @@ INDEXES = {
|
|
|
29
30
|
"overview": Overview,
|
|
30
31
|
"analytic": Analytic,
|
|
31
32
|
"action": Action,
|
|
33
|
+
"migration": MigrationRecord,
|
|
32
34
|
"user": User,
|
|
33
35
|
"view": View,
|
|
34
36
|
"dossier": Dossier,
|
|
@@ -119,6 +121,10 @@ class HowlerDatastore(object):
|
|
|
119
121
|
def action(self) -> ESCollection[Action]:
|
|
120
122
|
return self.ds.action
|
|
121
123
|
|
|
124
|
+
@property
|
|
125
|
+
def migration(self) -> ESCollection[MigrationRecord]:
|
|
126
|
+
return self.ds.migration
|
|
127
|
+
|
|
122
128
|
@property
|
|
123
129
|
def user(self) -> ESCollection[User]:
|
|
124
130
|
return self.ds.user
|