├── .githooks ├── README.md ├── pre-commit └── pre-push ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ ├── dab-consumers.yml │ ├── dvcs_check.yml │ ├── linting.yml │ ├── release.yml │ ├── sanity.yml │ └── sonar-pr.yml ├── .gitignore ├── .help_text_check.ignore ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── SECURITY.md ├── ansible_base ├── __init__.py ├── activitystream │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── filtering.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_entry_created_by.py │ │ ├── 0003_alter_entry_options.py │ │ ├── 0004_alter_entry_created_alter_entry_created_by.py │ │ ├── 0005_alter_entry_changes_alter_entry_content_type_and_more.py │ │ └── __init__.py │ ├── models │ │ ├── __init__.py │ │ └── entry.py │ ├── serializers.py │ ├── signals.py │ ├── urls.py │ └── views.py ├── api_documentation │ ├── __init__.py │ ├── apps.py │ ├── customizations.py │ ├── migrations │ │ └── __init__.py │ └── urls.py ├── authentication │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── authenticator_configurators │ │ ├── __init__.py │ │ └── github.py │ ├── authenticator_plugins │ │ ├── __init__.py │ │ ├── _radiusauth.py │ │ ├── azuread.py │ │ ├── base.py │ │ ├── github.py │ │ ├── github_enterprise.py │ │ ├── github_enterprise_org.py │ │ ├── github_enterprise_team.py │ │ ├── github_org.py │ │ ├── github_team.py │ │ ├── google_oauth2.py │ │ ├── keycloak.py │ │ ├── ldap.py │ │ ├── local.py │ │ ├── oidc.py │ │ ├── radius.py │ │ ├── saml.py │ │ ├── tacacs.py │ │ └── utils.py │ ├── backend.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── authenticators.py │ ├── middleware.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_authenticator_users.py │ │ ├── 0003_alter_authenticatormap_authenticator.py │ │ ├── 0004_remove_authenticator_created_on_and_more.py │ │ ├── 0005_alter_authenticator_created_by_and_more.py │ │ ├── 0006_alter_authenticatoruser_unique_together.py │ │ ├── 0007_remove_authenticator_users_and_more.py │ │ ├── 0008_remove_authenticator_users_unique.py │ │ ├── 0009_alter_authenticatoruser_provider_and_more.py │ │ ├── 0010_alter_authenticatoruser_unique_together.py │ │ ├── 0011_authenticatormap_role_and_more.py │ │ ├── 0012_alter_authenticatormap_map_type.py │ │ ├── 0013_alter_authenticator_order.py │ │ ├── 0014_authenticator_auto_migrate_users_to.py │ │ ├── 0015_alter_authenticator_category_and_more.py │ │ ├── 0016_alter_authenticatoruser_access_allowed_and_more.py │ │ ├── 0017_alter_authenticator_slug.py │ │ └── __init__.py │ ├── models │ │ ├── __init__.py │ │ ├── authenticator.py │ │ ├── authenticator_map.py │ │ └── authenticator_user.py │ ├── serializers │ │ ├── __init__.py │ │ ├── authenticator.py │ │ └── authenticator_map.py │ ├── session.py │ ├── social_auth.py │ ├── urls.py │ ├── utils │ │ ├── authentication.py │ │ ├── claims.py │ │ ├── trigger_definition.py │ │ └── user.py │ └── views │ │ ├── __init__.py │ │ ├── authenticator.py │ │ ├── authenticator_map.py │ │ ├── authenticator_plugins.py │ │ ├── authenticator_users.py │ │ ├── trigger_definition.py │ │ └── ui_auth.py ├── feature_flags │ ├── __init__.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── serializers.py │ ├── urls.py │ ├── utils.py │ └── views.py ├── help_text_check │ ├── __init__.py │ ├── apps.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── help_text_check.py │ └── urls.py ├── jwt_consumer │ ├── __init__.py │ ├── apps.py │ ├── awx │ │ ├── __init__.py │ │ └── auth.py │ ├── common │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── cache.py │ │ ├── cert.py │ │ ├── exceptions.py │ │ └── util.py │ ├── eda │ │ ├── __init__.py │ │ └── auth.py │ ├── hub │ │ ├── __init__.py │ │ └── auth.py │ ├── migrations │ │ └── __init__.py │ ├── redirect.html │ ├── urls.py │ └── views.py ├── lib │ ├── __init__.py │ ├── abstract_models │ │ ├── __init__.py │ │ ├── common.py │ │ ├── immutable.py │ │ ├── organization.py │ │ ├── team.py │ │ └── user.py │ ├── admin │ │ ├── __init__.py │ │ └── readonly.py │ ├── backends │ │ ├── __init__.py │ │ └── prefixed_user_auth.py │ ├── cache │ │ └── fallback_cache.py │ ├── channels │ │ ├── __init__.py │ │ └── middleware.py │ ├── checks.py │ ├── constants.py │ ├── dynamic_config │ │ ├── __init__.py │ │ ├── dynaconf_helpers.py │ │ ├── dynamic_settings.py │ │ ├── dynamic_urls.py │ │ └── settings_logic.py │ ├── logging │ │ ├── __init__.py │ │ ├── filters │ │ │ ├── __init__.py │ │ │ └── request_id.py │ │ └── runtime.py │ ├── middleware │ │ ├── __init__.py │ │ └── logging │ │ │ ├── __init__.py │ │ │ └── log_request.py │ ├── redis │ │ ├── __init__.py │ │ └── client.py │ ├── routers │ │ ├── __init__.py │ │ └── association_resource_router.py │ ├── serializers │ │ ├── __init__.py │ │ ├── common.py │ │ ├── fields.py │ │ ├── mixins.py │ │ └── validation.py │ ├── sessions │ │ ├── __init__.py │ │ └── stores │ │ │ ├── __init__.py │ │ │ └── cached_dynamic_timeout.py │ ├── templatetags │ │ ├── __init__.py │ │ ├── requests.py │ │ └── util.py │ ├── testing │ │ ├── __init__.py │ │ ├── fixtures.py │ │ └── util.py │ └── utils │ │ ├── __init__.py │ │ ├── address.py │ │ ├── auth.py │ │ ├── collection.py │ │ ├── create_system_user.py │ │ ├── db.py │ │ ├── encryption.py │ │ ├── hashing.py │ │ ├── models.py │ │ ├── requests.py │ │ ├── response.py │ │ ├── settings.py │ │ ├── string.py │ │ ├── translations.py │ │ ├── validation.py │ │ └── views │ │ ├── __init__.py │ │ ├── ansible_base.py │ │ ├── django_app_api.py │ │ ├── permissions.py │ │ └── urls.py ├── oauth2_provider │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── authentication.py │ ├── checks │ │ ├── __init__.py │ │ └── permisssions_check.py │ ├── fixtures.py │ ├── management │ │ └── commands │ │ │ ├── cleanup_tokens.py │ │ │ ├── create_oauth2_token.py │ │ │ └── revoke_oauth2_tokens.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_oauth2refreshtoken_options_and_more.py │ │ ├── 0003_remove_oauth2application_logo_data.py │ │ ├── 0004_alter_oauth2accesstoken_scope.py │ │ ├── 0005_hash_existing_tokens.py │ │ ├── 0006_alter_oauth2accesstoken_created_and_more.py │ │ ├── 0007_alter_oauth2accesstoken_application_and_more.py │ │ ├── 0008_oauth2application_app_url.py │ │ ├── __init__.py │ │ └── _utils.py │ ├── models │ │ ├── __init__.py │ │ ├── access_token.py │ │ ├── application.py │ │ ├── id_token.py │ │ └── refresh_token.py │ ├── permissions.py │ ├── serializers │ │ ├── __init__.py │ │ ├── application.py │ │ └── token.py │ ├── urls.py │ ├── utils.py │ └── views │ │ ├── __init__.py │ │ ├── application.py │ │ ├── authorization_root.py │ │ ├── permissions.py │ │ ├── token.py │ │ └── user_mixin.py ├── rbac │ ├── __init__.py │ ├── admin.py │ ├── api │ │ ├── permissions.py │ │ ├── related.py │ │ ├── router.py │ │ ├── serializers.py │ │ └── views.py │ ├── apps.py │ ├── caching.py │ ├── evaluations.py │ ├── managed.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ └── RBAC_checks.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_objectrole_provides_teams_and_more.py │ │ ├── 0003_alter_dabpermission_codename_and_more.py │ │ ├── __init__.py │ │ └── _utils.py │ ├── models.py │ ├── permission_registry.py │ ├── policies.py │ ├── prefetch.py │ ├── triggers.py │ ├── urls.py │ └── validators.py ├── resource_registry │ ├── __init__.py │ ├── apps.py │ ├── fields.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── resource_sync.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_remove_resource_id.py │ │ ├── 0003_alter_resource_object_id.py │ │ ├── 0004_remove_resourcetype_migrated.py │ │ ├── 0005_resource_is_partially_migrated_and_more.py │ │ ├── 0006_alter_resource_service_id.py │ │ ├── 0007_alter_resource_ansible_id_and_more.py │ │ └── __init__.py │ ├── models │ │ ├── __init__.py │ │ ├── resource.py │ │ └── service_identifier.py │ ├── registry.py │ ├── resource_server.py │ ├── rest_client.py │ ├── serializers.py │ ├── shared_types.py │ ├── signals │ │ ├── __init__.py │ │ └── handlers.py │ ├── tasks │ │ ├── __init__.py │ │ └── sync.py │ ├── urls.py │ ├── utils │ │ ├── __init__.py │ │ ├── auth_code.py │ │ ├── resource_type_processor.py │ │ ├── resource_type_serializers.py │ │ ├── service_backed_sso_pipeline.py │ │ ├── settings.py │ │ ├── sso_provider.py │ │ └── sync_to_resource_server.py │ └── views.py ├── rest_filters │ ├── __init__.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── rest_framework │ │ ├── __init__.py │ │ ├── ansible_id_backend.py │ │ ├── field_lookup_backend.py │ │ ├── order_backend.py │ │ └── type_filter_backend.py │ └── utils.py └── rest_pagination │ ├── __init__.py │ ├── apps.py │ ├── default_paginator.py │ └── migrations │ └── __init__.py ├── compose ├── README.md ├── ingress │ ├── Dockerfile │ ├── certs │ │ └── .keepme │ ├── entrypoint.sh │ └── nginx.conf └── nginx │ ├── Dockerfile │ └── nginx.conf ├── docker-compose.yml ├── docs ├── Installation.md ├── apps │ ├── activitystream.md │ ├── api_documentation.md │ ├── authentication │ │ ├── authentication.md │ │ ├── authenticator_plugins.md │ │ └── management_commands.md │ ├── feature_flags.md │ ├── help_text_check.md │ ├── oauth2_provider.md │ ├── rbac │ │ ├── README.md │ │ ├── for_app_developers.md │ │ ├── for_clients.md │ │ ├── for_dab_developers.md │ │ ├── for_users.md │ │ └── images │ │ │ ├── team_obj.svg │ │ │ ├── team_org.svg │ │ │ ├── user_obj.svg │ │ │ ├── user_org.svg │ │ │ └── user_system.svg │ ├── resource_registry.md │ ├── rest_filters.md │ ├── rest_pagination.md │ └── service_backed_sso.md ├── apps_or_lib.md ├── lib │ ├── advisory_lock.md │ ├── channels_authentication.md │ ├── default_models.md │ ├── dynamic_config.md │ ├── encryption.md │ ├── organizations.md │ ├── redis.md │ ├── requests.md │ ├── routers.md │ ├── serializers.md │ ├── sessions.md │ ├── validation.md │ └── views.md ├── logging.md ├── testing.md └── vscode.md ├── manage.py ├── pyproject.toml ├── requirements ├── requirements.in ├── requirements_activitystream.in ├── requirements_all.txt ├── requirements_api_documentation.in ├── requirements_authentication.in ├── requirements_channels.in ├── requirements_dev.txt ├── requirements_feature_flags.in ├── requirements_jwt_consumer.in ├── requirements_oauth2_provider.in ├── requirements_rbac.in ├── requirements_redis_client.in ├── requirements_resource_registry.in ├── requirements_rest_filters.in ├── requirements_testing.txt └── updater.sh ├── sonar-project.properties ├── test_app ├── README.md ├── __init__.py ├── admin.py ├── apps.py ├── authentication │ ├── __init__.py │ ├── logged_basic_auth.py │ └── service_token_auth.py ├── defaults.py ├── example_files │ └── settings.yaml ├── management │ └── commands │ │ └── create_demo_data.py ├── managers.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_set_up_resources_test_data.py │ ├── 0003_immutablelogentrynotcommon_immutablelogentry.py │ ├── 0004_multiplefieldsmodel_animal.py │ ├── 0005_city.py │ ├── 0006_team_admins_team_users.py │ ├── 0007_alter_animal_created_by_alter_animal_modified_by_and_more.py │ ├── 0008_secretcolor.py │ ├── 0009_city_state.py │ ├── 0010_manageduser.py │ ├── 0011_publicdata.py │ ├── 0012_encryptionjsonmodel.py │ ├── 0013_alter_manageduser_managers_alter_user_managers.py │ ├── 0014_autoextrauuidmodel_manualextrauuidmodel_and_more.py │ ├── 0015_logentry.py │ ├── 0016_organization_extra_field.py │ ├── 0017_thingsomeoneshares_thingsomeoneowns.py │ ├── 0018_alter_animal_created_alter_animal_created_by_and_more.py │ └── __init__.py ├── models.py ├── resource_api.py ├── router.py ├── scripts │ ├── bootstrap.sh │ └── container_startup_uwsgi.sh ├── serializers.py ├── settings.py ├── sqlite3settings.py ├── sqlite_defaults.py ├── static │ └── test_templatetags_util_inline_file.css ├── templates │ └── index.html ├── tests │ ├── __init__.py │ ├── activitystream │ │ ├── models │ │ │ ├── __init__.py │ │ │ └── test_entry.py │ │ ├── test_api.py │ │ └── test_signals.py │ ├── authentication │ │ ├── __init__.py │ │ ├── authenticator_plugins │ │ │ ├── __init__.py │ │ │ ├── test_azuread.py │ │ │ ├── test_base.py │ │ │ ├── test_github.py │ │ │ ├── test_github_enterprise.py │ │ │ ├── test_github_enterprise_org.py │ │ │ ├── test_github_enterprise_team.py │ │ │ ├── test_github_org.py │ │ │ ├── test_github_team.py │ │ │ ├── test_google_oauth2.py │ │ │ ├── test_keycloak.py │ │ │ ├── test_ldap.py │ │ │ ├── test_ldap_get_or_build_user.py │ │ │ ├── test_local.py │ │ │ ├── test_oidc.py │ │ │ ├── test_radius.py │ │ │ ├── test_saml.py │ │ │ └── test_tacacs.py │ │ ├── conftest.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── test_authenticators.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── test_authenticator.py │ │ │ └── test_authenticator_user.py │ │ ├── serializers │ │ │ ├── __init__.py │ │ │ ├── test_authenticator.py │ │ │ └── test_authenticator_map.py │ │ ├── test_backend.py │ │ ├── test_middleware.py │ │ ├── test_migrate_user_to_authenticator.py │ │ ├── test_social_auth.py │ │ ├── test_urls.py │ │ ├── utils │ │ │ ├── __init__.py │ │ │ ├── test_authentication.py │ │ │ ├── test_claims.py │ │ │ ├── test_claims_reconcile_user.py │ │ │ └── test_user.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── test_authenticator_map.py │ │ │ ├── test_authenticator_plugins.py │ │ │ ├── test_authenticator_user.py │ │ │ ├── test_authenticators.py │ │ │ └── test_ui_auth.py │ ├── conftest.py │ ├── feature_flags │ │ ├── __init__.py │ │ └── test_api.py │ ├── fixtures │ │ ├── __init__.py │ │ ├── authenticator_plugins │ │ │ ├── __init__.py │ │ │ ├── broken.py │ │ │ ├── custom.py │ │ │ └── really_broken.py │ │ ├── static │ │ │ └── resource_sync │ │ │ │ ├── metadata │ │ │ │ └── response │ │ │ │ ├── resource-types │ │ │ │ ├── shared.organization │ │ │ │ │ └── manifest │ │ │ │ │ │ └── response │ │ │ │ └── shared.user │ │ │ │ │ └── manifest │ │ │ │ │ └── response │ │ │ │ └── resources │ │ │ │ ├── 3e3cc6a4-72fa-43ec-9e17-76ae5a3846ca │ │ │ │ └── response │ │ │ │ ├── 97447387-8596-404f-b0d0-6429b04c8d22 │ │ │ │ └── response │ │ │ │ └── b19ff84f-df6a-462a-ac81-167b1dc8f933 │ │ │ │ └── response │ │ └── test_fixtures.py │ ├── help_text_check │ │ └── management │ │ │ └── commands │ │ │ └── test_help_text_check.py │ ├── jwt_consumer │ │ ├── __init__.py │ │ ├── awx │ │ │ ├── __init__.py │ │ │ └── test_auth.py │ │ ├── common │ │ │ ├── __init__.py │ │ │ ├── test_auth.py │ │ │ ├── test_cache.py │ │ │ ├── test_cert.py │ │ │ ├── test_exceptions.py │ │ │ └── test_util.py │ │ ├── eda │ │ │ ├── __init__.py │ │ │ └── test_auth.py │ │ ├── hub │ │ │ ├── __init__.py │ │ │ └── test_auth.py │ │ └── test_views.py │ ├── lib │ │ ├── __init__.py │ │ ├── abstract_models │ │ │ ├── __init__.py │ │ │ ├── test_common.py │ │ │ ├── test_immutable.py │ │ │ └── test_organization.py │ │ ├── cache │ │ │ ├── __init__.py │ │ │ └── test_fallback_cache.py │ │ ├── channels │ │ │ ├── __init__.py │ │ │ └── test_middleware.py │ │ ├── dynamic_config │ │ │ ├── __init__.py │ │ │ ├── test_dynaconf_settings.py │ │ │ ├── test_dynamic_settings.py │ │ │ └── test_settings_logic.py │ │ ├── logging │ │ │ ├── filters │ │ │ │ └── test_request_id.py │ │ │ ├── middleware │ │ │ │ └── test_traceback_logger.py │ │ │ └── test_runtime.py │ │ ├── redis │ │ │ └── test_client.py │ │ ├── routers │ │ │ ├── __init__.py │ │ │ └── test_association_resoure_router.py │ │ ├── serializers │ │ │ ├── test_common.py │ │ │ ├── test_fields.py │ │ │ └── test_validate.py │ │ ├── sessions │ │ │ └── stores │ │ │ │ └── test_cached_dynamic_timeout.py │ │ ├── templatetags │ │ │ ├── __init__.py │ │ │ ├── test_requests.py │ │ │ └── test_util.py │ │ ├── test_prefixed_authentication_backend.py │ │ ├── testing │ │ │ ├── __init__.py │ │ │ └── test_fixtures.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ ├── test_address.py │ │ │ ├── test_auth.py │ │ │ ├── test_create_system_user.py │ │ │ ├── test_db.py │ │ │ ├── test_encryption.py │ │ │ ├── test_hashing.py │ │ │ ├── test_models.py │ │ │ ├── test_requests.py │ │ │ ├── test_response.py │ │ │ ├── test_settings.py │ │ │ ├── test_validation.py │ │ │ └── test_views.py │ ├── oauth2_provider │ │ ├── __init__.py │ │ ├── checks │ │ │ ├── __init__.py │ │ │ └── test_permissions_check.py │ │ ├── management │ │ │ └── commands │ │ │ │ ├── test_cleanup_tokens.py │ │ │ │ ├── test_create_oauth2_token.py │ │ │ │ └── test_revoke_oauth2_tokens.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── test_utils.py │ │ ├── test_authentication.py │ │ ├── test_models.py │ │ ├── test_rbac.py │ │ ├── test_utils.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── test_application.py │ │ │ ├── test_authorization_root.py │ │ │ ├── test_authorize.py │ │ │ └── test_token.py │ ├── rbac │ │ ├── api │ │ │ ├── test_ansible_id_assignments.py │ │ │ ├── test_assignment_permissions.py │ │ │ ├── test_rbac_fields.py │ │ │ ├── test_rbac_permissions.py │ │ │ ├── test_rbac_serializers.py │ │ │ ├── test_rbac_validation.py │ │ │ ├── test_rbac_views.py │ │ │ └── test_user_permissions.py │ │ ├── app_api │ │ │ └── test_options.py │ │ ├── compatibility │ │ │ ├── conftest.py │ │ │ ├── test_codename_nonstandard.py │ │ │ ├── test_concrete_inheritance.py │ │ │ ├── test_model_name_conflict.py │ │ │ ├── test_non_id_primary_key.py │ │ │ ├── test_parent_field_rename.py │ │ │ └── test_uuid_support.py │ │ ├── conftest.py │ │ ├── features │ │ │ ├── test_cache_parent_perms.py │ │ │ ├── test_creator_permission.py │ │ │ ├── test_nested_resources.py │ │ │ ├── test_public_model.py │ │ │ ├── test_role_tracking.py │ │ │ └── test_singleton_roles.py │ │ ├── models │ │ │ ├── test_object_role.py │ │ │ ├── test_permissions.py │ │ │ ├── test_role_assignments.py │ │ │ ├── test_role_definition.py │ │ │ └── test_uniqueness.py │ │ ├── test_ansible_id_alias_filter.py │ │ ├── test_managed.py │ │ ├── test_management_rbac_checks.py │ │ ├── test_migrations.py │ │ ├── test_permission_assignment.py │ │ ├── test_permission_evaluation.py │ │ ├── test_policies.py │ │ ├── test_triggers.py │ │ └── test_validators.py │ ├── resource_registry │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── models │ │ │ ├── test_resource_field.py │ │ │ └── test_service_id.py │ │ ├── test_metadata_api.py │ │ ├── test_migration.py │ │ ├── test_models.py │ │ ├── test_resource_sync.py │ │ ├── test_resource_types_api.py │ │ ├── test_resources_api.py │ │ ├── test_resources_api_rest_client.py │ │ ├── test_service_backed_sso.py │ │ ├── test_signals.py │ │ ├── test_utils.py │ │ └── test_views.py │ ├── rest_filters │ │ ├── __init__.py │ │ ├── rest_framework │ │ │ ├── __init__.py │ │ │ ├── test_field_lookup_backend.py │ │ │ ├── test_order_backend.py │ │ │ └── test_type_filter_backend.py │ │ └── test_utils.py │ ├── rest_pagination │ │ ├── __init__.py │ │ └── test_default_paginator.py │ ├── test_checks.py │ └── test_demo_data.py ├── urls.py ├── uwsgi.ini ├── views.py └── wsgi.py └── tools ├── ansible └── release.yml ├── dev_postgres └── Dockerfile └── vscode ├── launch.json ├── settings.json ├── tasks.json └── vscodebootstrap.sh /.githooks/README.md: -------------------------------------------------------------------------------- 1 | # .githooks 2 | 3 | This folder contains executable files that will be invoked by git at certain git operations. 4 | 5 | By default git hooks are located i `.git/hooks` folder at the root of the repository. Since the default folder is hidden by most IDEs, this repository reconfigures git's hook location in order to make the hooks visible and easier to maintain. 6 | 7 | ## Configuration 8 | 9 | Normal development flows (see file [../README.md](../README.md) ) will call git to change the location of git hooks. 10 | 11 | To make this change manually you can invoke following command from the root of the repository: 12 | 13 | ```sh 14 | make git_hooks_config 15 | ``` 16 | 17 | ## Git hooks implementation 18 | 19 | Git hooks are simply executable files that follow the rules below: 20 | 21 | * have executable permissions 22 | * file name must correspond to git hook name with no extension(no `.sh` or `.py`) (see documentation section below) 23 | 24 | Return code other than zero(0) will cause the git operation that triggerred the hook to fail, while zero(0) return code indicates success and git opertaion will succeed. 25 | 26 | ## Documentation 27 | 28 | Git hooks documentation: 29 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Code of Conduct 2 | 3 | Please see the official [Ansible Community Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html). 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | description: "🐞 Create a report to help us improve" 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Bug Report issues are for **concrete, actionable bugs** only. 9 | 10 | - type: textarea 11 | id: summary 12 | attributes: 13 | label: Bug Summary 14 | description: Briefly describe the problem. 15 | validations: 16 | required: false 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | blank_issues_enabled: true 3 | contact_links: 4 | - name: 📝 Ansible Code of Conduct 5 | url: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html?utm_medium=github&utm_source=issue_template_chooser 6 | about: django-ansible-base uses the Ansible Code of Conduct; ❤ Be nice to other members of the community. ☮ Behave. 7 | -------------------------------------------------------------------------------- /.github/workflows/dvcs_check.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: DVCS (Find Jira Key) 3 | on: 4 | pull_request_target: 5 | jobs: 6 | dvcs_pr_checker: 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | runs-on: ubuntu-latest 11 | name: Check the PR for DVCS integration 12 | steps: 13 | - uses: ansible/dvcs-action@devel 14 | with: 15 | github_token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Linting 3 | env: 4 | LC_ALL: "C.UTF-8" # prevent ERROR: Ansible could not initialize the preferred locale: unsupported locale setting 5 | on: 6 | pull_request: 7 | push: 8 | jobs: 9 | common-tests: 10 | name: ${{ matrix.tests.name }} 11 | runs-on: ubuntu-latest 12 | permissions: 13 | packages: write 14 | contents: read 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | tests: 19 | - name: api-flake8 20 | command: check_flake8 21 | - name: api-black 22 | command: check_black 23 | - name: api-isort 24 | command: check_isort 25 | steps: 26 | - name: Install make 27 | run: sudo apt install make 28 | 29 | - uses: actions/checkout@v4 30 | with: 31 | show-progress: false 32 | 33 | - name: Install python 3.11 34 | uses: actions/setup-python@v4 35 | with: 36 | python-version: 3.11 37 | 38 | - name: Install requirments 39 | run: pip3.11 install -r requirements/requirements_dev.txt 40 | 41 | - name: Run check ${{ matrix.tests.name }} 42 | run: make ${{ matrix.tests.command }} 43 | -------------------------------------------------------------------------------- /.github/workflows/sanity.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Sanity 3 | env: 4 | LC_ALL: "C.UTF-8" # prevent ERROR: Ansible could not initialize the preferred locale: unsupported locale setting 5 | on: 6 | pull_request: 7 | push: 8 | jobs: 9 | help_text: 10 | name: Help Test Check 11 | runs-on: ubuntu-latest 12 | permissions: 13 | packages: read 14 | contents: read 15 | strategy: 16 | fail-fast: false 17 | steps: 18 | - name: Install make 19 | run: sudo apt install make 20 | 21 | - uses: actions/checkout@v4 22 | with: 23 | show-progress: false 24 | 25 | - name: Install build requirements 26 | run: sudo apt-get update && sudo apt-get install -y libsasl2-dev libldap2-dev libssl-dev libxmlsec1-dev 27 | 28 | - name: Install python 3.11 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: 3.11 32 | 33 | - name: Install requirements 34 | run: pip3.11 install -r requirements/requirements_all.txt -r requirements/requirements_dev.txt 35 | 36 | - name: Run help text check 37 | run: ./manage.py help_text_check --applications=dab --ignore-file=./.help_text_check.ignore 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User level pre-commit hooks 2 | pre-commit-user 3 | 4 | # Make target touch files 5 | .docker-compose-built 6 | 7 | # Python & setuptools 8 | __pycache__ 9 | /build 10 | /deb-build 11 | /reprepro 12 | /rpm-build 13 | /tar-build 14 | /setup-bundle-build 15 | /dist 16 | /*.egg-info 17 | *.py[c,o] 18 | /.eggs 19 | .coverage* 20 | coverage.xml 21 | coverage.json 22 | django-ansible-base-test-results.xml 23 | htmlcov 24 | *.tox 25 | venv/ 26 | .venv/ 27 | 28 | # Mac OS X 29 | *.DS_Store 30 | 31 | # VSCode 32 | .vscode/ 33 | 34 | # Editors 35 | *.sw[poj] 36 | *~ 37 | 38 | # SQLite 39 | *.sqlite3 40 | *.sqlite3_gw* 41 | *.sqlite3-journal 42 | 43 | # Container customizations 44 | container-startup.yml 45 | tools/generated/* 46 | 47 | # Gets created when testing sonar-scanner locally 48 | .scannerwork 49 | 50 | # generated ssl certs 51 | compose/ingress/certs/* 52 | 53 | # static files 54 | test_app/static_collected/ 55 | -------------------------------------------------------------------------------- /.help_text_check.ignore: -------------------------------------------------------------------------------- 1 | dab_authentication.AuthenticatorUser.created # Inherited from social auth 2 | dab_authentication.AuthenticatorUser.extra_data # Inherited from social auth 3 | dab_authentication.AuthenticatorUser.modified # Inherited from social auth 4 | dab_authentication.AuthenticatorUser.uid # Inherited from social auth 5 | 6 | dab_oauth2_provider.OAuth2AccessToken.expires # Inherited from django-oauth-toolkit 7 | dab_oauth2_provider.OAuth2AccessToken.id_token # Inherited from django-oauth-toolkit 8 | dab_oauth2_provider.OAuth2AccessToken.source_refresh_token # Inherited from django-oauth-toolkit 9 | 10 | dab_oauth2_provider.OAuth2Application.algorithm # Inherited from django-oauth-toolkit 11 | dab_oauth2_provider.OAuth2Application.client_id # Inherited from django-oauth-toolkit 12 | 13 | dab_oauth2_provider.OAuth2IDToken.application # Inherited from django-oauth-toolkit 14 | dab_oauth2_provider.OAuth2IDToken.expires # Inherited from django-oauth-toolkit 15 | dab_oauth2_provider.OAuth2IDToken.jti # Inherited from django-oauth-toolkit 16 | dab_oauth2_provider.OAuth2IDToken.scope # Inherited from django-oauth-toolkit 17 | dab_oauth2_provider.OAuth2IDToken.user # Inherited from django-oauth-toolkit 18 | 19 | dab_oauth2_provider.OAuth2RefreshToken.access_token # Inherited from django-oauth-toolkit 20 | dab_oauth2_provider.OAuth2RefreshToken.application # Inherited from django-oauth-toolkit 21 | dab_oauth2_provider.OAuth2RefreshToken.revoked # Inherited from django-oauth-toolkit 22 | dab_oauth2_provider.OAuth2RefreshToken.user # Inherited from django-oauth-toolkit 23 | 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/centos/centos:stream9 2 | 3 | RUN sed -i '/^\[crb\]$/,/^enabled=0$/ s/enabled=0/enabled=1/' /etc/yum.repos.d/centos.repo 4 | RUN dnf -y install \ 5 | python3.11 \ 6 | python3.11-pip \ 7 | python3.11-devel \ 8 | gcc \ 9 | openldap-devel \ 10 | xmlsec1 \ 11 | xmlsec1-openssl \ 12 | xmlsec1-devel \ 13 | libtool-ltdl-devel \ 14 | libpq-devel \ 15 | libpq \ 16 | postgresql 17 | 18 | # Create /etc/ansible-automation-platform/testapp folder 19 | RUN mkdir -p /etc/ansible-automation-platform/testapp 20 | # add settings.yaml to /etc/ansible-automation-platform/ 21 | COPY test_app/example_files/*.yaml /etc/ansible-automation-platform/testapp/ 22 | 23 | RUN python3.11 -m venv /venv 24 | 25 | COPY requirements/requirements_all.txt /tmp/requirements_all.txt 26 | RUN /venv/bin/pip install -r /tmp/requirements_all.txt 27 | 28 | COPY requirements/requirements_dev.txt /tmp/requirements_dev.txt 29 | RUN /venv/bin/pip install -r /tmp/requirements_dev.txt 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | exclude manage.py 2 | recursive-exclude docs * 3 | recursive-exclude test_app * 4 | recursive-exclude tools * 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-ansible-base 2 | 3 | ## What is it? 4 | Django-ansible-base is exactly what it says it is. A base for any Ansible application which will leverage Django. 5 | 6 | ## Documentation 7 | 8 | Docs for django-ansible-base features can be found in the [docs](docs) directory. 9 | 10 | Information about the test_app and how to start/use it can be found in [test_app/README.md](test_app/README.md) 11 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | For all security related bugs, email security@ansible.com instead of using this issue tracker and you will receive a prompt response. 2 | 3 | For more information on the Ansible community's practices regarding responsible disclosure, see https://www.ansible.com/security 4 | -------------------------------------------------------------------------------- /ansible_base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/__init__.py -------------------------------------------------------------------------------- /ansible_base/activitystream/__init__.py: -------------------------------------------------------------------------------- 1 | from ansible_base.activitystream.signals import no_activity_stream 2 | 3 | __all__ = ['no_activity_stream'] 4 | -------------------------------------------------------------------------------- /ansible_base/activitystream/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from ansible_base.activitystream.models import Entry 4 | from ansible_base.lib.admin import ReadOnlyAdmin 5 | 6 | admin.site.register(Entry, ReadOnlyAdmin) 7 | -------------------------------------------------------------------------------- /ansible_base/activitystream/filtering.py: -------------------------------------------------------------------------------- 1 | from ansible_base.rest_filters.rest_framework.field_lookup_backend import FieldLookupBackend 2 | 3 | 4 | class ActivityStreamFilterBackend(FieldLookupBackend): 5 | TREAT_JSONFIELD_AS_TEXT = False 6 | -------------------------------------------------------------------------------- /ansible_base/activitystream/migrations/0002_alter_entry_created_by.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-04-05 13:06 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('dab_activitystream', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='entry', 18 | name='created_by', 19 | field=models.ForeignKey(default=None, editable=False, help_text='The user who created this resource', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created+', to=settings.AUTH_USER_MODEL), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /ansible_base/activitystream/migrations/0003_alter_entry_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-04-13 21:57 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('dab_activitystream', '0002_alter_entry_created_by'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='entry', 15 | options={'ordering': ['id'], 'verbose_name_plural': 'Entries'}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /ansible_base/activitystream/migrations/0004_alter_entry_created_alter_entry_created_by.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-11-21 11:14 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('dab_activitystream', '0003_alter_entry_options'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='entry', 18 | name='created', 19 | field=models.DateTimeField(auto_now_add=True, help_text='The date/time this resource was created.'), 20 | ), 21 | migrations.AlterField( 22 | model_name='entry', 23 | name='created_by', 24 | field=models.ForeignKey(default=None, editable=False, help_text='The user who created this resource.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created+', to=settings.AUTH_USER_MODEL), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /ansible_base/activitystream/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/activitystream/migrations/__init__.py -------------------------------------------------------------------------------- /ansible_base/activitystream/models/__init__.py: -------------------------------------------------------------------------------- 1 | from ansible_base.activitystream.models.entry import AuditableModel, Entry # noqa: F401 2 | -------------------------------------------------------------------------------- /ansible_base/activitystream/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework import routers 3 | 4 | from ansible_base.activitystream import views 5 | 6 | router = routers.SimpleRouter() 7 | router.register( 8 | 'activitystream', 9 | views.EntryReadOnlyViewSet, 10 | basename='activitystream', 11 | ) 12 | 13 | api_version_urls = [path('', include(router.urls))] 14 | -------------------------------------------------------------------------------- /ansible_base/api_documentation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/api_documentation/__init__.py -------------------------------------------------------------------------------- /ansible_base/api_documentation/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | from ansible_base.api_documentation.customizations import apply_authentication_customizations 4 | 5 | 6 | class ApiDocumentationConfig(AppConfig): 7 | default_auto_field = 'django.db.models.BigAutoField' 8 | name = 'ansible_base.api_documentation' 9 | label = 'dab_api_documentation' 10 | 11 | def ready(self): 12 | from django.conf import settings 13 | 14 | if 'ansible_base.authentication' in settings.INSTALLED_APPS: 15 | apply_authentication_customizations() 16 | -------------------------------------------------------------------------------- /ansible_base/api_documentation/customizations.py: -------------------------------------------------------------------------------- 1 | def apply_authentication_customizations() -> None: 2 | """Declare schema of DAB authentication classes 3 | 4 | This follows docs which reccomends OpenApiAuthenticationExtension to register an authentication class 5 | https://drf-spectacular.readthedocs.io/en/latest/customization.html#specify-authentication-with-openapiauthenticationextension 6 | As long as this class is resolved on import, drf-spectacular will be aware of it. 7 | This is called from api_documentation ready method. 8 | Imports are in-line, because dependencies may not be satisfied depending on what apps are installed. 9 | """ 10 | from drf_spectacular.authentication import SessionScheme 11 | 12 | from ansible_base.authentication.session import SessionAuthentication 13 | 14 | class MyAuthenticationScheme(SessionScheme): 15 | target_class = SessionAuthentication 16 | name = 'SessionAuthentication' 17 | -------------------------------------------------------------------------------- /ansible_base/api_documentation/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/api_documentation/migrations/__init__.py -------------------------------------------------------------------------------- /ansible_base/api_documentation/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView 3 | 4 | from ansible_base.api_documentation.apps import ApiDocumentationConfig 5 | 6 | app_name = ApiDocumentationConfig.label 7 | api_version_urls = [ 8 | path('docs/schema/', SpectacularAPIView.as_view(), name='schema'), 9 | path('docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), 10 | path('docs/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), 11 | ] 12 | -------------------------------------------------------------------------------- /ansible_base/authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/authentication/__init__.py -------------------------------------------------------------------------------- /ansible_base/authentication/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from ansible_base.authentication.models import Authenticator, AuthenticatorMap, AuthenticatorUser 4 | 5 | admin.site.register(Authenticator) 6 | admin.site.register(AuthenticatorMap) 7 | admin.site.register(AuthenticatorUser) 8 | -------------------------------------------------------------------------------- /ansible_base/authentication/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | import ansible_base.lib.checks # noqa: F401 - register checks 4 | 5 | 6 | class AuthenticationConfig(AppConfig): 7 | default_auto_field = 'django.db.models.BigAutoField' 8 | name = 'ansible_base.authentication' 9 | label = 'dab_authentication' 10 | verbose_name = 'Pluggable Authentication' 11 | -------------------------------------------------------------------------------- /ansible_base/authentication/authenticator_configurators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/authentication/authenticator_configurators/__init__.py -------------------------------------------------------------------------------- /ansible_base/authentication/authenticator_plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/authentication/authenticator_plugins/__init__.py -------------------------------------------------------------------------------- /ansible_base/authentication/authenticator_plugins/github.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from social_core.backends.github import GithubOAuth2 4 | 5 | from ansible_base.authentication.authenticator_configurators.github import GithubConfiguration 6 | from ansible_base.authentication.authenticator_plugins.base import AbstractAuthenticatorPlugin 7 | from ansible_base.authentication.social_auth import SocialAuthMixin, SocialAuthValidateCallbackMixin 8 | 9 | logger = logging.getLogger('ansible_base.authentication.authenticator_plugins.github') 10 | 11 | 12 | class AuthenticatorPlugin(SocialAuthMixin, SocialAuthValidateCallbackMixin, GithubOAuth2, AbstractAuthenticatorPlugin): 13 | configuration_class = GithubConfiguration 14 | logger = logger 15 | type = "github" 16 | category = "sso" 17 | configuration_encrypted_fields = ['SECRET'] 18 | -------------------------------------------------------------------------------- /ansible_base/authentication/authenticator_plugins/github_enterprise.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from social_core.backends.github_enterprise import GithubEnterpriseOAuth2 4 | 5 | from ansible_base.authentication.authenticator_configurators.github import GithubEnterpriseConfiguration 6 | from ansible_base.authentication.authenticator_plugins.base import AbstractAuthenticatorPlugin 7 | from ansible_base.authentication.social_auth import SocialAuthMixin, SocialAuthValidateCallbackMixin 8 | 9 | logger = logging.getLogger('ansible_base.authentication.authenticator_plugins.github_enterprise') 10 | 11 | 12 | class AuthenticatorPlugin(SocialAuthMixin, SocialAuthValidateCallbackMixin, GithubEnterpriseOAuth2, AbstractAuthenticatorPlugin): 13 | configuration_class = GithubEnterpriseConfiguration 14 | logger = logger 15 | type = "github-enterprise" 16 | category = "sso" 17 | configuration_encrypted_fields = ['ENTERPRISE_SECRET'] 18 | -------------------------------------------------------------------------------- /ansible_base/authentication/authenticator_plugins/github_enterprise_org.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from social_core.backends.github_enterprise import GithubEnterpriseOrganizationOAuth2 4 | 5 | from ansible_base.authentication.authenticator_configurators.github import GithubEnterpriseOrgConfiguration 6 | from ansible_base.authentication.authenticator_plugins.base import AbstractAuthenticatorPlugin 7 | from ansible_base.authentication.social_auth import SocialAuthMixin, SocialAuthValidateCallbackMixin 8 | 9 | logger = logging.getLogger('ansible_base.authentication.authenticator_plugins.github_enterprise_organization') 10 | 11 | 12 | class AuthenticatorPlugin(SocialAuthMixin, SocialAuthValidateCallbackMixin, GithubEnterpriseOrganizationOAuth2, AbstractAuthenticatorPlugin): 13 | configuration_class = GithubEnterpriseOrgConfiguration 14 | logger = logger 15 | type = "github-enterprise-org" 16 | category = "sso" 17 | configuration_encrypted_fields = ['ENTERPRISE_ORG_SECRET'] 18 | -------------------------------------------------------------------------------- /ansible_base/authentication/authenticator_plugins/github_enterprise_team.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from social_core.backends.github_enterprise import GithubEnterpriseTeamOAuth2 4 | 5 | from ansible_base.authentication.authenticator_configurators.github import GithubEnterpriseTeamConfiguration 6 | from ansible_base.authentication.authenticator_plugins.base import AbstractAuthenticatorPlugin 7 | from ansible_base.authentication.social_auth import SocialAuthMixin, SocialAuthValidateCallbackMixin 8 | 9 | logger = logging.getLogger('ansible_base.authentication.authenticator_plugins.github_enterprise_team') 10 | 11 | 12 | class AuthenticatorPlugin(SocialAuthMixin, SocialAuthValidateCallbackMixin, GithubEnterpriseTeamOAuth2, AbstractAuthenticatorPlugin): 13 | configuration_class = GithubEnterpriseTeamConfiguration 14 | logger = logger 15 | type = "github-enterprise-team" 16 | category = "sso" 17 | configuration_encrypted_fields = ['SECRET'] 18 | -------------------------------------------------------------------------------- /ansible_base/authentication/authenticator_plugins/github_org.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from social_core.backends.github import GithubOrganizationOAuth2 4 | 5 | from ansible_base.authentication.authenticator_configurators.github import GithubOrganizationConfiguration 6 | from ansible_base.authentication.authenticator_plugins.base import AbstractAuthenticatorPlugin 7 | from ansible_base.authentication.social_auth import SocialAuthMixin, SocialAuthValidateCallbackMixin 8 | 9 | logger = logging.getLogger('ansible_base.authentication.authenticator_plugins.github_organization') 10 | 11 | 12 | class AuthenticatorPlugin(SocialAuthMixin, SocialAuthValidateCallbackMixin, GithubOrganizationOAuth2, AbstractAuthenticatorPlugin): 13 | configuration_class = GithubOrganizationConfiguration 14 | logger = logger 15 | type = "github-org" 16 | category = "sso" 17 | configuration_encrypted_fields = ['SECRET'] 18 | -------------------------------------------------------------------------------- /ansible_base/authentication/authenticator_plugins/github_team.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from social_core.backends.github import GithubTeamOAuth2 4 | 5 | from ansible_base.authentication.authenticator_configurators.github import GithubTeamConfiguration 6 | from ansible_base.authentication.authenticator_plugins.base import AbstractAuthenticatorPlugin 7 | from ansible_base.authentication.social_auth import SocialAuthMixin, SocialAuthValidateCallbackMixin 8 | 9 | logger = logging.getLogger('ansible_base.authentication.authenticator_plugins.github_team') 10 | 11 | 12 | class AuthenticatorPlugin(SocialAuthMixin, SocialAuthValidateCallbackMixin, GithubTeamOAuth2, AbstractAuthenticatorPlugin): 13 | configuration_class = GithubTeamConfiguration 14 | logger = logger 15 | type = "github-team" 16 | category = "sso" 17 | configuration_encrypted_fields = ['SECRET'] 18 | -------------------------------------------------------------------------------- /ansible_base/authentication/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/authentication/management/__init__.py -------------------------------------------------------------------------------- /ansible_base/authentication/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/authentication/management/commands/__init__.py -------------------------------------------------------------------------------- /ansible_base/authentication/migrations/0002_alter_authenticator_users.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-02-11 03:14 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 11 | ('dab_authentication', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='authenticator', 17 | name='users', 18 | field=models.ManyToManyField(blank=True, help_text='The list of users who have authenticated from this authenticator', related_name='authenticators', to=settings.AUTH_USER_MODEL), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /ansible_base/authentication/migrations/0003_alter_authenticatormap_authenticator.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.6 on 2024-02-14 16:31 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('dab_authentication', '0002_alter_authenticator_users'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='authenticatormap', 16 | name='authenticator', 17 | field=models.ForeignKey(help_text='The authenticator this mapping belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='authenticator_maps', to='dab_authentication.authenticator'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /ansible_base/authentication/migrations/0006_alter_authenticatoruser_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-04-10 19:57 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 11 | ('dab_authentication', '0005_alter_authenticator_created_by_and_more'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterUniqueTogether( 16 | name='authenticatoruser', 17 | unique_together={('provider', 'uid', 'user')}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /ansible_base/authentication/migrations/0007_remove_authenticator_users_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-04-09 17:04 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('dab_authentication', '0006_alter_authenticatoruser_unique_together'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='authenticator', 16 | name='users', 17 | ), 18 | migrations.AlterField( 19 | model_name='authenticatoruser', 20 | name='provider', 21 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='authenticator_provider', to='dab_authentication.authenticator', to_field='slug'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /ansible_base/authentication/migrations/0008_remove_authenticator_users_unique.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-04-10 20:53 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('dab_authentication', '0007_remove_authenticator_users_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='authenticator', 15 | name='users_unique', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /ansible_base/authentication/migrations/0009_alter_authenticatoruser_provider_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-04-19 12:39 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('dab_authentication', '0008_remove_authenticator_users_unique'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='authenticatoruser', 18 | name='provider', 19 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='authenticator_providers', to='dab_authentication.authenticator', to_field='slug'), 20 | ), 21 | migrations.AlterField( 22 | model_name='authenticatoruser', 23 | name='user', 24 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='authenticator_users', to=settings.AUTH_USER_MODEL), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /ansible_base/authentication/migrations/0010_alter_authenticatoruser_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-04-21 16:39 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('dab_authentication', '0009_alter_authenticatoruser_provider_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterUniqueTogether( 14 | name='authenticatoruser', 15 | unique_together={('provider', 'uid')}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /ansible_base/authentication/migrations/0011_authenticatormap_role_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-05-28 08:15 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('dab_authentication', '0010_alter_authenticatoruser_unique_together'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='authenticatormap', 15 | name='role', 16 | field=models.CharField(blank=True, default=None, help_text='The role this map will grant the authenticating user to the targeted object', max_length=512, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='authenticatormap', 20 | name='map_type', 21 | field=models.CharField(choices=[('allow', 'allow'), ('is_superuser', 'is_superuser'), ('role', 'role'), ('organization', 'organization'), ('team', 'team')], default='team', help_text='What does the map work on, a team, organization, a user flag or is this an allow rule', max_length=17), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /ansible_base/authentication/migrations/0012_alter_authenticatormap_map_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-06-06 17:04 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('dab_authentication', '0011_authenticatormap_role_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='authenticatormap', 15 | name='map_type', 16 | field=models.CharField(choices=[('allow', 'allow'), ('is_superuser', 'is_superuser'), ('role', 'role'), ('organization', 'organization'), ('team', 'team')], default='team', help_text='What will the map grant the user? System access (allow) a team or organization membership, the superuser flag or a role in the system', max_length=17), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /ansible_base/authentication/migrations/0013_alter_authenticator_order.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-07-31 20:25 2 | 3 | import ansible_base.authentication.models.authenticator 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('dab_authentication', '0012_alter_authenticatormap_map_type'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='authenticator', 16 | name='order', 17 | field=models.IntegerField(default=ansible_base.authentication.models.authenticator.get_next_authenticator_order, help_text='The order in which an authenticator will be tried. This only pertains to username/password authenticators'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /ansible_base/authentication/migrations/0014_authenticator_auto_migrate_users_to.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-09-10 14:32 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('dab_authentication', '0013_alter_authenticator_order'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='authenticator', 16 | name='auto_migrate_users_to', 17 | field=models.ForeignKey(help_text='Automatically move users from this authenticator to the target authenticator when a matching user logs in via the target authenticator. For this to work, the field used for the user ID on both authenticators needs to have the same value. This should only be used when migrating users between two authentication mechanisms that share the same user database (such as when both IDPs share the same LDAP user directory).', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='auto_migrate_users_from', to='dab_authentication.authenticator'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /ansible_base/authentication/migrations/0017_alter_authenticator_slug.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.17 on 2025-02-04 20:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('dab_authentication', '0016_alter_authenticatoruser_access_allowed_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='authenticator', 15 | name='slug', 16 | field=models.SlugField(default=None, editable=False, help_text='An immutable identifier for the authenticator; used to generate the sso uri for sso authenticator types', max_length=1024, unique=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /ansible_base/authentication/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/authentication/migrations/__init__.py -------------------------------------------------------------------------------- /ansible_base/authentication/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .authenticator import Authenticator 2 | from .authenticator_map import AuthenticatorMap 3 | from .authenticator_user import AuthenticatorUser 4 | 5 | __all__ = ( 6 | 'Authenticator', 7 | 'AuthenticatorMap', 8 | 'AuthenticatorUser', 9 | ) 10 | -------------------------------------------------------------------------------- /ansible_base/authentication/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | from .authenticator import AuthenticatorSerializer # noqa: 401 2 | from .authenticator_map import AuthenticatorMapSerializer # noqa: 401 3 | -------------------------------------------------------------------------------- /ansible_base/authentication/session.py: -------------------------------------------------------------------------------- 1 | from rest_framework import authentication 2 | 3 | 4 | class SessionAuthentication(authentication.SessionAuthentication): 5 | """ 6 | This class allows us to fail with a 401 if the user is not authenticated. 7 | """ 8 | 9 | def authenticate_header(self, request): 10 | return "Session" 11 | -------------------------------------------------------------------------------- /ansible_base/authentication/utils/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from django.contrib.auth.models import AbstractUser 4 | 5 | from ansible_base.authentication.authenticator_plugins.utils import get_authenticator_plugin 6 | from ansible_base.authentication.models import AuthenticatorUser 7 | from ansible_base.lib.utils.models import is_system_user 8 | 9 | 10 | def can_user_change_password(user: Optional[AbstractUser]) -> bool: 11 | """ 12 | See if the given user is allowed to change their password. 13 | True if they are authenticated from the `local` authenticator 14 | False otherwise. 15 | The system user can never change their password 16 | """ 17 | if user is None or is_system_user(user): 18 | # If we didn't actually get a user we can't say they can change their password 19 | # Or if we are the system user, we can not change our password ever 20 | return False 21 | 22 | auth_users = AuthenticatorUser.objects.filter(user=user) 23 | if auth_users.count() == 0: 24 | # If the user has no associations we can set a password for them so they can login through the local authenticator 25 | return True 26 | 27 | for auth_user in auth_users: 28 | try: 29 | plugin = get_authenticator_plugin(auth_user.provider.type) 30 | if plugin.type == 'local': 31 | return True 32 | except ImportError: 33 | pass 34 | 35 | return False 36 | -------------------------------------------------------------------------------- /ansible_base/authentication/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .authenticator import AuthenticatorViewSet # noqa: F401 2 | from .authenticator_map import AuthenticatorMapViewSet # noqa: F401 3 | from .authenticator_plugins import AuthenticatorPluginView # noqa: F401 4 | from .trigger_definition import TriggerDefinitionView # noqa: F401 5 | from .ui_auth import UIAuth # noqa: F401 6 | -------------------------------------------------------------------------------- /ansible_base/authentication/views/authenticator_map.py: -------------------------------------------------------------------------------- 1 | from rest_framework.viewsets import ModelViewSet 2 | 3 | from ansible_base.authentication.models import AuthenticatorMap 4 | from ansible_base.authentication.serializers import AuthenticatorMapSerializer 5 | from ansible_base.lib.utils.views.django_app_api import AnsibleBaseDjangoAppApiView 6 | 7 | 8 | class AuthenticatorMapViewSet(AnsibleBaseDjangoAppApiView, ModelViewSet): 9 | """ 10 | API endpoint that allows authenticator maps to be viewed or edited. 11 | """ 12 | 13 | queryset = AuthenticatorMap.objects.all().order_by("id") 14 | serializer_class = AuthenticatorMapSerializer 15 | -------------------------------------------------------------------------------- /ansible_base/authentication/views/authenticator_plugins.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | 3 | from ansible_base.authentication.authenticator_plugins.utils import get_authenticator_class, get_authenticator_plugins 4 | from ansible_base.lib.utils.views.django_app_api import AnsibleBaseDjangoAppApiView 5 | 6 | 7 | class AuthenticatorPluginView(AnsibleBaseDjangoAppApiView): 8 | def get(self, request, format=None): 9 | plugins = get_authenticator_plugins() 10 | resp = {"authenticators": []} 11 | 12 | for p in plugins: 13 | try: 14 | klass = get_authenticator_class(p) 15 | config = klass.configuration_class() 16 | config_schema = config.get_configuration_schema() 17 | resp['authenticators'].append( 18 | {"type": p, "configuration_schema": config_schema, "documentation_url": getattr(config, "documentation_url", None)} 19 | ) 20 | except ImportError as ie: 21 | # If we got an import error its already logged and we can move on 22 | if 'errors' not in resp: 23 | resp['errors'] = [] 24 | resp['errors'].append(ie.__str__()) 25 | 26 | resp['authenticators'] = sorted(resp['authenticators'], key=lambda k: k['type']) 27 | 28 | return Response(resp) 29 | -------------------------------------------------------------------------------- /ansible_base/authentication/views/trigger_definition.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | 3 | from ansible_base.authentication.utils.trigger_definition import TRIGGER_DEFINITION 4 | from ansible_base.lib.utils.views.django_app_api import AnsibleBaseDjangoAppApiView 5 | 6 | 7 | class TriggerDefinitionView(AnsibleBaseDjangoAppApiView): 8 | def get(self, request, format=None): 9 | return Response(TRIGGER_DEFINITION) 10 | -------------------------------------------------------------------------------- /ansible_base/feature_flags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/feature_flags/__init__.py -------------------------------------------------------------------------------- /ansible_base/feature_flags/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FeatureFlagsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'ansible_base.feature_flags' 7 | label = 'dab_feature_flags' 8 | verbose_name = 'Feature Flags' 9 | -------------------------------------------------------------------------------- /ansible_base/feature_flags/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/feature_flags/migrations/__init__.py -------------------------------------------------------------------------------- /ansible_base/feature_flags/serializers.py: -------------------------------------------------------------------------------- 1 | from flags.state import flag_state 2 | from rest_framework import serializers 3 | 4 | from .utils import get_django_flags 5 | 6 | 7 | class FeatureFlagSerializer(serializers.Serializer): 8 | """Serialize list of feature flags""" 9 | 10 | def to_representation(self) -> dict: 11 | return_data = {} 12 | feature_flags = get_django_flags() 13 | for feature_flag in feature_flags: 14 | return_data[feature_flag] = flag_state(feature_flag) 15 | 16 | return return_data 17 | -------------------------------------------------------------------------------- /ansible_base/feature_flags/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from ansible_base.feature_flags import views 4 | from ansible_base.feature_flags.apps import FeatureFlagsConfig 5 | 6 | app_name = FeatureFlagsConfig.label 7 | 8 | api_version_urls = [ 9 | path('feature_flags_state/', views.FeatureFlagsStateListView.as_view(), name='featureflags-list'), 10 | ] 11 | api_urls = [] 12 | root_urls = [] 13 | -------------------------------------------------------------------------------- /ansible_base/feature_flags/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def get_django_flags(): 5 | return getattr(settings, 'FLAGS', {}) 6 | -------------------------------------------------------------------------------- /ansible_base/feature_flags/views.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy as _ 2 | from rest_framework.response import Response 3 | 4 | from ansible_base.feature_flags.serializers import FeatureFlagSerializer 5 | from ansible_base.lib.utils.views.ansible_base import AnsibleBaseView 6 | 7 | from .utils import get_django_flags 8 | 9 | 10 | class FeatureFlagsStateListView(AnsibleBaseView): 11 | """ 12 | A view class for displaying feature flags 13 | """ 14 | 15 | serializer_class = FeatureFlagSerializer 16 | filter_backends = [] 17 | name = _('Feature Flags') 18 | http_method_names = ['get', 'head'] 19 | 20 | def get(self, request, format=None): 21 | self.serializer = FeatureFlagSerializer() 22 | return Response(self.serializer.to_representation()) 23 | 24 | def get_queryset(self): 25 | return get_django_flags() 26 | -------------------------------------------------------------------------------- /ansible_base/help_text_check/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/help_text_check/__init__.py -------------------------------------------------------------------------------- /ansible_base/help_text_check/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HelpTextCheckConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'ansible_base.help_text_check' 7 | label = 'dab_help_text_check' 8 | verbose_name = 'Django Model Help Text Checker' 9 | -------------------------------------------------------------------------------- /ansible_base/help_text_check/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/help_text_check/management/__init__.py -------------------------------------------------------------------------------- /ansible_base/help_text_check/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/help_text_check/management/commands/__init__.py -------------------------------------------------------------------------------- /ansible_base/help_text_check/urls.py: -------------------------------------------------------------------------------- 1 | api_version_urls = [] 2 | api_urls = [] 3 | -------------------------------------------------------------------------------- /ansible_base/jwt_consumer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/jwt_consumer/__init__.py -------------------------------------------------------------------------------- /ansible_base/jwt_consumer/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class JwtConsumerConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'ansible_base.jwt_consumer' 7 | label = 'dab_jwt_consumer' 8 | -------------------------------------------------------------------------------- /ansible_base/jwt_consumer/awx/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/jwt_consumer/awx/__init__.py -------------------------------------------------------------------------------- /ansible_base/jwt_consumer/awx/auth.py: -------------------------------------------------------------------------------- 1 | # Python 2 | import logging 3 | 4 | from ansible_base.jwt_consumer.common.auth import JWTAuthentication 5 | 6 | logger = logging.getLogger('ansible_base.jwt_consumer.awx.auth') 7 | 8 | 9 | class AwxJWTAuthentication(JWTAuthentication): 10 | use_rbac_permissions = True 11 | -------------------------------------------------------------------------------- /ansible_base/jwt_consumer/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/jwt_consumer/common/__init__.py -------------------------------------------------------------------------------- /ansible_base/jwt_consumer/common/exceptions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.exceptions import APIException 2 | 3 | # 498 is not a standard error code, so we have to manually define it 4 | HTTP_498_INVALID_TOKEN = 498 5 | 6 | 7 | class InvalidService(Exception): 8 | def __init__(self, service): 9 | super().__init__(f"This authentication class requires {service}.") 10 | 11 | 12 | class InvalidTokenException(APIException): 13 | status_code = HTTP_498_INVALID_TOKEN 14 | status_text = "Invalid Token" 15 | default_detail = "Invalid or expired token." 16 | default_code = "invalid_token" 17 | -------------------------------------------------------------------------------- /ansible_base/jwt_consumer/eda/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/jwt_consumer/eda/__init__.py -------------------------------------------------------------------------------- /ansible_base/jwt_consumer/eda/auth.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from drf_spectacular.extensions import OpenApiAuthenticationExtension 4 | 5 | from ansible_base.jwt_consumer.common.auth import JWTAuthentication 6 | 7 | logger = logging.getLogger("ansible_base.jwt_consumer.eda.auth") 8 | 9 | 10 | class EDAJWTAuthentication(JWTAuthentication): 11 | use_rbac_permissions = True 12 | 13 | 14 | class EDAJWTAuthScheme(OpenApiAuthenticationExtension): 15 | target_class = EDAJWTAuthentication 16 | name = "EDAJWTAuthentication" 17 | 18 | def get_security_definition(self, auto_schema): 19 | return {"type": "apiKey", "name": "X-DAB-JW-TOKEN", "in": "header"} 20 | -------------------------------------------------------------------------------- /ansible_base/jwt_consumer/hub/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/jwt_consumer/hub/__init__.py -------------------------------------------------------------------------------- /ansible_base/jwt_consumer/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/jwt_consumer/migrations/__init__.py -------------------------------------------------------------------------------- /ansible_base/jwt_consumer/urls.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.urls import re_path 4 | 5 | from ansible_base.jwt_consumer.apps import JwtConsumerConfig 6 | from ansible_base.jwt_consumer.views import PlatformUIRedirectView 7 | 8 | logger = logging.getLogger('ansible_base.jwt_consumer.urls') 9 | 10 | # This is a special case because the application has to include this in a very specific location 11 | # in order for the redirect to be picked up. 12 | # Therefore we will not add it to our standard api_urls/api_root_urls/root_url variables. 13 | 14 | app_name = JwtConsumerConfig.label 15 | urlpatterns = [ 16 | re_path(r'', PlatformUIRedirectView.as_view()), 17 | ] 18 | -------------------------------------------------------------------------------- /ansible_base/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/lib/__init__.py -------------------------------------------------------------------------------- /ansible_base/lib/abstract_models/__init__.py: -------------------------------------------------------------------------------- 1 | from .common import CommonModel, ImmutableCommonModel, NamedCommonModel, UniqueNamedCommonModel 2 | from .immutable import ImmutableModel 3 | from .organization import AbstractOrganization 4 | from .team import AbstractTeam 5 | from .user import AbstractDABUser 6 | 7 | __all__ = ( 8 | 'AbstractOrganization', 9 | 'AbstractTeam', 10 | 'AbstractDABUser', 11 | 'CommonModel', 12 | 'ImmutableModel', 13 | 'ImmutableCommonModel', 14 | 'NamedCommonModel', 15 | 'UniqueNamedCommonModel', 16 | ) 17 | -------------------------------------------------------------------------------- /ansible_base/lib/abstract_models/immutable.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class ImmutableModel(models.Model): 5 | """ 6 | A save-once (immutable) base model. Simply blocks any save attempts after the first. 7 | """ 8 | 9 | class Meta: 10 | abstract = True 11 | 12 | def save(self, *args, **kwargs): 13 | if self.pk: 14 | raise ValueError(f"{self.__class__.__name__} is immutable and cannot be modified.") 15 | 16 | return super().save(*args, **kwargs) 17 | -------------------------------------------------------------------------------- /ansible_base/lib/abstract_models/organization.py: -------------------------------------------------------------------------------- 1 | """Organization models.""" 2 | 3 | from django.db import models 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from .common import UniqueNamedCommonModel 7 | 8 | 9 | class AbstractOrganization(UniqueNamedCommonModel): 10 | """An abstract base class for organizations.""" 11 | 12 | class Meta: 13 | abstract = True 14 | 15 | description = models.TextField( 16 | null=False, 17 | default="", 18 | blank=True, 19 | help_text=_("The organization description."), 20 | ) 21 | -------------------------------------------------------------------------------- /ansible_base/lib/abstract_models/team.py: -------------------------------------------------------------------------------- 1 | """Organization models.""" 2 | 3 | from django.conf import settings 4 | from django.db import models 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | from .common import NamedCommonModel 8 | 9 | 10 | class AbstractTeam(NamedCommonModel): 11 | """ 12 | An abstract base class for teams. 13 | A team groups users that work on the same resources, so that permissions can be assigned efficiently. 14 | """ 15 | 16 | class Meta: 17 | abstract = True 18 | unique_together = [('organization', 'name')] 19 | ordering = ('organization__name', 'name') 20 | 21 | description = models.TextField( 22 | null=False, 23 | blank=True, 24 | default="", 25 | help_text=_("The team description."), 26 | ) 27 | 28 | organization = models.ForeignKey( 29 | settings.ANSIBLE_BASE_ORGANIZATION_MODEL, 30 | blank=False, 31 | null=False, 32 | on_delete=models.CASCADE, 33 | related_name="teams", 34 | help_text=_("The organization of this team."), 35 | ) 36 | -------------------------------------------------------------------------------- /ansible_base/lib/abstract_models/user.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser, UserManager 2 | 3 | 4 | class AbstractDABUser(AbstractUser): 5 | class Meta(AbstractUser.Meta): 6 | abstract = True 7 | 8 | all_objects = UserManager() 9 | objects = UserManager() 10 | -------------------------------------------------------------------------------- /ansible_base/lib/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from ansible_base.lib.admin.readonly import ReadOnlyAdmin 2 | 3 | __all__ = [ 4 | "ReadOnlyAdmin", 5 | ] 6 | -------------------------------------------------------------------------------- /ansible_base/lib/admin/readonly.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | 4 | class ReadOnlyAdmin(admin.ModelAdmin): 5 | """ 6 | A ModelAdmin that provides read-only access to the model. 7 | """ 8 | 9 | def has_add_permission(self, request, obj=None): 10 | return False 11 | 12 | def has_delete_permission(self, request, obj=None): 13 | return False 14 | 15 | def has_change_permission(self, request, obj=None): 16 | return False 17 | -------------------------------------------------------------------------------- /ansible_base/lib/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/lib/backends/__init__.py -------------------------------------------------------------------------------- /ansible_base/lib/backends/prefixed_user_auth.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | from django.contrib.auth.backends import ModelBackend 5 | 6 | PREFIX = getattr(settings, "RENAMED_USERNAME_PREFIX", None) 7 | logger = logging.getLogger('ansible_base.authentication.authenticator_plugins.github_enterprise_team') 8 | 9 | 10 | class PrefixedUserAuthenticationMixin: 11 | def authenticate(self, request, **kwargs): 12 | if not PREFIX: 13 | return None 14 | if username := kwargs.get("username", None): 15 | if not username.startswith(PREFIX): 16 | kwargs["username"] = PREFIX + username 17 | return super().authenticate(request, **kwargs) 18 | 19 | 20 | class PrefixedUserAuthBackend(PrefixedUserAuthenticationMixin, ModelBackend): 21 | pass 22 | -------------------------------------------------------------------------------- /ansible_base/lib/channels/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/lib/channels/__init__.py -------------------------------------------------------------------------------- /ansible_base/lib/checks.py: -------------------------------------------------------------------------------- 1 | import django.apps 2 | from django.core.checks import Error, register 3 | from django.db import models 4 | 5 | 6 | @register() 7 | def check_charfield_has_max_length(app_configs, **kwargs): 8 | errors = [] 9 | for model in django.apps.apps.get_models(): 10 | for field in model._meta.fields: 11 | if isinstance(field, models.CharField) and field.max_length is None: 12 | errors.append( 13 | Error( 14 | 'CharField must have a max_length', 15 | hint=f"Add max_length parameter for field '{field.name}' in {model.__name__}", 16 | id='ansible_base.E001', 17 | ) 18 | ) 19 | return errors 20 | -------------------------------------------------------------------------------- /ansible_base/lib/constants.py: -------------------------------------------------------------------------------- 1 | STATUS_GOOD = 'good' 2 | STATUS_FAILED = 'failed' 3 | STATUS_DEGRADED = 'degraded' 4 | -------------------------------------------------------------------------------- /ansible_base/lib/dynamic_config/__init__.py: -------------------------------------------------------------------------------- 1 | from .dynaconf_helpers import ( 2 | export, 3 | factory, 4 | load_dab_settings, 5 | load_envvars, 6 | load_python_file_with_injected_context, 7 | load_standard_settings_files, 8 | toggle_feature_flags, 9 | validate, 10 | ) 11 | 12 | __all__ = [ 13 | "export", 14 | "factory", 15 | "load_dab_settings", 16 | "load_envvars", 17 | "load_python_file_with_injected_context", 18 | "load_standard_settings_files", 19 | "toggle_feature_flags", 20 | "validate", 21 | ] 22 | -------------------------------------------------------------------------------- /ansible_base/lib/dynamic_config/dynamic_urls.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import logging 3 | 4 | from django.conf import settings 5 | 6 | logger = logging.getLogger('ansible_base.lib.dynamic_config.dynamic_urls') 7 | 8 | 9 | url_types = ['api_version_urls', 'root_urls', 'api_urls'] 10 | for url_type in url_types: 11 | globals()[url_type] = [] 12 | 13 | installed_apps = getattr(settings, 'INSTALLED_APPS', []) 14 | for app in installed_apps: 15 | if app.startswith('ansible_base.'): 16 | if not importlib.util.find_spec(f'{app}.urls'): 17 | logger.debug(f'Module {app} does not specify urls.py') 18 | continue 19 | url_module = __import__(f'{app}.urls', fromlist=url_types) 20 | logger.debug(f'Including URLS from {app}.urls') 21 | for url_type in ['api_version_urls', 'root_urls', 'api_urls']: 22 | urls = getattr(url_module, url_type, []) 23 | globals()[url_type].extend(urls) 24 | -------------------------------------------------------------------------------- /ansible_base/lib/logging/__init__.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | thread_local = threading.local() 4 | -------------------------------------------------------------------------------- /ansible_base/lib/logging/filters/__init__.py: -------------------------------------------------------------------------------- 1 | from ansible_base.lib.logging.filters.request_id import RequestIdFilter 2 | 3 | __all__ = ('RequestIdFilter',) 4 | -------------------------------------------------------------------------------- /ansible_base/lib/logging/filters/request_id.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import uuid 3 | 4 | from ansible_base.lib.logging import thread_local 5 | from ansible_base.lib.utils.collection import first_matching 6 | 7 | 8 | class RequestIdFilter(logging.Filter): 9 | def filter(self, record): 10 | """ 11 | This "filter" is used to add the request id to the log record. 12 | It will always return True, so that the message is always logged, 13 | even if there is no request id. 14 | """ 15 | 16 | # Always a default so we can use it in the logging formatter. 17 | record.request_id = "" 18 | 19 | request = getattr(thread_local, "request", None) 20 | 21 | if request is None: 22 | # request never got added to the thread local, so we can't add the request id to the log. 23 | # But we still want to log the message. 24 | return True 25 | 26 | headers = request.META.keys() 27 | request_id_header = first_matching(lambda x: x.lower().replace('-', '_') == "http_x_request_id", headers, default=None) 28 | if request_id_header is None: 29 | # We have a request, but no request id header. Still want to log the message. 30 | return True 31 | request_id = request.META.get(request_id_header) 32 | try: 33 | uuid.UUID(request_id) 34 | except ValueError: 35 | # Invalid request id, still want to log the message, but not with the invalid request id. 36 | pass 37 | else: 38 | record.request_id = request_id 39 | return True 40 | -------------------------------------------------------------------------------- /ansible_base/lib/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/lib/middleware/__init__.py -------------------------------------------------------------------------------- /ansible_base/lib/middleware/logging/__init__.py: -------------------------------------------------------------------------------- 1 | from ansible_base.lib.middleware.logging.log_request import LogRequestMiddleware, LogTracebackMiddleware 2 | 3 | __all__ = ('LogRequestMiddleware', 'LogTracebackMiddleware') 4 | -------------------------------------------------------------------------------- /ansible_base/lib/redis/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import RedisClient 2 | 3 | __all__ = ['RedisClient'] 4 | -------------------------------------------------------------------------------- /ansible_base/lib/routers/__init__.py: -------------------------------------------------------------------------------- 1 | from .association_resource_router import AssociationResourceRouter 2 | 3 | __all__ = ('AssociationResourceRouter',) 4 | -------------------------------------------------------------------------------- /ansible_base/lib/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/lib/serializers/__init__.py -------------------------------------------------------------------------------- /ansible_base/lib/serializers/mixins.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from rest_framework import serializers 4 | 5 | logger = logging.getLogger('ansible_base.lib.serializers.mixins') 6 | 7 | 8 | # Derived from: https://github.com/encode/django-rest-framework/discussions/8606 9 | class ImmutableFieldsMixin(serializers.ModelSerializer): 10 | # Mixin enabling the usage of Meta.immutable_fields for setting fields read_only after object creation. 11 | 12 | # Currently, using this without issues requires outside considerations: 13 | # 1. overrides to get_serializer for the related viewsets, 14 | # since by default, rest_framework's SimpleMetadata class does not try to provide initialize a serializer 15 | # with an instance value on elements with a primary key field. 16 | 17 | # See ansible_base.authentication.views.AuthenticatorViewSet for an example. 18 | # 2. The generated OpenAPI spec will treat immutable fields as valid parameters on PUT and PATCH endpoints 19 | 20 | def get_extra_kwargs(self): 21 | kwargs = super().get_extra_kwargs() 22 | immutable_fields = getattr(self.Meta, "immutable_fields", []) 23 | 24 | # Make field read_only if instance already exists 25 | for field in immutable_fields: 26 | kwargs.setdefault(field, {}) 27 | kwargs[field]["read_only"] = bool(self.instance) 28 | 29 | return kwargs 30 | -------------------------------------------------------------------------------- /ansible_base/lib/serializers/validation.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.db import transaction 4 | from django.utils.translation import gettext_lazy as _ 5 | from rest_framework.exceptions import APIException 6 | from rest_framework.fields import BooleanField 7 | 8 | from ansible_base.lib.utils.validation import to_python_boolean 9 | 10 | logger = logging.getLogger('ansible_base.lib.serializers.validation') 11 | 12 | validate_field = 'validate' 13 | 14 | 15 | class APIException202(APIException): 16 | status_code = 202 17 | 18 | 19 | class ValidationSerializerMixin: 20 | model_validate = BooleanField(default=False, write_only=True) 21 | 22 | def save(self, **kwargs): 23 | want_validate = to_python_boolean(self.context['request'].query_params.get(validate_field, False)) 24 | 25 | if not want_validate: 26 | return super().save(**kwargs) 27 | 28 | with transaction.atomic(): 29 | # If the save fails it will raise an exception and the transaction will be aborted 30 | super().save(**kwargs) 31 | # Otherwise we need to raise our own exception to roll back the transaction 32 | raise APIException202(_("Request would have been accepted")) 33 | -------------------------------------------------------------------------------- /ansible_base/lib/sessions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/lib/sessions/__init__.py -------------------------------------------------------------------------------- /ansible_base/lib/sessions/stores/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/lib/sessions/stores/__init__.py -------------------------------------------------------------------------------- /ansible_base/lib/sessions/stores/cached_dynamic_timeout.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.contrib.sessions.backends.cached_db import SessionStore as CachedDBSessionStore 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from ansible_base.lib.utils.settings import get_setting 7 | 8 | DEFAULT_SESSION_TIMEOUT = 30 * 60 9 | logger = logging.getLogger('ansible_base.lib.sessions.stores.cached_dynamic_timeout') 10 | 11 | 12 | class SessionStore(CachedDBSessionStore): 13 | cache_key_prefix = 'ansible_base.lib.sessions.stores.cached_dynamic_timeout' 14 | 15 | def get_session_cookie_age(self): 16 | timeout = get_setting('SESSION_COOKIE_AGE', DEFAULT_SESSION_TIMEOUT) 17 | if not isinstance(timeout, int): 18 | logger.error( 19 | _('SESSION_COOKIE_AGE was set to %(timeout)s which is an invalid int, defaulting to %(default)s') 20 | % {'timeout': timeout, 'default': DEFAULT_SESSION_TIMEOUT} 21 | ) 22 | timeout = DEFAULT_SESSION_TIMEOUT 23 | return timeout 24 | -------------------------------------------------------------------------------- /ansible_base/lib/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/lib/templatetags/__init__.py -------------------------------------------------------------------------------- /ansible_base/lib/templatetags/requests.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from ansible_base.lib.utils import requests as dab_requests 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.simple_tag 9 | def is_proxied_request(): 10 | return dab_requests.is_proxied_request() 11 | -------------------------------------------------------------------------------- /ansible_base/lib/templatetags/util.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from django import template 5 | from django.conf import settings 6 | from django.utils.safestring import mark_safe 7 | 8 | logger = logging.getLogger("ansible_base.lib.templatetags.util") 9 | register = template.Library() 10 | 11 | 12 | @register.simple_tag 13 | def inline_file(relative_path, is_safe=False, fatal=False): 14 | path = os.path.join(settings.BASE_DIR, relative_path) 15 | try: 16 | with open(path, 'r') as file: 17 | contents = file.read() 18 | if is_safe: 19 | contents = mark_safe(contents) 20 | return contents 21 | except Exception: 22 | logger.exception(f"Failed to read file {path}") 23 | if fatal: 24 | raise 25 | -------------------------------------------------------------------------------- /ansible_base/lib/testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/lib/testing/__init__.py -------------------------------------------------------------------------------- /ansible_base/lib/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/lib/utils/__init__.py -------------------------------------------------------------------------------- /ansible_base/lib/utils/collection.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for Python collections 3 | """ 4 | 5 | 6 | def first_matching(predicate, collection, default=StopIteration("No item matched the predicate.")): 7 | """ 8 | Return the first element from `collection` that satisfies `predicate`. 9 | If `default` is an exception, raise it if no element satisfies `predicate`. 10 | If `default` is not an exception, return it if no element satisfies `predicate`. 11 | 12 | :param predicate: A function that takes an element from `collection` and returns a boolean. 13 | :param collection: An iterable. 14 | :param default: The value to return if no element satisfies `predicate`. 15 | :return: The first element from `collection` that satisfies `predicate`. 16 | If no element satisfies `predicate`, return `default`, or raise an exception if `default` is an exception. 17 | """ 18 | 19 | for i in collection: 20 | if predicate(i): 21 | return i 22 | 23 | if isinstance(default, Exception): 24 | raise default 25 | 26 | return default 27 | -------------------------------------------------------------------------------- /ansible_base/lib/utils/hashing.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | from typing import Callable, Optional, Type 4 | 5 | from django.db.models import Model 6 | from rest_framework.serializers import Serializer 7 | 8 | 9 | def hash_serializer_data(instance: Model, serializer: Type[Serializer], field: Optional[str] = None, hasher: Callable = hashlib.sha256): 10 | """ 11 | Takes an instance, serialize it and take the .data or the specified field 12 | as input for the hasher function. 13 | """ 14 | serialized_data = serializer(instance).data 15 | if field: 16 | serialized_data = serialized_data[field] 17 | metadata_json = json.dumps(serialized_data, sort_keys=True).encode("utf-8") 18 | return hasher(metadata_json).hexdigest() 19 | 20 | 21 | def hash_string(inp: str, hasher: Callable = hashlib.sha256, algo=""): 22 | """ 23 | Takes a string and hashes it with the given hasher function. 24 | If algo is given, it is prepended to the hash between dollar signs ($) 25 | before the hash is returned. 26 | 27 | NOTE: There is no salt or pepper here, so this is not secure for passwords. 28 | It is, however, useful for *random* strings like tokens, that need to be secured. 29 | """ 30 | hash = hasher(inp.encode("utf-8")).hexdigest() 31 | if algo: 32 | return f"${algo}${hash}" 33 | return hash 34 | -------------------------------------------------------------------------------- /ansible_base/lib/utils/string.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from django.utils.encoding import smart_str 4 | 5 | 6 | def make_json_safe(value): 7 | if isinstance(value, (list, dict, str, int, float, bool, type(None))): 8 | return value 9 | 10 | return smart_str(value) 11 | 12 | 13 | def is_empty(value: Optional[str]) -> bool: 14 | """Checks if the value is an empty string (stripping whitespaces)""" 15 | return value is None or str(value).strip() == '' 16 | -------------------------------------------------------------------------------- /ansible_base/lib/utils/translations.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext 2 | 3 | ''' 4 | Simple helper class that can be used to translate messages or not translate messages. 5 | Ex: Function that calls a raise ValidationError and logger.error one after the other. 6 | ''' 7 | 8 | 9 | class translatableConditionally: 10 | def __init__(self, message): 11 | self.message = message 12 | 13 | def not_translated(self): 14 | return self.message 15 | 16 | def translated(self): 17 | return gettext(self.message) 18 | -------------------------------------------------------------------------------- /ansible_base/lib/utils/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/lib/utils/views/__init__.py -------------------------------------------------------------------------------- /ansible_base/lib/utils/views/urls.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | 3 | from rest_framework.schemas.generators import EndpointEnumerator 4 | from rest_framework.views import APIView 5 | 6 | 7 | def get_api_view_functions(urlpatterns=None) -> set[Type[APIView]]: 8 | """ 9 | Extract view classes from a urlpatterns list using the show_urls helper functions 10 | 11 | :param urlpatterns: django urlpatterns list 12 | :return: set of all view classes used by the urlpatterns list 13 | """ 14 | views = set() 15 | 16 | enumerator = EndpointEnumerator() 17 | # Get all active APIViews from urlconf 18 | endpoints = enumerator.get_api_endpoints(patterns=urlpatterns) 19 | for _, _, func in endpoints: 20 | # ApiView.as_view() breadcrumb 21 | if hasattr(func, 'cls'): 22 | views.add(func.cls) 23 | 24 | return views 25 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/oauth2_provider/__init__.py -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin # noqa: F401 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.core.checks import register 3 | 4 | 5 | class Oauth2ProviderConfig(AppConfig): 6 | default_auto_field = 'django.db.models.BigAutoField' 7 | name = 'ansible_base.oauth2_provider' 8 | label = 'dab_oauth2_provider' 9 | 10 | def ready(self): 11 | # Load checks 12 | from ansible_base.oauth2_provider.checks.permisssions_check import oauth2_permission_scope_check 13 | 14 | register(oauth2_permission_scope_check, "oauth2_permissions", deploy=True) 15 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/checks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/oauth2_provider/checks/__init__.py -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/management/commands/cleanup_tokens.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.core.management import call_command 4 | from django.core.management.base import BaseCommand 5 | 6 | from ansible_base.oauth2_provider.models import OAuth2AccessToken, OAuth2RefreshToken 7 | 8 | 9 | class Command(BaseCommand): 10 | def init_logging(self): 11 | log_levels = dict(enumerate([logging.ERROR, logging.INFO, logging.DEBUG, 0])) 12 | self.logger = logging.getLogger('ansible_base.oauth2_provider.commands.cleanup_tokens') 13 | self.logger.setLevel(log_levels.get(self.verbosity, 0)) 14 | handler = logging.StreamHandler(stream=self.stream) 15 | handler.setFormatter(logging.Formatter('%(message)s')) 16 | self.logger.addHandler(handler) 17 | self.logger.propagate = False 18 | 19 | def execute(self, *args, **options): 20 | self.verbosity = int(options.get('verbosity', 1)) 21 | self.stream = options.get('stderr') 22 | self.init_logging() 23 | total_accesstokens = OAuth2AccessToken.objects.all().count() 24 | total_refreshtokens = OAuth2RefreshToken.objects.all().count() 25 | call_command("cleartokens") 26 | self.logger.info("Expired OAuth 2 Access Tokens deleted: {}".format(total_accesstokens - OAuth2AccessToken.objects.all().count())) 27 | self.logger.info("Expired OAuth 2 Refresh Tokens deleted: {}".format(total_refreshtokens - OAuth2RefreshToken.objects.all().count())) 28 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/management/commands/create_oauth2_token.py: -------------------------------------------------------------------------------- 1 | # Django 2 | from django.contrib.auth import get_user_model 3 | from django.core.exceptions import ObjectDoesNotExist 4 | from django.core.management.base import BaseCommand, CommandError 5 | 6 | from ansible_base.oauth2_provider.serializers import OAuth2TokenSerializer 7 | 8 | User = get_user_model() 9 | 10 | 11 | class Command(BaseCommand): 12 | """Command that creates an OAuth2 token for a certain user. Returns the value of created token.""" 13 | 14 | help = 'Creates an OAuth2 token for a user.' 15 | 16 | def add_arguments(self, parser): 17 | parser.add_argument('--user', dest='user', type=str) 18 | 19 | def handle(self, *args, **options): 20 | if not options['user']: 21 | raise CommandError('Username not supplied. Usage: create_oauth2_token --user=username.') 22 | try: 23 | user = User.objects.get(username=options['user']) 24 | except ObjectDoesNotExist: 25 | raise CommandError('The user does not exist.') 26 | config = {'user': user, 'scope': 'write'} 27 | serializer_obj = OAuth2TokenSerializer() 28 | 29 | class FakeRequest(object): 30 | def __init__(self): 31 | self.user = user 32 | 33 | serializer_obj.context['request'] = FakeRequest() 34 | serializer_obj.create(config) 35 | self.stdout.write(serializer_obj.unencrypted_token) 36 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/migrations/0003_remove_oauth2application_logo_data.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-05-09 16:12 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('dab_oauth2_provider', '0002_alter_oauth2refreshtoken_options_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='oauth2application', 15 | name='logo_data', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/migrations/0004_alter_oauth2accesstoken_scope.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-06-06 22:46 2 | 3 | import ansible_base.oauth2_provider.models.access_token 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('dab_oauth2_provider', '0003_remove_oauth2application_logo_data'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='oauth2accesstoken', 16 | name='scope', 17 | field=models.CharField(default='write', help_text="Allowed scopes, further restricts user's permissions. Must be a simple space-separated string with allowed scopes ['read', 'write'].", max_length=32, validators=[ansible_base.oauth2_provider.models.access_token.validate_scope]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/migrations/0005_hash_existing_tokens.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | from ansible_base.oauth2_provider.migrations._utils import hash_tokens 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("dab_oauth2_provider", "0004_alter_oauth2accesstoken_scope"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RunPython(hash_tokens), 13 | ] 14 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/migrations/0008_oauth2application_app_url.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.17 on 2025-02-25 14:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('dab_oauth2_provider', '0007_alter_oauth2accesstoken_application_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='oauth2application', 15 | name='app_url', 16 | field=models.URLField(blank=True, default=None, help_text='The URL of this application.', null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/oauth2_provider/migrations/__init__.py -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/migrations/_utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from ansible_base.lib.utils.hashing import hash_string 4 | 5 | 6 | def hash_tokens(apps, schema_editor): 7 | OAuth2AccessToken = apps.get_model("dab_oauth2_provider", "OAuth2AccessToken") 8 | OAuth2RefreshToken = apps.get_model("dab_oauth2_provider", "OAuth2RefreshToken") 9 | for model in (OAuth2AccessToken, OAuth2RefreshToken): 10 | for token in model.objects.all(): 11 | # Never re-hash a hashed token 12 | if token.token.startswith("$"): 13 | continue 14 | hashed = hash_string(token.token, hasher=hashlib.sha256, algo="sha256") 15 | token.token = hashed 16 | token.save() 17 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/models/id_token.py: -------------------------------------------------------------------------------- 1 | import oauth2_provider.models as oauth2_models 2 | from django.conf import settings 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from ansible_base.lib.abstract_models.common import CommonModel 6 | 7 | activitystream = object 8 | if 'ansible_base.activitystream' in settings.INSTALLED_APPS: 9 | from ansible_base.activitystream.models import AuditableModel 10 | 11 | activitystream = AuditableModel 12 | 13 | 14 | class OAuth2IDToken(CommonModel, oauth2_models.AbstractIDToken, activitystream): 15 | class Meta(oauth2_models.AbstractIDToken.Meta): 16 | verbose_name = _('id token') 17 | swappable = "OAUTH2_PROVIDER_ID_TOKEN_MODEL" 18 | 19 | updated = None # Tracked in CommonModel with 'modified', no need for this 20 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/models/refresh_token.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | import oauth2_provider.models as oauth2_models 4 | from django.conf import settings 5 | from django.db import models 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | from ansible_base.lib.abstract_models.common import CommonModel 9 | from ansible_base.lib.utils.hashing import hash_string 10 | from ansible_base.lib.utils.models import prevent_search 11 | 12 | activitystream = object 13 | if 'ansible_base.activitystream' in settings.INSTALLED_APPS: 14 | from ansible_base.activitystream.models import AuditableModel 15 | 16 | activitystream = AuditableModel 17 | 18 | 19 | class OAuth2RefreshToken(CommonModel, oauth2_models.AbstractRefreshToken, activitystream): 20 | class Meta(oauth2_models.AbstractRefreshToken.Meta): 21 | verbose_name = _('refresh token') 22 | ordering = ('id',) 23 | swappable = "OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL" 24 | 25 | token = prevent_search(models.CharField(max_length=255, help_text=_("The refresh token value."))) 26 | updated = None # Tracked in CommonModel with 'modified', no need for this 27 | 28 | def save(self, *args, **kwargs): 29 | if not self.pk: 30 | self.token = hash_string(self.token, hasher=hashlib.sha256, algo="sha256") 31 | super().save(*args, **kwargs) 32 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/permissions.py: -------------------------------------------------------------------------------- 1 | import oauth2_provider.models as oauth2_models 2 | from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope 3 | from rest_framework.permissions import BasePermission, IsAuthenticated 4 | 5 | # This is not in views/permissions.py because we import that in other oauth2_provider files 6 | # and when we try to specify this OAuth2ScopePermission in an app's settings.py, we get a 7 | # cyclic dependency. 8 | 9 | 10 | class OAuth2ScopePermission(BasePermission): 11 | """ 12 | A DRF Permission class to be used by apps that functions in the following way: 13 | 14 | - If an OAuth 2 token is used to authenticate, then its scope must contain the required scope 15 | (i.e. "read" cannot use "unsafe" methods) 16 | - Otherwise, fall back. 17 | """ 18 | 19 | def has_permission(self, request, view): 20 | is_authenticated = IsAuthenticated().has_permission(request, view) 21 | is_oauth = False 22 | has_oauth_permission = False 23 | if is_authenticated and request.auth and isinstance(request.auth, oauth2_models.AbstractAccessToken): 24 | is_oauth = True 25 | scopes = request.auth.scope.split() 26 | if 'write' in scopes and 'read' not in scopes: 27 | request.auth.scope += ' read' # write implies read 28 | token_permission = TokenHasReadWriteScope() 29 | has_oauth_permission = token_permission.has_permission(request, view) 30 | return is_authenticated and (not is_oauth or has_oauth_permission) 31 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | from .application import OAuth2ApplicationSerializer # noqa: F401 2 | from .token import OAuth2TokenSerializer # noqa: F401 3 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path, re_path 2 | from oauth2_provider import views as oauth_views 3 | 4 | from ansible_base.lib.routers import AssociationResourceRouter 5 | from ansible_base.oauth2_provider import views as oauth2_provider_views 6 | from ansible_base.oauth2_provider.apps import Oauth2ProviderConfig 7 | 8 | app_name = Oauth2ProviderConfig.label 9 | 10 | router = AssociationResourceRouter() 11 | 12 | router.register( 13 | r'applications', 14 | oauth2_provider_views.OAuth2ApplicationViewSet, 15 | basename='application', 16 | related_views={ 17 | 'tokens': (oauth2_provider_views.OAuth2TokenViewSet, 'access_tokens'), 18 | }, 19 | ) 20 | 21 | router.register( 22 | r'tokens', 23 | oauth2_provider_views.OAuth2TokenViewSet, 24 | basename='token', 25 | ) 26 | 27 | api_version_urls = [ 28 | path('', include(router.urls)), 29 | ] 30 | 31 | root_urls = [ 32 | re_path(r'^o/$', oauth2_provider_views.ApiOAuthAuthorizationRootView.as_view(), name='oauth_authorization_root_view'), 33 | re_path(r"^o/authorize/$", oauth_views.AuthorizationView.as_view(), name="authorize"), 34 | re_path(r"^o/token/$", oauth2_provider_views.TokenView.as_view(), name="token"), 35 | re_path(r"^o/revoke_token/$", oauth_views.RevokeTokenView.as_view(), name="revoke-token"), 36 | ] 37 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from django.contrib.auth import get_user_model 4 | 5 | from ansible_base.authentication.models import Authenticator 6 | 7 | User = get_user_model() 8 | 9 | 10 | def is_external_account(user: User) -> Optional[Authenticator]: 11 | """ 12 | Determines whether the user is associated with any external 13 | login source. If they are, return the source. Otherwise, None. 14 | 15 | :param user: The user to test 16 | :return: If the user is associated with any external login source, return it (the first, if multiple) 17 | Otherwise, return None 18 | """ 19 | authenticator_users = user.authenticator_users.all() 20 | local = 'ansible_base.authentication.authenticator_plugins.local' 21 | for auth_user in authenticator_users: 22 | if auth_user.provider.type != local: 23 | return auth_user.provider 24 | 25 | return None 26 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .application import OAuth2ApplicationViewSet # noqa: F401 2 | from .authorization_root import ApiOAuthAuthorizationRootView # noqa: F401 3 | from .token import OAuth2TokenViewSet, TokenView # noqa: F401 4 | from .user_mixin import DABOAuth2UserViewsetMixin # noqa: F401 5 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/views/application.py: -------------------------------------------------------------------------------- 1 | from rest_framework.viewsets import ModelViewSet 2 | 3 | from ansible_base.lib.utils.views.django_app_api import AnsibleBaseDjangoAppApiView 4 | from ansible_base.lib.utils.views.permissions import IsSuperuserOrAuditor 5 | from ansible_base.oauth2_provider.models import OAuth2Application 6 | from ansible_base.oauth2_provider.permissions import OAuth2ScopePermission 7 | from ansible_base.oauth2_provider.serializers import OAuth2ApplicationSerializer 8 | 9 | 10 | class OAuth2ApplicationViewSet(AnsibleBaseDjangoAppApiView, ModelViewSet): 11 | queryset = OAuth2Application.objects.all() 12 | serializer_class = OAuth2ApplicationSerializer 13 | permission_classes = [OAuth2ScopePermission, IsSuperuserOrAuditor] 14 | -------------------------------------------------------------------------------- /ansible_base/oauth2_provider/views/authorization_root.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from django.utils.translation import gettext_lazy as _ 4 | from rest_framework import permissions 5 | from rest_framework.response import Response 6 | 7 | from ansible_base.lib.utils.response import get_relative_url 8 | from ansible_base.lib.utils.views.django_app_api import AnsibleBaseDjangoAppApiView 9 | 10 | 11 | class ApiOAuthAuthorizationRootView(AnsibleBaseDjangoAppApiView): 12 | permission_classes = (permissions.AllowAny,) 13 | name = _("API OAuth 2 Authorization Root") 14 | versioning_class = None 15 | swagger_topic = 'Authentication' 16 | 17 | def get(self, request, format=None): 18 | data = OrderedDict() 19 | data['authorize'] = get_relative_url('authorize') 20 | data['revoke_token'] = get_relative_url('revoke-token') 21 | data['token'] = get_relative_url('token') 22 | return Response(data) 23 | -------------------------------------------------------------------------------- /ansible_base/rbac/__init__.py: -------------------------------------------------------------------------------- 1 | from ansible_base.rbac.permission_registry import permission_registry 2 | 3 | __all__ = [ 4 | 'permission_registry', 5 | ] 6 | -------------------------------------------------------------------------------- /ansible_base/rbac/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from ansible_base.lib.admin import ReadOnlyAdmin 4 | from ansible_base.rbac.models import ObjectRole, RoleDefinition, RoleEvaluation, RoleTeamAssignment, RoleUserAssignment 5 | 6 | admin.site.register(RoleDefinition) 7 | # TODO: assignments will still not be functional in the admin pages without custom logic 8 | admin.site.register(RoleUserAssignment) 9 | admin.site.register(RoleTeamAssignment) 10 | admin.site.register(ObjectRole, ReadOnlyAdmin) 11 | admin.site.register(RoleEvaluation, ReadOnlyAdmin) 12 | -------------------------------------------------------------------------------- /ansible_base/rbac/api/router.py: -------------------------------------------------------------------------------- 1 | from ansible_base.lib.routers import AssociationResourceRouter 2 | from ansible_base.rbac.api import views 3 | 4 | router = AssociationResourceRouter() 5 | 6 | router.register( 7 | r'role_definitions', 8 | views.RoleDefinitionViewSet, 9 | related_views={ 10 | 'user_assignments': (views.RoleUserAssignmentViewSet, 'user_assignments'), 11 | 'team_assignments': (views.RoleTeamAssignmentViewSet, 'team_assignments'), 12 | }, 13 | basename='roledefinition', 14 | ) 15 | router.register(r'role_user_assignments', views.RoleUserAssignmentViewSet, basename='roleuserassignment') 16 | router.register(r'role_team_assignments', views.RoleTeamAssignmentViewSet, basename='roleteamassignment') 17 | -------------------------------------------------------------------------------- /ansible_base/rbac/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | from ansible_base.rbac.permission_registry import permission_registry 4 | 5 | 6 | class AnsibleRBACConfig(AppConfig): 7 | default_auto_field = 'django.db.models.BigAutoField' 8 | name = 'ansible_base.rbac' 9 | label = 'dab_rbac' 10 | verbose_name = 'DAB shared RBAC' 11 | 12 | def ready(self): 13 | permission_registry.call_when_apps_ready(self.apps, self) 14 | -------------------------------------------------------------------------------- /ansible_base/rbac/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/rbac/migrations/__init__.py -------------------------------------------------------------------------------- /ansible_base/rbac/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from ansible_base.rbac.api.router import router 4 | from ansible_base.rbac.api.views import RoleMetadataView 5 | from ansible_base.rbac.apps import AnsibleRBACConfig 6 | 7 | app_name = AnsibleRBACConfig.label 8 | 9 | api_version_urls = [ 10 | path('', include(router.urls)), 11 | path(r'role_metadata/', RoleMetadataView.as_view(), name="role-metadata"), 12 | ] 13 | 14 | root_urls = [] 15 | 16 | api_urls = [] 17 | -------------------------------------------------------------------------------- /ansible_base/resource_registry/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/resource_registry/__init__.py -------------------------------------------------------------------------------- /ansible_base/resource_registry/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/resource_registry/management/__init__.py -------------------------------------------------------------------------------- /ansible_base/resource_registry/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/resource_registry/management/commands/__init__.py -------------------------------------------------------------------------------- /ansible_base/resource_registry/migrations/0003_alter_resource_object_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.6 on 2024-03-02 14:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('dab_resource_registry', '0002_remove_resource_id'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='resource', 15 | name='object_id', 16 | field=models.TextField(db_index=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /ansible_base/resource_registry/migrations/0004_remove_resourcetype_migrated.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-03-29 19:51 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('dab_resource_registry', '0003_alter_resource_object_id'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='resourcetype', 15 | name='migrated', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /ansible_base/resource_registry/migrations/0005_resource_is_partially_migrated_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-08-08 19:52 2 | 3 | import ansible_base.resource_registry.models.service_identifier 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('dab_resource_registry', '0004_remove_resourcetype_migrated'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='resource', 16 | name='is_partially_migrated', 17 | field=models.BooleanField(default=False, help_text="This gets set to True when a resource has been copied into the resource server, but the service_id hasn't been updated yet."), 18 | ), 19 | migrations.AlterField( 20 | model_name='resource', 21 | name='service_id', 22 | field=models.UUIDField(default=ansible_base.resource_registry.models.service_identifier.service_id, help_text='ID of the service responsible for managing this resource.'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /ansible_base/resource_registry/migrations/0006_alter_resource_service_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-11-21 11:14 2 | 3 | import ansible_base.resource_registry.models.service_identifier 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('dab_resource_registry', '0005_resource_is_partially_migrated_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='resource', 16 | name='service_id', 17 | field=models.UUIDField(default=ansible_base.resource_registry.models.service_identifier.service_id, help_text='The ID of the service responsible for managing this resource.'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /ansible_base/resource_registry/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/resource_registry/migrations/__init__.py -------------------------------------------------------------------------------- /ansible_base/resource_registry/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .resource import Resource, ResourceType, init_resource_from_object # noqa: 401 2 | from .service_identifier import service_id # noqa: 401 3 | -------------------------------------------------------------------------------- /ansible_base/resource_registry/models/service_identifier.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.db import models 4 | 5 | 6 | class ServiceID(models.Model): 7 | """ 8 | Provides a globally unique ID for this service. 9 | """ 10 | 11 | id = models.UUIDField(default=uuid.uuid4, primary_key=True, null=False, editable=False) 12 | 13 | def save(self, *args, **kwargs): 14 | if ServiceID.objects.exists(): 15 | raise RuntimeError("This service already has a ServiceID") 16 | 17 | return super().save() 18 | 19 | 20 | _service_id = None 21 | 22 | 23 | def service_id(): 24 | global _service_id 25 | if not _service_id: 26 | _service_id = str(ServiceID.objects.first().pk) 27 | return _service_id 28 | -------------------------------------------------------------------------------- /ansible_base/resource_registry/resource_server.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import TypedDict 3 | 4 | import jwt 5 | from django.conf import settings 6 | 7 | from ansible_base.resource_registry.models import service_id 8 | 9 | 10 | class ResourceServerConfig(TypedDict): 11 | URL: str 12 | SECRET_KEY: str 13 | VALIDATE_HTTPS: bool 14 | JWT_ALGORITHM: str 15 | 16 | 17 | def get_resource_server_config() -> ResourceServerConfig: 18 | defaults = {"JWT_ALGORITHM": "HS256", "VALIDATE_HTTPS": True} 19 | defaults.update(settings.RESOURCE_SERVER) 20 | return defaults 21 | 22 | 23 | def get_service_token(user_id=None, expiration=60, **kwargs): 24 | config = get_resource_server_config() 25 | payload = { 26 | "iss": str(service_id()), 27 | **kwargs, 28 | } 29 | 30 | if user_id is not None: 31 | payload["sub"] = user_id 32 | 33 | if expiration is not None: 34 | payload["exp"] = datetime.now() + timedelta(seconds=expiration) 35 | 36 | return jwt.encode(payload, config["SECRET_KEY"], config["JWT_ALGORITHM"]) 37 | -------------------------------------------------------------------------------- /ansible_base/resource_registry/signals/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/resource_registry/signals/__init__.py -------------------------------------------------------------------------------- /ansible_base/resource_registry/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/resource_registry/tasks/__init__.py -------------------------------------------------------------------------------- /ansible_base/resource_registry/urls.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.urls import include, path 4 | from rest_framework import routers 5 | 6 | from ansible_base.resource_registry import views 7 | 8 | logger = logging.getLogger('ansible_base.resource-urls') 9 | 10 | service_router = routers.SimpleRouter() 11 | 12 | service_router.register(r'resources', views.ResourceViewSet) 13 | service_router.register(r'resource-types', views.ResourceTypeViewSet) 14 | 15 | service = [ 16 | path('metadata/', views.ServiceMetadataView.as_view(), name="service-metadata"), 17 | path('validate-local-account/', views.ValidateLocalUserView.as_view(), name="validate-local-account"), 18 | path('', include(service_router.urls)), 19 | path('', views.ServiceIndexRootView.as_view(), name='service-index-root'), 20 | ] 21 | 22 | urlpatterns = [ 23 | path('service-index/', include(service)), 24 | ] 25 | -------------------------------------------------------------------------------- /ansible_base/resource_registry/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/resource_registry/utils/__init__.py -------------------------------------------------------------------------------- /ansible_base/resource_registry/utils/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def resource_server_defined() -> bool: 5 | return bool(getattr(settings, 'RESOURCE_SERVER', {}).get('URL', '')) 6 | -------------------------------------------------------------------------------- /ansible_base/rest_filters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/rest_filters/__init__.py -------------------------------------------------------------------------------- /ansible_base/rest_filters/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | import ansible_base.lib.checks # noqa: F401 - register checks 4 | 5 | 6 | class RestFiltersConfig(AppConfig): 7 | default_auto_field = 'django.db.models.BigAutoField' 8 | name = 'ansible_base.rest_filters' 9 | label = 'dab_rest_filters' 10 | -------------------------------------------------------------------------------- /ansible_base/rest_filters/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/rest_filters/migrations/__init__.py -------------------------------------------------------------------------------- /ansible_base/rest_filters/rest_framework/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/rest_filters/rest_framework/__init__.py -------------------------------------------------------------------------------- /ansible_base/rest_pagination/__init__.py: -------------------------------------------------------------------------------- 1 | from .default_paginator import DefaultPaginator # noqa: F401 2 | -------------------------------------------------------------------------------- /ansible_base/rest_pagination/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RestPaginationConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'ansible_base.rest_pagination' 7 | label = 'dab_rest_pagination' 8 | -------------------------------------------------------------------------------- /ansible_base/rest_pagination/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/ansible_base/rest_pagination/migrations/__init__.py -------------------------------------------------------------------------------- /compose/README.md: -------------------------------------------------------------------------------- 1 | ## `compose` Directory 2 | This directory contains the Dockerfile and configuration files for Nginx and Ingress services used in the `test_app`. 3 | At the moment, this code is only for testing and development purposes, and is not used in production. 4 | 5 | -------------------------------------------------------------------------------- /compose/ingress/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile to build the image for dab_ingress service 2 | 3 | FROM mirror.gcr.io/library/nginx:1.27 4 | 5 | RUN mkdir -p /etc/nginx/ssl && chown -R nginx:nginx /etc/nginx/ssl 6 | COPY entrypoint.sh /entrypoint.sh 7 | COPY nginx.conf /etc/nginx/conf.d/default.conf 8 | 9 | # Modify the default Nginx configuration to allow it to run as a non-root user 10 | # This follows Nginx Docker Hub instructions: https://hub.docker.com/_/nginx 11 | # By default, Nginx writes its PID file to /var/run/nginx.pid, which is a restricted location. 12 | # We change it to /tmp/nginx.pid so that it can be accessed by a non-root user. 13 | RUN sed -i -E '/^user\s+nginx;/d' /etc/nginx/nginx.conf && \ 14 | sed -i -E 's|pid\s+/var/run/nginx.pid;|pid /tmp/nginx.pid;|' /etc/nginx/nginx.conf && \ 15 | sed -i '/http {/a \ 16 | client_body_temp_path /tmp/client_temp;\n\ 17 | proxy_temp_path /tmp/proxy_temp;\n\ 18 | fastcgi_temp_path /tmp/fastcgi_temp;\n\ 19 | uwsgi_temp_path /tmp/uwsgi_temp;\n\ 20 | scgi_temp_path /tmp/scgi_temp;' /etc/nginx/nginx.conf 21 | 22 | USER nginx -------------------------------------------------------------------------------- /compose/ingress/certs/.keepme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/compose/ingress/certs/.keepme -------------------------------------------------------------------------------- /compose/ingress/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | if [[ ! -f /etc/nginx/ssl/selfsigned.key ]]; then 4 | openssl req \ 5 | -x509 \ 6 | -nodes \ 7 | -days 365 \ 8 | -newkey rsa:2048 \ 9 | -keyout /etc/nginx/ssl/selfsigned.key \ 10 | -out /etc/nginx/ssl/selfsigned.crt \ 11 | -subj "/CN=localhost" 12 | fi 13 | 14 | nginx -g "daemon off;" 15 | -------------------------------------------------------------------------------- /compose/ingress/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | 4 | ssl_certificate /etc/nginx/ssl/selfsigned.crt; 5 | ssl_certificate_key /etc/nginx/ssl/selfsigned.key; 6 | 7 | location / { 8 | proxy_pass http://nginx:80; 9 | proxy_set_header Host $host; 10 | proxy_set_header X-Real-IP $remote_addr; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | proxy_set_header X-Forwarded-Proto $scheme; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /compose/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile to build the image for dab_nginx service 2 | 3 | FROM mirror.gcr.io/library/nginx:1.27 4 | 5 | COPY nginx.conf /etc/nginx/conf.d/default.conf 6 | 7 | # Modify the default Nginx configuration to allow it to run as a non-root user 8 | # This follows Nginx Docker Hub instructions: https://hub.docker.com/_/nginx 9 | # By default, Nginx writes its PID file to /var/run/nginx.pid, which is a restricted location. 10 | # We change it to /tmp/nginx.pid so that it can be accessed by a non-root user. 11 | RUN sed -i -E '/^user\s+nginx;/d' /etc/nginx/nginx.conf && \ 12 | sed -i -E 's|pid\s+/var/run/nginx.pid;|pid /tmp/nginx.pid;|' /etc/nginx/nginx.conf && \ 13 | sed -i '/http {/a \ 14 | client_body_temp_path /tmp/client_temp;\n\ 15 | proxy_temp_path /tmp/proxy_temp;\n\ 16 | fastcgi_temp_path /tmp/fastcgi_temp;\n\ 17 | uwsgi_temp_path /tmp/uwsgi_temp;\n\ 18 | scgi_temp_path /tmp/scgi_temp;' /etc/nginx/nginx.conf 19 | 20 | USER nginx -------------------------------------------------------------------------------- /compose/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | location / { 5 | proxy_pass http://test_app:8000; 6 | proxy_set_header Host $host; 7 | proxy_set_header X-Real-IP $remote_addr; 8 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 9 | 10 | proxy_set_header X-Forwarded-Proto $scheme; 11 | #proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | postgres: 4 | build: 5 | context: tools/dev_postgres 6 | dockerfile: Dockerfile 7 | container_name: dab_postgres 8 | ports: 9 | - "55432:5432" 10 | 11 | test_app: 12 | build: 13 | context: . 14 | working_dir: /src 15 | command: './test_app/scripts/container_startup_uwsgi.sh' 16 | volumes: 17 | - '.:/src:z' 18 | ports: 19 | - '8000:8000' 20 | depends_on: 21 | postgres: 22 | condition: service_healthy 23 | environment: 24 | DJANGO_SETTINGS_MODULE: test_app.settings 25 | DB_HOST: postgres 26 | DB_PORT: 5432 27 | DB_USER: dab 28 | DB_PASSWORD: dabing 29 | 30 | # the following 2 parameters makes the container interactive 31 | # for debugging purposes, a breakpoint can be set in the code 32 | # and the container can be attached to with `docker attach test_app` 33 | # and the code can be debugged interactively 34 | stdin_open: true 35 | tty: true 36 | 37 | # This is the intermediate application reverse proxy without ssl 38 | nginx: 39 | build: ./compose/nginx 40 | image: "dab_nginx:1.27" 41 | ports: 42 | - '80:80' 43 | depends_on: 44 | - test_app 45 | 46 | # This is the ssl terminated "ingress" reverse proxy 47 | ingress: 48 | build: ./compose/ingress 49 | image: "dab_ingress:1.27" 50 | command: './entrypoint.sh' 51 | ports: 52 | - "443:443" 53 | depends_on: 54 | - test_app 55 | - nginx 56 | -------------------------------------------------------------------------------- /docs/apps/authentication/management_commands.md: -------------------------------------------------------------------------------- 1 | # Management commands 2 | 3 | django-ansible-base includes built in management commands. 4 | 5 | # ansible_base.authentication.management.commands.authenticators 6 | 7 | This command provide a CLI interface into authenticators. It includes listing/enabling and disabling and adding a default local authentication along with a built in admin/password user. Building of the default local authenticator and user needs to be done if you have removed the default Model login and are instead using the local authenticator class (see authentication.md) 8 | -------------------------------------------------------------------------------- /docs/apps/rbac/README.md: -------------------------------------------------------------------------------- 1 | # Role-Based Access Control (RBAC) 2 | 3 | As a feature of django-ansible-base, this provides access control for already-authenticated users. 4 | Not having access for a specific request sent to the server usually results in a 403 permission denied response. 5 | 6 | ## DAB RBAC Documentation 7 | 8 | The docs for the RBAC app are split according to the intended audience 9 | 10 | ### For Users 11 | 12 | [Link to documentation for users](for_users.md) 13 | 14 | These convey high-level definitions and concepts for the RBAC system. 15 | This is necessarily vague, because this is only useful for someone using 16 | the permissions system through a UI or something like that. 17 | So this does not give specific locations for where roles or permissions 18 | are managed, but tells what the expected outcome is from using the system. 19 | 20 | ### For API Clients 21 | 22 | [Link to documentation for API clients](for_clients.md) 23 | 24 | Since DAB RBAC vendors some endpoints itself, this outlines how to use those endpoints. 25 | 26 | ### For Django App Developers 27 | 28 | [Link to documentation for Django app developers](for_app_developers.md) 29 | 30 | This shows how to enable DAB RBAC in your Django project, register your models, etc. 31 | 32 | ### For DAB Developers 33 | 34 | [Link to documentation for django-ansible-base (DAB) developers](for_dab_developers.md) 35 | 36 | Internal docs for people working on DAB RBAC itself. 37 | -------------------------------------------------------------------------------- /docs/apps/rest_pagination.md: -------------------------------------------------------------------------------- 1 | # Pagination 2 | 3 | django-ansible-base provides a method for paginating rest framework list views. 4 | 5 | ## Installation 6 | 7 | Add `ansible_base.rest_pagination` to your installed apps: 8 | 9 | ``` 10 | INSTALLED_APPS = [ 11 | ... 12 | 'ansible_base.rest_pagination', 13 | ] 14 | ``` 15 | 16 | ### Additional Settings 17 | Additional settings are required to enable pagination on your rest endpoints. 18 | This will happen automatically if using [dynamic_settings](../Installation.md) 19 | 20 | To manually enable filtering without dynamic settings the following items need to be included in your settings: 21 | ``` 22 | REST_FRAMEWORK = { 23 | ... 24 | 'DEFAULT_PAGINATION_CLASS': 'ansible_base.rest_pagination.DefaultPaginator' 25 | ... 26 | } 27 | ``` 28 | 29 | 30 | ### Runtime settings 31 | 32 | The paginator will look for two runtime settings: 33 | `MAX_PAGE_SIZE` - the maximum number of page items allowed by the server, defaults to 200 34 | `DEFAULT_PAGE_SIZE` - the number of page items if left unspecified by the request, defaults to 50 35 | -------------------------------------------------------------------------------- /docs/lib/advisory_lock.md: -------------------------------------------------------------------------------- 1 | ## Database Named Locks 2 | 3 | Django-ansible-base hosts its own specialized utility for obtaining named locks. 4 | This follows the same contract as documented in the django-pglocks library 5 | 6 | https://pypi.org/project/django-pglocks/ 7 | 8 | Due to a multitude of needs relevant to production use, discovered through its 9 | use in AWX, a number of points of divergence have emerged such as: 10 | 11 | - the need to have it not error when running sqlite3 tests 12 | - stuck processes holding the lock forever (adding pg-level idle timeout) 13 | 14 | The use for the purpose of a task would typically look like this 15 | 16 | ```python 17 | from ansible_base.lib.utils.db import advisory_lock 18 | 19 | 20 | def my_task(): 21 | with advisory_lock('my_task_lock', wait=False) as held: 22 | if held is False: 23 | return 24 | # continue to run logic in my_task 25 | ``` 26 | 27 | This is very useful to assure that no other process _in the cluster_ connected 28 | to the same postgres instance runs `my_task` at the same time as the process 29 | calling it here. 30 | 31 | The specific choice of `wait=False` and what to do when another task holds the lock, 32 | is the choice of the programmer in the specific case. 33 | In this case, the `return` would be okay in the situation where `my_task` is idempotent, 34 | and there is a "fallback" schedule in case a call was missed. 35 | The blocking/non-blocking choices are very dependent on the specific design and situation. 36 | -------------------------------------------------------------------------------- /docs/lib/organizations.md: -------------------------------------------------------------------------------- 1 | # Organizations 2 | 3 | The django-ansible-base project provides the 4 | `ansible_base.lib.abstract_models.organization.AbstractOrganization` base class. Projects that implement 5 | organizations MUST inherit this model. 6 | 7 | The `AbstractOrganization` has the following fields: 8 | 9 | * `name` – A unique name of the organization (maximum 512 characters), 10 | * `description` – A description of the organization, 11 | * `users` – A many to many relationship to the user model (defined by the `AUTH_USER_MODEL` setting), 12 | * `teams` – A many to many relationship to the team model (defined by the `ANSIBLE_BASE_TEAM_MODEL` setting), 13 | 14 | for the list of remaining fields see `ansible_base.lib.abstract_models.common.CommonModel`. 15 | 16 | The user and the team models will receive an additional field named `organizations`, that references 17 | related organizations of a respective model. 18 | 19 | ## Configuration 20 | 21 | The `ANSIBLE_BASE_TEAM_MODEL` setting is mandatory and must be defined in django settings. 22 | It must be of the format `.`. 23 | 24 | Example: 25 | 26 | ```python 27 | ANSIBLE_BASE_TEAM_MODEL = 'example.Team' 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/lib/requests.md: -------------------------------------------------------------------------------- 1 | ## Remote Headers 2 | 3 | If you want to know what machine is making the request to your service you might be tempted to look at the `REMOTE_ADDR` header passed by Nginx. 4 | 5 | If you are running behind a proxy or load balancer the value in this header could be the IP of said device thus not indicating the actual host making the request. 6 | 7 | DAB offers two functions called `get_remote_host` and `get_remote_hosts` to help deal with this. 8 | 9 | `get_remote_hosts` will attempt to look at the setting `REMOTE_HOST_HEADERS` (defaulted to `['REMOTE_ADDR', 'REMOTE_HOST']`). For any named header in that array the code will split the header (if present) on `,` and then return an array of all the addresses found (in order with duplicates). 10 | Additionally if the header `HTTP_X_TRUSTED_PROXY` and is present and is properly signed with with a private key (the public key will pull from ansible_base.jwt_consumer.common.auth.get_decryption_key), the method will automatically prepend the following to `REMOTE_HOST_HEADERS`: `['HTTP_X_FORWARDED_FOR', 'HTTP_X_ENVOY_EXTERNAL_ADDRESS']`. 11 | 12 | `get_remote_host` will return the first entry found by `get_remote_hosts` or None. 13 | -------------------------------------------------------------------------------- /docs/lib/serializers.md: -------------------------------------------------------------------------------- 1 | # Serializers 2 | 3 | django-ansible-base can house common serializer fields. These are in `ansible_base.lib.serializers.fields`. 4 | 5 | `ansible_base.lib.serializers.fields.URLField` can handle doing URL validation (see validation.md). 6 | 7 | 8 | 9 | # ValidationSerializerMixin 10 | 11 | django-ansible-base offers a `ValidationSerializerMixin` for DRF serializers. This is in `ansible_base.lib.serializers.validation`. 12 | 13 | Adding this mixin to your serializers will allow for "validation" of a POST/PUT by returning a HTTP 202 response from the `.save()` method if the passed in object passes validation and could be saved. 14 | Validation is indicated to the serializer by passing a GET parameter of `validate=[True|False]`. 15 | * If unspecified validation will be false. 16 | * If multiple validate params are offered any being True turn on the validation login. 17 | 18 | *Note*: if you use the DRF filter class from DAB its already configured to ignore the validate parameters. If you are using your own filtering class you will need to ensure that the filter is not used as part of the lookup model. 19 | -------------------------------------------------------------------------------- /docs/lib/sessions.md: -------------------------------------------------------------------------------- 1 | ## Cached Dynamic Timeout Session Store 2 | 3 | This is a session store which is a child class of the `cached_db` session store. 4 | 5 | It does exactly the same thing but calls `get_preference('SESSION_COOKE_AGE')` to determine how long to allow the sessions to remain valid. 6 | 7 | To take advantage of this, add this setting: 8 | ``` 9 | SESSION_ENGINE = "ansible_base.lib.sessions.stores.cached_dynamic_timeout" 10 | ``` 11 | 12 | Make sure you have a default setting for `SESSION_COOKIE_AGE` like: 13 | ``` 14 | # Seconds before sessions expire. 15 | # Note: This setting may be overridden by database settings. 16 | SESSION_COOKIE_AGE = 1800 17 | ``` 18 | 19 | And then make sure that your `get_preference` function can dynamically return a value for `SESSION_COOKIE_AGE`. 20 | 21 | After changing this value in the system new sessions cut with it will expire based on the setting. 22 | -------------------------------------------------------------------------------- /docs/vscode.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Configure VSCode to allow developers to use the built-in vscode debugging tool for test_app. 4 | 5 | ### Prerequisites 6 | 7 | 1. create a `.env` file in django-ansible-base folder with the following content, 8 | 9 | ```python 10 | DJANGO_SETTINGS_MODULE=test_app.sqlite3settings 11 | ``` 12 | 13 | 2. Copy the tools/vscode/ contents into .vscode/ in your django-ansible-base folder 14 | 15 | Now you should be able to run the test_app server in debug mode via VSCode 16 | 17 | 3. Restart VSCode so that it detects the new launch configuration 18 | 19 | ### Launch the debugger 20 | 21 | Click the Run and Debug tab in VSCode and click the drop down to select `Test App Server` and click green triangle to run it 22 | 23 | Set a debug point in the code and it should trigger 24 | 25 | Sometimes it is useful to play around in shell_plus environment. Launch the a shell_plus process by selecting `Test App Shell Plus`. You can start this while the `Test App Server` is running. 26 | 27 | ### Running Tests 28 | 29 | the `settings.json` test file allows you to run a test via VSCode. 30 | 31 | Click the Testing icon and navigate to the test you wish to run. Click either Run or Debug test. 32 | 33 | 34 | ### Resetting database 35 | 36 | Delete the `django-ansible-base/db.sqlite3` file and re-run the server to get a fresh database. 37 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_app.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /requirements/requirements.in: -------------------------------------------------------------------------------- 1 | # 2 | # Only generic requirements for django-ansible-base (or the common feature) should be listed here. 3 | # if you are add a new feature which requires dependencies they should be in a separate requirements_.in file 4 | # 5 | cryptography 6 | Django>=4.2.21,<4.3.0 # CVE-2024-45230, CVE-2024-56374 7 | djangorestframework 8 | django-crum 9 | inflection 10 | sqlparse>=0.5.2 # https://github.com/ansible/django-ansible-base/security/dependabot/9 11 | dynaconf>=3.2.10,<4.0.0 # Dynaconf 4.0.0 is expected to be a major release with breaking changes 12 | -------------------------------------------------------------------------------- /requirements/requirements_activitystream.in: -------------------------------------------------------------------------------- 1 | # None -------------------------------------------------------------------------------- /requirements/requirements_api_documentation.in: -------------------------------------------------------------------------------- 1 | drf-spectacular 2 | -------------------------------------------------------------------------------- /requirements/requirements_authentication.in: -------------------------------------------------------------------------------- 1 | social-auth-app-django==5.4.1 2 | social-auth-core<=4.5.4 3 | tabulate 4 | 5 | # These should eventually be split out when the authentications move into their own repo 6 | 7 | # LDAP Authenticator Plugins 8 | django-auth-ldap 9 | python-ldap 10 | ldap-filter 11 | 12 | # Social Authenticator Plugins 13 | python3-saml 14 | tacacs_plus 15 | 16 | xmlsec==1.3.13 # Pin for https://github.com/xmlsec/python-xmlsec/issues/314 17 | 18 | # RADIUS Authenticator Plugin 19 | pyrad 20 | -------------------------------------------------------------------------------- /requirements/requirements_channels.in: -------------------------------------------------------------------------------- 1 | channels -------------------------------------------------------------------------------- /requirements/requirements_dev.txt: -------------------------------------------------------------------------------- 1 | ansible # Used in build process to generate some configs 2 | black==25.1.0 # Linting tool, if changed update pyproject.toml as well 3 | build 4 | django==4.2.21 5 | django-debug-toolbar 6 | django-extensions 7 | djangorestframework 8 | flake8==7.1.1 # Linting tool, if changed update pyproject.toml as well 9 | Flake8-pyproject==1.2.3 # Linting tool, if changed update pyproject.toml as well 10 | ipython 11 | isort==6.0.0 # Linting tool, if changed update pyproject.toml as well 12 | tox 13 | tox-docker 14 | typeguard 15 | pytest 16 | pytest-asyncio 17 | pytest-xdist 18 | pytest-cov 19 | pytest-django 20 | setuptools-scm 21 | sqlparse==0.5.2 22 | psycopg[binary] 23 | sdb 24 | -------------------------------------------------------------------------------- /requirements/requirements_feature_flags.in: -------------------------------------------------------------------------------- 1 | # DAB Feature Flags 2 | django-flags -------------------------------------------------------------------------------- /requirements/requirements_jwt_consumer.in: -------------------------------------------------------------------------------- 1 | pyjwt 2 | requests 3 | -------------------------------------------------------------------------------- /requirements/requirements_oauth2_provider.in: -------------------------------------------------------------------------------- 1 | django-oauth-toolkit<2.4.0 2 | -------------------------------------------------------------------------------- /requirements/requirements_rbac.in: -------------------------------------------------------------------------------- 1 | # None 2 | -------------------------------------------------------------------------------- /requirements/requirements_redis_client.in: -------------------------------------------------------------------------------- 1 | django-redis 2 | redis 3 | -------------------------------------------------------------------------------- /requirements/requirements_resource_registry.in: -------------------------------------------------------------------------------- 1 | # NOTE: Only dependencies needed to use ansible_base.resource_registry.* should go here. 2 | # Deps specific to django-ansible-base tests should go in requirements_dev.txt 3 | asgiref 4 | pyjwt 5 | requests 6 | urllib3 7 | -------------------------------------------------------------------------------- /requirements/requirements_rest_filters.in: -------------------------------------------------------------------------------- 1 | # None 2 | -------------------------------------------------------------------------------- /requirements/requirements_testing.txt: -------------------------------------------------------------------------------- 1 | # NOTE: Only dependencies needed to use ansible_base.lib.testing.* should go here. 2 | # Deps specific to django-ansible-base tests should go in requirements_dev.txt 3 | cryptography 4 | pytest 5 | pytest-django 6 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # Complete documentation with many more options at: 2 | # https://docs.sonarqube.org/latest/analysis/analysis-parameters/ 3 | 4 | #sonar.host.url=https://sonarqube.corp.redhat.com 5 | 6 | ## The unique project identifier. This is mandatory. 7 | # Do not duplicate or reuse! 8 | # Available characters: [a-zA-Z0-9_:\.\-] 9 | # Must have least one non-digit. 10 | # Recommended format: : 11 | sonar.projectKey=ansible_django-ansible-base 12 | 13 | sonar.organization=ansible 14 | 15 | # Customize what paths to scan. Default is . 16 | sonar.sources=. 17 | # Excluse test directories from sources 18 | sonar.exclusions=test_app/**/* 19 | # Set tests root directory 20 | sonar.tests=test_app/tests 21 | 22 | # Verbose name of project displayed in WUI. Default is set to the projectKey. This field is optional. 23 | sonar.projectName=django-ansible-base 24 | 25 | # Version of project. This field is optional. 26 | #sonar.projectVersion=1.0 27 | 28 | # Tell sonar scanner where coverage files exist 29 | sonar.python.coverage.reportPaths=coverage.xml 30 | 31 | sonar.issue.ignore.multicriteria=e1 32 | # Ignore "should be a variable" 33 | sonar.issue.ignore.multicriteria.e1.ruleKey=python:S1192 34 | sonar.issue.ignore.multicriteria.e1.resourceKey=**/migrations/**/* 35 | 36 | # Only scan with python3 37 | sonar.python.version=3.9,3.10,3.11 38 | 39 | # Ignore code dupe for the migrations 40 | sonar.cpd.exclusions=**/migrations/*.py 41 | -------------------------------------------------------------------------------- /test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/__init__.py -------------------------------------------------------------------------------- /test_app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from django.contrib.auth.models import Group 4 | 5 | from test_app import models 6 | 7 | admin.site.register(models.Animal) 8 | admin.site.register(models.EncryptionModel) 9 | admin.site.register(models.Organization) 10 | admin.site.register(models.Team) 11 | admin.site.register(models.User, UserAdmin) 12 | admin.site.unregister(Group) 13 | admin.site.register(models.RelatedFieldsTestModel) 14 | admin.site.register(models.Namespace) 15 | admin.site.register(models.CollectionImport) 16 | admin.site.register(models.Inventory) 17 | admin.site.register(models.InstanceGroup) 18 | admin.site.register(models.ExampleEvent) 19 | -------------------------------------------------------------------------------- /test_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestAppConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'test_app' 7 | -------------------------------------------------------------------------------- /test_app/authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/authentication/__init__.py -------------------------------------------------------------------------------- /test_app/authentication/logged_basic_auth.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.utils.encoding import smart_str 4 | from drf_spectacular.authentication import BasicScheme 5 | from rest_framework import authentication 6 | 7 | logger = logging.getLogger('test_app.authentication.logged_basic_auth') 8 | 9 | 10 | class LoggedBasicAuthentication(authentication.BasicAuthentication): 11 | def authenticate(self, request): 12 | ret = super(LoggedBasicAuthentication, self).authenticate(request) 13 | if ret: 14 | username = ret[0].username if ret[0] else '' 15 | logger.info(smart_str(f"User {username} performed a {request.method} to {request.path} through the API via basic auth")) 16 | return ret 17 | 18 | def authenticate_header(self, request): 19 | return super(LoggedBasicAuthentication, self).authenticate_header(request) 20 | 21 | 22 | # NOTE: This file is common to many of the services and will allow DRF to return a 401 instead of a 403 on failed login. 23 | # This is the expected behavior we want so we need this file in test_app to mimic other applications 24 | 25 | 26 | class MyAuthenticationScheme(BasicScheme): 27 | target_class = LoggedBasicAuthentication 28 | name = 'LoggedBasicAuthentication' # name used in the schema 29 | -------------------------------------------------------------------------------- /test_app/authentication/service_token_auth.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | from django.contrib.auth import get_user_model 3 | from rest_framework.authentication import BaseAuthentication 4 | 5 | from ansible_base.lib.utils.auth import get_user_by_ansible_id 6 | from ansible_base.lib.utils.models import get_system_user 7 | from ansible_base.resource_registry.resource_server import get_resource_server_config 8 | 9 | User = get_user_model() 10 | 11 | 12 | class ServiceTokenAuthentication(BaseAuthentication): 13 | keyword = "Token" 14 | 15 | def authenticate(self, request): 16 | token = request.headers.get("X-ANSIBLE-SERVICE-AUTH", None) 17 | 18 | if token is None: 19 | return None 20 | 21 | cfg = get_resource_server_config() 22 | 23 | try: 24 | data = jwt.decode( 25 | token, 26 | cfg["SECRET_KEY"], 27 | algorithms=cfg["JWT_ALGORITHM"], 28 | required=["iss", "exp"], 29 | ) 30 | 31 | if "sub" in data: 32 | return (get_user_by_ansible_id(data["sub"]), None) 33 | else: 34 | return (get_system_user(), None) 35 | 36 | except jwt.exceptions.PyJWTError as e: 37 | print(e) 38 | return None 39 | -------------------------------------------------------------------------------- /test_app/example_files/settings.yaml: -------------------------------------------------------------------------------- 1 | # This file exists only for testing the dynamic settings loader 2 | # capabilities of loading yaml settings from the standard location 3 | # which is /etc/ansible-automation-platform/{appname}/settings.yaml 4 | just_a_test: 42 -------------------------------------------------------------------------------- /test_app/managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import UserManager 2 | 3 | 4 | class UserUnmanagedManager(UserManager): 5 | def get_queryset(self): 6 | return super().get_queryset().filter(managed=False) 7 | -------------------------------------------------------------------------------- /test_app/migrations/0006_team_admins_team_users.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-03-18 12:43 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('test_app', '0005_city'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='team', 16 | name='admins', 17 | field=models.ManyToManyField(blank=True, help_text='The list of admins for this team', related_name='teams_administered', to=settings.AUTH_USER_MODEL), 18 | ), 19 | migrations.AddField( 20 | model_name='team', 21 | name='users', 22 | field=models.ManyToManyField(blank=True, help_text='The list of users on this team', related_name='teams', to=settings.AUTH_USER_MODEL), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /test_app/migrations/0008_secretcolor.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-04-05 17:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('test_app', '0007_alter_animal_created_by_alter_animal_modified_by_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='SecretColor', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('color', models.CharField(default='blue', max_length=20, null=True)), 18 | ], 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /test_app/migrations/0009_city_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-04-15 20:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('test_app', '0008_secretcolor'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='city', 15 | name='state', 16 | field=models.CharField(editable=False, max_length=100, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /test_app/migrations/0010_manageduser.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-05-28 18:26 2 | 3 | from django.conf import settings 4 | import django.contrib.auth.models 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('test_app', '0009_city_state'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='ManagedUser', 18 | fields=[ 19 | ('user_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), 20 | ('managed', models.BooleanField(default=False)), 21 | ], 22 | options={ 23 | 'verbose_name': 'user', 24 | 'verbose_name_plural': 'users', 25 | 'abstract': False, 26 | }, 27 | bases=('test_app.user',), 28 | managers=[ 29 | ('objects', django.contrib.auth.models.UserManager()), 30 | ], 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /test_app/migrations/0012_encryptionjsonmodel.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-05-08 18:30 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('test_app', '0011_publicdata'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='EncryptionJSONModel', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('modified', models.DateTimeField(auto_now=True, help_text='The date/time this resource was created')), 20 | ('created', models.DateTimeField(auto_now_add=True, help_text='The date/time this resource was created')), 21 | ('testing1', models.JSONField(default=dict, null=True)), 22 | ('created_by', models.ForeignKey(default=None, editable=False, help_text='The user who created this resource', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created+', to=settings.AUTH_USER_MODEL)), 23 | ('modified_by', models.ForeignKey(default=None, editable=False, help_text='The user who last modified this resource', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_modified+', to=settings.AUTH_USER_MODEL)), 24 | ], 25 | options={ 26 | 'ordering': ['id'], 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /test_app/migrations/0013_alter_manageduser_managers_alter_user_managers.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-07-17 14:46 2 | 3 | import django.contrib.auth.models 4 | from django.db import migrations 5 | import test_app.managers 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('test_app', '0012_encryptionjsonmodel'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelManagers( 16 | name='manageduser', 17 | managers=[ 18 | ('objects', test_app.managers.UserUnmanagedManager()), 19 | ('all_objects', django.contrib.auth.models.UserManager()), 20 | ], 21 | ), 22 | migrations.AlterModelManagers( 23 | name='user', 24 | managers=[ 25 | ('all_objects', django.contrib.auth.models.UserManager()), 26 | ('objects', django.contrib.auth.models.UserManager()), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /test_app/migrations/0015_logentry.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.6 on 2024-08-08 17:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('test_app', '0014_autoextrauuidmodel_manualextrauuidmodel_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='LogEntry', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('message', models.CharField(max_length=400)), 18 | ], 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /test_app/migrations/0016_organization_extra_field.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-08-08 01:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('test_app', '0015_logentry'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='organization', 15 | name='extra_field', 16 | field=models.CharField(max_length=100, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /test_app/migrations/0017_thingsomeoneshares_thingsomeoneowns.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-09-05 21:15 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('test_app', '0016_organization_extra_field'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='ThingSomeoneShares', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('thing', models.CharField(max_length=256)), 20 | ('owner', models.ManyToManyField(related_name='things_i_share', to=settings.AUTH_USER_MODEL)), 21 | ], 22 | ), 23 | migrations.CreateModel( 24 | name='ThingSomeoneOwns', 25 | fields=[ 26 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('thing', models.CharField(max_length=256)), 28 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='things_i_own', to=settings.AUTH_USER_MODEL)), 29 | ], 30 | options={ 31 | 'unique_together': {('owner', 'thing')}, 32 | }, 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /test_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/migrations/__init__.py -------------------------------------------------------------------------------- /test_app/scripts/container_startup_uwsgi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | PIP=/venv/bin/pip 7 | PYTHON=/venv/bin/python3 8 | DYNACONF=/venv/bin/dynaconf 9 | 10 | $PIP install uwsgi 11 | 12 | echo "settings.DATABASE ..." 13 | $PYTHON manage.py shell -c 'from django.conf import settings; print(settings.DATABASES)' 14 | 15 | echo "DAB overridden settings ..." 16 | $DYNACONF -i test_app.settings.DYNACONF list -k ANSIBLE_BASE_OVERRIDDEN_SETTINGS --json 17 | 18 | echo "Read the custom settings file data just as a test" 19 | $DYNACONF -i test_app.settings.DYNACONF inspect -k JUST_A_TEST 20 | 21 | $PYTHON manage.py migrate 22 | $PYTHON manage.py collectstatic --clear --noinput 23 | DJANGO_SUPERUSER_PASSWORD=password DJANGO_SUPERUSER_USERNAME=admin DJANGO_SUPERUSER_EMAIL=admin@stuff.invalid $PYTHON manage.py createsuperuser --noinput || true 24 | $PYTHON manage.py authenticators --initialize 25 | $PYTHON manage.py create_demo_data 26 | 27 | # $PYTHON manage.py runserver 0.0.0.0:8000 28 | cd /src 29 | PYTHONPATH=. /venv/bin/uwsgi --ini test_app/uwsgi.ini 30 | -------------------------------------------------------------------------------- /test_app/sqlite3settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is kept for backwards compatibility with deployments settings 3 | DJANGO_SETTINGS_MODULE to test_app.sqlite3settings 4 | """ 5 | 6 | from ansible_base.lib.dynamic_config import export 7 | 8 | from .settings import DYNACONF 9 | 10 | DYNACONF.load_file("sqlite_defaults.py") 11 | export(__name__, DYNACONF) 12 | -------------------------------------------------------------------------------- /test_app/sqlite_defaults.py: -------------------------------------------------------------------------------- 1 | """ 2 | Variables defined in this file will override the settings from defaults.py 3 | to merge back to existing settings, use the following dynaconf pattern: 4 | 5 | # Dunder merging 6 | EXISTING_STRUCTURE__NESTED_KEY = value 7 | EXISTING_STRUCTURE__NESTED_KEY__NESTED_KEY = value 8 | 9 | # Merge Marker as @token 10 | EXISTING_STRUCTURE = "@merge key=value" 11 | EXISTING_STRUCTURE = '@merge {"key": "value"}' 12 | EXISTING_LIST = "@merge item1, item2, item3" 13 | EXISTING_LIST = "@merge_unique item1" 14 | EXISTING_LIST = "@insert 0 item" 15 | 16 | # Merge marker as item 17 | EXISTING_STRUCTURE = { 18 | "key": "value", 19 | "dynaconf_merge": True 20 | } 21 | EXISTING_LIST = ["item1", "dynaconf_merge"] 22 | """ 23 | 24 | DATABASES = { 25 | "default": { 26 | "ENGINE": "django.db.backends.sqlite3", 27 | "NAME": "db.sqlite3", 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test_app/static/test_templatetags_util_inline_file.css: -------------------------------------------------------------------------------- 1 | * { color: red; font-family: 'Comic Sans MS', 'Comic Sans'; } 2 | div > p.foo { color: blue; } 3 | -------------------------------------------------------------------------------- /test_app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/__init__.py -------------------------------------------------------------------------------- /test_app/tests/activitystream/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/activitystream/models/__init__.py -------------------------------------------------------------------------------- /test_app/tests/activitystream/models/test_entry.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.contrib.contenttypes.models import ContentType 3 | 4 | from ansible_base.lib.utils.response import get_relative_url 5 | 6 | 7 | def test_activitystream_entry_immutable(system_user, animal): 8 | """ 9 | Trying to modify an Entry object should raise an exception. 10 | """ 11 | entry = animal.activity_stream_entries.first() 12 | entry.operation = "delete" 13 | with pytest.raises(ValueError) as excinfo: 14 | entry.save() 15 | 16 | assert "Entry is immutable" in str(excinfo.value) 17 | 18 | 19 | def test_activitystream_auditablemodel_related(admin_api_client, user, organization): 20 | url = get_relative_url('user-detail', kwargs={'pk': user.pk}) 21 | response = admin_api_client.get(url) 22 | assert response.status_code == 200 23 | assert 'activity_stream' in response.data['related'] 24 | activity_stream_url = response.data['related']['activity_stream'] 25 | content_type = ContentType.objects.get_for_model(user) 26 | assert f'object_id={user.pk}' in activity_stream_url 27 | assert f'content_type={content_type.pk}' in activity_stream_url 28 | 29 | # organization isn't an AuditableModel, so it shouldn't show AS in related 30 | url = get_relative_url('organization-detail', kwargs={'pk': organization.pk}) 31 | response = admin_api_client.get(url) 32 | assert response.status_code == 200 33 | assert 'activity_stream' not in response.data['related'] 34 | -------------------------------------------------------------------------------- /test_app/tests/authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/authentication/__init__.py -------------------------------------------------------------------------------- /test_app/tests/authentication/authenticator_plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/authentication/authenticator_plugins/__init__.py -------------------------------------------------------------------------------- /test_app/tests/authentication/authenticator_plugins/test_base.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_base.authentication.authenticator_plugins.base import _field_required 4 | 5 | 6 | class DummyField: 7 | def __init__(self, required, allow_null): 8 | if required is not None: 9 | self.required = required 10 | if allow_null is not None: 11 | self.allow_null = allow_null 12 | 13 | 14 | @pytest.mark.parametrize( 15 | "required,allow_null,expected_result", 16 | [ 17 | (None, None, True), 18 | (True, None, True), 19 | (False, None, False), 20 | (None, True, False), 21 | (None, False, True), 22 | (True, False, True), 23 | (False, False, False), 24 | (True, True, True), 25 | (False, True, False), 26 | ], 27 | ) 28 | def test__field_required(required, allow_null, expected_result): 29 | field = DummyField(required, allow_null) 30 | assert _field_required(field) is expected_result 31 | -------------------------------------------------------------------------------- /test_app/tests/authentication/authenticator_plugins/test_ldap_get_or_build_user.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from unittest import mock 3 | from unittest.mock import MagicMock 4 | 5 | import pytest 6 | 7 | from ansible_base.authentication.authenticator_plugins import ldap 8 | 9 | """ 10 | This module is separated from the rest of test_ldap.py because it reloads the module 11 | which will replace the auth utils module with newly loaded classes. 12 | So it gives best isolation to keep this in a module that does not have 13 | other imports from the same module, which can leave stale references. 14 | """ 15 | 16 | 17 | @pytest.mark.django_db 18 | @pytest.mark.parametrize( 19 | "username", 20 | [ 21 | ("Timmy"), 22 | ("TIMMY"), 23 | ("TiMmY"), 24 | ], 25 | ) 26 | def test_get_or_build_user(username, ldap_authenticator): 27 | with mock.patch( 28 | 'ansible_base.authentication.utils.authentication.get_or_create_authenticator_user', return_value=(None, None, None) 29 | ) as get_or_create_authenticator_user: 30 | importlib.reload(ldap) 31 | plugin = ldap.AuthenticatorPlugin(database_instance=ldap_authenticator) 32 | ldap_object = MagicMock() 33 | plugin.get_or_build_user(username, ldap_object) 34 | assert get_or_create_authenticator_user.called 35 | assert username.lower() in get_or_create_authenticator_user.call_args[0] 36 | assert username not in get_or_create_authenticator_user.call_args[0] 37 | -------------------------------------------------------------------------------- /test_app/tests/authentication/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_base.lib.testing.util import copy_fixture 4 | from ansible_base.rbac.models import RoleDefinition 5 | 6 | SYSTEM_ROLE_NAME = 'System role' 7 | TEAM_MEMBER_ROLE_NAME = 'Team Member' 8 | ORG_MEMBER_ROLE_NAME = 'Organization Member' 9 | 10 | 11 | @pytest.fixture 12 | def system_role(): 13 | return RoleDefinition.objects.create( 14 | name=SYSTEM_ROLE_NAME, 15 | ) 16 | 17 | 18 | @copy_fixture(copies=3) # noqa: F405 19 | @pytest.fixture 20 | def global_role(randname): 21 | return RoleDefinition.objects.create(name=randname("Global Role")) 22 | 23 | 24 | @pytest.fixture 25 | def default_rbac_roles_claims(): 26 | return {'system': {'roles': {}}, 'organizations': {}} 27 | -------------------------------------------------------------------------------- /test_app/tests/authentication/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/authentication/management/__init__.py -------------------------------------------------------------------------------- /test_app/tests/authentication/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/authentication/models/__init__.py -------------------------------------------------------------------------------- /test_app/tests/authentication/models/test_authenticator_user.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode 2 | 3 | import pytest 4 | 5 | from ansible_base.authentication.models.authenticator_user import b64_encode_binary_data_in_dict 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "input,output", 10 | [ 11 | (True, True), 12 | (1, 1), 13 | ('hi', 'hi'), 14 | (b'hi', b64encode(b'hi').decode('utf-8')), 15 | (['a', b'b', 'c'], ['a', b64encode(b'b').decode('utf-8'), 'c']), 16 | ({'a': b'b'}, {'a': b64encode(b'b').decode('utf-8')}), 17 | ( 18 | {"a": b'b', 'c': ['d', b'e'], 'f': {'g': b'h'}}, 19 | {"a": b64encode(b'b').decode('utf-8'), 'c': ['d', b64encode(b'e').decode('utf-8')], 'f': {'g': b64encode(b'h').decode('utf-8')}}, 20 | ), 21 | ], 22 | ) 23 | def test_b64_encode_binary_data_in_dict(input, output): 24 | assert b64_encode_binary_data_in_dict(input) == output 25 | -------------------------------------------------------------------------------- /test_app/tests/authentication/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/authentication/serializers/__init__.py -------------------------------------------------------------------------------- /test_app/tests/authentication/test_middleware.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from social_core.exceptions import AuthException 3 | 4 | from ansible_base.authentication.middleware import SocialExceptionHandlerMiddleware 5 | 6 | 7 | def test_social_exception_handler_mw(): 8 | class Strategy: 9 | def setting(self, name): 10 | return settings.LOGIN_ERROR_URL 11 | 12 | class Backend: 13 | def __init__(self): 14 | self.name = "test" 15 | 16 | class Request: 17 | def __init__(self): 18 | self.social_strategy = Strategy() 19 | self.backend = Backend() 20 | 21 | mw = SocialExceptionHandlerMiddleware(None) 22 | url = mw.get_redirect_uri(Request(), AuthException("test")) 23 | assert url == "/?auth_failed" 24 | -------------------------------------------------------------------------------- /test_app/tests/authentication/test_urls.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from unittest import mock 3 | 4 | import pytest 5 | from django.urls.resolvers import URLResolver 6 | 7 | from ansible_base.authentication import views 8 | 9 | 10 | @pytest.mark.parametrize( 11 | 'view_set,expect_url', 12 | ( 13 | (None, False), 14 | (views.AuthenticatorMapViewSet, True), 15 | ), 16 | ) 17 | def test_authentication_user_in_urls(view_set, expect_url): 18 | from ansible_base.authentication import urls 19 | 20 | with mock.patch('ansible_base.authentication.views.authenticator_users.get_authenticator_user_view', return_value=view_set): 21 | importlib.reload(urls) 22 | url_names = [] 23 | for url in urls.api_version_urls: 24 | if isinstance(url, URLResolver): 25 | for url in url.url_patterns: 26 | url_names.append(url.name) 27 | else: 28 | url_names.append(url.name) 29 | 30 | expected_url_name = 'authenticator-users-list' 31 | if expect_url: 32 | assert expected_url_name in url_names 33 | else: 34 | assert expected_url_name not in url_names 35 | -------------------------------------------------------------------------------- /test_app/tests/authentication/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/authentication/utils/__init__.py -------------------------------------------------------------------------------- /test_app/tests/authentication/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/authentication/views/__init__.py -------------------------------------------------------------------------------- /test_app/tests/authentication/views/test_authenticators.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_base.lib.utils.response import get_relative_url 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_authenticators_view_denies_delete_last_enabled_authenticator(admin_api_client, system_user, local_authenticator): 8 | """ 9 | Test that the admin can't delete the last enabled authenticator. 10 | """ 11 | 12 | url = get_relative_url("authenticator-detail", kwargs={'pk': local_authenticator.pk}) 13 | response = admin_api_client.delete(url) 14 | assert response.status_code == 400 15 | assert response.data['details'] == "Authenticator cannot be deleted, as no authenticators would be enabled" 16 | 17 | 18 | @pytest.mark.django_db 19 | def test_authenticators_metadata_not_instanced_on_create(admin_api_client, local_authenticator): 20 | url = get_relative_url("authenticator-list") 21 | response = admin_api_client.options(url) 22 | assert response.status_code == 200 23 | assert response.data['actions']['POST']['slug']["read_only"] is False 24 | 25 | 26 | def test_authenticators_metadata_instanced_on_update(admin_api_client, local_authenticator): 27 | url = get_relative_url("authenticator-detail", kwargs={'pk': local_authenticator.pk}) 28 | response = admin_api_client.options(url) 29 | assert response.status_code == 200 30 | assert response.data['actions']['PUT']['slug']["read_only"] is True 31 | -------------------------------------------------------------------------------- /test_app/tests/feature_flags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/feature_flags/__init__.py -------------------------------------------------------------------------------- /test_app/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /test_app/tests/fixtures/authenticator_plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/fixtures/authenticator_plugins/__init__.py -------------------------------------------------------------------------------- /test_app/tests/fixtures/authenticator_plugins/broken.py: -------------------------------------------------------------------------------- 1 | import sys # noqa: F401 2 | 3 | from nothing import this_does_not_exist # noqa: F401 4 | -------------------------------------------------------------------------------- /test_app/tests/fixtures/authenticator_plugins/custom.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.contrib.auth import get_user_model 4 | 5 | from ansible_base.authentication.authenticator_plugins.base import AbstractAuthenticatorPlugin 6 | 7 | logger = logging.getLogger('test_app.tests.fixtures.authenticator_plugins.custom') 8 | 9 | 10 | class AuthenticatorPlugin(AbstractAuthenticatorPlugin): 11 | def __init__(self, database_instance=None, *args, **kwargs): 12 | super().__init__(database_instance, *args, **kwargs) 13 | self.configuration_encrypted_fields = [] 14 | self.type = "custom" 15 | self.set_logger(logger) 16 | self.category = "password" 17 | 18 | def authenticate(self, request, username=None, password=None, **kwargs): 19 | if username == "admin" and password == "hello123": 20 | user = get_user_model().objects.get(username=username) 21 | return user 22 | 23 | return None 24 | -------------------------------------------------------------------------------- /test_app/tests/fixtures/authenticator_plugins/really_broken.py: -------------------------------------------------------------------------------- 1 | import sys # noqa: F401 2 | 3 | from nothing import this_does_not_exist # noqa: F401 4 | -------------------------------------------------------------------------------- /test_app/tests/fixtures/static/resource_sync/metadata/response: -------------------------------------------------------------------------------- 1 | { 2 | "service_id": "57592fbc-7ecb-405f-9f5f-ebad20932d38", 3 | "service_type": "aap" 4 | } 5 | -------------------------------------------------------------------------------- /test_app/tests/fixtures/static/resource_sync/resource-types/shared.organization/manifest/response: -------------------------------------------------------------------------------- 1 | ansible_id,resource_hash 2 | 3e3cc6a4-72fa-43ec-9e17-76ae5a3846ca,557392e5e430431c7b7810da8e2a9ed8be0596985a58ec054a15e0ec5fda331e 3 | -------------------------------------------------------------------------------- /test_app/tests/fixtures/static/resource_sync/resource-types/shared.user/manifest/response: -------------------------------------------------------------------------------- 1 | ansible_id,resource_hash 2 | 97447387-8596-404f-b0d0-6429b04c8d22,70ce14cc1560981cc9912ba7f6d154171d823040ab51f17a27b811a8359db368 3 | -------------------------------------------------------------------------------- /test_app/tests/fixtures/static/resource_sync/resources/3e3cc6a4-72fa-43ec-9e17-76ae5a3846ca/response: -------------------------------------------------------------------------------- 1 | { 2 | "object_id": "1", 3 | "name": "Serious Company", 4 | "ansible_id": "3e3cc6a4-72fa-43ec-9e17-76ae5a3846ca", 5 | "service_id": "57592fbc-7ecb-405f-9f5f-ebad20932d38", 6 | "resource_type": "shared.organization", 7 | "has_serializer": true, 8 | "is_partially_migrated": false, 9 | 10 | "resource_data": { 11 | "name": "Serious Company", 12 | "description": "A VERY serious company" 13 | }, 14 | "url": "/api/server/v1/service-index/resources/3e3cc6a4-72fa-43ec-9e17-76ae5a3846ca/" 15 | } 16 | -------------------------------------------------------------------------------- /test_app/tests/fixtures/static/resource_sync/resources/97447387-8596-404f-b0d0-6429b04c8d22/response: -------------------------------------------------------------------------------- 1 | { 2 | "object_id": "2", 3 | "name": "theceo", 4 | "ansible_id": "97447387-8596-404f-b0d0-6429b04c8d22", 5 | "service_id": "57592fbc-7ecb-405f-9f5f-ebad20932d38", 6 | "resource_type": "shared.user", 7 | "has_serializer": true, 8 | "is_partially_migrated": false, 9 | "resource_data": { 10 | "username": "theceo", 11 | "email": "theceo@seriouscompany.com", 12 | "first_name": "The", 13 | "last_name": "CEO" 14 | }, 15 | "url": "/api/server/v1/service-index/resources/97447387-8596-404f-b0d0-6429b04c8d22/" 16 | } 17 | -------------------------------------------------------------------------------- /test_app/tests/fixtures/static/resource_sync/resources/b19ff84f-df6a-462a-ac81-167b1dc8f933/response: -------------------------------------------------------------------------------- 1 | { 2 | "object_id": "2", 3 | "name": "was_renamed", 4 | "ansible_id": "b19ff84f-df6a-462a-ac81-167b1dc8f933", 5 | "service_id": "57592fbc-7ecb-405f-9f5f-ebad20932d38", 6 | "resource_type": "shared.user", 7 | "has_serializer": true, 8 | "is_partially_migrated": false, 9 | "resource_data": { 10 | "username": "was_renamed", 11 | "email": "theceo@seriouscompany.com", 12 | "first_name": "The", 13 | "last_name": "CEO" 14 | }, 15 | "url": "/api/server/v1/service-index/resources/b19ff84f-df6a-462a-ac81-167b1dc8f933/" 16 | } 17 | -------------------------------------------------------------------------------- /test_app/tests/fixtures/test_fixtures.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_clients_do_not_conflict(unauthenticated_api_client, user_api_client, admin_api_client): 8 | assert dict(user_api_client.cookies) != dict(admin_api_client.cookies) 9 | assert dict(unauthenticated_api_client.cookies) == {} 10 | 11 | 12 | class ThrowawayObject: 13 | def method_that_should_be_patched(self): 14 | raise Exception("object was not patched properly") 15 | 16 | 17 | def test_mock_method(create_mock_method): 18 | with pytest.raises(StopIteration): 19 | fields_list = [ 20 | {"field": "hi"}, 21 | ] 22 | 23 | with mock.patch("test_app.tests.fixtures.test_fixtures.ThrowawayObject.method_that_should_be_patched", create_mock_method(fields_list)): 24 | test_object = ThrowawayObject() 25 | # Test good path: assert that calling object fields are overwritten 26 | for fields in fields_list: 27 | test_object.method_that_should_be_patched() 28 | for field, value in fields.items(): 29 | assert getattr(test_object, field, None) == value 30 | # Test bad path: ie assert that the mock method throws an exception when it is called more than expected 31 | test_object.method_that_should_be_patched() 32 | -------------------------------------------------------------------------------- /test_app/tests/jwt_consumer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/jwt_consumer/__init__.py -------------------------------------------------------------------------------- /test_app/tests/jwt_consumer/awx/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/jwt_consumer/awx/__init__.py -------------------------------------------------------------------------------- /test_app/tests/jwt_consumer/awx/test_auth.py: -------------------------------------------------------------------------------- 1 | from ansible_base.jwt_consumer.awx.auth import AwxJWTAuthentication 2 | 3 | 4 | def test_awx_process_permissions(user, caplog): 5 | authentication = AwxJWTAuthentication() 6 | assert authentication.use_rbac_permissions is True 7 | -------------------------------------------------------------------------------- /test_app/tests/jwt_consumer/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/jwt_consumer/common/__init__.py -------------------------------------------------------------------------------- /test_app/tests/jwt_consumer/common/test_cache.py: -------------------------------------------------------------------------------- 1 | # The cache is tested by test_auth and test_cert 2 | -------------------------------------------------------------------------------- /test_app/tests/jwt_consumer/common/test_exceptions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_base.jwt_consumer.common import exceptions 4 | 5 | 6 | def test_invalid_service_exception(): 7 | service = 'testing' 8 | with pytest.raises(exceptions.InvalidService) as e: 9 | raise exceptions.InvalidService(service) 10 | assert f"This authentication class requires {service}." in str(e) 11 | -------------------------------------------------------------------------------- /test_app/tests/jwt_consumer/eda/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/jwt_consumer/eda/__init__.py -------------------------------------------------------------------------------- /test_app/tests/jwt_consumer/eda/test_auth.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest.mock import MagicMock 3 | 4 | from ansible_base.jwt_consumer.eda.auth import EDAJWTAuthentication 5 | 6 | 7 | def test_eda_process_permissions(user, caplog): 8 | authentication = EDAJWTAuthentication() 9 | assert authentication.use_rbac_permissions is True 10 | 11 | 12 | def test_eda_jwt_auth_scheme(): 13 | sys.modules['aap_eda.core'] = MagicMock() 14 | from ansible_base.jwt_consumer.eda.auth import EDAJWTAuthScheme # noqa: E402 15 | 16 | scheme = EDAJWTAuthScheme(None) 17 | response = scheme.get_security_definition(None) 18 | assert 'name' in response and response['name'] == 'X-DAB-JW-TOKEN' 19 | -------------------------------------------------------------------------------- /test_app/tests/jwt_consumer/hub/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/jwt_consumer/hub/__init__.py -------------------------------------------------------------------------------- /test_app/tests/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/lib/__init__.py -------------------------------------------------------------------------------- /test_app/tests/lib/abstract_models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/lib/abstract_models/__init__.py -------------------------------------------------------------------------------- /test_app/tests/lib/abstract_models/test_organization.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytest 4 | from django.db import IntegrityError 5 | 6 | from test_app.models import Organization, Team 7 | 8 | 9 | @pytest.mark.django_db 10 | def test_organization_model(system_user): 11 | org = Organization.objects.create(name="acme", description="ACME Corp.") 12 | 13 | assert org.name == "acme" 14 | assert org.description == "ACME Corp." 15 | assert isinstance(org.created, datetime) 16 | assert org.created_by == system_user 17 | assert isinstance(org.modified, datetime) 18 | assert org.modified_by == system_user 19 | 20 | # I'm not sure why I have to add a delete in here. 21 | # If I don't it complains on cleanup that the org is referencing a user id 1 which no longer exists 22 | org.delete() 23 | 24 | 25 | @pytest.mark.django_db 26 | def test_organization_model_unique(): 27 | Organization.objects.create(name="acme", description="ACME Corp.") 28 | with pytest.raises(IntegrityError): 29 | Organization.objects.create(name="acme", description="Second ACME Corp.") 30 | 31 | 32 | @pytest.mark.django_db 33 | def test_organization_model_teams(): 34 | org = Organization.objects.create(name="acme") 35 | team = Team.objects.create(name="red", organization=org) 36 | 37 | assert list(org.teams.all()) == [team] 38 | -------------------------------------------------------------------------------- /test_app/tests/lib/cache/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/lib/cache/__init__.py -------------------------------------------------------------------------------- /test_app/tests/lib/channels/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/lib/channels/__init__.py -------------------------------------------------------------------------------- /test_app/tests/lib/channels/test_middleware.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import AsyncMock, Mock, patch 2 | 3 | import pytest 4 | 5 | import ansible_base.lib.channels.middleware as middleware 6 | 7 | 8 | @pytest.mark.django_db(transaction=True) 9 | @pytest.mark.asyncio 10 | async def test_middleware_auth_pass(local_authenticator, user): 11 | inner = AsyncMock() 12 | auth = middleware.DrfAuthMiddleware(inner) 13 | scope = {"session": {}, "headers": [(b"Authorization", b"Basic dXNlcjpwYXNzd29yZA==")]} 14 | await auth(scope, Mock(), Mock()) 15 | 16 | assert scope["user"] 17 | inner.assert_awaited_once() 18 | 19 | 20 | @pytest.mark.django_db(transaction=True) 21 | @pytest.mark.asyncio 22 | @patch('ansible_base.lib.channels.middleware.WebsocketDenier') 23 | async def test_middleware_auth_denied(denier_class, system_user, local_authenticator, user): 24 | denier = AsyncMock() 25 | denier_class.return_value = denier 26 | 27 | inner = AsyncMock() 28 | auth = middleware.DrfAuthMiddleware(inner) 29 | scope = {"session": {}, "headers": {}} 30 | await auth(scope, Mock(), Mock()) 31 | 32 | assert "user" not in scope 33 | inner.assert_not_awaited() 34 | denier.assert_awaited_once() 35 | -------------------------------------------------------------------------------- /test_app/tests/lib/dynamic_config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/lib/dynamic_config/__init__.py -------------------------------------------------------------------------------- /test_app/tests/lib/dynamic_config/test_settings_logic.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_base.lib.dynamic_config.settings_logic import get_dab_settings 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "caches,expect_exception", 8 | [ 9 | ({}, False), 10 | ({"default": {"BACKEND": "junk"}}, False), 11 | ({"default": {"BACKEND": "not_ansible_base.lib.cache.fallback_cache.DABCacheWithFallback"}}, True), 12 | ({"default": {"BACKEND": "ansible_base.lib.cache.fallback_cache.DABCacheWithFallback"}}, True), 13 | ({"default": {"BACKEND": "ansible_base.lib.cache.fallback_cache.DABCacheWithFallback"}, "primary": {}}, True), 14 | ({"default": {"BACKEND": "ansible_base.lib.cache.fallback_cache.DABCacheWithFallback"}, "fallback": {}}, True), 15 | ({"default": {"BACKEND": "ansible_base.lib.cache.fallback_cache.DABCacheWithFallback"}, "primary": {}, "fallback": {}}, False), 16 | ], 17 | ) 18 | def test_cache_settings(caches, expect_exception): 19 | try: 20 | get_dab_settings(installed_apps=[], caches=caches) 21 | except RuntimeError: 22 | if not expect_exception: 23 | raise 24 | -------------------------------------------------------------------------------- /test_app/tests/lib/logging/middleware/test_traceback_logger.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, mock 2 | 3 | from django.http import HttpRequest 4 | 5 | from ansible_base.lib.middleware.logging.log_request import LogTracebackMiddleware 6 | 7 | 8 | class TestLogTracebackMiddleware(TestCase): 9 | def test_log_traceback_middleware(self): 10 | get_response = mock.MagicMock() 11 | request = HttpRequest() 12 | request.method = "GET" 13 | request.path = "/test" 14 | 15 | middleware = LogTracebackMiddleware(get_response) 16 | response = middleware(request) 17 | 18 | # ensure get_response has been returned 19 | self.assertEqual(get_response.return_value, response) 20 | 21 | # mock handling signal while there is a request in transactions stored 22 | middleware.transactions = {"foobarid": request} 23 | middleware.handle_signal() 24 | -------------------------------------------------------------------------------- /test_app/tests/lib/logging/test_runtime.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from ansible_base.lib.logging.runtime import log_excess_runtime 5 | 6 | logger = logging.getLogger('test_app.tests.lib.logging') 7 | 8 | 9 | def sleep_for(delta): 10 | time.sleep(delta) 11 | 12 | 13 | def test_no_log_needed(caplog): 14 | df = log_excess_runtime(logger)(sleep_for) 15 | df(0) 16 | assert caplog.text == '' 17 | 18 | 19 | def test_debug_log(caplog): 20 | df = log_excess_runtime(logger, debug_cutoff=0.0)(sleep_for) 21 | with caplog.at_level(logging.DEBUG): 22 | df(0) 23 | assert "Running 'sleep_for' took " in caplog.text 24 | 25 | 26 | def test_info_log(caplog): 27 | df = log_excess_runtime(logger, debug_cutoff=0.0, cutoff=0.05)(sleep_for) 28 | with caplog.at_level(logging.INFO): 29 | df(0.1) 30 | assert "Running 'sleep_for' took " in caplog.text 31 | 32 | 33 | def extra_message(log_data): 34 | log_data['foo'] = 'bar' 35 | 36 | 37 | def test_extra_msg_and_data(caplog): 38 | df = log_excess_runtime(logger, debug_cutoff=0.0, add_log_data=True, msg='extra_message log foo={foo}')(extra_message) 39 | with caplog.at_level(logging.DEBUG): 40 | df() 41 | assert "extra_message log foo=bar" in caplog.text 42 | -------------------------------------------------------------------------------- /test_app/tests/lib/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/lib/routers/__init__.py -------------------------------------------------------------------------------- /test_app/tests/lib/serializers/test_fields.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from rest_framework.serializers import ValidationError 3 | 4 | from ansible_base.lib.serializers.fields import UserAttrMap 5 | 6 | 7 | def test_check_user_attribute_map_success(): 8 | attr_map = UserAttrMap() 9 | attr_map.run_validation(data={"username": "uid", "email": "mail", "first_name": "givenName", "last_name": "sn"}) 10 | assert True 11 | 12 | 13 | @pytest.mark.parametrize( 14 | "user_attr_map, exception", 15 | [ 16 | ({"email": False}, {"email": "Must be a string"}), 17 | ({"username": "uid"}, {"email": "Must be present"}), 18 | ({"weird_field": "oh_no"}, {"email": "Must be present", "weird_field": "Is not valid"}), 19 | ({"weird_field": "oh_no", "email": "mail"}, {"weird_field": "Is not valid"}), 20 | ("string!", 'Expected a dictionary of items but got type "str".'), 21 | ], 22 | ) 23 | def test_check_user_attribute_map_exceptions(user_attr_map, exception): 24 | with pytest.raises(ValidationError) as generated_exception: 25 | attr_map = UserAttrMap() 26 | attr_map.run_validation(data=user_attr_map) 27 | assert generated_exception.value.args[0] == exception 28 | -------------------------------------------------------------------------------- /test_app/tests/lib/sessions/stores/test_cached_dynamic_timeout.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.test import override_settings 3 | 4 | from ansible_base.lib.sessions.stores.cached_dynamic_timeout import DEFAULT_SESSION_TIMEOUT, SessionStore 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "setting,expected", 9 | [ 10 | (-1, -1), 11 | (0, 0), 12 | (12, 12), 13 | ('a', DEFAULT_SESSION_TIMEOUT), 14 | # We don't need to test if the setting is not passed because that would really test get_preference 15 | ], 16 | ) 17 | def test_get_session_cookie_age(setting, expected): 18 | with override_settings(SESSION_COOKIE_AGE=setting): 19 | session_store = SessionStore() 20 | assert session_store.get_session_cookie_age() == expected 21 | -------------------------------------------------------------------------------- /test_app/tests/lib/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/lib/templatetags/__init__.py -------------------------------------------------------------------------------- /test_app/tests/lib/templatetags/test_util.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.utils.safestring import SafeString 3 | 4 | from ansible_base.lib.templatetags.util import inline_file 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "path,is_safe,fatal", 9 | [ 10 | ("test_app/static/test_templatetags_util_inline_file.css", True, False), 11 | ("test_app/static/test_templatetags_util_inline_file.css", False, False), 12 | ("test_app/static/does_not_exist.css", True, True), 13 | ("test_app/static/does_not_exist.css", False, True), 14 | ], 15 | ) 16 | def test_inline_file(path, is_safe, fatal): 17 | if 'does_not_exist' in path: 18 | if fatal: 19 | with pytest.raises(FileNotFoundError): 20 | inline_file(path, is_safe, fatal) 21 | else: 22 | assert inline_file(path, is_safe, fatal) is None 23 | else: 24 | result = inline_file(path, is_safe, fatal) 25 | assert "div > p.foo" in result 26 | if is_safe: 27 | assert isinstance(result, SafeString) 28 | else: 29 | assert isinstance(result, str) 30 | -------------------------------------------------------------------------------- /test_app/tests/lib/test_prefixed_authentication_backend.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from rest_framework.test import APIClient 3 | 4 | from ansible_base.lib.utils.response import get_relative_url 5 | from test_app.models import User 6 | 7 | 8 | @pytest.fixture 9 | @pytest.mark.django_db(transaction=True) 10 | def prefixed_user(local_authenticator): 11 | user = User.objects.create(username='dab:foo') 12 | user.set_password("pass") 13 | user.save() 14 | return user 15 | 16 | 17 | def test_prefixed_user_can_login_with_original_username(prefixed_user): 18 | url = get_relative_url("rest_framework:login") 19 | me_url = get_relative_url("user-me") 20 | client = APIClient() 21 | 22 | data = {"username": "foo", "password": "pass"} 23 | resp = client.post(url, data=data, follow=True) 24 | resp = client.get(me_url) 25 | 26 | assert resp.status_code == 200 27 | assert resp.data["username"] == "dab:foo" 28 | 29 | 30 | def test_prefixed_user_can_login_with_prefixed_username(prefixed_user): 31 | url = get_relative_url("rest_framework:login") 32 | me_url = get_relative_url("user-me") 33 | client = APIClient() 34 | 35 | data = {"username": "dab:foo", "password": "pass"} 36 | resp = client.post(url, data=data, follow=True) 37 | resp = client.get(me_url) 38 | 39 | assert resp.status_code == 200 40 | assert resp.data["username"] == "dab:foo" 41 | -------------------------------------------------------------------------------- /test_app/tests/lib/testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/lib/testing/__init__.py -------------------------------------------------------------------------------- /test_app/tests/lib/testing/test_fixtures.py: -------------------------------------------------------------------------------- 1 | def test_settings_override_mutable(settings_override_mutable, settings): 2 | """ 3 | Ensure that when we modify a mutable setting, it gets reverted. 4 | """ 5 | assert settings.LOGGING['handlers']['console']['formatter'] == "simple" 6 | 7 | with settings_override_mutable('LOGGING'): 8 | settings.LOGGING['handlers']['console']['formatter'] = "not so simple" 9 | assert settings.LOGGING['handlers']['console']['formatter'] == "not so simple" 10 | 11 | del settings.LOGGING['handlers']['console']['formatter'] 12 | assert 'formattter' not in settings.LOGGING['handlers']['console'] 13 | 14 | assert settings.LOGGING['handlers']['console']['formatter'] == "simple" 15 | -------------------------------------------------------------------------------- /test_app/tests/lib/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/lib/utils/__init__.py -------------------------------------------------------------------------------- /test_app/tests/lib/utils/test_hashing.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from rest_framework.serializers import CharField, IntegerField, Serializer, UUIDField 4 | from typeguard import suppress_type_checks 5 | 6 | from ansible_base.lib.utils.hashing import hash_serializer_data 7 | 8 | DATA = {"name": "foo", "id": 1234, "uuid": uuid.uuid4()} 9 | 10 | 11 | class DataSerializer(Serializer): 12 | name = CharField() 13 | id = IntegerField() 14 | uuid = UUIDField() 15 | 16 | 17 | @suppress_type_checks 18 | def test_hash_serializer_data_idempotency(): 19 | """Test hashing same data gives same output""" 20 | assert hash_serializer_data(DATA, DataSerializer) == hash_serializer_data(DATA, DataSerializer) 21 | 22 | 23 | @suppress_type_checks 24 | def test_hash_serializer_data_difference(): 25 | """Test hashing different data changes the hash""" 26 | assert hash_serializer_data(DATA, DataSerializer) != hash_serializer_data({**DATA, **{"id": 4567}}, DataSerializer) 27 | 28 | 29 | @suppress_type_checks 30 | def test_hash_serializer_with_nested_field(): 31 | """Test hashing can be performed on nested data""" 32 | NESTED_DATA = {"field": {"name": "foo", "id": 1234}} 33 | 34 | class NestedSerializer(Serializer): 35 | def to_representation(self, instance): 36 | return instance 37 | 38 | assert hash_serializer_data(NESTED_DATA, NestedSerializer, "field") == hash_serializer_data(NESTED_DATA["field"], NestedSerializer) 39 | -------------------------------------------------------------------------------- /test_app/tests/oauth2_provider/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/oauth2_provider/__init__.py -------------------------------------------------------------------------------- /test_app/tests/oauth2_provider/checks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/oauth2_provider/checks/__init__.py -------------------------------------------------------------------------------- /test_app/tests/oauth2_provider/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/oauth2_provider/migrations/__init__.py -------------------------------------------------------------------------------- /test_app/tests/oauth2_provider/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_base.oauth2_provider.models import OAuth2AccessToken, OAuth2RefreshToken 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_oauth2_revoke_access_then_refresh_token(oauth2_admin_access_token): 8 | token = oauth2_admin_access_token[0] 9 | refresh_token = oauth2_admin_access_token[0].refresh_token 10 | assert OAuth2AccessToken.objects.count() == 1 11 | assert OAuth2RefreshToken.objects.count() == 1 12 | 13 | token.revoke() 14 | assert OAuth2AccessToken.objects.count() == 0 15 | assert OAuth2RefreshToken.objects.count() == 1 16 | assert not refresh_token.revoked 17 | 18 | refresh_token.revoke() 19 | assert OAuth2AccessToken.objects.count() == 0 20 | assert OAuth2RefreshToken.objects.count() == 1 21 | 22 | 23 | @pytest.mark.django_db 24 | def test_oauth2_revoke_refresh_token(oauth2_admin_access_token): 25 | refresh_token = oauth2_admin_access_token[0].refresh_token 26 | assert OAuth2AccessToken.objects.count() == 1 27 | assert OAuth2RefreshToken.objects.count() == 1 28 | 29 | refresh_token.revoke() 30 | assert OAuth2AccessToken.objects.count() == 0 31 | # the same OAuth2RefreshToken is recycled 32 | new_refresh_token = OAuth2RefreshToken.objects.all().first() 33 | assert refresh_token == new_refresh_token 34 | assert new_refresh_token.revoked 35 | -------------------------------------------------------------------------------- /test_app/tests/oauth2_provider/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_base.authentication.models import Authenticator, AuthenticatorUser 4 | from ansible_base.oauth2_provider.utils import is_external_account 5 | 6 | 7 | @pytest.mark.parametrize("link_local, link_ldap, expected", [(False, False, None), (True, False, None), (False, True, "ldap"), (True, True, "ldap")]) 8 | def test_oauth2_provider_is_external_account_with_user(user, local_authenticator, ldap_authenticator, link_local, link_ldap, expected): 9 | if link_local: 10 | # Link the user to the local authenticator 11 | local_au = AuthenticatorUser(provider=local_authenticator, user=user) 12 | local_au.save() 13 | if link_ldap: 14 | # Link the user to the ldap authenticator 15 | ldap_au = AuthenticatorUser(provider=ldap_authenticator, user=user) 16 | ldap_au.save() 17 | 18 | if expected == "ldap": 19 | expected = ldap_authenticator 20 | assert is_external_account(user) == expected 21 | 22 | 23 | def test_oauth2_provider_is_external_account_import_error(user, local_authenticator): 24 | au = AuthenticatorUser(provider=local_authenticator, user=user) 25 | au.save() 26 | local_authenticator.type = "test_app.tests.fixtures.authenticator_plugins.broken" 27 | # Avoid save() which would raise an ImportError 28 | Authenticator.objects.bulk_update([local_authenticator], ['type']) 29 | assert is_external_account(user) 30 | -------------------------------------------------------------------------------- /test_app/tests/oauth2_provider/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/oauth2_provider/views/__init__.py -------------------------------------------------------------------------------- /test_app/tests/oauth2_provider/views/test_authorization_root.py: -------------------------------------------------------------------------------- 1 | from ansible_base.lib.utils.response import get_relative_url 2 | 3 | 4 | def test_oauth2_provider_authorization_root_view(admin_api_client, unauthenticated_api_client, user_api_client): 5 | """ 6 | As an admin, accessing /o/ gives an index of oauth endpoints. 7 | """ 8 | url = get_relative_url("oauth_authorization_root_view") 9 | for client in (admin_api_client, unauthenticated_api_client, user_api_client): 10 | response = admin_api_client.get(url) 11 | assert response.status_code == 200 12 | assert 'authorize' in response.data 13 | -------------------------------------------------------------------------------- /test_app/tests/rbac/compatibility/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_base.rbac.models import RoleDefinition 4 | from ansible_base.rbac.permission_registry import permission_registry 5 | from test_app.models import UUIDModel 6 | 7 | 8 | @pytest.fixture 9 | def uuid_obj(organization): 10 | return UUIDModel.objects.create(organization=organization) 11 | 12 | 13 | @pytest.fixture 14 | def uuid_rd(): 15 | rd, _ = RoleDefinition.objects.get_or_create( 16 | permissions=['change_uuidmodel', 'view_uuidmodel', 'delete_uuidmodel', 'view_manualextrauuidmodel'], 17 | name='manage UUID model', 18 | content_type=permission_registry.content_type_model.objects.get_for_model(UUIDModel), 19 | ) 20 | return rd 21 | -------------------------------------------------------------------------------- /test_app/tests/rbac/compatibility/test_model_name_conflict.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.apps import apps 3 | from django.contrib.admin.models import LogEntry as AdminLogEntry 4 | 5 | from ansible_base.lib.utils.response import get_relative_url 6 | from ansible_base.rbac import permission_registry 7 | from ansible_base.rbac.management import create_dab_permissions 8 | from ansible_base.rbac.models import DABPermission 9 | from test_app.models import LogEntry 10 | 11 | 12 | def test_same_name_not_registered(): 13 | assert permission_registry.is_registered(LogEntry) 14 | assert not permission_registry.is_registered(AdminLogEntry) 15 | 16 | 17 | @pytest.mark.django_db 18 | def test_does_not_create_unregistered_permission_entries(): 19 | permission_ct = DABPermission.objects.count() 20 | # we should not have anything in the admin app registered 21 | create_dab_permissions(apps.get_app_config('admin'), apps=apps) 22 | assert permission_ct == DABPermission.objects.count() # permission count did not change 23 | 24 | 25 | @pytest.mark.django_db 26 | def test_create_custom_role_name_conflict_model(admin_api_client): 27 | url = get_relative_url('roledefinition-list') 28 | data = dict(name='Single Log Entry Viewer', content_type='aap.logentry', permissions=['aap.view_logentry']) 29 | response = admin_api_client.post(url, data=data, format="json") 30 | assert response.status_code == 201, response.data 31 | assert 'id' in response.data, response.data 32 | -------------------------------------------------------------------------------- /test_app/tests/rbac/features/test_cache_parent_perms.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.test import override_settings 3 | 4 | 5 | @pytest.mark.django_db 6 | @override_settings(ANSIBLE_BASE_CACHE_PARENT_PERMISSIONS=False) 7 | def test_parent_permissions_not_cached(rando, organization, org_inv_rd, inventory): 8 | org_inv_rd.give_permission(rando, organization) 9 | assert rando.has_obj_perm(inventory, 'change_inventory') 10 | assert not rando.has_obj_perm(organization, 'change_inventory') 11 | 12 | 13 | @pytest.mark.django_db 14 | @override_settings(ANSIBLE_BASE_CACHE_PARENT_PERMISSIONS=True) 15 | def test_parent_permissions_cached(rando, organization, org_inv_rd, inventory): 16 | org_inv_rd.give_permission(rando, organization) 17 | assert rando.has_obj_perm(inventory, 'change_inventory') 18 | assert rando.has_obj_perm(organization, 'change_inventory') 19 | -------------------------------------------------------------------------------- /test_app/tests/rbac/models/test_object_role.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | 5 | from ansible_base.rbac.models import ObjectRole 6 | from test_app.models import User 7 | 8 | 9 | @pytest.mark.django_db 10 | def test_existing_object_role_race(inv_rd, inventory): 11 | user1 = User.objects.create(username='user1') 12 | inv_rd.give_permission(user1, inventory) 13 | 14 | user2 = User.objects.create(username='user2') 15 | with mock.patch('django.db.models.query.QuerySet.first', return_value=None): 16 | assert ObjectRole.objects.filter(object_id=inventory.pk).first() is None # sanity 17 | inv_rd.give_permission(user2, inventory) 18 | assert user2.has_obj_perm(inventory, 'change') 19 | -------------------------------------------------------------------------------- /test_app/tests/rbac/models/test_permissions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.contrib.auth.models import Permission 3 | 4 | from test_app.models import Inventory, ProxyInventory 5 | 6 | 7 | @pytest.mark.django_db 8 | def test_inventory_permissions_duplicated(): 9 | "This assures that test_app has more than one model with the same permission" 10 | view_inv_perms = Permission.objects.filter(codename='view_inventory') 11 | assert view_inv_perms.count() == 2 12 | assert set(perm.content_type.model_class() for perm in view_inv_perms) == set([Inventory, ProxyInventory]) 13 | -------------------------------------------------------------------------------- /test_app/tests/rbac/models/test_role_assignments.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_base.rbac.models import RoleUserAssignment 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_load_assignment_list(rando, inventory, inv_rd, global_inv_rd): 8 | assignment = inv_rd.give_permission(rando, inventory) 9 | global_inv_rd.give_global_permission(rando) 10 | assert assignment.id in [asmt.id for asmt in RoleUserAssignment.objects.only('id')] 11 | 12 | 13 | @pytest.mark.django_db 14 | def test_load_assignment_property(rando, inventory, inv_rd, global_inv_rd): 15 | assignment = inv_rd.give_permission(rando, inventory) 16 | global_inv_rd.give_global_permission(rando) 17 | assert str(assignment.object_id) in [asmt.object_id for asmt in RoleUserAssignment.objects.only('object_id')] 18 | -------------------------------------------------------------------------------- /test_app/tests/rbac/test_management_rbac_checks.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | 3 | import pytest 4 | from django.contrib.contenttypes.models import ContentType 5 | 6 | from ansible_base.rbac.management.commands.RBAC_checks import Command 7 | from ansible_base.rbac.models import ObjectRole, RoleDefinition 8 | from test_app.models import Inventory 9 | 10 | 11 | def run_and_get_output(): 12 | cmd = Command() 13 | cmd.stdout = StringIO() 14 | cmd.handle() 15 | return cmd.stdout.getvalue() 16 | 17 | 18 | @pytest.mark.django_db 19 | def test_successful_no_data(): 20 | assert "checking for up-to-date role evaluations" in run_and_get_output() 21 | 22 | 23 | @pytest.mark.django_db 24 | def test_role_definition_wrong_model(organization): 25 | inventory = Inventory.objects.create(name='foo-inv', organization=organization) 26 | rd, _ = RoleDefinition.objects.get_or_create(name='foo-def', permissions=['view_organization']) 27 | orole = ObjectRole.objects.create(object_id=inventory.id, content_type=ContentType.objects.get_for_model(inventory), role_definition=rd) 28 | assert f"Object role {orole} has permission view_organization for an unlike content type" in run_and_get_output() 29 | -------------------------------------------------------------------------------- /test_app/tests/rbac/test_policies.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_base.rbac.policies import can_change_user 4 | from test_app.models import User 5 | 6 | 7 | @pytest.mark.django_db 8 | def test_org_admin_can_not_change_superuser(org_admin_rd, organization): 9 | org_admin = User.objects.create(username='org-admin') 10 | org_admin_rd.give_permission(org_admin, organization) 11 | 12 | admin = User.objects.create(username='new-superuser', is_superuser=True) 13 | assert not can_change_user(org_admin, admin) 14 | 15 | 16 | @pytest.mark.django_db 17 | def test_unrelated_can_not_change_user(): 18 | alice = User.objects.create(username='alice') 19 | bob = User.objects.create(username='bob') 20 | 21 | for first, second in [(alice, bob), (bob, alice)]: 22 | assert not can_change_user(first, second) 23 | 24 | 25 | @pytest.mark.django_db 26 | def test_superuser_can_change_new_user(admin_user): 27 | alice = User.objects.create(username='alice') 28 | assert can_change_user(admin_user, alice) 29 | 30 | 31 | @pytest.mark.django_db 32 | def test_user_can_manage_themselves(): 33 | alice = User.objects.create(username='alice') 34 | assert can_change_user(alice, alice) 35 | -------------------------------------------------------------------------------- /test_app/tests/resource_registry/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/resource_registry/__init__.py -------------------------------------------------------------------------------- /test_app/tests/resource_registry/conftest.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from unittest import mock 3 | 4 | import pytest 5 | 6 | from ansible_base.resource_registry import apps 7 | 8 | 9 | @pytest.fixture 10 | def enable_reverse_sync(settings): 11 | """ 12 | Useful for tests that deal with testing the reverse sync logic 13 | """ 14 | 15 | @contextmanager 16 | def f(mock_away_sync=False): 17 | # This is kind of a dance. We don't want to break other tests by 18 | # leaving the save method monkeypatched when they are expecting syncing 19 | # to be disabled. So we patch the save method, yield, reset 20 | # RESOURCE_SERVER_SYNC_ENABLED, undo the patch (disconnect_resource_signals), 21 | # and then reconnect signals (so the resource registry stuff still works) but 22 | # this time we don't monkeypatch the save method since RESOURCE_SERVER_SYNC_ENABLED 23 | # is back to its original value. 24 | is_enabled = settings.RESOURCE_SERVER_SYNC_ENABLED 25 | settings.RESOURCE_SERVER_SYNC_ENABLED = True 26 | apps.connect_resource_signals(sender=None) 27 | if mock_away_sync: 28 | with mock.patch('ansible_base.resource_registry.utils.sync_to_resource_server.get_resource_server_client'): 29 | yield 30 | else: 31 | yield 32 | apps.disconnect_resource_signals(sender=None) 33 | settings.RESOURCE_SERVER_SYNC_ENABLED = is_enabled 34 | apps.connect_resource_signals(sender=None) 35 | 36 | return f 37 | -------------------------------------------------------------------------------- /test_app/tests/resource_registry/models/test_service_id.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_base.resource_registry.models.service_identifier import ServiceID 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_service_id_already_exists(): 8 | "The resource registry already creates this, so we expect an error here" 9 | with pytest.raises(RuntimeError) as exc: 10 | ServiceID.objects.create() 11 | assert 'This service already has a ServiceID' in str(exc) 12 | 13 | 14 | @pytest.mark.django_db 15 | def test_service_id_does_not_yet_exist(): 16 | ServiceID.objects.first().delete() # clear out what migration created 17 | ServiceID.objects.create() # expect no error 18 | -------------------------------------------------------------------------------- /test_app/tests/resource_registry/test_metadata_api.py: -------------------------------------------------------------------------------- 1 | from ansible_base.lib.utils.response import get_relative_url 2 | from ansible_base.resource_registry.models import service_id 3 | 4 | 5 | def test_service_metadata(admin_api_client): 6 | """Test that the resource list is working.""" 7 | url = get_relative_url("service-metadata") 8 | resp = admin_api_client.get(url) 9 | 10 | assert resp.status_code == 200 11 | assert resp.data["service_type"] == "aap" 12 | assert resp.data["service_id"] == service_id() 13 | -------------------------------------------------------------------------------- /test_app/tests/resource_registry/test_migration.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_base.resource_registry.models import Resource 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_existing_resources_created_in_post_migration(): 8 | """ 9 | Test that resources that existed before the registry was added got 10 | created successfully. 11 | """ 12 | assert Resource.objects.filter(name="migration resource", content_type__resource_type__name="aap.resourcemigrationtestmodel").exists() 13 | -------------------------------------------------------------------------------- /test_app/tests/resource_registry/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ansible_base.resource_registry.models import ResourceType 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_get_conflicting_resource(admin_api_client, team): 8 | team_type = ResourceType.objects.get(name="shared.team") 9 | org_id = str(team.organization.resource.ansible_id) 10 | dupe = team_type.get_conflicting_resource({"name": team.name, "organization": org_id}) 11 | 12 | assert dupe.ansible_id == team.resource.ansible_id 13 | 14 | dupe = team_type.get_conflicting_resource({"name": "I don't exist", "organization": org_id}) 15 | assert dupe is None 16 | -------------------------------------------------------------------------------- /test_app/tests/rest_filters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/rest_filters/__init__.py -------------------------------------------------------------------------------- /test_app/tests/rest_filters/rest_framework/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/rest_filters/rest_framework/__init__.py -------------------------------------------------------------------------------- /test_app/tests/rest_filters/rest_framework/test_type_filter_backend.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock, Mock 2 | 3 | import pytest 4 | from django.core.exceptions import FieldError 5 | from rest_framework.exceptions import ParseError 6 | 7 | from ansible_base.authentication.models import Authenticator 8 | from ansible_base.authentication.views import AuthenticatorViewSet 9 | from ansible_base.rest_filters.rest_framework.type_filter_backend import TypeFilterBackend 10 | 11 | 12 | @pytest.mark.parametrize( 13 | "query", 14 | ( 15 | ({}), 16 | ({'not_type': 'something'}), 17 | ({'type': 'a,b'}), 18 | ({'type': 'a'}), 19 | ), 20 | ) 21 | @pytest.mark.django_db 22 | def test_TypeFilterBackend_filter_query_set(query): 23 | filter = TypeFilterBackend() 24 | request = MagicMock() 25 | request.query_params = query 26 | filter.filter_queryset(request, Authenticator.objects.all(), AuthenticatorViewSet) 27 | 28 | 29 | def test_TypeFilterBackend_filter_query_set_exception(): 30 | filter = TypeFilterBackend() 31 | request = MagicMock() 32 | request.query_params.items = Mock(side_effect=FieldError("missing field")) 33 | 34 | with pytest.raises(ParseError): 35 | filter.filter_queryset(request, Authenticator.objects.all(), AuthenticatorViewSet) 36 | -------------------------------------------------------------------------------- /test_app/tests/rest_filters/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from rest_framework.exceptions import ParseError, PermissionDenied 3 | 4 | from ansible_base.authentication.models import Authenticator 5 | from ansible_base.rest_filters.utils import get_field_from_path 6 | from test_app.models import EncryptionModel 7 | 8 | 9 | def test_invalid_field_hop(): 10 | with pytest.raises(ParseError) as excinfo: 11 | get_field_from_path(Authenticator, 'created_by__last_name__user') 12 | assert 'No related model for' in str(excinfo) 13 | 14 | 15 | def test_invalid_field_filter(): 16 | test_field = EncryptionModel.encrypted_fields[0] 17 | with pytest.raises(PermissionDenied) as excinfo: 18 | get_field_from_path(EncryptionModel, test_field) 19 | 20 | assert f"Filtering on field {test_field} is not allowed." in str(excinfo) 21 | -------------------------------------------------------------------------------- /test_app/tests/rest_pagination/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/django-ansible-base/d758999f02b543382c9b2b7777f27de6ddb81835/test_app/tests/rest_pagination/__init__.py -------------------------------------------------------------------------------- /test_app/tests/test_checks.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from ansible_base.authentication.models.authenticator import Authenticator 4 | from ansible_base.lib.checks import check_charfield_has_max_length 5 | 6 | 7 | def test_check_charfield_has_max_length_fails(): 8 | with mock.patch.object(Authenticator._meta.get_field('type'), 'max_length', new=None): 9 | errors = check_charfield_has_max_length(None) 10 | assert len(errors) == 1 11 | assert errors[0].id == 'ansible_base.E001' 12 | -------------------------------------------------------------------------------- /test_app/tests/test_demo_data.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from test_app.management.commands.create_demo_data import Command 4 | from test_app.models import Organization 5 | 6 | 7 | @pytest.mark.django_db 8 | def test_demo_data_with_existing_data(admin_user): 9 | Organization.objects.create(name='stub') 10 | Command().handle() 11 | assert Organization.objects.filter(name='AWX_community').exists() 12 | assert Organization.objects.filter(name='stub').exists() 13 | 14 | 15 | @pytest.mark.django_db 16 | def test_demo_data_create_data(admin_user): 17 | Command().handle() 18 | assert Organization.objects.filter(name='AWX_community').exists() 19 | 20 | 21 | @pytest.mark.django_db 22 | def test_demo_data_idempotent(admin_user): 23 | Command().handle() 24 | assert Organization.objects.filter(name='AWX_community').exists() 25 | Command().handle() 26 | assert Organization.objects.filter(name='AWX_community').count() == 1 27 | -------------------------------------------------------------------------------- /test_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import include, path, re_path 5 | 6 | from ansible_base.authentication.views.ui_auth import UIAuth 7 | from ansible_base.lib.dynamic_config.dynamic_urls import api_urls, api_version_urls, root_urls 8 | from ansible_base.resource_registry.urls import urlpatterns as resource_api_urls 9 | from test_app import views 10 | from test_app.router import router as test_app_router 11 | 12 | urlpatterns = [ 13 | path('', views.index_view), 14 | path('api/v1/ui_auth/', UIAuth.as_view(), name='ui-auth-view'), 15 | path('api/v1/', include(api_version_urls)), 16 | path('api/', include(api_urls)), 17 | path('', include(root_urls)), 18 | # views specific to test_app 19 | path('api/v1/', include(test_app_router.urls)), 20 | # Admin application 21 | re_path(r"^admin/", admin.site.urls, name="admin"), 22 | path('api/v1/', include(resource_api_urls)), 23 | path('api/v1/', views.api_root), 24 | path('api/v1/timeout_view/', views.timeout_view, name='test-timeout-view'), 25 | path('login/', include('rest_framework.urls')), 26 | path("__debug__/", include("debug_toolbar.urls")), 27 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 28 | -------------------------------------------------------------------------------- /test_app/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = test_app.wsgi:application 3 | master = true 4 | processes = 1 5 | http = :8000 6 | chmod-socket = 660 7 | vacuum = true 8 | 9 | # Log to stdout 10 | # logto = /dev/stdout 11 | log-master = true 12 | #disable-logging = true 13 | 14 | # Increase buffer size 15 | buffer-size = 32768 16 | 17 | # Give signal 6 (SIGABRT) to work with LogTracebackMiddleware 18 | http-timeout = 60 19 | harakiri = 60 20 | harakiri-graceful-timeout = 50 21 | harakiri-graceful-signal = 6 22 | py-call-osafterfork = true 23 | -------------------------------------------------------------------------------- /test_app/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test_app project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_app.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tools/dev_postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mirror.gcr.io/library/postgres:15 2 | 3 | ENV POSTGRES_DB=dab_db 4 | ENV POSTGRES_USER=dab 5 | ENV POSTGRES_PASSWORD=dabing 6 | 7 | USER postgres 8 | 9 | EXPOSE 5432 10 | 11 | HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD ["sh", "-c", "pg_isready -U \"$POSTGRES_USER\" -d \"$POSTGRES_DB\""] -------------------------------------------------------------------------------- /tools/vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Test App Server", 6 | "consoleTitle": "Test App Server", 7 | "type": "debugpy", 8 | "request": "launch", 9 | "program": "${workspaceFolder}/manage.py", 10 | "preLaunchTask": "Run vscodebootstrap.sh", 11 | "args": [ 12 | "runserver" 13 | ], 14 | "django": true, 15 | "autoStartBrowser": false 16 | }, 17 | { 18 | "name": "Test App Shell Plus", 19 | "consoleTitle": "Test App Shell Plus", 20 | "type": "debugpy", 21 | "request": "launch", 22 | "program": "${workspaceFolder}/manage.py", 23 | "args": [ 24 | "shell_plus" 25 | ], 26 | "django": true, 27 | "autoStartBrowser": false 28 | }, 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tools/vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "test_app" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true 7 | } 8 | -------------------------------------------------------------------------------- /tools/vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Run vscodebootstrap.sh", 6 | "type": "shell", 7 | "command": "sh", 8 | "args": [ 9 | "${workspaceFolder}/.vscode/vscodebootstrap.sh" 10 | ], 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tools/vscode/vscodebootstrap.sh: -------------------------------------------------------------------------------- 1 | python manage.py migrate 2 | python manage.py create_demo_data 3 | --------------------------------------------------------------------------------