howler-api 3.3.0.dev767__tar.gz → 3.3.0.dev773__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-3.3.0.dev767 → howler_api-3.3.0.dev773}/PKG-INFO +4 -4
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/user.py +2 -4
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/classification.py +162 -82
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/config.py +1 -1
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/azure.py +7 -2
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/oauth.py +40 -16
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/search.py +1 -1
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/config.py +1 -1
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/hit.py +7 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/user.py +1 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/random_data.py +14 -1
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/randomizer.py +6 -4
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/jwt_service.py +2 -2
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/user_service.py +38 -14
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/pyproject.toml +7 -7
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/README.md +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/add_label.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/change_field.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/demote.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/example_plugin.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/prioritization.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/promote.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/remove_label.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/transition.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/base.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/socket.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/action.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/analytic.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/auth.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/clue.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/configs.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/dossier.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/help.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/hit.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/notebook.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/overview.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/search.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/template.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/tool.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/view.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/app.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/README.md +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/classification.yml +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/exceptions.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/loader.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/logging/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/logging/audit.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/logging/format.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/net.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/net_static.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/random_user.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/swagger.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/cronjobs/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/cronjobs/retention.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/cronjobs/rules.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/README.md +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/bulk.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/collection.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/constants.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/exceptions.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/howler_store.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/operations.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/schemas.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/store.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/support/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/support/build.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/support/schemas.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/types.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/error.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/README.md +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/generate_mitre.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/generate_tlds.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/reindex_data.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/wipe_databases.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/gunicorn_config.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/healthz.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/discover.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/hit.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/workflow.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/ws.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/README.md +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/base.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/charter.txt +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/helper.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/howler_enum.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/action.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/analytic.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/aws.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/azure.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/cbs.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/clue.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/dossier.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/gcp.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/howler_data.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/lead.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/localized_label.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/overview.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/pivot.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/template.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/view.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/patched.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/plugins/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/plugins/config.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/README.md +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/events.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/set.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/security/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/security/socket.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/security/utils.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/action_service.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/analytic_service.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/auth_service.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/config_service.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/dossier_service.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/event_service.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/hit_service.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/lucene_service.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/notebook_service.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/overview_service.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/template_service.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/telemetry.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/__init__.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/annotations.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/chunk.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/constants.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/dict_utils.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/isotime.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/list_utils.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/lucene.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/path.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/socket_utils.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/str_utils.py +0 -0
- {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/uid.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: howler-api
|
|
3
|
-
Version: 3.3.0.
|
|
3
|
+
Version: 3.3.0.dev773
|
|
4
4
|
Summary: Howler - API server
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
|
|
@@ -38,7 +38,7 @@ Requires-Dist: opentelemetry-instrumentation-flask (==0.61b0)
|
|
|
38
38
|
Requires-Dist: opentelemetry-sdk (==1.40.0)
|
|
39
39
|
Requires-Dist: packaging (<25.0)
|
|
40
40
|
Requires-Dist: passlib (==1.7.4)
|
|
41
|
-
Requires-Dist: prometheus-client (==0.
|
|
41
|
+
Requires-Dist: prometheus-client (==0.25.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)
|
|
@@ -51,10 +51,10 @@ Requires-Dist: python-dotenv (>=1.1.0,<2.0.0)
|
|
|
51
51
|
Requires-Dist: pytz (>=2025.2,<2026.0)
|
|
52
52
|
Requires-Dist: pyyaml (==6.0.3)
|
|
53
53
|
Requires-Dist: redis (==4.6.0)
|
|
54
|
-
Requires-Dist: requests (==2.33.
|
|
54
|
+
Requires-Dist: requests (==2.33.1)
|
|
55
55
|
Requires-Dist: tzdata (>=2026.1,<2027.0)
|
|
56
56
|
Requires-Dist: validators (>=0.34,<0.36)
|
|
57
|
-
Requires-Dist: wsproto (==1.2
|
|
57
|
+
Requires-Dist: wsproto (==1.3.2)
|
|
58
58
|
Project-URL: Documentation, https://cybercentrecanada.github.io/howler/developer/backend/
|
|
59
59
|
Project-URL: Homepage, https://cybercentrecanada.github.io/howler/
|
|
60
60
|
Project-URL: Repository, https://github.com/CybercentreCanada/howler-api
|
|
@@ -128,9 +128,7 @@ def add_user_account(username, **_):
|
|
|
128
128
|
data["name"] = data["uname"]
|
|
129
129
|
|
|
130
130
|
# Add dynamic classification group
|
|
131
|
-
data["classification"] = user_service.get_dynamic_classification(
|
|
132
|
-
cast(str | None, data.get("classification", None)), data["email"]
|
|
133
|
-
)
|
|
131
|
+
data["classification"] = user_service.get_dynamic_classification(data)
|
|
134
132
|
|
|
135
133
|
# Clear non user account data
|
|
136
134
|
avatar = data.pop("avatar", None)
|
|
@@ -290,7 +288,7 @@ def set_user_account(username: str, **kwargs): # noqa: C901
|
|
|
290
288
|
data.pop("new_pass_confirm", None)
|
|
291
289
|
|
|
292
290
|
# Apply dynamic classification
|
|
293
|
-
data["classification"] = user_service.get_dynamic_classification(data
|
|
291
|
+
data["classification"] = user_service.get_dynamic_classification(data)
|
|
294
292
|
|
|
295
293
|
ret_val = user_service.save_user_account(username, data, kwargs["user"])
|
|
296
294
|
return ok({"success": ret_val})
|
|
@@ -38,7 +38,7 @@ class Classification(object):
|
|
|
38
38
|
"description",
|
|
39
39
|
]
|
|
40
40
|
self.original_definition = classification_definition
|
|
41
|
-
self.levels_map: dict[str,
|
|
41
|
+
self.levels_map: dict[str, int] = {}
|
|
42
42
|
self.levels_map_stl = {}
|
|
43
43
|
self.levels_map_lts = {}
|
|
44
44
|
self.levels_styles_map = {}
|
|
@@ -64,6 +64,9 @@ class Classification(object):
|
|
|
64
64
|
|
|
65
65
|
self.enforce = False
|
|
66
66
|
self.dynamic_groups = False
|
|
67
|
+
# dynamic group type is one of: email | group | all
|
|
68
|
+
# defaults to email for original behavior
|
|
69
|
+
self.dynamic_groups_type = "email"
|
|
67
70
|
|
|
68
71
|
# Add Invalid classification
|
|
69
72
|
self.levels_map["INV"] = self.INVALID_LVL
|
|
@@ -84,12 +87,10 @@ class Classification(object):
|
|
|
84
87
|
raise HowlerKeyError("Enforce not set!")
|
|
85
88
|
|
|
86
89
|
self.dynamic_groups = classification_definition.get("dynamic_groups", None)
|
|
87
|
-
if self.
|
|
90
|
+
if self.dynamic_groups is None:
|
|
88
91
|
raise HowlerKeyError("Dynamic groups not set!")
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
self._classification_cache = self.list_all_classification_combinations()
|
|
92
|
-
self._classification_cache_short = self.list_all_classification_combinations(long_format=False)
|
|
93
|
+
self.dynamic_groups_type = classification_definition.get("dynamic_groups_type", self.dynamic_groups_type)
|
|
93
94
|
|
|
94
95
|
if classification_definition.get("levels", None) is None:
|
|
95
96
|
raise HowlerKeyError("No classification levels provided!")
|
|
@@ -186,6 +187,16 @@ class Classification(object):
|
|
|
186
187
|
self.description[short_name] = x.get("description", "N/A")
|
|
187
188
|
self.description[name] = self.description[short_name]
|
|
188
189
|
|
|
190
|
+
# Build the classification cache AFTER all maps are populated so
|
|
191
|
+
# that normalize_classification can fully validate each combination.
|
|
192
|
+
# Using normalized=True filters out invalid combos (e.g. subgroups
|
|
193
|
+
# that violate limited_to_group constraints).
|
|
194
|
+
if self.enforce:
|
|
195
|
+
self._classification_cache = self.list_all_classification_combinations(normalized=True)
|
|
196
|
+
self._classification_cache_short = self.list_all_classification_combinations(
|
|
197
|
+
long_format=False, normalized=True
|
|
198
|
+
)
|
|
199
|
+
|
|
189
200
|
if not self.is_valid(classification_definition["unrestricted"]):
|
|
190
201
|
raise InvalidDefinition("Classification definition's unrestricted classification is invalid.")
|
|
191
202
|
|
|
@@ -238,40 +249,46 @@ class Classification(object):
|
|
|
238
249
|
|
|
239
250
|
return items
|
|
240
251
|
|
|
241
|
-
def _get_c12n_level_index(self, c12n: str) -> str:
|
|
252
|
+
def _get_c12n_level_index(self, c12n: str) -> tuple[int, str]:
|
|
242
253
|
# Parse classifications in uppercase mode only
|
|
243
254
|
c12n = c12n.upper()
|
|
244
255
|
|
|
245
|
-
lvl = c12n.
|
|
256
|
+
lvl, _, remain = c12n.partition("//")
|
|
246
257
|
if lvl in self.levels_map:
|
|
247
|
-
return self.levels_map[lvl]
|
|
258
|
+
return self.levels_map[lvl], remain
|
|
248
259
|
elif lvl in self.levels_map_lts:
|
|
249
|
-
return self.levels_map[self.levels_map_lts[lvl]]
|
|
260
|
+
return self.levels_map[self.levels_map_lts[lvl]], remain
|
|
250
261
|
elif lvl in self.levels_aliases:
|
|
251
|
-
return self.levels_map[self.levels_aliases[lvl]]
|
|
262
|
+
return self.levels_map[self.levels_aliases[lvl]], remain
|
|
252
263
|
else:
|
|
253
264
|
raise InvalidClassification(
|
|
254
265
|
"Classification level '%s' was not found in your classification definition." % lvl
|
|
255
266
|
)
|
|
256
267
|
|
|
257
268
|
def _get_c12n_level_text(self, lvl_idx: int, long_format: bool = True) -> str:
|
|
258
|
-
text = self.levels_map.get(str(lvl_idx), None)
|
|
269
|
+
text: Any = self.levels_map.get(str(lvl_idx), None)
|
|
270
|
+
|
|
259
271
|
if not text:
|
|
260
272
|
raise InvalidClassification(
|
|
261
273
|
"Classification level number '%s' was not found in your classification definition." % lvl_idx
|
|
262
274
|
)
|
|
275
|
+
|
|
263
276
|
if long_format:
|
|
264
277
|
return self.levels_map_stl[text]
|
|
278
|
+
|
|
265
279
|
return text
|
|
266
280
|
|
|
267
|
-
def _get_c12n_required(self, c12n: str, long_format: bool = True) -> List:
|
|
281
|
+
def _get_c12n_required(self, c12n: str, long_format: bool = True) -> tuple[List, list[str]]:
|
|
268
282
|
# Parse classifications in uppercase mode only
|
|
269
283
|
c12n = c12n.upper()
|
|
270
284
|
|
|
271
285
|
return_set = set()
|
|
272
286
|
part_set = set(c12n.split("/"))
|
|
287
|
+
unused = []
|
|
273
288
|
|
|
274
289
|
for p in part_set:
|
|
290
|
+
if not p:
|
|
291
|
+
continue
|
|
275
292
|
if p in self.access_req_map_lts:
|
|
276
293
|
return_set.add(self.access_req_map_lts[p])
|
|
277
294
|
elif p in self.access_req_map_stl:
|
|
@@ -279,27 +296,37 @@ class Classification(object):
|
|
|
279
296
|
elif p in self.access_req_aliases:
|
|
280
297
|
for a in self.access_req_aliases[p]:
|
|
281
298
|
return_set.add(a)
|
|
299
|
+
else:
|
|
300
|
+
unused.append(p)
|
|
282
301
|
|
|
283
302
|
if long_format:
|
|
284
|
-
return sorted([self.access_req_map_stl[r] for r in return_set])
|
|
285
|
-
return sorted(list(return_set))
|
|
303
|
+
return sorted([self.access_req_map_stl[r] for r in return_set]), unused
|
|
304
|
+
return sorted(list(return_set)), unused
|
|
286
305
|
|
|
287
|
-
def _get_c12n_groups(
|
|
306
|
+
def _get_c12n_groups(
|
|
307
|
+
self, c12n: list[str], long_format: bool = True, get_dynamic_groups: bool = True, auto_select: bool = False
|
|
308
|
+
) -> Tuple[List, List, list[str]]:
|
|
288
309
|
# Parse classifications in uppercase mode only
|
|
289
|
-
c12n = c12n.upper()
|
|
290
310
|
|
|
291
311
|
g1_set = set()
|
|
292
312
|
g2_set = set()
|
|
293
313
|
others = set()
|
|
294
314
|
|
|
295
|
-
grp_part = c12n.split("//")
|
|
296
315
|
groups = []
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
316
|
+
subgroups = []
|
|
317
|
+
|
|
318
|
+
for gp in c12n:
|
|
319
|
+
# If there is a rel marking we know we have groups
|
|
320
|
+
if gp.startswith("REL "):
|
|
321
|
+
gp = gp.replace("REL TO ", "")
|
|
322
|
+
gp = gp.replace("REL ", "")
|
|
323
|
+
temp_group = set([x.strip() for x in gp.split(",")])
|
|
324
|
+
for t in temp_group:
|
|
325
|
+
groups.extend(t.split("/"))
|
|
326
|
+
else:
|
|
327
|
+
# if there is not a rel marking we either have a subgroup or a solitary_display_name
|
|
328
|
+
# alias for a group, which we will filter out later
|
|
329
|
+
subgroups.append(gp)
|
|
303
330
|
|
|
304
331
|
for g in groups:
|
|
305
332
|
if g in self.groups_map_lts:
|
|
@@ -309,33 +336,63 @@ class Classification(object):
|
|
|
309
336
|
elif g in self.groups_aliases:
|
|
310
337
|
for a in self.groups_aliases[g]:
|
|
311
338
|
g1_set.add(a)
|
|
312
|
-
|
|
339
|
+
else:
|
|
340
|
+
others.add(g)
|
|
341
|
+
|
|
342
|
+
for g in subgroups:
|
|
343
|
+
if g in self.subgroups_map_lts:
|
|
313
344
|
g2_set.add(self.subgroups_map_lts[g])
|
|
314
345
|
elif g in self.subgroups_map_stl:
|
|
315
346
|
g2_set.add(g)
|
|
316
347
|
elif g in self.subgroups_aliases:
|
|
317
348
|
for a in self.subgroups_aliases[g]:
|
|
318
349
|
g2_set.add(a)
|
|
350
|
+
# Here is where we catch any solitary_display_name aliases for groups within the subgroup sections
|
|
351
|
+
elif g in self.groups_aliases:
|
|
352
|
+
# Check that this alias is actually a solitary name, don't
|
|
353
|
+
# let other aliases leak outside the REL marking
|
|
354
|
+
groups = self.groups_aliases[g]
|
|
355
|
+
if len(groups) > 1:
|
|
356
|
+
raise InvalidClassification(f"Unclear use of alias: {g}")
|
|
357
|
+
g1_set.add(groups[0])
|
|
319
358
|
else:
|
|
320
|
-
|
|
359
|
+
raise InvalidClassification(f"Unknown Subgroup: {g}")
|
|
360
|
+
|
|
361
|
+
# If dynamic groups are active all remaining parts should be groups found under a
|
|
362
|
+
# REL TO marking that we can merge in with the other groups
|
|
363
|
+
if self.dynamic_groups and get_dynamic_groups:
|
|
364
|
+
g1_set.update(others)
|
|
365
|
+
others = set()
|
|
366
|
+
|
|
367
|
+
# Check if there are any required group assignments
|
|
368
|
+
for subgroup in g2_set:
|
|
369
|
+
required = self.params_map.get(subgroup, {}).get("require_group", None)
|
|
370
|
+
if required:
|
|
371
|
+
g1_set.add(required)
|
|
372
|
+
|
|
373
|
+
# Check if there are any forbidden group assignments
|
|
374
|
+
for subgroup in g2_set:
|
|
375
|
+
limited_to_group = self.params_map.get(subgroup, {}).get("limited_to_group", None)
|
|
376
|
+
if limited_to_group is not None:
|
|
377
|
+
if len(g1_set) > 1 or (len(g1_set) == 1 and g1_set != set([limited_to_group])):
|
|
378
|
+
raise InvalidClassification(
|
|
379
|
+
f"Subgroup {subgroup} is limited to group {limited_to_group} (found: {', '.join(g1_set)})"
|
|
380
|
+
)
|
|
321
381
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
and o not in self.access_req_aliases
|
|
328
|
-
and o not in self.levels_map
|
|
329
|
-
and o not in self.levels_map_lts
|
|
330
|
-
and o not in self.levels_aliases
|
|
331
|
-
):
|
|
332
|
-
g1_set.add(o)
|
|
382
|
+
# Do auto select
|
|
383
|
+
if auto_select and g1_set:
|
|
384
|
+
g1_set.update(self.groups_auto_select_short)
|
|
385
|
+
if auto_select and g2_set:
|
|
386
|
+
g2_set.update(self.subgroups_auto_select_short)
|
|
333
387
|
|
|
388
|
+
# Swap to long format if required
|
|
334
389
|
if long_format:
|
|
335
|
-
return
|
|
336
|
-
[self.
|
|
390
|
+
return (
|
|
391
|
+
sorted([self.groups_map_stl.get(r, r) for r in g1_set]),
|
|
392
|
+
sorted([self.subgroups_map_stl[r] for r in g2_set]),
|
|
393
|
+
list(others),
|
|
337
394
|
)
|
|
338
|
-
return sorted(list(g1_set)), sorted(list(g2_set))
|
|
395
|
+
return sorted(list(g1_set)), sorted(list(g2_set)), list(others)
|
|
339
396
|
|
|
340
397
|
@staticmethod
|
|
341
398
|
def _can_see_required(user_req: List, req: List) -> bool:
|
|
@@ -446,11 +503,22 @@ class Classification(object):
|
|
|
446
503
|
return out
|
|
447
504
|
|
|
448
505
|
def _get_classification_parts(
|
|
449
|
-
self,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
506
|
+
self,
|
|
507
|
+
c12n: str,
|
|
508
|
+
long_format: bool = True,
|
|
509
|
+
get_dynamic_groups: bool = True,
|
|
510
|
+
auto_select: bool = False,
|
|
511
|
+
ignore_unused: bool = False,
|
|
512
|
+
) -> Tuple[int, list[str], list[str], list[str]]:
|
|
513
|
+
lvl_idx, unused = self._get_c12n_level_index(c12n)
|
|
514
|
+
req, unused_parts = self._get_c12n_required(unused, long_format=long_format)
|
|
515
|
+
|
|
516
|
+
groups, subgroups, unused_parts = self._get_c12n_groups(
|
|
517
|
+
unused_parts, long_format=long_format, get_dynamic_groups=get_dynamic_groups, auto_select=auto_select
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
if unused_parts and not ignore_unused:
|
|
521
|
+
raise InvalidClassification(f"Unparsable classification parts: {', '.join(unused_parts)}")
|
|
454
522
|
|
|
455
523
|
return lvl_idx, req, groups, subgroups
|
|
456
524
|
|
|
@@ -473,7 +541,7 @@ class Classification(object):
|
|
|
473
541
|
# Public functions
|
|
474
542
|
# ++++++++++++++++++++++++
|
|
475
543
|
# noinspection PyUnusedLocal
|
|
476
|
-
def list_all_classification_combinations(self, long_format: bool = True) -> Set:
|
|
544
|
+
def list_all_classification_combinations(self, long_format: bool = True, normalized: bool = False) -> Set:
|
|
477
545
|
combinations = set()
|
|
478
546
|
|
|
479
547
|
levels = self._list_items_and_aliases(self.original_definition["levels"], long_format=long_format)
|
|
@@ -529,7 +597,10 @@ class Classification(object):
|
|
|
529
597
|
|
|
530
598
|
temp_combinations = copy(combinations)
|
|
531
599
|
for p in itertools.product(temp_combinations, sgrp_cbs):
|
|
532
|
-
|
|
600
|
+
# A combo already has a group part when it contains "//REL TO " (explicit)
|
|
601
|
+
# or ends with a solitary display name like "//ANY" (after replacement).
|
|
602
|
+
has_group = "//REL TO " in p[0] or any(p[0].endswith(f"//{sol_name}") for sol_name in solitary_names)
|
|
603
|
+
if has_group:
|
|
533
604
|
cl = "/".join(p)
|
|
534
605
|
|
|
535
606
|
if cl.endswith("/"):
|
|
@@ -537,13 +608,25 @@ class Classification(object):
|
|
|
537
608
|
else:
|
|
538
609
|
combinations.add(cl)
|
|
539
610
|
else:
|
|
540
|
-
|
|
611
|
+
# No group present — subgroups are joined with "//" (not "//REL TO ")
|
|
612
|
+
# to match the format produced by _get_normalized_classification_text.
|
|
613
|
+
# "REL TO" is reserved for groups in _get_c12n_groups.
|
|
614
|
+
cl = "//".join(p)
|
|
541
615
|
|
|
542
|
-
if cl.endswith("//
|
|
543
|
-
combinations.add(cl[:-
|
|
616
|
+
if cl.endswith("//"):
|
|
617
|
+
combinations.add(cl[:-2])
|
|
544
618
|
else:
|
|
545
619
|
combinations.add(cl)
|
|
546
620
|
|
|
621
|
+
if normalized:
|
|
622
|
+
good = []
|
|
623
|
+
for x in combinations:
|
|
624
|
+
try:
|
|
625
|
+
good.append(self.normalize_classification(x, long_format=long_format))
|
|
626
|
+
except InvalidClassification:
|
|
627
|
+
pass
|
|
628
|
+
return set(good)
|
|
629
|
+
|
|
547
630
|
return combinations
|
|
548
631
|
|
|
549
632
|
# noinspection PyUnusedLocal
|
|
@@ -581,7 +664,8 @@ class Classification(object):
|
|
|
581
664
|
return out
|
|
582
665
|
|
|
583
666
|
def get_access_control_parts(self, c12n: str, user_classification: bool = False) -> Dict:
|
|
584
|
-
"""
|
|
667
|
+
"""
|
|
668
|
+
Returns a dictionary containing the different access parameters Lucene needs to build it's queries
|
|
585
669
|
|
|
586
670
|
Args:
|
|
587
671
|
c12n: The classification to get the parts from
|
|
@@ -591,12 +675,8 @@ class Classification(object):
|
|
|
591
675
|
c12n = self.UNRESTRICTED
|
|
592
676
|
|
|
593
677
|
try:
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
access_lvl = self._get_c12n_level_index(c12n)
|
|
598
|
-
access_req = self._get_c12n_required(c12n, long_format=False)
|
|
599
|
-
access_grp1, access_grp2 = self._get_c12n_groups(c12n, long_format=False)
|
|
678
|
+
parts = self._get_classification_parts(c12n, long_format=False, auto_select=not user_classification)
|
|
679
|
+
access_lvl, access_req, access_grp1, access_grp2 = parts
|
|
600
680
|
|
|
601
681
|
return {
|
|
602
682
|
"__access_lvl__": access_lvl,
|
|
@@ -679,7 +759,8 @@ class Classification(object):
|
|
|
679
759
|
)
|
|
680
760
|
|
|
681
761
|
def is_accessible(self, user_c12n: str, c12n: str, ignore_invalid: bool = False) -> bool:
|
|
682
|
-
"""
|
|
762
|
+
"""
|
|
763
|
+
Given a user classification, check if a user is allow to see a certain classification
|
|
683
764
|
|
|
684
765
|
Args:
|
|
685
766
|
user_c12n: Maximum classification for the user
|
|
@@ -698,16 +779,10 @@ class Classification(object):
|
|
|
698
779
|
return True
|
|
699
780
|
|
|
700
781
|
try:
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
c12n = self.normalize_classification(c12n, skip_auto_select=True)
|
|
782
|
+
user_lvl, user_req, user_groups, user_subgroups = self._get_classification_parts(user_c12n)
|
|
783
|
+
lvl, req, groups, subgroups = self._get_classification_parts(c12n)
|
|
704
784
|
|
|
705
|
-
|
|
706
|
-
user_groups, user_subgroups = self._get_c12n_groups(user_c12n)
|
|
707
|
-
req = self._get_c12n_required(c12n)
|
|
708
|
-
groups, subgroups = self._get_c12n_groups(c12n)
|
|
709
|
-
|
|
710
|
-
if self._get_c12n_level_index(user_c12n) >= self._get_c12n_level_index(c12n):
|
|
785
|
+
if int(user_lvl) >= int(lvl):
|
|
711
786
|
if not self._can_see_required(user_req, req):
|
|
712
787
|
return False
|
|
713
788
|
if not self._can_see_groups(user_groups, groups):
|
|
@@ -812,7 +887,7 @@ class Classification(object):
|
|
|
812
887
|
|
|
813
888
|
return True
|
|
814
889
|
|
|
815
|
-
def max_classification(self, c12n_1: str, c12n_2: str, long_format: bool = True) -> str:
|
|
890
|
+
def max_classification(self, c12n_1: str | None, c12n_2: str | None, long_format: bool = True) -> str:
|
|
816
891
|
"""Mixes to classification and returns to most restrictive form for them
|
|
817
892
|
|
|
818
893
|
Args:
|
|
@@ -833,9 +908,10 @@ class Classification(object):
|
|
|
833
908
|
c12n_2 = self.normalize_classification(c12n_2)
|
|
834
909
|
|
|
835
910
|
if c12n_1 is None:
|
|
836
|
-
return c12n_2
|
|
911
|
+
return c12n_2 if c12n_2 else self.UNRESTRICTED
|
|
912
|
+
|
|
837
913
|
if c12n_2 is None:
|
|
838
|
-
return c12n_1
|
|
914
|
+
return c12n_1 if c12n_1 else self.UNRESTRICTED
|
|
839
915
|
|
|
840
916
|
lvl_idx_1, req_1, groups_1, subgroups_1 = self._get_classification_parts(c12n_1, long_format=long_format)
|
|
841
917
|
lvl_idx_2, req_2, groups_2, subgroups_2 = self._get_classification_parts(c12n_2, long_format=long_format)
|
|
@@ -899,8 +975,16 @@ class Classification(object):
|
|
|
899
975
|
long_format=long_format, # type: ignore
|
|
900
976
|
)
|
|
901
977
|
|
|
902
|
-
def normalize_classification(
|
|
903
|
-
|
|
978
|
+
def normalize_classification(
|
|
979
|
+
self,
|
|
980
|
+
c12n: str,
|
|
981
|
+
long_format: bool = True,
|
|
982
|
+
skip_auto_select: bool = False,
|
|
983
|
+
get_dynamic_groups: bool = True,
|
|
984
|
+
ignore_unused: bool = False,
|
|
985
|
+
) -> str:
|
|
986
|
+
"""
|
|
987
|
+
Normalize a given classification by applying the rules defined in the classification definition.
|
|
904
988
|
This function will remove any invalid parts and add missing parts to the classification.
|
|
905
989
|
It will also ensure that the display of the classification is always done the same way
|
|
906
990
|
|
|
@@ -916,19 +1000,16 @@ class Classification(object):
|
|
|
916
1000
|
return self.UNRESTRICTED
|
|
917
1001
|
|
|
918
1002
|
# Has the classification has already been normalized before?
|
|
919
|
-
if long_format and c12n in self._classification_cache:
|
|
1003
|
+
if long_format and c12n in self._classification_cache and get_dynamic_groups:
|
|
920
1004
|
return c12n
|
|
921
|
-
if not long_format and c12n in self._classification_cache_short:
|
|
1005
|
+
if not long_format and c12n in self._classification_cache_short and get_dynamic_groups:
|
|
922
1006
|
return c12n
|
|
923
1007
|
|
|
924
|
-
lvl_idx, req, groups, subgroups = self._get_classification_parts(
|
|
1008
|
+
lvl_idx, req, groups, subgroups = self._get_classification_parts(
|
|
1009
|
+
c12n, long_format=long_format, get_dynamic_groups=get_dynamic_groups, ignore_unused=ignore_unused
|
|
1010
|
+
)
|
|
925
1011
|
new_c12n = self._get_normalized_classification_text(
|
|
926
|
-
lvl_idx,
|
|
927
|
-
req,
|
|
928
|
-
groups,
|
|
929
|
-
subgroups,
|
|
930
|
-
long_format=long_format,
|
|
931
|
-
skip_auto_select=skip_auto_select,
|
|
1012
|
+
lvl_idx, req, groups, subgroups, long_format=long_format, skip_auto_select=skip_auto_select
|
|
932
1013
|
)
|
|
933
1014
|
if long_format:
|
|
934
1015
|
self._classification_cache.add(new_c12n)
|
|
@@ -937,7 +1018,7 @@ class Classification(object):
|
|
|
937
1018
|
|
|
938
1019
|
return new_c12n
|
|
939
1020
|
|
|
940
|
-
def build_user_classification(self, c12n_1: str
|
|
1021
|
+
def build_user_classification(self, c12n_1: str, c12n_2: str, long_format: bool = True) -> str:
|
|
941
1022
|
"""Mixes to classification and return the classification marking that would give access to the most data
|
|
942
1023
|
|
|
943
1024
|
Args:
|
|
@@ -959,7 +1040,6 @@ class Classification(object):
|
|
|
959
1040
|
|
|
960
1041
|
if c12n_1 is None:
|
|
961
1042
|
return c12n_2
|
|
962
|
-
|
|
963
1043
|
if c12n_2 is None:
|
|
964
1044
|
return c12n_1
|
|
965
1045
|
|
|
@@ -10,7 +10,7 @@ from howler.remote.datatypes.user_quota_tracker import UserQuotaTracker
|
|
|
10
10
|
#################################################################
|
|
11
11
|
# Configuration
|
|
12
12
|
|
|
13
|
-
CLASSIFICATION = loader.get_classification()
|
|
13
|
+
CLASSIFICATION = loader.get_classification(os.getenv("HWL_CLASSIFICATION_PATH", None))
|
|
14
14
|
|
|
15
15
|
AUDIT = config.ui.audit
|
|
16
16
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import requests
|
|
2
2
|
|
|
3
|
-
from howler.common.exceptions import HowlerException
|
|
3
|
+
from howler.common.exceptions import HowlerAttributeError, HowlerException
|
|
4
4
|
from howler.common.logging import get_logger
|
|
5
5
|
from howler.config import config
|
|
6
6
|
from howler.utils.str_utils import default_string_value
|
|
@@ -20,7 +20,12 @@ def azure_obo(token: str) -> str:
|
|
|
20
20
|
Returns:
|
|
21
21
|
str: The new access token with updated privileges
|
|
22
22
|
"""
|
|
23
|
-
|
|
23
|
+
if "azure" in config.auth.oauth.providers:
|
|
24
|
+
azure_provider_config = config.auth.oauth.providers["azure"]
|
|
25
|
+
elif "entraid" in config.auth.oauth.providers:
|
|
26
|
+
azure_provider_config = config.auth.oauth.providers["entraid"]
|
|
27
|
+
else:
|
|
28
|
+
raise HowlerAttributeError("No azure/entraid-based provider configured!")
|
|
24
29
|
|
|
25
30
|
logger.debug("OBOing to MS Graph")
|
|
26
31
|
# Azure is a special case here, as we need to OBO to MS Graph
|
|
@@ -60,7 +60,7 @@ def parse_profile(profile: dict[str, Any], provider_config: OAuthProvider) -> di
|
|
|
60
60
|
uname = profile.get("uname", profile.get("preferred_username", email_adr))
|
|
61
61
|
|
|
62
62
|
# Did we default to email?
|
|
63
|
-
if
|
|
63
|
+
if uname is not None and email_adr is not None and uname.lower() == email_adr.lower():
|
|
64
64
|
# 1. Use provided regex matcher
|
|
65
65
|
if provider_config.uid_regex:
|
|
66
66
|
match = re.match(provider_config.uid_regex, uname)
|
|
@@ -93,6 +93,8 @@ def parse_profile(profile: dict[str, Any], provider_config: OAuthProvider) -> di
|
|
|
93
93
|
# Compute access, roles and classification using auto_properties
|
|
94
94
|
access = True
|
|
95
95
|
roles = ["user"]
|
|
96
|
+
groups: list[str] = profile.get("groups", [])
|
|
97
|
+
assignments = []
|
|
96
98
|
classification = CLASSIFICATION_ENGINE.UNRESTRICTED
|
|
97
99
|
if provider_config.auto_properties:
|
|
98
100
|
for auto_prop in provider_config.auto_properties:
|
|
@@ -129,18 +131,33 @@ def parse_profile(profile: dict[str, Any], provider_config: OAuthProvider) -> di
|
|
|
129
131
|
classification = CLASSIFICATION_ENGINE.build_user_classification(
|
|
130
132
|
classification, auto_prop.value
|
|
131
133
|
)
|
|
134
|
+
logger.debug("Classification: %s", classification)
|
|
132
135
|
break
|
|
133
136
|
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
# Append groups from matching patterns
|
|
138
|
+
elif auto_prop.type == "group":
|
|
139
|
+
if re.match(auto_prop.pattern, value):
|
|
140
|
+
groups.append(auto_prop.value)
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
# Append assignments from matching patterns
|
|
144
|
+
elif auto_prop.type == "assignment":
|
|
145
|
+
if re.match(auto_prop.pattern, value):
|
|
146
|
+
assignments.append(auto_prop.value)
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
# Infer roles from groups (legacy)
|
|
150
|
+
if groups and provider_config.role_map:
|
|
136
151
|
for user_type in USER_TYPES:
|
|
137
152
|
if (
|
|
138
153
|
user_type in provider_config.role_map
|
|
139
|
-
and provider_config.role_map[user_type] in
|
|
154
|
+
and provider_config.role_map[user_type] in groups
|
|
140
155
|
and user_type not in roles
|
|
141
156
|
):
|
|
142
157
|
roles.append(user_type)
|
|
143
158
|
|
|
159
|
+
# TODO: re-export assignments once they're actually used.
|
|
160
|
+
# This may need a refactor once the tags stuff is figured out
|
|
144
161
|
return dict(
|
|
145
162
|
access=access,
|
|
146
163
|
type=roles,
|
|
@@ -150,7 +167,7 @@ def parse_profile(profile: dict[str, Any], provider_config: OAuthProvider) -> di
|
|
|
150
167
|
email=email_adr,
|
|
151
168
|
password="__NO_PASSWORD__", # noqa: S106
|
|
152
169
|
avatar=profile.get("picture", provider_config.picture_url or alternate),
|
|
153
|
-
groups=
|
|
170
|
+
groups=groups,
|
|
154
171
|
)
|
|
155
172
|
|
|
156
173
|
|
|
@@ -166,9 +183,9 @@ def fetch_avatar( # noqa: C901
|
|
|
166
183
|
# Generic picture url endpoint, i.e. MS Graph
|
|
167
184
|
if url == provider_config.picture_url:
|
|
168
185
|
headers = {}
|
|
169
|
-
|
|
170
186
|
token: str | None = None
|
|
171
|
-
|
|
187
|
+
|
|
188
|
+
if oauth_provider in ["entraid", "azure"]:
|
|
172
189
|
if not access_token:
|
|
173
190
|
raise HowlerValueError("An azure access token is necessary to retrieve the profile picture") # noqa: TRY301
|
|
174
191
|
|
|
@@ -206,13 +223,13 @@ def fetch_avatar( # noqa: C901
|
|
|
206
223
|
return None
|
|
207
224
|
|
|
208
225
|
|
|
209
|
-
def fetch_groups(token: str):
|
|
210
|
-
"""Fetch a user's groups
|
|
226
|
+
def fetch_groups(token: str): # noqa: C901
|
|
227
|
+
"""Fetch a user's groups from an external endpoint"""
|
|
211
228
|
oauth_provider = jwt_service.get_provider(token)
|
|
212
229
|
oauth_provider_config = config.auth.oauth.providers[oauth_provider]
|
|
213
230
|
|
|
214
231
|
if oauth_provider_config.groups_url:
|
|
215
|
-
if oauth_provider
|
|
232
|
+
if oauth_provider in ["entraid", "azure"]:
|
|
216
233
|
try:
|
|
217
234
|
token = azure_obo(token)
|
|
218
235
|
except HowlerException:
|
|
@@ -232,13 +249,20 @@ def fetch_groups(token: str):
|
|
|
232
249
|
result = result[part]
|
|
233
250
|
|
|
234
251
|
detailed_group_data = []
|
|
252
|
+
|
|
235
253
|
for group in result:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
254
|
+
# Only process groups that are groups (not directory roles)
|
|
255
|
+
if group.get("@odata.type") == "#microsoft.graph.group":
|
|
256
|
+
group_id = group.get("id")
|
|
257
|
+
display_name = group.get("displayName")
|
|
258
|
+
if display_name: # Only add if display name exists
|
|
259
|
+
detailed_group_data.append(
|
|
260
|
+
{
|
|
261
|
+
"id": group_id,
|
|
262
|
+
"name": display_name,
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
logger.debug("Added group: %s (%s)", display_name, group_id)
|
|
242
266
|
|
|
243
267
|
return sorted(detailed_group_data, key=lambda g: g.get("name", "").lower())
|
|
244
268
|
|
|
@@ -5,7 +5,7 @@ from howler.datastore.collection import ESCollection
|
|
|
5
5
|
from howler.odm.models.user import User
|
|
6
6
|
|
|
7
7
|
# List of indices where queries are protected with classification access control
|
|
8
|
-
ACCESS_CONTROLLED_INDICES:
|
|
8
|
+
ACCESS_CONTROLLED_INDICES: set[str] = {"hit"}
|
|
9
9
|
|
|
10
10
|
ADMIN_INDEX_MAP: dict[str, Callable[[], ESCollection]] = {}
|
|
11
11
|
|
|
@@ -176,7 +176,7 @@ class OAuthAutoProperty(BaseModel):
|
|
|
176
176
|
|
|
177
177
|
field: str = Field(description="Field to apply `pattern` to")
|
|
178
178
|
pattern: str = Field(description="Regex pattern for auto-prop assignment")
|
|
179
|
-
type: Literal["access", "classification", "role"] = Field(
|
|
179
|
+
type: Literal["access", "classification", "role", "group", "assignment"] = Field(
|
|
180
180
|
description="Type of property assignment on pattern match",
|
|
181
181
|
)
|
|
182
182
|
value: str = Field(description="Assigned property value")
|