Compare commits

...

258 Commits

Author SHA1 Message Date
AlanCoding
71b84fb980 Rewrite the s3 upload step to fix breakage with new Ansible version 2025-09-30 10:28:48 -04:00
jessicamack
98697a8ce7 Fix Grafana notification bug (#16104)
* accept empty string for dashboard and panel IDs

* update grafana tests and add new one
2025-09-29 10:04:58 -04:00
Alan Rominger
f1edbd8ef5 Add npm cache path to fix UI building (push images job) (#16097)
* Add npm cache path to fix UI building

* skip version switch
2025-09-23 09:16:41 -04:00
Alan Rominger
d0a99c37c1 Use action before schema logic, fix failure to upload schema (#16099)
Use action before schema logic
2025-09-23 09:16:07 -04:00
Andrea Restle-Lay
1f06d1bb9a [AAP-44277] License module now validates API responses for subscription IDs. (Moved from Tower) (#16096)
* resolve bug and add simple unit tests

* Update awx_collection/plugins/modules/license.py

Co-authored-by: Andrew Potozniak <tyraziel@gmail.com>

---------

Co-authored-by: Andrew Potozniak <tyraziel@gmail.com>
2025-09-22 15:27:47 -04:00
Alan Rominger
873f5c0ecc Remove some attached methods from User model (#15325)
Remove archaic monkey patches (#15338)

Remove some attached methods from User model

Test user-org sublist URLs we did not test before
2025-09-22 14:19:08 -04:00
AlanCoding
b31da105ad Merge remote-tracking branch 'awx/devel' into merge_26_2 2025-09-18 16:36:03 -04:00
Dirk Jülich
a285843cf2 AAP-35227 Extend role_check.py to delete orphaned InstanceLink objects as well (#7105) 2025-09-18 16:13:06 -04:00
AlanCoding
dd02d56de6 Prefer devel setup.cfg and TODO marks for expected awx-plugin 2025-09-18 15:57:51 -04:00
AlanCoding
b1944ba676 Remove code intended to be removed 2025-09-18 14:41:44 -04:00
AlanCoding
24818b510d Re-apply PR 15862 2025-09-18 09:41:41 -04:00
AlanCoding
55a7591f89 Resolve actions conflicts and delete unwatned files
Bump migrations and delete some files

Resolve remaining conflicts

Fix requirements

Flake8 fixes

Prefer devel changes for schema

Use correct versions

Remove sso connected stuff

Update to modern actions and collection fixes

Remove unwated alias

Version problems in actions

Fix more versioning problems

Update warning string

Messed it up again

Shorten exception

More removals

Remove pbr license

Remove tests deleted in devel

Remove unexpected files

Remove some content missed in the rebase

Use sleep_task from devel

Restore devel live conftest file

Add in settings that got missed

Prefer devel version of collection test

Finish repairing .github path

Remove unintended test file duplication

Undo more unintended file additions
2025-09-17 10:23:19 -04:00
Alan Rominger
38f858303d Use DAB utility to sync RoleDefinition compat role create (#7104) 2025-09-17 10:23:19 -04:00
Stevenson Michel
0fa8135691 Fix Role Definition Reverse Sync (#7097)
* created manual sync for role definition

* made changes for only read role
2025-09-17 10:23:19 -04:00
TVo
e63eba247f AAP-37812 Added mention about setting correct env variable in cli usage (#16091)
Added mention about setting correct env variable in cli usage
2025-09-09 15:22:08 -06:00
AlanCoding
8fb6a3a633 Merge remote-tracking branch 'tower/test_stable-2.6' into merge_26_2 2025-09-04 23:06:53 -04:00
thedoubl3j
7dc4f149a7 Fix rebase merge conflicts
* had to rebase and accept both in some cases
* remove unused imports
2025-09-04 15:17:54 -04:00
John Westcott IV
2c96c48a5c feat: Add ORG_ADMINS_CAN_SEE_ALL_USERS and MANAGE_ORGANIZATION_AUTH to settings migration (#7075)
- Add ORG_ADMINS_CAN_SEE_ALL_USERS and MANAGE_ORGANIZATION_AUTH to the
  settings_to_migrate list in SettingsMigrator
- Create comprehensive unit tests for SettingsMigrator class with
  parameterized test cases
- Tests cover all migration scenarios including the new organizational
  settings
- Refactored tests use pytest.mark.parametrize for better maintainability
  and coverage

Co-authored-by: Claude <claude@anthropic.com>
2025-09-04 15:13:21 -04:00
Hao Liu
58dcd2f5dc Bump setuptools to 80.9.0 (#7076)
Updated setuptools version from 78.1.1 to 80.9.0 in Makefile, requirements.in, and requirements.txt to ensure compatibility and address any potential issues with older versions.
2025-09-04 15:13:21 -04:00
Peter Braun
25896a8772 Fix credential types no org (#7078)
* Allow creating galaxy credential types without an organization (#16077)

* remove requirement for galaxy credentials to belong to an organization

* remove organization check for galaxy credential type

* add functional test
2025-09-04 15:13:20 -04:00
Andrew Potozniak
d96727c3bd Remove 'Controller' from name (#7077) 2025-09-04 15:13:20 -04:00
Hao Liu
d8737435fa [stable-2.6] Bump dependency (#7070)
* Update Python dependencies

Relaxed or updated version constraints for several dependencies in requirements files and Makefile, including Cython, asciichartpy, msgpack, python-daemon, and pyyaml. These changes address build issues, remove unnecessary pins, and update to newer compatible versions.

* remove docutils license

* we no longer have this as a dep so we don't need to carry its license

* Update dependencies to address security vulnerabilities

Bumped versions of cryptography, protobuf, and idna in requirements to address CVE-2024-26130, CVE-2025-4565, and CVE-2024-3651. These updates improve security by resolving known vulnerabilities in the affected packages.

---------

Co-authored-by: thedoubl3j <jljacks93@gmail.com>
2025-09-04 15:13:20 -04:00
Madhu Kanoor
bb46268eec [AAP-52144] Remove AWX Prefix from the SAML migrator (#7072)
We were adding an AWX prefix to SAML migrator
2025-09-04 15:13:20 -04:00
Peter Braun
af2efec2b4 fix: do not create multiple mappers for lists of emails or usernames (#7063)
* fix: do not create multiple mappers for lists of emails or usernames

* fix: create multiple matchers, don't rely on matches_or

* fix tests

* truncate mapper names to a max of 128 chars

* better naming scheme for matchers
2025-09-04 15:13:20 -04:00
John Westcott IV
a7eb1ef763 [AAP-51531] Fix LDAP authentication mapping and bug in LDAP migration (#7061)
* Add LDAP support to gateway_mapping and expand test coverage

- Add new process_ldap_user_list function for LDAP group processing
- Add auth_type parameter to org_map_to_gateway_format and team_map_to_gateway_format
- Support both 'sso' and 'ldap' authentication types in mapping functions
- Fix syntax error and logic bug in existing code
- Add comprehensive unit tests for process_ldap_user_list function (13 test cases)
- Add unit tests for auth_type parameter functionality
- Update helper functions to support new auth_type parameter
- All tests pass and maintain backward compatibility

Technical changes:
- process_ldap_user_list handles None, boolean, string, and list inputs
- Proper type hints with mypy compatibility
- LDAP groups use 'has_or' trigger format vs SSO attribute matching
- Boolean True/False create Always/Never Allow triggers for LDAP
- Maintains proper ordering and mapper structure

Co-authored-by: Claude (Anthropic AI Assistant) <claude@anthropic.com>

* Fix empty list bug in process_ldap_user_list and add comprehensive tests

- Fix process_ldap_user_list to return empty list for empty input instead of creating invalid trigger
- Empty list [] now correctly returns no triggers instead of trigger with empty has_or array
- Add test case for empty list behavior in both LDAP and SSO functions
- Update existing test_empty_list to expect correct behavior (0 triggers)
- Maintain backward compatibility for all other input types
- Comprehensive testing confirms no regression in existing functionality

Bug Details:
- Before: process_ldap_user_list([]) returned [{'name': 'Match User Groups', 'trigger': {'groups': {'has_or': []}}}]
- After: process_ldap_user_list([]) returns [] (correct behavior)
- SSO function already handled this correctly

This prevents potential Gateway issues with empty has_or arrays and ensures logical consistency.

Co-authored-by: Claude (Anthropic AI Assistant) <claude@anthropic.com>

* Add comprehensive LDAP migrator tests and fix category handling

- Add comprehensive unit test suite for LDAPMigrator class (26 tests)
- Test LDAP configuration scenarios including multiple instances, mappings, and edge cases
- Add tests for mixed boolean/group mappings, special characters in org names, and empty configs
- Fix LDAP authenticator category to always be 'ldap' (not 'ldap<suffix>')
- Add auth_type='ldap' parameter to org_map_to_gateway_format and team_map_to_gateway_format calls
- Include AAP-51531 reference comments for specific test cases
- All tests passing (26/26)

Co-authored-by: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude (Anthropic AI Assistant) <claude@anthropic.com>
2025-09-04 15:13:20 -04:00
Zack Kayyali
e9928ff513 Disable SAML Authenticator upon migrate (#7062) 2025-09-04 15:13:20 -04:00
John Westcott IV
df1c453c37 Fix type hints in gateway_mapping.py process_sso_user_list function (#7060)
- Added missing Pattern and Any imports from typing
- Fixed users parameter type hint to include Pattern[str]
- Simplified overly complex return type annotation to use Any
- Added proper type narrowing with isinstance() and cast()
- Resolved mypy errors about incompatible list item types

Co-authored-by: Claude (Anthropic AI Assistant) <claude@anthropic.com>
2025-09-04 15:13:19 -04:00
Madhu Kanoor
5a89d7bc29 fix: order of role and attr in saml user_flags (#7050)
https://issues.redhat.com/browse/AAP-51127

Co-authored-by: Peter Braun <pbraun@redhat.com>
2025-09-04 15:13:19 -04:00
John Westcott IV
505ec560c8 feat: comprehensive refactor of SSO org/team mapping for Gateway authentication export (#7047)
This commit completely refactors how SSO organization and team mappings are processed
and exported for Gateway authentication, moving from a group-based approach to a more
flexible attribute-based system.

Key Changes:
- Introduced new process_sso_user_list() function for centralized user processing
- Enhanced boolean handling to support both native booleans and string representations
- Added email detection and regex pattern support for flexible user matching
- Refactored trigger generation from groups-based to attributes-based system

Gateway Mapping Enhancements (awx/main/utils/gateway_mapping.py):
- Added email regex detection for automatic email vs username classification
- Added pattern_to_slash_format() for regex pattern conversion
- Enhanced process_sso_user_list() with support for:
  - Boolean values: True/False and ["true"]/["false"]
  - String usernames and email addresses with automatic detection
  - Regex patterns with both username and email matching
  - Custom email_attr and username_attr parameters
- Refactored team_map_to_gateway_format() to use new processing system
- Refactored org_map_to_gateway_format() to use new processing system
- Changed trigger structure from {"groups": {"has_or": [...]}} to attribute-based triggers
- Improved naming convention to include trigger type in mapping names

Comprehensive Test Coverage (awx/main/tests/unit/utils/test_auth_migration.py):
- Added complete TestProcessSSOUserList class with 8 comprehensive test methods
- Enhanced TestOrgMapToGatewayFormat with string boolean and new functionality tests
- Enhanced TestTeamMapToGatewayFormat with string boolean and new functionality tests
- Added tests for email detection, regex patterns, and custom attributes
- Verified backward compatibility and integration functionality
- All existing tests updated to work with new attribute-based trigger system

Breaking Changes:
- Trigger structure changed from group-based to attribute-based
- Mapping names now include trigger descriptions for better clarity
- Function signatures updated to include email_attr and username_attr parameters

Co-Authored with Claude-4 via Cursor

Co-authored-by: Peter Braun <pbraun@redhat.com>
2025-09-04 15:13:19 -04:00
Peter Braun
4f2d28db51 Aap 50951 (#7053)
* disable authenticators that require updating the redirect URL and add groups claim to AzureAD migrator

* update tests
2025-09-04 15:13:19 -04:00
Zack Kayyali
0b17007764 AAP-49910 - Delete legacy authenticator code 2025-09-04 15:13:19 -04:00
Stevenson Michel
dfad93cf4c Deprecate legacy OAuth2 Application feature (#7045)
* Marked APIs legacy OAuth applications as deprecated

* Readded deprecation

* Fixed linter

* Added more deprecated mark to Oauth2 Api apps

* Fixed deprecation errors

* Fix tests
2025-09-04 15:13:19 -04:00
Peter Braun
6bd7c3831f enable azure ad authenticator by default (#7043) 2025-09-04 15:13:19 -04:00
Seth Foster
7b56f23c0e Prevent remote sync if rbac sync is disabled (#7044)
Syncing from new rbac to old rbac locally calls the
disable_rbac_sync() context manager.

If rbac sync is disabled, we do not need to remote
sync, as we can assume the remote syncing already
occurred in the viewset.

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
2025-09-04 15:13:18 -04:00
Peter Braun
3e1b9b2c88 Improve redirect override (#7042)
* handle login redirect for the oidc migrator

* handle updating login override redirect centrally in the settings migrator

* update unit tests
2025-09-04 15:13:18 -04:00
Bruno Cesar Rocha
cf0bc16cf7 fix: tacacs+ -> TACACSPLUS (#7039)
* fix: tacacs+ -> TACACSPLUS

Gateway doesn't allow `+` to be used in slug.

AAP-50774

* Fixed assertion

---------

Co-authored-by: Andrew Potozniak <potozniak@redhat.com>
2025-09-04 15:13:18 -04:00
Andrew Potozniak
a0b6083d4e Slightly better error handling for non 200 status codes from Gateway. (#7038)
* Slightly better error handling for non 200 status codes from Gateway.

* Apply suggestion from @chrismeyersfsu

Co-authored-by: Chris Meyers <chrismeyersfsu@users.noreply.github.com>

---------

Co-authored-by: Chris Meyers <chrismeyersfsu@users.noreply.github.com>
2025-09-04 15:13:18 -04:00
Andrew Potozniak
d452098123 [AAP-50446] Error handling enhancements and GATEWAY_BASE_URL override (#7037)
* Added better error handling and messaging when the service token authentication is broken.  Allowed for GATEWAY_BASE_URL to override the service token's base url if it is set in the environment variables.
Co-Authored-By: Cursor (claude-4-sonnet)

* Removed GATEWAY_BASE_URL override for service token auth.
2025-09-04 15:13:18 -04:00
Alan Rominger
c5fb0c351d AAP-47283 [2.6] Unified display of RBAC & synchronization (#7001)
* Working branch for testing DAB RBAC changes

* AAP-48392 Handle DAB RBAC either before or after new type model (for merge) (#16045)

* Handle DAB RBAC either before or after new type model

* Translate CT to DAB CT

* Fix for rearrangement of post_migration methods

* Directly include RBAC service URLs

* Add a run before remote permission additions

* Sync old rbac to remote rbac (#7025)

Signed-off-by: Seth Foster <fosterbseth@gmail.com>

* Set DAB requirement back to devel

---------

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
Co-authored-by: Seth Foster <fosterseth@users.noreply.github.com>
2025-09-04 15:13:10 -04:00
Peter Braun
a3f2401740 feat: exit code 1 if any migration fails (#7036)
* feat: exit code 1 if any migration fails

* update tests

* remove unused variables
2025-09-04 15:03:59 -04:00
Peter Braun
ad461a3aab fix: inconsistent return values in github migrator (#7035)
* fix: inconsistent return values in github migrator

* feat: check setting value before updating and report correct status

* fix linter issues
2025-09-04 15:03:59 -04:00
Peter Braun
44c53b02ae Aap 49709 - settings migration (#7023)
* migrate settings using the existing authenticator framework

* add method to get settings value to gateway client

* add transformer functions for settings

* Switched back to PUT for settings updates

* Started wiring in testing changes

* Added settings_* aggregation results.  Added skip-github option.  Added tests.

Assisted-by: Cursor

* Added --skip-all-authenticators command line argument.  Added GoogleOAuth testing.  Added tests for skipping all authenticators.

Assisted-by: Cursor

* wip: migrate other missing settings

* update login_redirect_override in google_oauth2

* impement login redirect for azuread

* implement login redirect for github

* implement login redirect for saml

* set LOGIN_REDIRECT_OVERRIDE even if no authenticator matched

* extract logic for login redirect override to base class

* use urlparse to compare valid redirect urls

* Preserve the original query parameters

* Fix flake8 issues

* Preserve the query parameter in sso_login_url

Gateway sets the sso_login_url to

/api/gateway/social/login/aap-saml-keycloak/?idp=IdP

The idp needs to be preserved when creating the redirect

* Update awx/main/utils/gateway_client.py

Co-authored-by: Chris Meyers <chrismeyersfsu@users.noreply.github.com>

* Update awx/main/management/commands/import_auth_config_to_gateway.py

Co-authored-by: Chris Meyers <chrismeyersfsu@users.noreply.github.com>

* list of settings updated

* Update awx/main/utils/gateway_client.py

Co-authored-by: Chris Meyers <chrismeyersfsu@users.noreply.github.com>

* Update awx/sso/utils/base_migrator.py

Co-authored-by: Chris Meyers <chrismeyersfsu@users.noreply.github.com>

* fix tests

---------

Co-authored-by: Andrew Potozniak <potozniak@redhat.com>
Co-authored-by: Madhu Kanoor <mkanoor@redhat.com>
Co-authored-by: Chris Meyers <chrismeyersfsu@users.noreply.github.com>
2025-09-04 15:03:59 -04:00
Bruno Rocha
58e237a09a fix: address reviewer comments 2025-09-04 15:03:59 -04:00
Bruno Cesar Rocha
052166df39 feat: OIDC Migrator (#7026)
AAP-48486

Add implementation to the existing oidc_migrator
2025-09-04 15:03:59 -04:00
Andrew Potozniak
b4ba7595c6 Unit Test Refactor for test_import_auth_config_to_gateway (#7024)
* Refactoring

Assisted-by: Cursor
2025-09-04 15:03:58 -04:00
Madhu Kanoor
c5211df9ca [AAP-48863] SAML Mapping migration (#7011)
This PR maps the

* SOCIAL_AUTH_SAML_TEAM_ATTR
* SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR
* SOCIAL_AUTH_SAML_ORGANIZATION_ATTR

https://issues.redhat.com/browse/AAP-48863
2025-09-04 15:03:58 -04:00
Andrew Potozniak
5e0870a7ec AAP-48510 Enable Service Tokens with the Authentication Migration Management Command (#7017)
* Enabled Service Token Auth for Management Command import_auth_config_to_gateway

Co-authored-by: Peter Braun <pbraun@redhat.com>
Co-authored-by: Zack Kayyali <zkayyali@redhat.com>
Assisted-by: Cursor
2025-09-04 15:03:58 -04:00
jessicamack
0936b28f9b Migrate nested team memberships to direct team memberships (#7005)
* migrate team on team users

add setting to prevent team on team cases. remove tests that should fail now

* adjust tests for disallowing team on teams

* use RoleUserAssignment to retrieve users

* assign users with RoleUserAssignment instead

* fix broken test

* move methods out to utils file. add tests

* add missed positional arg

* test old rbac system also consolidates

* fix test
2025-09-04 15:03:58 -04:00
Fabricio Aguiar
8e58fee49c feat: Add migrator for Google OAuth2 authenticator (#7018)
Signed-off-by: Fabricio Aguiar <fabricio.aguiar@gmail.com>
2025-09-04 15:03:58 -04:00
Peter Braun
e746589019 Aap 49570 (#7022)
* consider global org and team maps for github authenticator

* consider global org and team maps for saml authenticator
2025-09-04 15:03:58 -04:00
Bruno Cesar Rocha
c4a6b28b87 feat: AAP-48499 TACACS+ authenticator migrator (#7014)
* feat: AAP-48499 TACACS+ authenticator migrator

Issue: AAP 48499

* enable by default
2025-09-04 15:03:58 -04:00
Bruno Cesar Rocha
abc4692231 feat: AAP-48498 RADIUS authenticator migrator (#7013)
* feat: AAP-48498 Radius authenticator migrator

Issue: AAP-48498

* fix: Namingm Style and tests

* enabled by default

* test: SECRET is now ignored unless --force is set
2025-09-04 15:03:57 -04:00
Peter Braun
ab9bde3698 add force flag to enforce updates even when authenticator already exists (#7015)
* add force flag to enforce updates even when authenticator already exists

* remove cleartext field

* update list of encrypted fields

* show updated and unchanged authenticators in report
2025-09-04 15:03:57 -04:00
Peter Braun
c5e55fe0f5 Aap 48489 (#7003)
* collect controller ldap configuration

* translate role mapping and submit ldap authenticator

* implement require and deny group mapping

* remove all references of awx in the naming

* fix linter issues

* address PR feedback

* update ldap authenticator naming

* update github authenticator naming

* assume that server_uri is always a string

* update order of evaluation for require and deny groups

* cleanup and move ldap related functions into the ldap migrator

* add skip option for saml

* update saml authenticator to new slug format

* update azuread authenticator to new slug format
2025-09-04 15:03:57 -04:00
John Westcott IV
6b2e9a66d5 Adding Azure AD export command (#7010) 2025-09-04 15:03:57 -04:00
Madhu Kanoor
512857c2a9 [AAP-48496] SAML Migration from Controller to Gateway (#6998)
This PR migrates the SAML configuration from the Controller
to the Gateway, it intentionally skips setting the CALLBACK_URL
so that the Gateway can fill in the appropriate URL.
2025-09-04 15:03:57 -04:00
Seth Foster
c2c0f2b828 [2.6] Remove controller specific role definitions (#7002)
Remove Controller specific roles

Removes
- Controller Organization Admin
- Controller Organization Member
- Controller Team Admin
- Controller Team Member
- Controller System Auditor

Going forward the platform role definitions
will be used, e.g. Organization Member

The migration will take care of any assignments
with those controller specific roles and use
the platform roles instead.

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
2025-09-04 15:03:57 -04:00
Jake Jackson
534549139c bump DAB dep to devel from stable-2.5 (#6988)
* update dab dependency for 2.6 development
2025-09-04 15:03:57 -04:00
Peter Braun
d98118a108 compare authenticators and mappers before recreating them (#6989)
* compare authenticators and mappers before recreating them

* add unit tests

* fix linter errors

* refactor and improve: better implementation for get_authenticator_by_slug and removal of redundant code

* add submit_authenticator method to handle create vs. update in a generic way

* remove unused import
2025-09-04 15:03:56 -04:00
Peter Braun
e4758e8b4b Split up migrators (#6986)
* split up migration into classes for each authenticator

* remove unused import

* remove unused code

* remove unused class
2025-09-04 15:03:56 -04:00
Hao Liu
46710c4d86 AAP-48070 Remove ALLOW_LOCAL_RESOURCE_MANAGEMENT setting and enable local resource management (#16033) (#6985)
Remove ALLOW_LOCAL_RESOURCE_MANAGEMENT setting and enable local resource management

This commit removes the ALLOW_LOCAL_RESOURCE_MANAGEMENT setting and all associated
functionality, making the behavior as if the setting is always enabled.

Changes:
- Remove ALLOW_LOCAL_RESOURCE_MANAGEMENT setting from defaults.py
- Remove @immutablesharedfields decorator and all related logic
- Remove decorator applications from Organization, Team, and User API views
- Remove role assignment restrictions in UserRolesList and RoleUsersList
- Remove test file for immutablesharedfields functionality
- Clean up unused imports

Result: Organizations, Teams, and Users can now always be created, modified,
and deleted via the API without platform ingress restrictions.
2025-09-04 15:03:54 -04:00
Hao Liu
b70e884484 AAP-47495 Hide CSRF_TRUSTED_ORIGINS (#16035) (#6984)
Hide CSRF_TRUSTED_ORIGINS
2025-09-04 15:02:40 -04:00
Peter Braun
05b6f4fcb9 Aap 47760 - initial auth migration management command (#6981)
* wip: management command for authenticator export to GateWay

* wip: implement ldap auth config migration

* refactor: split concerns into gathering config and converting / recreating config

* refactor: dry run by default

* use the authenticator slug for idempotency

* move to correct utils path

* use env vars instead of flags, fix linter errors

* remove unused import
2025-09-04 15:02:38 -04:00
Peter Braun
243e27c7a9 Aap 49452 - support CONTROLLER_OPTIONAL_API_URLPATTERN_PREFIX in awxkit (#16085)
* fix: awxkit should honor CONTROLLER_OPTIONAL_API_URLPATTERN_PREFIX if defined

* add unit tests

* update tests
2025-09-03 15:22:38 -04:00
Dan Leehr
7fe525a533 Fix issue with some modules not honoring Controller API prefix (#16080)
* Fix issue where export module does not honor CONTROLLER_OPTIONAL_API_URLPATTERN_PREFIX

* Add unit test and handle leading/trailing slashes

* Reformat

* Refactor for clarity

* Remove unused import
2025-09-03 14:58:07 -04:00
Stevenson Michel
c36ce902db AAP-42929 : Retrieval of Projects of a Team and Teams of a Project (#7086)
* Fixed merge conflicts

* fix linters

* Added test for projectTeamsList
2025-09-03 14:05:17 -04:00
Lila Yasin
44e9dee9c7 [Bug Fix 4.6] AAP-49077 Task stdout escapes quotes twice only with Controller API api/v2/jobs/{id}/stdout/?format=txt (#7071)
* Move logic to unified job model instead of view

* Refine logic to only apply to double escaped characters to prevent touching unicord chars

* Refine logic to only apply to stdout so that it does not impact webhook notifications

* Revise naming to reflect correction to escapes, not just escape quotes

* Update code comments to reflect fixing double escapes vs double escaped quotes specifically

* Add regex for 5 most common python escape chars to make fix more robust
2025-09-02 14:49:13 -04:00
Dan Leehr
51eb109dbe Fix issue with some modules not honoring Controller API prefix (#16080)
* Fix issue where export module does not honor CONTROLLER_OPTIONAL_API_URLPATTERN_PREFIX

* Add unit test and handle leading/trailing slashes

* Reformat

* Refactor for clarity

* Remove unused import
2025-09-02 17:48:24 +02:00
Peter Braun
5ca76f3d64 Aap 49452 - support CONTROLLER_OPTIONAL_API_URLPATTERN_PREFIX in awxkit (#16085)
* fix: awxkit should honor CONTROLLER_OPTIONAL_API_URLPATTERN_PREFIX if defined

* add unit tests

* update tests
2025-09-02 14:47:32 +02:00
jessicamack
e3a9d9fbe8 [AAP-51443]CVE-2025-48432 (#7073)
* bump Django version to patch with additional hardening
2025-08-29 15:57:16 -04:00
Peter Braun
8b13c75f2e Allow creating galaxy credential types without an organization (#16077) (#7074)
* remove requirement for galaxy credentials to belong to an organization

* remove organization check for galaxy credential type
2025-08-28 15:15:36 +02:00
Jake Jackson
36ec5efc88 update work flow to actually fail (#7069)
* the workflow has been failing silently without catching a merge
  conflict. this removes the fail pretty logic previously implemented.
* just fail if a merge conflict is encountered
2025-08-21 18:49:54 +00:00
Lila Yasin
4e332ac2c7 AAP-45933 [2.5 Backport] AAP-4865 bug fact storage (#6945)
* Revise start_fact_cache and finish_fact_cache to use JSON file (#15970)

* Revise start_fact_cache and finish_fact_cache to use JSON file with host list inside it

* Revise artifacts path to be relative to the job private_data_dir

* Update calls to start_fact_cache and finish_fact_cache to agree with new reference to artifacts_dir

* Prevents unnecessary updates to ansible_facts_modified, fixing timestamp-related test failures.

* Import bulk_update_sorted_by_id

* Removed assert that calls ansible_facts_new which was removed in the backported pr

* Add import of Host back
2025-08-20 10:22:15 -04:00
Lila Yasin
b730bfa193 Continue work on collection ci (#16071)
* Fix some patterns in collection test playbooks

* Revert change to ansible.builtin.user

* Revert change to WFJT for dup label error

* Add error handling and fix references

* Add back lookup organization

* Fix all remainingfailing syntax in workflow_job_template

* Allow creating galaxy credential types without an organization (#16077)

* remove requirement for galaxy credentials to belong to an organization

* remove organization check for galaxy credential type

---------

Co-authored-by: AlanCoding <arominge@redhat.com>
Co-authored-by: Peter Braun <pbraun@redhat.com>
2025-08-20 10:19:53 -04:00
Jake Jackson
8fe4223eac [AAP-47384] CVE 2025 47273 (#7054)
* Update requirements for setuptools

* first pass and need to commit

* update makefile and run updater script

* updated makefile per readme
* ran updater script

* Patch irc backend to avoid namespace collision w/ jaraco

When importing the IRC backend, jaraco resolves to
the version vendored inside setuptools:

1) importing irc backend…
irc_backend ERROR: ModuleNotFoundError("No module named 'jaraco.stream'")

2) sys.modules['jaraco'] after failure:
present: True
type: <class 'module'>
__file__: /var/lib/awx/venv/awx/lib64/python3.11/site-packages/setuptools/_vendor/jaraco/__init__.py
__path__: ['/var/lib/awx/venv/awx/lib64/python3.11/site-packages/setuptools/_vendor/jaraco']
__spec__: ModuleSpec(name='jaraco',
loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f006a0eccd0>,
origin='/var/lib/awx/venv/awx/lib64/python3.11/site-packages/setuptools/_vendor/jaraco/__init__.py',
submodule_search_locations=['/var/lib/awx/venv/awx/lib64/python3.11/site-packages/setuptools/_vendor/jaraco'])

Since setuptools does not vendor jaraco.stream, it blew up. This patch ensures
jaraco.stream gets imported *before* attempting to import the irc modules.

* Revert "[4.6][dependency] CVE 2025 47273 (#7020)" (#7027)

This reverts commit e8b2920aec.

* reformatted irc backend with black

* ran black to fix linting issues

* Reapply "[4.6][dependency] CVE 2025 47273 (#7020)" (#7027)

This reverts commit 0c6df9b13398a93569fae7558e1a0e72cbe8fb6c.

* add flake8 ignore since jaraco.stream is needed

* jaraco.stream is not directly called in the file but is needed by irc
  so ignore the linter failure

---------

Co-authored-by: Shane McDonald <me@shanemcd.com>
2025-08-19 15:59:24 +00:00
Peter Braun
461678df08 Allow creating galaxy credential types without an organization (#16077)
* remove requirement for galaxy credentials to belong to an organization

* remove organization check for galaxy credential type
2025-08-18 14:21:24 +02:00
Peter Braun
e8c4b302ad remove requirement for galaxy credentials to belong to an organization (#16075) (#7066) 2025-08-15 16:27:22 -04:00
Chris Meyers
e82de50edb Fix controller_oauthtoken regression and more
* aap_token now functions like controller_oauthtoken
* lookup('awx.awx.controller_api', ...) fixed
2025-08-15 10:00:37 -04:00
Robin Bobbitt
11f31ef796 AAP-43883: clear cached LICENSE setting on change (#16065) (#7064)
* clear LICENSE from cache on change



* Adds tests for license cache clearing

Generated by Cursor (claude-4-sonnet)



* test fixes

Generated with Cursor (claude-4-sonnet)



---------

Signed-off-by: Robin Y Bobbitt <rbobbitt@redhat.com>
Co-authored-by: Jake Jackson <jljacks93@gmail.com>
2025-08-14 14:02:34 -04:00
Peter Braun
09b539bc34 remove requirement for galaxy credentials to belong to an organization (#16075) 2025-08-14 14:50:40 +00:00
Robin Bobbitt
9033e829fe fixes UnboundLocalError in POST /attach (#16062) (#7059)
* fixes UnboundLocalError in POST /attach
* bust cache for credentials before attaching subscription
---------

Signed-off-by: Robin Y Bobbitt <rbobbitt@redhat.com>
2025-08-14 09:56:25 -04:00
Elyézer Rezende
4757785016 Pin ansible-core for collection tests (#7030)
Signed-off-by: Elyézer Rezende <elyezermr@gmail.com>
2025-08-12 14:43:52 -04:00
Zack Kayyali
902f2634a6 AAP-49910 - Delete legacy authenticator code 2025-08-11 11:25:50 -04:00
Robin Bobbitt
793c85ef24 AAP-43883: clear cached LICENSE setting on change (#16065)
* clear LICENSE from cache on change

Signed-off-by: Robin Y Bobbitt <rbobbitt@redhat.com>

* Adds tests for license cache clearing

Generated by Cursor (claude-4-sonnet)

Signed-off-by: Robin Y Bobbitt <rbobbitt@redhat.com>

* test fixes

Generated with Cursor (claude-4-sonnet)

Signed-off-by: Robin Y Bobbitt <rbobbitt@redhat.com>

---------

Signed-off-by: Robin Y Bobbitt <rbobbitt@redhat.com>
Co-authored-by: Jake Jackson <jljacks93@gmail.com>
2025-08-07 03:00:41 +00:00
Robin Bobbitt
290dec8bf8 fixes UnboundLocalError in POST /attach (#16062)
* fixes UnboundLocalError in POST /attach

Signed-off-by: Robin Y Bobbitt <rbobbitt@redhat.com>

* bust cache for credentials before attaching subscription

Signed-off-by: Robin Y Bobbitt <rbobbitt@redhat.com>

---------

Signed-off-by: Robin Y Bobbitt <rbobbitt@redhat.com>
2025-08-06 21:24:57 +00:00
Lila Yasin
80f9f87181 Bug fix for AAP-47771 data migration update (#16058)
* Bug fix for AAP-47771 this data migration updates existing CredentialType entries
in the database and changes the kind from github_app to github_app_lookup

* Combine migration 0203 into 0202

* Add test to ensure reconciliation issue has been resolved
2025-08-06 15:17:53 -04:00
Lila Yasin
cd12f4dcac Update Collections Syntax to get Collection related CI Checks Passing (#16061)
* Fix collection task breaking collection ci checks

* Patch ansible.module_utils.basic._ANSIBLE_PROFILE directly

* Conditionalize other santity assertions

* Remove added blank lines and identifier from Fail if absent and no identifier set
2025-08-06 14:56:21 -04:00
Jake Jackson
3ccc5e5f2c add stable to release workflows
* we changed branch naming schema so adding in the new name
2025-07-24 15:54:19 -04:00
Jake Jackson
550ae51aec Revert "[4.6][dependency] CVE 2025 47273 (#7020)" (#7027)
This reverts commit e8b2920aec.
2025-07-23 13:22:25 -04:00
Jake Jackson
e8b2920aec [4.6][dependency] CVE 2025 47273 (#7020)
* Update requirements for setuptools

* first pass and need to commit

* update makefile and run updater script
2025-07-22 15:21:06 -04:00
Alan Rominger
7977e8639c Use full slug in DAB RBAC test (#16053) 2025-07-14 11:14:34 -04:00
Jake Jackson
03cd450669 [AAP-47877] Backport collection updates (#6992)
* Update collection args (#16025)

* update collection arguments

* Add integration testing for new param

* fix: sanity check failures

---------

Co-authored-by: Sean Sullivan <ssulliva@redhat.com>
Co-authored-by: Alan Rominger <arominge@redhat.com>

* update formatting for sanity testing

* fixing indentation for sanity suite

* adjust tests to use new token name

* update tests to use aap_token instead of controller_oauthtoken

* add back aliases for backward compat

* we have integration tests that still leverage the old token name
* while we can rename these, this tells me that customers might still
  have them in the wild and breaking them in a z stream is no bueno

* revert alias changes

---------

Co-authored-by: Peter Braun <pbraun@redhat.com>
Co-authored-by: Sean Sullivan <ssulliva@redhat.com>
Co-authored-by: Alan Rominger <arominge@redhat.com>
2025-07-10 10:14:40 -04:00
Jake Jackson
1d4b555a2c Update feature_branch_sync.yml (#7006)
fix typo in workflow title
2025-07-10 02:37:35 +00:00
Luis Villa
69df7d0e27 [AAP-48771]wfjt migration to catch renaming (#6991)
* wfjt migration to catch renaming

* Added rename_wfjt function to template constraint migration
* Add test to add duplicate names and verify that the duplicates are renamed

* move object creation

* add missing rename_wfjt operation

* fix linter issues

* fix tox issues

* test manually and move operation

* added back credential type validation code
2025-07-09 15:51:55 -04:00
Alan Rominger
bf0567ca41 AAP-48392 Handle DAB RBAC either before or after new type model (for merge) (#16045)
* Handle DAB RBAC either before or after new type model

* Translate CT to DAB CT

* Fixes for content type switch

* Use more compatible coding pattern

* Deeper purge of content_type_id

* revert, turns out that did not work

* More content type replacements

* Revert changes to serializer

* Revert another content_type change

* Fix for rearrangement of post_migration methods

* Remove thing I am not going to do

* Revert branch pin that was temporary
2025-07-02 14:28:43 -04:00
Jake Jackson
ec0732ce94 AAP-48139 add branch sync between release_4.6 and stable-2.6 (#6982)
* add branch sync between release_4.6 and stable-2.6

* add a new workflow to force push commits in release_4.6 to
  stable-2.6

* Update workflow to use matrix keyword


---------

Co-authored-by: Jake Jackson
2025-06-30 19:56:08 -04:00
Hao Liu
d6482d3898 AAP-48070 Remove ALLOW_LOCAL_RESOURCE_MANAGEMENT setting and enable local resource management (#16033)
Remove ALLOW_LOCAL_RESOURCE_MANAGEMENT setting and enable local resource management

This commit removes the ALLOW_LOCAL_RESOURCE_MANAGEMENT setting and all associated
functionality, making the behavior as if the setting is always enabled.

Changes:
- Remove ALLOW_LOCAL_RESOURCE_MANAGEMENT setting from defaults.py
- Remove @immutablesharedfields decorator and all related logic
- Remove decorator applications from Organization, Team, and User API views
- Remove role assignment restrictions in UserRolesList and RoleUsersList
- Remove test file for immutablesharedfields functionality
- Clean up unused imports

Result: Organizations, Teams, and Users can now always be created, modified,
and deleted via the API without platform ingress restrictions.
2025-06-30 10:15:26 -04:00
Hao Liu
20b203ea8e AAP-47495 Hide CSRF_TRUSTED_ORIGINS (#16035)
Hide CSRF_TRUSTED_ORIGINS
2025-06-30 09:58:19 -04:00
jessicamack
1afd23043d Remove api version from hardcoded inventory url (#16039) (#6980)
* update url endpoints

* reformat line for length
2025-06-25 22:53:03 +02:00
jessicamack
1330a1b353 Remove api version from hardcoded inventory url (#16039)
* update url endpoints

* reformat line for length
2025-06-25 21:54:21 +02:00
Matthew Sandoval
11a9a2b066 Pin receptorctl 1.5.7 (#6979) 2025-06-24 19:48:55 +00:00
Lila Yasin
5752c7a8e2 [2.5 Backport] AAP-46038 database deadlock (#6947)
Sort both bulk updates and add batch size to facts bulk update to resolve deadlock issue

Update tests to expect batch_size to agree with changes

Add utility method to bulk update and sort hosts and applied that to the appropriate locations

Update functional tests to use bulk_update_sorted_by_id since update_hosts has been deleted

Add comment NOSONAR to get rid of Sonarqube warning since this is just a test and it's not actually a security issue

Fix failing test test_finish_job_fact_cache_clear & test_finish_job_fact_cache_with_existing_data

---------

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
Co-authored-by: Alan Rominger <arominge@redhat.com>
Co-authored-by: Seth Foster <fosterbseth@gmail.com>
2025-06-16 15:32:55 -04:00
Alan Rominger
3d027bafd0 AAP-44233 Create credential types in new migration step (#6969)
* Update database to credential types in new migration file

* bump migration

* Add assertion

* Pre-delete credentials so we test recreation
2025-06-11 16:26:42 -04:00
Jake Jackson
ee19ee0c10 Update workflow to allow the workflow to write (#6975)
* Update the workflow to allow the action to write our branches from it.
* Also added username and email as git by default will want to know who
  is performing the action (edge case). Using github actions bot is
standard practice
2025-06-11 20:07:38 +00:00
Alan Rominger
f1e5cadce7 🧪 Delegate artifact merge and garbage collection to GH (#16019) (#6973)
* 🧪 Unpersist Git creds @ cov combine job

This is one of the things Zizmor [[1]] warns about.

[1]: https://docs.zizmor.sh

* 🧪 Download all coverage artifacts in one go

* 🧪 Delegate artifact garbage collection to GH

This is implemented by setting the retention days input to 1 on the
initial upload.

Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <webknjaz@redhat.com>
2025-06-10 16:54:59 -04:00
Satoe Imaishi
a238c5dd09 Bump django to 4.2.21 (#6964) 2025-06-10 10:11:43 -04:00
Jake Jackson
d26c7fedb8 Add workflow to rebase release branches (#6968)
* Adds a workflow that rebases release_4.6 onto release_4.6-next
2025-06-10 10:11:43 -04:00
Jiří Jeřábek (Jiri Jerabek)
f4347d05a9 cherry-pick 222f387 to release_4.6 (#6971) 2025-06-10 10:11:42 -04:00
Hao Liu
4eefce622d Fixes pytest CI error (#6970)
```
  /var/lib/awx/venv/awx/lib64/python3.11/site-packages/_pytest/python.py:163:
  PytestReturnNotNoneWarning: Expected None, but
  awx/main/tests/unit/test_tasks.py::TestJobCredentials::test_custom_environment_injectors_with_boolean_extra_vars
  returned ['successful', 0], which will be an error in a future version
  of pytest.  Did you mean to use `assert` instead of `return`?
```

* Dug into the git blame for this one
  060585434a is the commit for any
  historians. It was wrongfully carried over from a mock pexpect
  implementation. Our new tests are nice. They don't go as far as trying
  to run the task so they do not need to mock pexpect. That is why it is
  safe to remove this code without finding it a new home.

Co-authored-by: Chris Meyers <chris.meyers.fsu@gmail.com>
2025-06-10 10:11:42 -04:00
TVo
57b8773613 [2.5/4.6 Backport] AAP-40782 Reduce queued stuck jobs (#6962)
* [2.5/4.6 Backport] AAP-40782 Reduce queued stuck jobs

* [2.5/4.6 Backport] AAP-40782 Reduce queued stuck jobs

* Incrp'd review feedback from @AlanCoding

* Reformatted 4 files per CI-check for api-linters
2025-06-10 10:11:42 -04:00
Alan Rominger
d0776dabdf AAP-32143 Make the JT name uniqueness enforced at the database level (#15956) (#6958)
* Make the JT name uniqueness enforced at the database level

* Forgot demo project fixture

* New approach, done by adding a new field

* Update for linters and failures

* Fix logical error in migration test

* Revert some test changes based on review comment

* Do not rename first template, add test

* Avoid name-too-long rename errors

* Insert migration into place

* Move existing files with git

* Bump migrations of existing

* Update migration test

* Awkward bump

* Fix migration file link

* update test reference again
2025-06-10 10:11:42 -04:00
Peter Braun
2d730abb82 fix: allow unknown keyword arguments (#6972)
Co-authored-by: Peter Braun <pbranu@redhat.com>
2025-06-10 10:11:42 -04:00
Seth Foster
8896f75f9b Restore basic auth for subscriptions API (#6961)
When POSTing to console.redhat.com, fallback
to using basic auth method if OAUTH via
service accounts fails

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
2025-06-10 10:11:42 -04:00
Mauricio Magnani Jr
bb6bf33b9e fix: ensure temp files are cleaned up after failed HCC (#6952) 2025-05-21 13:18:24 -04:00
Dirk Jülich
5cf3a09163 AAP-17690 Inventory variables sourced from git project are not getting deleted after being removed from source (#15928) (#6946)
* Delete existing all-group vars on inventory sync (with overwrite-vars=True) instead of merging them.

* Implementation of inv var handling with file as db.

* Improve serialization to file of inv vars for src update

* Include inventory-level variable editing into inventory source update handling

* Add group vars to inventory source update handling

* Add support for overwrite_vars to new inventory source handling

* Persist inventory var history in the database instead of a file.

* Remove logging which was needed during development.

* Remove further debugging code and improve comments

* Move special handling for user edits of variables into serializers

* Relate the inventory variable history model to its inventory

* Allow for inventory variables to have the value 'None'

* Fix KeyError in new inventory variable handling

* Add unique-together constraint for new model InventoryGroupVariablesWithHistory

* Use only one special invsrc_id for initial update and manual updates

* Fix internal server error when creating a new inventory

* Print the empty string for a variable with value 'None'

* Fix comment which incorrectly states old behaviour

* Fix inventory_group_variables_update tests which did not take the new handling of None into account

* Allow any type for Ansible-core variable values

* Refactor misleading method names

* Fix internal server error when savig vars from group form

* Remove superfluous json conversion in front of JSONField

* Call variable update from create/update instead from validate

* Use group_id instead of group_name in model InventoryGroupVariablesWithHistory

* Disable new variable update handling for all regular (non-'all') groups

* Add live test to verify AAP-17690 (inv var deleted from source)

* Add functional tests to verify inventory variables update logic

* Fix migration which was corrupted by a rebase

* Add a more complex live test and resolve linter complaints

* Force overwrite_vars=False for updates from source on all-group

* Change behavior with respect to overwrite_vars
2025-05-19 21:38:33 +02:00
Seth Foster
54db6c792b Revert "unpin sqlparse dependency (#6911)" (#6950)
This reverts commit 3e122778e4.
2025-05-16 22:53:54 +00:00
Peter Braun
3e122778e4 unpin sqlparse dependency (#6911)
* unpin sqlparse dependency

* remove sqlparse license
2025-05-15 15:51:34 -04:00
Elijah DeLee
f98b2e2455 introduce age for workers and mandatory retirement
Retire workers after a certain age, allowing them to finish their
current task if they are not idle.

This mitigates any issues like memory leaks in long running workers,
especially if systems stay busy for months at a time.

Introduce new optional setting WORKER_MAX_LIFETIME_SECONDS, defaulting to 4 hours
2025-05-15 14:15:50 -04:00
Seth Foster
12dcc10416 [4.6] Update subscription API to use service accounts (#6927)
* Update subscription API to use service accounts

Update code to pull subscriptions from
console.redhat.com instead of
subscription.rhsm.redhat.com

Uses service account client ID and client secret
instead of username/password, which is being
deprecated in July 2025.

Additional changes:

- In awx.awx.subscriptions module, use new service
account params rather than old basic auth params

- Update awx.awx.license module to use subscription_id
instead of pool_id. This is due to using a different API,
which identifies unique subscriptions by subscriptionID
instead of pool ID.

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
Co-authored-by: Chris Meyers <chris.meyers.fsu@gmail.com>
Co-authored-by: Peter Braun <pbraun@redhat.com>

* fix token name

Signed-off-by: Seth Foster <fosterbseth@gmail.com>

* Fix Subscriptions credentials fallback

Ensure service account authentication is being used
when falling back to using SUBSCRIPTIONS_CLIENT_ID.

Additional change:
Subscription data can return two types of capacities:
Sockets and Nodes

For determining overall capacity
if capacity name is Nodes:
  capacity quantity x subscription quantity
if capacity name is Sockets:
  capacity quantity / 2 (minimum of 1) x subscription quantity

Signed-off-by: Seth Foster <fosterbseth@gmail.com>

---------

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
Co-authored-by: Chris Meyers <chris.meyers.fsu@gmail.com>
Co-authored-by: Peter Braun <pbraun@redhat.com>
2025-05-13 15:41:35 -04:00
Mauricio Magnani Jr
6bd39aea4b OPA server hostname with https or http results in connection errors (#6921)
Signed-off-by: Mauricio Magnani <magnani@redhat.com>
2025-05-13 14:41:29 -04:00
Hao Liu
b7a3c6b025 Delete UI test from CI (#6831)
In release_4.6 we no longer ship UI in AAP 2.5 so there's no reason to waste time on CI test for UIs
2025-05-13 14:40:07 -04:00
jessicamack
ba7ee23298 [backport][4.6] Update Azure Key Vault plugin to use Managed Identity (#6939)
* Bug on file name. Commiting to remove it.

* Update azure_kv plugin to use ManagedIdentity. Add testing.
2025-05-13 10:05:24 -04:00
Jake Jackson
825a48bb32 [4.6] Insights Credential Help Text Update (#6937)
* Update help text for insights cred

* update help text for the insights cred per new mock ups
2025-05-08 10:45:14 -04:00
thedoubl3j
eb6aebff00 Update logic to not over write ec2 replace
* fix replace logic so that we don't over write and stay only at vmware
  when ec2 is selected
* add an env.json for functional testing
2025-05-08 15:06:58 +02:00
thedoubl3j
60114ab929 add logic to replace cred name when using esxi
* similar to aws, allow the use of the standard vmware cred
2025-05-08 15:06:58 +02:00
Peter Braun
0e28d2590a fix: keep processing events, even if previous event data cannot be pa… (#15965) (#6922)
* fix: keep processing events, even if previous event data cannot be parsed

* change log level to warning
2025-05-05 13:26:42 +02:00
jessicamack
2bc08b421d Bring WFJT job access to parity with UnifiedJobAccess (#15344) (#6935)
* Bring WFJT job access to parity with UnifiedJobAccess

* Run linters

Co-authored-by: Alan Rominger <arominge@redhat.com>
2025-04-30 13:43:47 -04:00
Matthew Sandoval
cae8a4e16c Pin receptorctl 1.5.5 (#6931) 2025-04-29 11:01:22 -07:00
Chris Meyers
41d3729501 Allow ipv6 address for TOWER_URL_BASE setting
* Django url validators support ipv6, our custom URLField
  allow_plain_hostname feature was messing with the hostname before it
  was passed to Django validators.
* This change dodges the allow_plan_hostname transformations if the
  value looks like it's an ipv6 one.
2025-04-29 09:55:16 -04:00
TVo
a3303bb74b Pin ansible runner241 and receptorctl154 (#6919)
* Updated pinned runner and receptorctl in controller.

* Bumped receptorctl to 1.5.4
2025-04-22 11:13:58 -07:00
Alan Rominger
6a10e0ea5c AAP-41139 [4.6][dependencies] Bump Django 2 minor versions (#6892)
* Initial requirement bump for Django CVE

* Run updater script
2025-04-16 14:41:05 -04:00
Bruno Rocha
6690d71357 [4.6][Backport][Feature] feat: Manage Django Settings with Dynaconf (#6910)
Dynaconf is being added from DAB factory to load Django Settings
2025-04-15 15:17:16 -04:00
Hao Liu
ae0a8a80eb [4.6] Fix Cert base authentication for OPA (#6909)
* Remove unused setting

* Fix mTLS auth to OPA server

- Workaround https://github.com/Turall/OPA-python-client/issues/32
- Add tests for `opa_cert_file` context manager
2025-04-15 11:07:33 -04:00
Hao Liu
4532c627e3 Update dependencies for DAB (#6914)
Co-authored-by: Satoe Imaishi <simaishi@redhat.com>
2025-04-15 09:55:45 -04:00
Lila Yasin
87cb6dc0b9 AAP-39365 facts are unintentionally deleted when the inventory is modified during a job execution (#15910) (#6905)
* Added test_jobs.py to the model unit test folder in orther to show the undesired behaviour with fact cache



* Added test_jobs.py to the model unit test folder in orther to show the undesired behaviour with fact cache



* Solved undesired behaviour with fact_cache



* Solved bug with slices



* Remove unused imports

Remove now unused line of code which was commented out by the contributor

Revert "Remove now unused line of code which was commented out by the contributor"

This reverts commit f1a056a2356d56bc7256957a18503bd14dcfd8aa.

* Add back line that had been commented out as this line makes hosts specific to the particular slice when applicable

Revise private_data_dir fixture to see if it improves code coverage

Checked out awx/main/tests/unit/models/test_jobs.py in devel to see if it resolves git diff issue

* Fix formatting in awx/main/tests/unit/models/test_jobs.py

Rename for loop from host in hosts to hosts in hosts_cahced and remove unneeded continue

Revise finish_fact_cache to utilize inventory rather than hosts

Remove local var hosts that was assigned but unused

Revert change in start_fact_cache hosts_cached back to hosts

Revise the way we are handling hosts_cached and joining the file

Revert "Revise the way we are handling hosts_cached and joining the file"

This reverts commit e6e3d2f09c1b79a9bce3647a72e3dad97fe0aed8.

Reapply "Revise the way we are handling hosts_cached and joining the file"

This reverts commit a42b7ae69133fee24d3a5f1b456d9c343d111df9.

Revert some of my changes to get back to a better working state

Rename for loop to host in hosts_cached and remove unneeded continue

Remove jobs job.get_hosts_for_fact_cache() from post run hook, fix if statement after continue block, and revise how we are calling hosts in finish for loop

Add test_invalid_host_facts to test_jobs to increase code coverage

Update method signature to use hosts_cached and updated other references to hosts in finish_facts_cached

Rename hosts iterator to hosts_cached to agree with naming elsewhere

Revise test_invalid_host_facts to get more code coverage

Revise test_invalid_host_facts to increase codecov

Revise test_pre_post_run_hook_facts_deleted_sliced to ensure we are hitting the assertionerror for code cov

Revise  mock_inventory.hosts. to hit assert failure

Add revision of hosts and facts to force failure to satisfy code cov

Fix failure in test_pre_post_run_hook_facts_deleted_sliced

Add back for loop to create failures and add assert to hit them

Remove hosts.iterator() from both start_fact_cache and finish_fact_cache

Remove unused import of Queryset to satisfy api-lint

Fix typo in docstring hasnot to has not

Move hosts_cached.append(host) to outer loop in start_fact_cache

Add class to help support cached hosts resolving host.name issue with hosts_cached

* Add live tests for ansible facts

Remove fixture needed for local work only maybe

Revert "Add class to help support cached hosts resolving host.name issue with hosts_cached"

This reverts commit 99d998cfb9960baafe887de80bd9b01af50513ec.

* Move hosts_cached.append(host) outside of try except

* Move hosts_cached.append(host) to the beginning of start_fact_cache

---------

Signed-off-by: onetti7 <davonebat@gmail.com>
Co-authored-by: onetti7 <davonebat@gmail.com>
Co-authored-by: Alan Rominger <arominge@redhat.com>
2025-04-11 11:08:26 -04:00
Lila Yasin
bbcdef18a7 [2.5][Backport] AAP 30045 incorrect deprecation warning for awx.awx.schedule rrule (#6903)
* Make lookup plugins return lists to fix failures (#15625)

* Make lookup plugins return lists to fix failures

* Update unit tests

* Use lookup for test failures, update docs

* Grammar fix from review

Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>

---------

Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>

* Fix cherry-pick merge issue, should resolve failing linter

---------

Co-authored-by: Alan Rominger <arominge@redhat.com>
Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
2025-04-11 11:02:45 -04:00
Jake Jackson
d35d7f62ec Esxi inventory plugin (#6895)
* Add in ESXI plugin as a choice

* added in vmware esxi as an inventory source
* made a migration that may not be needed but will need to circle back

* black formatting

* linter fixes that I missed in the first commit, squash

* Update esxi to use_fqcn to true

* added use_fqcn on the esxi cred to true to correctly lay down
  collection name

* add fqcn true

* updated vmware esxi to use true for fqcn

* Update defaults and re-order migrations

* updated defaults to add correct env var to get empty
* re-ordered migrations to be in line with others

* Add condition to replace vmware_esxi cred

* replace direct name match with vmware cred since source supports old
  cred

* add skeleton test

* quick pass, needs more
* squash this

* Add tests for creating inventory ESXI source

* add test case to test creating an inventory with different cred type
  to source name

* update test and linting

* added correct cred return since esxi uses same cred

* assert on status code

* assert that we received a 204

* Added new folder for vmware_exsi and empty json file.

* Corrected the misspelling of folder name to 'esxi'

* fixed misspelling for `vmware_`

---------

Co-authored-by: Thanhnguyet Vo <thavo@redhat.com>
2025-04-10 14:32:17 -04:00
Lila Yasin
9d9c125e47 [4.6][Backport][Feature] feat: 38589 GitHub App Authentication (#15807) (#6887)
* feat: 38589 GitHub App Authentication (#15807)

* feat: 38589 GitHub App Authentication

Allows both git@<personal-token> and x-access-token@<github-access-token> when authenticating using git.
This allows GitHub App tokens to work without interfering with existing authentication types.

---------

Co-authored-by: Jake Jackson <thedoubl3j@Jakes-MacBook-Pro.local>

* revert change made to allow UI to accept x-access-token, just use htt… (#15851)

revert change made to allow UI to accept x-access-token, just use https:// instead

* Add Github dep for new cred support if used (#15850)

* Add pygithub for new app token support

* fixed git requirements file with new
* added new github dep and relevant deps it needs

* add required licenses

* Add artifacts to satisfy license check

* Remove duplicated license

---------

Co-authored-by: Andrea Restle-Lay <arestlel@redhat.com>
Co-authored-by: Alan Rominger <arominge@redhat.com>

* Remove deps update it came with the cherry-pick and is not needed in this version

Remove unneeded deps updates from requirements.in

Remove point to awx-plugins as it is not needed in tower

* Add a credential plugin that uses GitHub Apps to get tokens

* Add github app tests

* Ran requirements updater script

Ran black on github_app_test to fix formatting issue

Add scm_github_app to managed credentials

Ran updater script to reflect new deps

Added github app info to def build_passwords in jobs.py, cred now appears in credential types

Update ManagedCredentialType for GitHub App to match what we have in awx-plugins

Update inputs in maManagedCredentialType to github_app_inputs to communicate with awx/main/credential_plugins/github_app.py

Revert incorrect change in ManagedCredentialType, change github_app_lookup to call inputs instead of github_app_inputs

Updated namespace to github_app_lookup to agree with nomenclature used in the rest of the implementation and to resolve failing API test

Remove import pointing to awx plugins and update to point to credential_plugins

Remove references to gh_app_plugin_mod and change to github_app

Remove from awx_plugins.interfaces._temporary_private_django_api import (  # noqa: WPS436 to resolve failing test

Remove flake8 typing & typing references that do not exist in this version of Tower

Remove references in jobs.py and  __init__.py since this is an external cred type and registered it in setup.cfg instead

Remove blank line

REvise name in cfg from github_app_lookup to github_app to see if that ifxes module not found error

Revise first declaration of github_app to agree with file name to see if that resolves issue

Rename line 174 to agree with what's in config

Fix reference to github_app_lookup to github_app

Linters compliaining about the github_app in __all__ not being defined, renamed to see if that aligns them

Fix naming in test to correspond to naming of cred type

Update naming to be more specific and add blank line to setup.cfg

Remove __all__ from githubapp.py to satisfy linters

Revert formatting change since it is not needed in this repository

* Add blank line at the end of requirements.in

---------

Co-authored-by: Andrea Restle-Lay <andrearestlelay@gmail.com>
Co-authored-by: Jake Jackson <thedoubl3j@Jakes-MacBook-Pro.local>
Co-authored-by: Jake Jackson <jljacks93@gmail.com>
Co-authored-by: Andrea Restle-Lay <arestlel@redhat.com>
Co-authored-by: Alan Rominger <arominge@redhat.com>
2025-04-10 14:32:17 -04:00
Chris Meyers
5dd81a04ce Aap 41580 indirect host count wildcard query (#15893)
* Support <collection_namespace>.<collection_name>.* indirect host query
  to match ANY module in the <collection_namespace>.<collection_name>
* Add tests for new wildcard indirect host count
* error checking of ansible event name
* error checking of ansible event query
2025-04-10 14:32:15 -04:00
Dirk Jülich
e060e44b05 AAP-37381 Apply Django password validators correctly. (#6902)
* Move call to django_validate_password to the correct method were the user object is available.
* Added tests for the Django password validation functionality.
2025-04-02 16:45:58 +02:00
Chris Meyers
db5b6d0019 Add changelog to awx collection 2025-03-25 13:41:53 -04:00
Chris Meyers
a2c8ecb4e6 Bump awx collection ansible required version 2025-03-25 13:41:53 -04:00
Chris Meyers
277bc581e7 Remove coarse grain unused import
* It would seem that fine-grain noqa pylint ignores do the job and are
  already in place. Prefer that over the coarse entire file ignore.
2025-03-25 13:41:53 -04:00
Chris Meyers
ef89c59a13 Fix ansible-lint empty lines in module docstrings 2025-03-25 13:41:53 -04:00
Chris Meyers
5872a88a57 Fix ansible-lint truthy in module docstrings 2025-03-25 13:41:53 -04:00
Chris Meyers
7fdd15f115 Fix ansible-lint indentation in module docstrings 2025-03-25 13:41:53 -04:00
Peter Braun
353f0adf36 [4.6][backport] do not count dark hosts as updated (#6877)
* feat: do not count dark hosts as updated (#15872)

* feat: do not count dark hosts as updated

* update functional tests

* Fix test flake due to host metric id enumeration (#15875)

---------

Co-authored-by: Alan Rominger <arominge@redhat.com>
2025-03-18 16:34:52 +01:00
Peter Braun
bdfd9dec74 update: use singular form ANSIBLE_COLLECTIONS_PATH (#15841) (#6876)
* update: use singular form ANSIBLE_COLLECTIONS_PATH

* update functional tests
2025-03-18 14:44:57 +01:00
Hao Liu
bad4e630ba Basic runtime enforcement of policy as code part 2 (#6875)
* Add `opa_query_path field` for Inventory, Organization and JobTemplate models (#6850)

Add `opa_query_path` model field to Inventory, Organizatio and JobTemplate. Add migration file and expose opa_query_path field in the related API serializers.

* Gather and evaluate `opa_query_path` fields and raise violation exceptions (#6864)

gather and evaluate all opa query related to a job execution during policy evaluation phase 

* Add OPA_AUTH_CUSTOM_HEADERS support (#6863)

* Extend policy input data serializers (#6890)

* Extend policy input data serializers

* Update help text for PaC related fields (#6891)

* Remove encrypted from OPA_AUTH_CUSTOMER_HEADER

Unable to encrypt a dict field

---------

Co-authored-by: Jiří Jeřábek (Jiri Jerabek) <Jerabekjirka@email.cz>
Co-authored-by: Alexander Saprykin <cutwatercore@gmail.com>
Co-authored-by: Tina Tien <98424339+tiyiprh@users.noreply.github.com>
2025-03-18 02:39:26 +00:00
Seth Foster
e9f2a14ebd Fix root container path for project updates in K8s
Modifies to_container_path to accept an optional
container_root parameter.

Normally this defaults to /runner, but in K8S
environments, project updates run from the
private_data_dir, e.g. /tmp/awx_1_123abc, not
/runner.

In that situation, we just pass in private_data_dir
as the container_root.

---------

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
Co-authored-by: TVo <thavo@redhat.com>
2025-03-13 13:34:12 -04:00
Alan Rominger
01fae57de2 Fix indirect host counting task test race condition (#15871) 2025-03-12 13:50:19 -04:00
Alan Rominger
c7ac45717b Pin drf-yasg to make api-test pass (#15887)
Ping drf-yasg to make api-test pass
2025-03-12 13:50:19 -04:00
Alan Rominger
c7b6b43913 [4.6] Attempt to fix ui-lint check by clearing cache forcefully (main fork) (#6885)
* Try to make ui-lint check pass in 4.6

* Implement solution suggested by Kia
2025-03-11 11:52:30 -04:00
Alan Rominger
1e6a7c0749 Prevent system auditor from downloading install bundle (#6805) 2025-03-11 10:54:02 -04:00
Alan Rominger
b5bc85e639 AAP-41692 [4.6] Update jinja2 for CVE (#6881)
* Initial bump of jinja2 lib

* Run updater script
2025-03-11 10:45:37 -04:00
Jake Jackson
f04bf5ccf0 update recetpor (#6869)
* bumped receptor version to latest
2025-03-04 15:16:58 -05:00
Peter Braun
28712a4c6e fix: audit record name should not be the hostname (#15864) (#6866)
* fix: audit record name should not be the hostname

* fix: update tests
2025-03-03 16:49:42 +01:00
Hao Liu
698d769a7a [Policy as Code] Monkey patch opa_client.base.BaseClient (#6865)
Workaround bug described in https://github.com/Turall/OPA-python-client/issues/29
2025-02-26 21:09:51 +00:00
Alan Rominger
529ee73fcd [4.6] Backport the "live" tests (#6859)
* Create a new pytest folder for live system testing with normal services (#15688)

* PoC for running dev env tests

* Replace in github actions

* Move folder to better location

* Further streamlining of new test folders

* Consolidate fixture, add writeup docs

* Use star import

* Push the wait-for-job to the conftest

Fix misused project cache identifier (#15690)

Fix project cache identifiers for new updates

Finish test and discover viable solution

Add comment on related task code

AAP-37989 Tests for exclude list with multiple jobs (#15722)

* Tests for exclude list with multiple jobs

Create test for using manual & file projects (#15754)

* Create test for using a manual project

* Chang default project factory to git, remove project files monkeypatch

* skip update of factory project

* Initial file scaffolding for feature

* Fill in galaxy and names

* Add README, describe project folders and dependencies

Add ee cleanup tests

* Adds cleanup tests to the live test.

Fix rsyslog permission error in github ubuntu tests from apparmor (#15717)

* Add test to detect rsyslog config problems

* Get dmesg output

* Disable apparmor for rsyslogd

Make awx/main/tests/live dramatically faster (#15780)

* Make awx/main/tests/live dramatically faster

* Add new setting to exclude list

* Fix rebase issues

* Did not want to backport this
2025-02-25 15:22:38 -05:00
jessicamack
ba053dfb51 Ship analytics data using service account token (#15812) (#6856)
Use oidc client to ship analytics data
2025-02-25 14:42:17 -05:00
Alan Rominger
b351dfb102 Undo temporary DAB change for requirements generation (#6862) 2025-02-25 09:30:15 -05:00
Alan Rominger
b502a9444a [4.6 backport] Feature indirect host counting (#15802) (#6858)
* Feature indirect host counting (#15802)

* AAP-37282 Add parse JQ data and test it for a `job` object in isolation (#15774)

* Add jq dependency

* Add file in progress

* Add license for jq

* Write test and get it passing

* Successfully test collection of `event_query.yml` data (#15761)

* Callback plugin method from cmeyers adapted to global collection list

Get tests passing

Mild rebranding

Put behind feature flag, flip true in dev

Add noqa flag

* Add missing wait_for_events

* feat: try grabbing query files from artifacts directory (#15776)

* Contract changes for the event_query collection callback plugin (#15785)

* Minor import changes to collection processing in callback plugin

* Move agreed location of event_query file

* feat: remaining schema changes for indirect host audits (#15787)

* Re-organize test file and move artifacts processing logic to callback (#15784)

* Rename the indirect host counting test file

* Combine artifacts saving logic

* Connect host audit model to jq logic via new task

* Add unit tests for indirect host counting (#15792)

* Do not get django flags from database (#15794)

* Document, implement, and test remaining indirect host audit fields (#15796)

* Document, implement, and test remaining indirect host audit fields

* Fix hashing

* AAP-39559 Wait for all event processing to finish, add fallback task (#15798)

* Wait for all event processing to finish, add fallback task

* Add flag check to periodic task

* feat: cleanup of old indirect host audit records (#15800)

* By default, do not count indirect hosts (#15801)

* By default, do not count indirect hosts

* Fix copy paste goof

* Fix linter issue from base branch

* prevent multiple tasks from processing the same job events, prevent p… (#15805)

prevent multiple tasks from processing the same job events, prevent periodic task from spawning another task per job

* Fix typos and other bugs found by Pablo review

* fix: rely on resolved_action instead of task, adapt to proposed query… (#15815)

* fix: rely on resolved_action instead of task, adapt to proposed query structure

* tests: update indirect host tests

* update remaining queries to new format

* update live test

* Remove polling loop for job finishing event processing (#15811)

* Remove polling loop for job finishing event processing

* Make awx/main/tests/live dramatically faster (#15780)

* AAP-37282 Add parse JQ data and test it for a `job` object in isolation (#15774)

* Add jq dependency

* Add file in progress

* Add license for jq

* Write test and get it passing

* Successfully test collection of `event_query.yml` data (#15761)

* Callback plugin method from cmeyers adapted to global collection list

Get tests passing

Mild rebranding

Put behind feature flag, flip true in dev

Add noqa flag

* Add missing wait_for_events

* feat: try grabbing query files from artifacts directory (#15776)

* Contract changes for the event_query collection callback plugin (#15785)

* Minor import changes to collection processing in callback plugin

* Move agreed location of event_query file

* feat: remaining schema changes for indirect host audits (#15787)

* Re-organize test file and move artifacts processing logic to callback (#15784)

* Rename the indirect host counting test file

* Combine artifacts saving logic

* Connect host audit model to jq logic via new task

* Document, implement, and test remaining indirect host audit fields (#15796)

* AAP-39559 Wait for all event processing to finish, add fallback task (#15798)

* Wait for all event processing to finish, add fallback task

* Add flag check to periodic task

* feat: cleanup of old indirect host audit records (#15800)

* prevent multiple tasks from processing the same job events, prevent p… (#15805)

prevent multiple tasks from processing the same job events, prevent periodic task from spawning another task per job

* Remove polling loop for job finishing event processing (#15811)

* Make awx/main/tests/live dramatically faster (#15780)

* reorder migrations to allow indirect instances backport

* cleanup for rebase and merge into devel

---------

Co-authored-by: Peter Braun <pbraun@redhat.com>
Co-authored-by: jessicamack <jmack@redhat.com>
Co-authored-by: Peter Braun <pbranu@redhat.com>
2025-02-24 21:55:44 +00:00
Hao Liu
2d648d1225 [Feature][release_4.6] Policy as Code MVP part 1 (#6848) 2025-02-24 15:58:57 -05:00
Hao Liu
b8a1e90b06 [CI][release_4.6] Fix schema upload (#6840) 2025-02-18 09:19:50 -05:00
Hao Liu
c0b9d3f428 Switch to ssh for private git requirements (#6838) 2025-02-17 22:44:29 -05:00
Hao Liu
376a791052 [CI][release_4.6] backport push development image base on repo name (#6837)
* Publish image base on git repo name instead of hard coded to AWX (#15828)

* Fix git credential for devel_image build (#15834)

* Continue if pre-warm cache fail in container build (#15835)

* Use correct devel image for docker-compose (#15836)
2025-02-17 16:26:19 -05:00
TVo
cb2df43580 [4.6_Backport] Added helper method for fetching serviceaccount token (#6823) 2025-02-17 15:31:33 -05:00
Hao Liu
ccb6360a96 AAP-39778[Backport][release_4.6] Add DAB Feature Flag common API (#6833)
* [AAP-39138] - Add DAB Feature Flag common API (#15786)
* Update django-ansible-base reference to ansible-automation-platform/django-ansible-base@stable-2.5

---------

Co-authored-by: Zack Kayyali <zkayyali@redhat.com>
2025-02-12 15:47:06 -05:00
Hao Liu
397fb297bf Add ability to provide token for private repo for requirements_git in container build (#15831) (#6830)
Add ability to provide auth to private repo for requirements_git
2025-02-12 20:00:37 +00:00
Alan Rominger
63bb4d66ef Do not get django flags from database (#15794) (#6820) 2025-02-10 10:46:22 -05:00
Alan Rominger
7017c28706 Limit to python 3.12 for 4.6 branch (#6817) 2025-02-05 10:48:07 -05:00
Hao Liu
48ee5b05ee Set feature flag base on setting (#15808) (#6811) 2025-02-05 09:30:32 +01:00
Seth Foster
386f85c59f Fix rrule fast forwarding across DST boundaries (#6815)
Fixes an issue where schedules were not running at
the correct time.

Details:

DST is Daylights Saving Time

If the rrule dtstart is "in" a DST period (i.e., March to November)
and the current date is outside of the DST, then the fast forwarding
is not correct.

This is because datetime timedeltas do not honor DST boundaries

The Fix:

Convert the rrule dtstart to UTC before doing operations. Then,
convert back to the original timezone at the end.

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
2025-02-04 15:27:21 -05:00
Lila Yasin
b7b15584af Remove Docker Desktop if statement (#15778) (#6797)
Remove docker desktop if statement
2025-02-03 13:35:19 -05:00
Adrià Sala
148f28f448 fix: azure credential awxkit client_id collision 2025-02-03 17:22:05 +01:00
Peter Braun
26b6eac849 fix: compatibility with black v25+ (#15789) (#6803) 2025-02-03 16:02:20 +00:00
Peter Braun
18ea5cc561 Use upload artifact v4 (#6807)
unique-ify name

psh, who needs loops

Folder management

Extracts into current path

Co-authored-by: Alan Rominger <arominge@redhat.com>
2025-02-03 15:10:55 +00:00
Lila Yasin
99b67f1e37 Add client_secret and client_id to credential_input_fields (#15734) (#6795) 2025-01-29 18:43:06 -05:00
Chris Meyers
cdd9e7263d Add insights service account support to collection 2025-01-24 16:29:18 -05:00
Seth Foster
edba126193 Add service account support to Insights credential
Adds fields client_id and client_secret which
will result in authentication via service account
on console.redhat.com

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
2025-01-24 14:42:01 -05:00
Adrià Sala
22ecb2030c feat: support insights service account credentials for project update (AAP-37464) 2025-01-22 16:33:11 +01:00
Rodrigo Toshiaki Horie
2e8114394b [4.6][dependency] update django for CVE-2024-56374 (#6784) 2025-01-20 18:58:30 -03:00
Peter Braun
3268c9b5fe Add input_inventories to ordered_associations (#15710) (#6782)
Co-authored-by: rev3r4nt <rev3r4nt@gmail.com>
2025-01-15 13:46:38 +01:00
Alan Rominger
f7cda7696c Bugfix: adjust incorrectly passed keywords with exclude-strings argument (#15721) (#6777)
* Fix incorrectly passed keywords with exclude-strings arg to ansible-runner worker cleanup command



* Keep the quotes for each arg and adjust test_receptor

---------

Signed-off-by: Sasa Jovicic <jovicic.sasa@hotmail.com>
Co-authored-by: Sasa Jovicic <jovicic.sasa@hotmail.com>
2025-01-08 13:44:10 -05:00
Jake Jackson
a209751f22 Fix CVE-2024-56201 update jinja2 (#6778) 2025-01-08 13:42:42 -05:00
Alan Rominger
5944d041e6 AAP-36536 Send job_lifecycle logs to external loggers (#15701) (#6776)
* Send job_lifecycle logs to external loggers

* Include structured data in message

* Attach the organization_id of the job
2025-01-07 15:23:52 -05:00
TVo
9c732d2406 AAP-36522 Opened PR to test and fix deprecation error in tower 4.6 checks (#6772)
* Opened PR to test AAP-36522.

* Test pin specific ansible-core version in the run_awx_devel actions.

* Fixed syntax for ansible-core version.

* updated makefile to match syntax for ansible version

* Reverts makefile changes; fixed syntax for upgrade ansible-core action.
2025-01-03 12:01:21 -05:00
Lila Yasin
b215699586 🧪 Run sanity tests w/ ansible-test-gh-action (#15539) (#6773)
* 🧪 Run sanity tests w/ `ansible-test-gh-action`

* 🧪 Upload sanity results to unified dashboard

Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <wk@sydorenko.org.ua>
2025-01-02 13:10:51 -05:00
Andrea Restle-Lay
38f72ac7ea host_metrics date fix to make summary dates (datetime.datetime) comparable to month: datetime.date (#15704) (#6770)
* host_metrics date fix

* AAP-36839 Remove excess comments

* fix extra date() conversion

* actual fix

* datetime is a library, use datetime.datetime

---------

Co-authored-by: Andrea Restle-Lay <arestlel@arestlel-thinkpadx1carbongen9.rht.csb>
2024-12-17 11:14:59 -05:00
Pablo H.
b361aef0fb chore: addressing CVE 2024-53908 (#6768) 2024-12-16 14:16:00 -05:00
Seth Foster
df79fa4ae1 bump grpcio CVE-2024-11407 (#6766)
Signed-off-by: Seth Foster <fosterbseth@gmail.com>
2024-12-16 13:23:24 -05:00
Seth Foster
9c2de6b535 Do not fast forward rrule if count is set (#6764)
Fixes a bug where a schedule that was created
to run only once will continue to run repeatedly.

e.g. an rrule with
dtstart 20240730; count 1; freq MINUTELY

This job will run on 20240730, and should never
run again.

However, the next time the schedule
update_computed_fields runs, the dtstart
will fast forward to today's date, and
next_run will be computed from that. This will trigger
the job to run again, which is not intended.

If count is set, we just should not fast forward the
rrule and always calculate next_run based on original
dtstart.

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
2024-12-10 16:14:46 -05:00
Peter Braun
0ce0023561 Fix: invalid response type on post request (#15609) (#6763) 2024-12-10 16:12:49 -05:00
Alan Rominger
f2ae68f302 Fix project cache identifiers for new updates (#6762)
Finish test and discover viable solution

Add comment on related task code
2024-12-10 15:23:54 -05:00
Alan Rominger
b3542c226d Fix error creating partition due to uncaught exception (#6761)
the primary fix is to simply add an exception class
  to those caught in the except block

This also adds live tests for the general scenario
  although this does not hit the new exception type
2024-12-10 15:22:09 -05:00
Peter Braun
56d3933154 feat: enable django flags support (#15660) (#6755)
* feat: enable django flags support

* add django flags license

* re-run updater script
2024-12-09 09:40:28 +01:00
Peter Braun
a1ec28aeb9 fix: reset state before evaluating named urls (#15683) (#6756) 2024-12-09 09:40:13 +01:00
Peter Braun
148afce455 deps: receptorctl v1.5.1 (#6760) 2024-12-06 16:12:58 +01:00
Hao Liu
f3b86b5193 Update defaults.py receptor typo (#15682) (#6754)
Fixing typo for  RECEPTOR_KEEP_WORK_ON_ERROR

Co-authored-by: linuxonfire <jaimescampositzel@gmail.com>
2024-12-05 09:35:21 -05:00
Hao Liu
82c967a66e Fix receptor work unit release after completion (#15679) (#6751)
Fix bug introduced by https://github.com/ansible/awx/pull/15392 that cause workunit to NOT be auto released after job completes
2024-12-03 12:28:55 -05:00
Peter Braun
8174a28716 update receptorctl to v1.5.0 (#6749) 2024-11-25 15:37:01 +01:00
Seth Foster
9c556db4c0 Make rrule fast forwarding stable (#15601) (#6744)
By stable, we mean future occurrences of the rrule
should be the same before and after the fast forward
operation.

The problem before was that we were fast forwarding to
7 days ago. For some rrules, this does not retain the old
occurrences. Thus, jobs would launch at unexpected times.

This change makes sure we fast forward in increments of
the rrule INTERVAL, thus the new dtstart should be in the
occurrence list of the old rrule.

Additionally, code is updated to fast forward
EXRULE (exclusion rules) in addition to RRULE

---------

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
2024-11-24 11:31:30 -05:00
Justin Downie
1d12f0c837 Back port for awx podAntiAffinity spec (#6733) 2024-11-22 16:15:26 -05:00
Satoe Imaishi
71a18c0d61 Bump uwsgi to 2.0.28 (#6736) 2024-11-22 10:54:52 -05:00
Hao Liu
c55fb369fa Update receptorctl to 1.4.11 (#6746) 2024-11-21 16:31:09 -05:00
Jake Jackson
2c3b4ff5d7 [4.6][dependency] update aiohttp to address vuln CVE-2024-52304 (#6740)
* update aiohttp to address vuln CVE-2024-52304

* add licenses for new deps
2024-11-21 16:21:34 -05:00
TVo
943964e14f 4.6_Backport changes made to aim.py from AWX. (#6739) 2024-11-13 12:16:25 -07:00
Alan Rominger
b97240417a Fix bug where unrelated jobs were linked as dependencies (#6735) 2024-11-06 14:41:57 -05:00
Kersom
6d959daca1 Revert "[4.6] Bump dependency (#6726)" (#6731)
This reverts commit 8fbe0c2b1f.
2024-10-31 11:36:22 -04:00
Kersom
8fbe0c2b1f [4.6] Bump dependency (#6726)
* Bump dependency

* Delete mini-create-react-context.txt

---------

Co-authored-by: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com>
2024-10-30 17:15:59 +00:00
David Newswanger
23528b7fef Add service backed SSO stage to saml pipeline (#6722) 2024-10-21 16:17:08 -04:00
Hao Liu
784ff3193d Pin DAB to 2024.10.17 (#6721) 2024-10-21 19:25:05 +00:00
Hao Liu
7972486594 Update receptorctl to 1.4.9 (#6718) 2024-10-17 11:27:21 -04:00
Chris Meyers
dbdbc7635a Redirect user to platform supported collection
* AAP 2.5 Controller 4.6 Org, User, and Team endpoints are restricted.
  When the user performs a restricted operation via the Controller
  collection, kindly notify them that they should be using the platform
  collection instead.
2024-10-17 11:02:19 -04:00
Rick Elrod
4820b084c1 Prettier DRF pages when using trusted proxy (#15579) (#6717)
This is a rather hacky, but fixes the DRF pages when going through a
trusted proxy.

Notably: This is meant to primarily fix the DRF pages on downstream
builds while leaving the upstream to function as-is.

When using a trusted proxy, the DRF login and logout endpoints now
redirect to the Platform login page (which respects ?next) and logout
endpoint respectively.

The CSS and JS is inlined because the trusted proxy might only proxy
to /api/ and not /static/ which is a harder problem to solve.

Signed-off-by: Rick Elrod <rick@elrod.me>
2024-10-16 13:58:01 -04:00
Hao Liu
d5388b3c56 Remove ui_next static file dir (#6715)
UI_NEXT is no longer being served in 4.6

Removing static file dir for ui-next fixes

```
WARNINGS:
?: (staticfiles.W004) The directory '/var/lib/awx/venv/awx/lib64/python3.11/site-packages/awx/ui_next/build' in the STATICFILES_DIRS setting does not exist.
CommandError: Inventory with id = 1038181458411569545 cannot be found
```
2024-10-15 19:58:33 +00:00
Hao Liu
433974aea6 [4.6][CI]Fix CI for newer debian image (#15583) (#6716)
* Fix CI for newer debian image (#15583)

* Fix CI for newer debian image

Signed-off-by: Rick Elrod <rick@elrod.me>

* Missed one

Signed-off-by: Rick Elrod <rick@elrod.me>

---------

Signed-off-by: Rick Elrod <rick@elrod.me>

* Update ci.yml

---------

Signed-off-by: Rick Elrod <rick@elrod.me>
Co-authored-by: Rick Elrod <rick@elrod.me>
2024-10-15 15:33:55 -04:00
Tomas Z
d1c85dae4d Upgrade django and sqlparse to pickup CVE fixes (#6709) 2024-10-04 15:51:12 -04:00
Alan Rominger
534b0209f4 Fix 500 error due to None data in DAB response (#15562) (#6714)
* Fix 500 error due to None data in DAB response

* NOQA for flake8 failures
2024-10-02 16:12:30 -04:00
Hao Liu
46becf15e9 Switch DAB back to devel to (#6713)
Enable event 2 development
2024-10-01 20:11:04 +00:00
Hao Liu
02795c9ed9 Filter out ANSIBLE_BASE from job env var (#6710) 2024-09-27 19:42:44 +00:00
Hao Liu
6574cfe3a9 Pin dependencies to prepare for release_4.6 release tag (#6707)
* Pin deps to release prep
- ansible-runner@2.4.0
- receptorctl@1.4.8
- django-ansible-base@c8fbc1e345d4908cc97eaae20771238a5dd35aad
2024-09-19 16:22:18 +00:00
Jake Jackson
fafed924e3 rebase and merge conflict resolution (#6692) 2024-09-17 16:46:12 +00:00
Peter Braun
d2f3c02945 fix: maintain order of insertions into m2m relationship tables (#15536) (#6703) 2024-09-17 18:29:36 +02:00
Jake Jackson
eb4f3c2864 update urllib to fix CVE-2024-37891 (#6700) 2024-09-17 12:14:28 -04:00
Jake Jackson
bcd18e161c fix CVE-2024-21520 (#6687) 2024-09-16 16:04:11 -04:00
Chris Meyers
b62d0ff8e6 Make analytics job ts settings hidden
* There isn't a great reason to allow the UI to edit these meta-data
  fields that denote the last time an analytics job ran.
* The only reason I hesitate to mark them uneditable in the API is that
  they are useful to change in order to influence when the jobs run.
  Mostly for debug purposes or 1-off.
2024-09-16 14:14:01 -04:00
Alan Rominger
c33947af7f [AAP 2.5] DAB 9.4, enable service redirect auth (#6672)
* Update settings from DAB features

* Delay implementation of the reverse sync setting

* Move auth class to actual end of pipeline
2024-09-16 13:41:26 -04:00
Elijah DeLee
30b005aa9d catch harakiri graceful signal in middlware and log debug info (#6673)
Middleware is from django_ansible_base
2024-09-16 16:25:03 +00:00
Peter Braun
a1e3919b1f update remaining urls for new UI (#15529) (#6699) 2024-09-15 10:19:51 -04:00
Peter Braun
0a8e92cab7 fix workflow job url (#15522) (#6697) 2024-09-15 00:05:18 +02:00
Seth Foster
30e2c3a8cd Validate org-user membership from gateway (#15508) (#6698)
Adding credential and execution environment roles
validates that the user belongs to the same org
as the credential or EE.

In some situations, the user-org membership has not
yet been synced from gateway to controller.

In this case, controller will make a request to
gateway to check if the user is part of the org.

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
2024-09-14 16:01:04 +00:00
Hao Liu
aef3d8750b [4.6] Fix additional UI URL generated by API (#15517) (#15518) (#6695)
* Fix: change to url in platform ui (#15518)

* Fix instance UI URL generated by API (#15517)

---------

Co-authored-by: Peter Braun <pbraun@redhat.com>
2024-09-13 21:12:08 -04:00
Peter Braun
f799376b3d Add OPTIONAL_UI_URL_PREFIX (#15506) (#6694)
# Add a postfix to the UI URL patterns for UI URL generated by the API
# example if set to '' UI URL generated by the API for jobs would be $TOWER_URL/jobs
# example if set to 'execution' UI URL generated by the API for jobs would be $TOWER_URL/execution/jobs

Co-authored-by: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com>
2024-09-13 20:06:40 +02:00
Peter Braun
96ec709e90 fix: avoid race conditions when removing multiple instance (#15495) (#6693)
* fix: avoid race conditions when removing multiple instance groups at once

* remove unused imports
2024-09-13 17:21:47 +02:00
Hao Liu
70f7ac72d4 Removes collection of unpartitioned_events table (#15501) (#6690)
Fixes: https://issues.redhat.com/browse/AAP-30995

Co-authored-by: Ladislav Smola <lsmola@redhat.com>
2024-09-11 18:39:41 +00:00
Seth Foster
4c9c22fea2 Translate new RBAC to old RBAC (#15490) (#6678)
User and Team assignments using the DAB
RBAC system will be translated back to the old
Role system.

This ensures better backward compatibility and
addresses some inconsistences in the UI that were
relying on older RBAC endpoints.

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
Co-authored-by: Alan Rominger <arominge@redhat.com>
2024-09-10 18:26:43 -04:00
Peter Braun
9914229a5a Hide AUTOMATION_ANALYTICS_LAST_GATHER (#15497) (#6684) 2024-09-10 07:44:21 -04:00
Hao Liu
ce0d176508 Fix analytic ship (#6679)
REDHAT_USERNAME and REDHAT_PASSWORD are default to empty string instead of None
2024-09-10 09:46:53 +02:00
Elijah DeLee
059f52f314 Unpin django-ansible-base for now (#6681) 2024-09-09 21:51:20 +00:00
Hao Liu
446046c4bf Remove OpenSSL pin (#6683) 2024-09-09 17:27:09 -04:00
Seth Foster
17e01e0eb0 Rename System Auditor to Controller System Auditor (#15470) (#6677)
This is to emphasize that this role is specific
to controller component. That is, not an auditor
for the entire AAP platform.

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
2024-09-09 17:07:14 -04:00
Rick Elrod
5a4b789488 Don't reverse sync preload script data (#6644)
Signed-off-by: Rick Elrod <rick@elrod.me>
2024-09-09 13:04:01 -04:00
Peter Braun
6dfe2e3a9f fix: avoid calling undefined method for anonymous users (#15440) (#6676) 2024-09-06 17:58:10 +02:00
Hao Liu
01ea091e8a Fix subscription username password setting name (#6675)
used in analytic
2024-09-06 08:58:48 -04:00
Seth Foster
effbd0e416 Fix SAMLAuth backend to correctly return social auth pipeline results (#15457) (#6669)
Co-authored-by: David Newswanger <gamma.dave@gmail.com>
2024-09-03 10:55:05 -04:00
Seth Foster
2334211ba0 Only refresh session if updating own password (#15426) (#6653)
Fixes bug where creating a new user will
request a new awx_sessionid cookie, invalidating
the previous session.

Do not refresh session if updating or
creating a password for a different user.

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
2024-09-03 10:54:13 -04:00
Hao Liu
15e28371eb Prevent automountServiceAccountToken (#6638)
* Prevent job pod from mounting serviceaccount token

* Add serializer validation for cg pod_spec_override

Prevent automountServiceAccountToken to be set to true and provide an error message when automountServiceAccountToken is being set to true
2024-09-03 09:51:17 -04:00
Peter Braun
64d2e10dc2 Fallback to use subscription cred for analytic upload (#15479) (#6668)
* Fallback to use subscription cred for analytic

Fall back to use SUBSCRIPTION_USERNAME/PASSWORD to upload analytic to if REDHAT_USERNAME/PASSWORD are not set

* Improve error message

* Guard against request with no query or data

* Add test for _send_to_analytics

Focus on credentials

* Supress sonarcloud warning about password

* Add test for analytic ship

Co-authored-by: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com>
2024-08-30 14:18:42 +02:00
Seth Foster
85bd7c3ca0 [4.6] Make controller specific team and org roles (#6662)
Adds the following managed Role Definitions

Controller Team Admin
Controller Team Member
Controller Organization Admin
Controller Organization Member

These have the same permission set as the
platform roles (without the Controller prefix)

Adding members to teams and orgs via the legacy RBAC system
will use these role definitions.

Other changes:
- Bump DAB to 2024.08.22
- Set ALLOW_LOCAL_ASSIGNING_JWT_ROLES to False in defaults.py.
This setting prevents assignments to the platform roles (e.g. Team Member).

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
2024-08-26 16:31:42 -04:00
Alan Rominger
77e999f7c8 Rewrite more access logic in terms of permissions instead of roles (#15453) (#6661)
* Rewrite more access logic in terms of permissions instead of roles

* Cut down supported logic because that would not work anyway

* Remove methods not needed anymore

* Create managed roles in test before delegating permissions
2024-08-26 14:37:52 -04:00
Elijah DeLee
01aa760510 Guard around race condition (#15452) (#6658)
I had the luck of running into this race condition that broke my deployment. No instance was ever able to register because on running "awx-manage" in some check of a setting, it would end up failing here with

```
  File "/var/lib/awx/venv/awx/lib64/python3.11/site-packages/awx/conf/license.py", line 10, in _get_validated_license_data
    return get_licenser().validate()
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/lib/awx/venv/awx/lib64/python3.11/site-packages/awx/main/utils/licensing.py", line 453, in validate
    automated_since = int(Instance.objects.order_by('id').first().created.timestamp())
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'created'
```
2024-08-20 20:51:33 +00:00
Rick Elrod
16a4c66c73 Fix a test in preparation for syncing description
Refs ansible/django-ansible-base#447

Signed-off-by: Rick Elrod <rick@elrod.me>
2024-08-19 07:37:01 -05:00
Rick Elrod
9fa5be015c Bump DAB to 2024.8.19
Signed-off-by: Rick Elrod <rick@elrod.me>
2024-08-19 07:37:01 -05:00
Jake Jackson
8b293e7046 update django to 4.2.15 to address multiple CVEs (#6636) 2024-08-15 13:32:26 -04:00
Jake Jackson
467024bc54 fix CVE-2024-33663 and bring in updates for social-auth-app-django (#6634) 2024-08-15 13:32:09 -04:00
jessicamack
bdf3f81016 Unpin channels-redis (#15329) (#6647)
* unpin channels-redis

The bug that initially caused the upgrade block has been resolved https://github.com/django/channels_redis/issues/332

* replace aioredis Exception with a redis Exception

Version 4.0.0 of channel-redis migrated the underlying Redis library from aioredis to redis-py. The Exception has been changed to an equivalent

* remove unused license

* remove UPGRADE BLOCKER in README

* remove hiredis

it was an indirect dependency from aioredis which was removed

* remove unused license

* add back hiredis

it's potentially providing a performance boost. install explicitly as a part of redis. upgrade to more recent version

* remove UPGRADE BLOCKER for hiredis

it was also addressed as a part of this PR
2024-08-12 15:03:46 -04:00
jessicamack
4a5cfdc11d Remove 'AWX' from setting endpoint (#15432) (#6646)
* Remove AWX from display text

Co-authored-by: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com>
2024-08-12 14:54:17 -04:00
111 changed files with 2306 additions and 1323 deletions

View File

@@ -172,9 +172,10 @@ jobs:
repository: ansible/awx-operator
path: awx-operator
- uses: ./awx/.github/actions/setup-python
- name: Setup python, referencing action at awx relative path
uses: ./awx/.github/actions/setup-python
with:
working-directory: awx
python-version: '3.x'
- name: Install playbook dependencies
run: |

View File

@@ -10,6 +10,7 @@ on:
- devel
- release_*
- feature_*
- stable-*
jobs:
push-development-images:
runs-on: ubuntu-latest

View File

@@ -85,9 +85,11 @@ jobs:
cp ../awx-logos/awx/ui/client/assets/* awx/ui/public/static/media/
- name: Setup node and npm for new UI build
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: awx/awx/ui/**/package-lock.json
- name: Prebuild new UI for awx image (to speed up build process)
working-directory: awx

View File

@@ -11,6 +11,7 @@ on:
- devel
- release_**
- feature_**
- stable-**
jobs:
push:
runs-on: ubuntu-latest
@@ -23,35 +24,25 @@ jobs:
with:
show-progress: false
- uses: ./.github/actions/setup-python
- name: Log in to registry
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- uses: ./.github/actions/setup-ssh-agent
- name: Build awx_devel image to use for schema gen
uses: ./.github/actions/awx_devel_image
with:
ssh-private-key: ${{ secrets.PRIVATE_GITHUB_KEY }}
- name: Pre-pull image to warm build cache
run: |
docker pull -q ghcr.io/${{ github.repository_owner }}/awx_devel:${GITHUB_REF##*/} || :
- name: Build image
run: |
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${GITHUB_REF##*/} make docker-compose-build
github-token: ${{ secrets.GITHUB_TOKEN }}
private-github-key: ${{ secrets.PRIVATE_GITHUB_KEY }}
- name: Generate API Schema
run: |
DEV_DOCKER_TAG_BASE=ghcr.io/${OWNER_LC} \
COMPOSE_TAG=${{ github.base_ref || github.ref_name }} \
docker run -u $(id -u) --rm -v ${{ github.workspace }}:/awx_devel/:Z \
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${GITHUB_REF##*/} /start_tests.sh genschema
--workdir=/awx_devel `make print-DEVEL_IMAGE_NAME` /start_tests.sh genschema
- name: Upload API Schema
env:
AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }}
AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_KEY }}
AWS_REGION: 'us-east-1'
run: |
ansible localhost -c local, -m command -a "{{ ansible_python_interpreter + ' -m pip install boto3'}}"
ansible localhost -c local -m aws_s3 \
-a "src=${{ github.workspace }}/schema.json bucket=awx-public-ci-files object=${GITHUB_REF##*/}/schema.json mode=put permission=public-read"
uses: keithweaver/aws-s3-github-action@v1.0.0
with:
command: cp
source: ${{ github.workspace }}/schema.json
destination: s3://awx-public-ci-files/${{ github.ref_name }}/schema.json
aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_KEY }}
aws_region: us-east-1

View File

@@ -77,7 +77,7 @@ RECEPTOR_IMAGE ?= quay.io/ansible/receptor:devel
SRC_ONLY_PKGS ?= cffi,pycparser,psycopg,twilio
# These should be upgraded in the AWX and Ansible venv before attempting
# to install the actual requirements
VENV_BOOTSTRAP ?= pip==21.2.4 setuptools==70.3.0 setuptools_scm[toml]==8.1.0 wheel==0.45.1 cython==3.0.11
VENV_BOOTSTRAP ?= pip==21.2.4 setuptools==80.9.0 setuptools_scm[toml]==8.0.4 wheel==0.42.0 cython==3.1.3
NAME ?= awx

View File

@@ -844,7 +844,7 @@ class ResourceAccessList(ParentMixin, ListAPIView):
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
ancestors = set(RoleEvaluation.objects.filter(content_type_id=content_type.id, object_id=obj.id).values_list('role_id', flat=True))
qs = User.objects.filter(has_roles__in=ancestors) | User.objects.filter(is_superuser=True)
auditor_role = RoleDefinition.objects.filter(name="Controller System Auditor").first()
auditor_role = RoleDefinition.objects.filter(name="Platform Auditor").first()
if auditor_role:
qs |= User.objects.filter(role_assignments__role_definition=auditor_role)
return qs.distinct()

View File

@@ -10,7 +10,7 @@ from rest_framework import permissions
# AWX
from awx.main.access import check_user_access
from awx.main.models import Inventory, UnifiedJob
from awx.main.models import Inventory, UnifiedJob, Organization
from awx.main.utils import get_object_or_400
logger = logging.getLogger('awx.api.permissions')
@@ -228,12 +228,19 @@ class InventoryInventorySourcesUpdatePermission(ModelAccessPermission):
class UserPermission(ModelAccessPermission):
def check_post_permissions(self, request, view, obj=None):
if not request.data:
return request.user.admin_of_organizations.exists()
return Organization.access_qs(request.user, 'change').exists()
elif request.user.is_superuser:
return True
raise PermissionDenied()
class IsSystemAdmin(permissions.BasePermission):
def has_permission(self, request, view):
if not (request.user and request.user.is_authenticated):
return False
return request.user.is_superuser
class IsSystemAdminOrAuditor(permissions.BasePermission):
"""
Allows write access only to system admin users.

View File

@@ -2839,7 +2839,7 @@ class ResourceAccessListElementSerializer(UserSerializer):
{
"role": {
"id": None,
"name": _("Controller System Auditor"),
"name": _("Platform Auditor"),
"description": _("Can view all aspects of the system"),
"user_capabilities": {"unattach": False},
},
@@ -3027,11 +3027,6 @@ class CredentialSerializer(BaseSerializer):
ret.remove(field)
return ret
def validate_organization(self, org):
if self.instance and (not self.instance.managed) and self.instance.credential_type.kind == 'galaxy' and org is None:
raise serializers.ValidationError(_("Galaxy credentials must be owned by an Organization."))
return org
def validate_credential_type(self, credential_type):
if self.instance and credential_type.pk != self.instance.credential_type.pk:
for related_objects in (
@@ -3107,9 +3102,6 @@ class CredentialSerializerCreate(CredentialSerializer):
if attrs.get('team'):
attrs['organization'] = attrs['team'].organization
if 'credential_type' in attrs and attrs['credential_type'].kind == 'galaxy' and list(owner_fields) != ['organization']:
raise serializers.ValidationError({"organization": _("Galaxy credentials must be owned by an Organization.")})
return super(CredentialSerializerCreate, self).validate(attrs)
def create(self, validated_data):
@@ -6006,7 +5998,7 @@ class InstanceGroupSerializer(BaseSerializer):
if self.instance and not self.instance.is_container_group:
raise serializers.ValidationError(_('pod_spec_override is only valid for container groups'))
pod_spec_override_json = None
pod_spec_override_json = {}
# defect if the value is yaml or json if yaml convert to json
try:
# convert yaml to json

View File

@@ -55,8 +55,7 @@ from wsgiref.util import FileWrapper
# django-ansible-base
from ansible_base.lib.utils.requests import get_remote_hosts
from ansible_base.rbac.models import RoleEvaluation, ObjectRole
from ansible_base.resource_registry.shared_types import OrganizationType, TeamType, UserType
from ansible_base.rbac.models import RoleEvaluation
# AWX
from awx.main.tasks.system import send_notifications, update_inventory_computed_fields
@@ -85,7 +84,6 @@ from awx.api.generics import (
from awx.api.views.labels import LabelSubListCreateAttachDetachView
from awx.api.versioning import reverse
from awx.main import models
from awx.main.models.rbac import get_role_definition
from awx.main.utils import (
camelcase_to_underscore,
extract_ansible_vars,
@@ -671,81 +669,16 @@ class ScheduleUnifiedJobsList(SubListAPIView):
name = _('Schedule Jobs List')
def immutablesharedfields(cls):
'''
Class decorator to prevent modifying shared resources when ALLOW_LOCAL_RESOURCE_MANAGEMENT setting is set to False.
Works by overriding these view methods:
- create
- delete
- perform_update
create and delete are overridden to raise a PermissionDenied exception.
perform_update is overridden to check if any shared fields are being modified,
and raise a PermissionDenied exception if so.
'''
# create instead of perform_create because some of our views
# override create instead of perform_create
if hasattr(cls, 'create'):
cls.original_create = cls.create
@functools.wraps(cls.create)
def create_wrapper(*args, **kwargs):
if settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT:
return cls.original_create(*args, **kwargs)
raise PermissionDenied({'detail': _('Creation of this resource is not allowed. Create this resource via the platform ingress.')})
cls.create = create_wrapper
if hasattr(cls, 'delete'):
cls.original_delete = cls.delete
@functools.wraps(cls.delete)
def delete_wrapper(*args, **kwargs):
if settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT:
return cls.original_delete(*args, **kwargs)
raise PermissionDenied({'detail': _('Deletion of this resource is not allowed. Delete this resource via the platform ingress.')})
cls.delete = delete_wrapper
if hasattr(cls, 'perform_update'):
cls.original_perform_update = cls.perform_update
@functools.wraps(cls.perform_update)
def update_wrapper(*args, **kwargs):
if not settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT:
view, serializer = args
instance = view.get_object()
if instance:
if isinstance(instance, models.Organization):
shared_fields = OrganizationType._declared_fields.keys()
elif isinstance(instance, models.User):
shared_fields = UserType._declared_fields.keys()
elif isinstance(instance, models.Team):
shared_fields = TeamType._declared_fields.keys()
attrs = serializer.validated_data
for field in shared_fields:
if field in attrs and getattr(instance, field) != attrs[field]:
raise PermissionDenied({field: _(f"Cannot change shared field '{field}'. Alter this field via the platform ingress.")})
return cls.original_perform_update(*args, **kwargs)
cls.perform_update = update_wrapper
return cls
@immutablesharedfields
class TeamList(ListCreateAPIView):
model = models.Team
serializer_class = serializers.TeamSerializer
@immutablesharedfields
class TeamDetail(RetrieveUpdateDestroyAPIView):
model = models.Team
serializer_class = serializers.TeamSerializer
@immutablesharedfields
class TeamUsersList(BaseUsersList):
model = models.User
serializer_class = serializers.UserSerializer
@@ -816,17 +749,9 @@ class TeamProjectsList(SubListAPIView):
def get_queryset(self):
team = self.get_parent_object()
self.check_parent_access(team)
model_ct = ContentType.objects.get_for_model(self.model)
parent_ct = ContentType.objects.get_for_model(self.parent_model)
rd = get_role_definition(team.member_role)
role = ObjectRole.objects.filter(object_id=team.id, content_type=parent_ct, role_definition=rd).first()
if role is None:
# Team has no permissions, therefore team has no projects
return self.model.objects.none()
else:
project_qs = self.model.accessible_objects(self.request.user, 'read_role')
return project_qs.filter(id__in=RoleEvaluation.objects.filter(content_type_id=model_ct.id, role=role).values_list('object_id'))
my_qs = self.model.accessible_objects(self.request.user, 'read_role')
team_qs = models.Project.accessible_objects(team, 'read_role')
return my_qs & team_qs
class TeamActivityStreamList(SubListAPIView):
@@ -941,13 +866,23 @@ class ProjectTeamsList(ListAPIView):
serializer_class = serializers.TeamSerializer
def get_queryset(self):
p = get_object_or_404(models.Project, pk=self.kwargs['pk'])
if not self.request.user.can_access(models.Project, 'read', p):
parent = get_object_or_404(models.Project, pk=self.kwargs['pk'])
if not self.request.user.can_access(models.Project, 'read', parent):
raise PermissionDenied()
project_ct = ContentType.objects.get_for_model(models.Project)
project_ct = ContentType.objects.get_for_model(parent)
team_ct = ContentType.objects.get_for_model(self.model)
all_roles = models.Role.objects.filter(Q(descendents__content_type=project_ct) & Q(descendents__object_id=p.pk), content_type=team_ct)
return self.model.accessible_objects(self.request.user, 'read_role').filter(pk__in=[t.content_object.pk for t in all_roles])
roles_on_project = models.Role.objects.filter(
content_type=project_ct,
object_id=parent.pk,
)
team_member_parent_roles = models.Role.objects.filter(children__in=roles_on_project, role_field='member_role', content_type=team_ct).distinct()
team_ids = team_member_parent_roles.values_list('object_id', flat=True)
my_qs = self.model.accessible_objects(self.request.user, 'read_role').filter(pk__in=team_ids)
return my_qs
class ProjectSchedulesList(SubListCreateAPIView):
@@ -1127,7 +1062,6 @@ class ProjectCopy(CopyAPIView):
copy_return_serializer_class = serializers.ProjectSerializer
@immutablesharedfields
class UserList(ListCreateAPIView):
model = models.User
serializer_class = serializers.UserSerializer
@@ -1184,14 +1118,6 @@ class UserRolesList(SubListAttachDetachAPIView):
role = get_object_or_400(models.Role, pk=sub_id)
content_types = ContentType.objects.get_for_models(models.Organization, models.Team, models.Credential) # dict of {model: content_type}
# Prevent user to be associated with team/org when ALLOW_LOCAL_RESOURCE_MANAGEMENT is False
if not settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT:
for model in [models.Organization, models.Team]:
ct = content_types[model]
if role.content_type == ct and role.role_field in ['member_role', 'admin_role']:
data = dict(msg=_(f"Cannot directly modify user membership to {ct.model}. Direct shared resource management disabled"))
return Response(data, status=status.HTTP_403_FORBIDDEN)
credential_content_type = content_types[models.Credential]
if role.content_type == credential_content_type:
if 'disassociate' not in request.data and role.content_object.organization and user not in role.content_object.organization.member_role:
@@ -1226,7 +1152,6 @@ class UserOrganizationsList(OrganizationCountsMixin, SubListAPIView):
model = models.Organization
serializer_class = serializers.OrganizationSerializer
parent_model = models.User
relationship = 'organizations'
def get_queryset(self):
parent = self.get_parent_object()
@@ -1240,7 +1165,6 @@ class UserAdminOfOrganizationsList(OrganizationCountsMixin, SubListAPIView):
model = models.Organization
serializer_class = serializers.OrganizationSerializer
parent_model = models.User
relationship = 'admin_of_organizations'
def get_queryset(self):
parent = self.get_parent_object()
@@ -1264,7 +1188,6 @@ class UserActivityStreamList(SubListAPIView):
return qs.filter(Q(actor=parent) | Q(user__in=[parent]))
@immutablesharedfields
class UserDetail(RetrieveUpdateDestroyAPIView):
model = models.User
serializer_class = serializers.UserSerializer
@@ -4239,13 +4162,6 @@ class RoleUsersList(SubListAttachDetachAPIView):
role = self.get_parent_object()
content_types = ContentType.objects.get_for_models(models.Organization, models.Team, models.Credential) # dict of {model: content_type}
if not settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT:
for model in [models.Organization, models.Team]:
ct = content_types[model]
if role.content_type == ct and role.role_field in ['member_role', 'admin_role']:
data = dict(msg=_(f"Cannot directly modify user membership to {ct.model}. Direct shared resource management disabled"))
return Response(data, status=status.HTTP_403_FORBIDDEN)
credential_content_type = content_types[models.Credential]
if role.content_type == credential_content_type:
if 'disassociate' not in request.data and role.content_object.organization and user not in role.content_object.organization.member_role:

View File

@@ -12,7 +12,7 @@ import re
import asn1
from awx.api import serializers
from awx.api.generics import GenericAPIView, Response
from awx.api.permissions import IsSystemAdminOrAuditor
from awx.api.permissions import IsSystemAdmin
from awx.main import models
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
@@ -48,7 +48,7 @@ class InstanceInstallBundle(GenericAPIView):
name = _('Install Bundle')
model = models.Instance
serializer_class = serializers.InstanceSerializer
permission_classes = (IsSystemAdminOrAuditor,)
permission_classes = (IsSystemAdmin,)
def get(self, request, *args, **kwargs):
instance_obj = self.get_object()

View File

@@ -53,18 +53,15 @@ from awx.api.serializers import (
CredentialSerializer,
)
from awx.api.views.mixin import RelatedJobsPreventDeleteMixin, OrganizationCountsMixin, OrganizationInstanceGroupMembershipMixin
from awx.api.views import immutablesharedfields
logger = logging.getLogger('awx.api.views.organization')
@immutablesharedfields
class OrganizationList(OrganizationCountsMixin, ListCreateAPIView):
model = Organization
serializer_class = OrganizationSerializer
@immutablesharedfields
class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
model = Organization
serializer_class = OrganizationSerializer
@@ -107,7 +104,6 @@ class OrganizationInventoriesList(SubListAPIView):
relationship = 'inventories'
@immutablesharedfields
class OrganizationUsersList(BaseUsersList):
model = User
serializer_class = UserSerializer
@@ -116,7 +112,6 @@ class OrganizationUsersList(BaseUsersList):
ordering = ('username',)
@immutablesharedfields
class OrganizationAdminsList(BaseUsersList):
model = User
serializer_class = UserSerializer
@@ -155,7 +150,6 @@ class OrganizationWorkflowJobTemplatesList(SubListCreateAPIView):
parent_key = 'organization'
@immutablesharedfields
class OrganizationTeamsList(SubListCreateAttachDetachAPIView):
model = Team
serializer_class = TeamSerializer

View File

@@ -8,6 +8,8 @@ import operator
from collections import OrderedDict
from django.conf import settings
from django.core.cache import cache
from django.db import connection
from django.utils.encoding import smart_str
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
@@ -26,6 +28,7 @@ from awx.api.generics import APIView
from awx.conf.registry import settings_registry
from awx.main.analytics import all_collectors
from awx.main.ha import is_ha_environment
from awx.main.tasks.system import clear_setting_cache
from awx.main.utils import get_awx_version, get_custom_venv_choices
from awx.main.utils.licensing import validate_entitlement_manifest
from awx.api.versioning import URLPathVersioning, reverse, drf_reverse
@@ -221,8 +224,12 @@ class ApiV2AttachView(APIView):
subscription_id = data.get('subscription_id', None)
if not subscription_id:
return Response({"error": _("No subscription ID provided.")}, status=status.HTTP_400_BAD_REQUEST)
# Ensure we always use the latest subscription credentials
cache.delete_many(['SUBSCRIPTIONS_CLIENT_ID', 'SUBSCRIPTIONS_CLIENT_SECRET'])
user = getattr(settings, 'SUBSCRIPTIONS_CLIENT_ID', None)
pw = getattr(settings, 'SUBSCRIPTIONS_CLIENT_SECRET', None)
if not (user and pw):
return Response({"error": _("Missing subscription credentials")}, status=status.HTTP_400_BAD_REQUEST)
if subscription_id and user and pw:
data = request.data.copy()
try:
@@ -245,6 +252,7 @@ class ApiV2AttachView(APIView):
if sub['subscription_id'] == subscription_id:
sub['valid_key'] = True
settings.LICENSE = sub
connection.on_commit(lambda: clear_setting_cache.delay(['LICENSE']))
return Response(sub)
return Response({"error": _("Error processing subscription metadata.")}, status=status.HTTP_400_BAD_REQUEST)
@@ -264,7 +272,6 @@ class ApiV2ConfigView(APIView):
'''Return various sitewide configuration settings'''
license_data = get_licenser().validate()
if not license_data.get('valid_key', False):
license_data = {}
@@ -328,6 +335,7 @@ class ApiV2ConfigView(APIView):
try:
license_data_validated = get_licenser().license_from_manifest(license_data)
connection.on_commit(lambda: clear_setting_cache.delay(['LICENSE']))
except Exception:
logger.warning(smart_str(u"Invalid subscription submitted."), extra=dict(actor=request.user.username))
return Response({"error": _("Invalid License")}, status=status.HTTP_400_BAD_REQUEST)
@@ -346,6 +354,7 @@ class ApiV2ConfigView(APIView):
def delete(self, request):
try:
settings.LICENSE = {}
connection.on_commit(lambda: clear_setting_cache.delay(['LICENSE']))
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception:
# FIX: Log

View File

@@ -639,7 +639,9 @@ class UserAccess(BaseAccess):
prefetch_related = ('resource',)
def filtered_queryset(self):
if settings.ORG_ADMINS_CAN_SEE_ALL_USERS and (self.user.admin_of_organizations.exists() or self.user.auditor_of_organizations.exists()):
if settings.ORG_ADMINS_CAN_SEE_ALL_USERS and (
Organization.access_qs(self.user, 'change').exists() or Organization.access_qs(self.user, 'audit').exists()
):
qs = User.objects.all()
else:
qs = (
@@ -1224,7 +1226,9 @@ class TeamAccess(BaseAccess):
)
def filtered_queryset(self):
if settings.ORG_ADMINS_CAN_SEE_ALL_USERS and (self.user.admin_of_organizations.exists() or self.user.auditor_of_organizations.exists()):
if settings.ORG_ADMINS_CAN_SEE_ALL_USERS and (
Organization.access_qs(self.user, 'change').exists() or Organization.access_qs(self.user, 'audit').exists()
):
return self.model.objects.all()
return self.model.objects.filter(
Q(organization__in=Organization.accessible_pk_qs(self.user, 'member_role')) | Q(pk__in=self.model.accessible_pk_qs(self.user, 'read_role'))
@@ -2564,7 +2568,7 @@ class NotificationTemplateAccess(BaseAccess):
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
return self.model.access_qs(self.user, 'view')
return self.model.objects.filter(
Q(organization__in=Organization.access_qs(self.user, 'add_notificationtemplate')) | Q(organization__in=self.user.auditor_of_organizations)
Q(organization__in=Organization.access_qs(self.user, 'add_notificationtemplate')) | Q(organization__in=Organization.access_qs(self.user, 'audit'))
).distinct()
@check_superuser
@@ -2599,7 +2603,7 @@ class NotificationAccess(BaseAccess):
def filtered_queryset(self):
return self.model.objects.filter(
Q(notification_template__organization__in=Organization.access_qs(self.user, 'add_notificationtemplate'))
| Q(notification_template__organization__in=self.user.auditor_of_organizations)
| Q(notification_template__organization__in=Organization.access_qs(self.user, 'audit'))
).distinct()
def can_delete(self, obj):

View File

@@ -105,6 +105,7 @@ register(
),
category=_('System'),
category_slug='system',
hidden=True,
)
register(
@@ -1093,3 +1094,13 @@ register(
category=('PolicyAsCode'),
category_slug='policyascode',
)
def policy_as_code_validate(serializer, attrs):
opa_host = attrs.get('OPA_HOST', '')
if opa_host and (opa_host.startswith('http://') or opa_host.startswith('https://')):
raise serializers.ValidationError({'OPA_HOST': _("OPA_HOST should not include 'http://' or 'https://' prefixes. Please enter only the hostname.")})
return attrs
register_validate('policyascode', policy_as_code_validate)

View File

@@ -4,6 +4,7 @@
from django.core.management.base import BaseCommand
from django.db import transaction
from crum import impersonate
from ansible_base.resource_registry.signals.handlers import no_reverse_sync
from awx.main.models import User, Organization, Project, Inventory, CredentialType, Credential, Host, JobTemplate
from awx.main.signals import disable_computed_fields
@@ -16,8 +17,9 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs):
# Wrap the operation in an atomic block, so we do not on accident
# create the organization but not create the project, etc.
with transaction.atomic():
self._handle()
with no_reverse_sync():
with transaction.atomic():
self._handle()
def _handle(self):
changed = False

View File

@@ -26,6 +26,11 @@ def change_inventory_source_org_unique(apps, schema_editor):
logger.info(f'Set database constraint rule for {r} inventory source objects')
def rename_wfjt(apps, schema_editor):
cls = apps.get_model('main', 'WorkflowJobTemplate')
_rename_duplicates(cls)
class Migration(migrations.Migration):
dependencies = [
@@ -40,6 +45,7 @@ class Migration(migrations.Migration):
name='org_unique',
field=models.BooleanField(blank=True, default=True, editable=False, help_text='Used internally to selectively enforce database constraint on name'),
),
migrations.RunPython(rename_wfjt, migrations.RunPython.noop),
migrations.RunPython(change_inventory_source_org_unique, migrations.RunPython.noop),
migrations.AddConstraint(
model_name='unifiedjobtemplate',

View File

@@ -1,9 +1,20 @@
from django.db import migrations
# AWX
from awx.main.models import CredentialType
from awx.main.utils.common import set_current_apps
def setup_tower_managed_defaults(apps, schema_editor):
set_current_apps(apps)
CredentialType.setup_tower_managed_defaults(apps)
class Migration(migrations.Migration):
dependencies = [
('main', '0200_template_name_constraint'),
]
operations = []
operations = [
migrations.RunPython(setup_tower_managed_defaults),
]

View File

@@ -0,0 +1,102 @@
# Generated by Django migration for converting Controller role definitions
from ansible_base.rbac.migrations._utils import give_permissions
from django.db import migrations
def convert_controller_role_definitions(apps, schema_editor):
"""
Convert Controller role definitions to regular role definitions:
- Controller Organization Admin -> Organization Admin
- Controller Organization Member -> Organization Member
- Controller Team Admin -> Team Admin
- Controller Team Member -> Team Member
- Controller System Auditor -> Platform Auditor
Then delete the old Controller role definitions.
"""
RoleDefinition = apps.get_model('dab_rbac', 'RoleDefinition')
RoleUserAssignment = apps.get_model('dab_rbac', 'RoleUserAssignment')
RoleTeamAssignment = apps.get_model('dab_rbac', 'RoleTeamAssignment')
Permission = apps.get_model('dab_rbac', 'DABPermission')
# Mapping of old Controller role names to new role names
role_mappings = {
'Controller Organization Admin': 'Organization Admin',
'Controller Organization Member': 'Organization Member',
'Controller Team Admin': 'Team Admin',
'Controller Team Member': 'Team Member',
}
for old_name, new_name in role_mappings.items():
# Find the old Controller role definition
old_role = RoleDefinition.objects.filter(name=old_name).first()
if not old_role:
continue # Skip if the old role doesn't exist
# Find the new role definition
new_role = RoleDefinition.objects.get(name=new_name)
# Collect all the assignments that need to be migrated
# Group by object (content_type + object_id) to batch the give_permissions calls
assignments_by_object = {}
# Get user assignments
user_assignments = RoleUserAssignment.objects.filter(role_definition=old_role).select_related('object_role')
for assignment in user_assignments:
key = (assignment.object_role.content_type_id, assignment.object_role.object_id)
if key not in assignments_by_object:
assignments_by_object[key] = {'users': [], 'teams': []}
assignments_by_object[key]['users'].append(assignment.user)
# Get team assignments
team_assignments = RoleTeamAssignment.objects.filter(role_definition=old_role).select_related('object_role')
for assignment in team_assignments:
key = (assignment.object_role.content_type_id, assignment.object_role.object_id)
if key not in assignments_by_object:
assignments_by_object[key] = {'users': [], 'teams': []}
assignments_by_object[key]['teams'].append(assignment.team.id)
# Use give_permissions to create new assignments with the new role definition
for (content_type_id, object_id), data in assignments_by_object.items():
if data['users'] or data['teams']:
give_permissions(
apps,
new_role,
users=data['users'],
teams=data['teams'],
object_id=object_id,
content_type_id=content_type_id,
)
# Delete the old role definition (this will cascade to delete old assignments and ObjectRoles)
old_role.delete()
# Create or get Platform Auditor
auditor_rd, created = RoleDefinition.objects.get_or_create(
name='Platform Auditor',
defaults={'description': 'Migrated singleton role giving read permission to everything', 'managed': True},
)
if created:
auditor_rd.permissions.add(*list(Permission.objects.filter(codename__startswith='view')))
old_rd = RoleDefinition.objects.filter(name='Controller System Auditor').first()
if old_rd:
for assignment in RoleUserAssignment.objects.filter(role_definition=old_rd):
RoleUserAssignment.objects.create(
user=assignment.user,
role_definition=auditor_rd,
)
# Delete the Controller System Auditor role
RoleDefinition.objects.filter(name='Controller System Auditor').delete()
class Migration(migrations.Migration):
dependencies = [
('main', '0201_create_managed_creds'),
]
operations = [
migrations.RunPython(convert_controller_role_definitions),
]

View File

@@ -0,0 +1,22 @@
import logging
from django.db import migrations
from awx.main.migrations._dab_rbac import consolidate_indirect_user_roles
logger = logging.getLogger('awx.main.migrations')
class Migration(migrations.Migration):
dependencies = [
('main', '0202_convert_controller_role_definitions'),
]
# The DAB RBAC app makes substantial model changes which by change-ordering comes after this
# not including run_before might sometimes work but this enforces a more strict and stable order
# for both applying migrations forwards and backwards
run_before = [("dab_rbac", "0004_remote_permissions_additions")]
operations = [
migrations.RunPython(consolidate_indirect_user_roles, migrations.RunPython.noop),
]

View File

@@ -1,34 +1,55 @@
# Generated by Django 4.2.10 on 2024-09-16 10:22
from django.db import migrations, models
from awx.main.migrations._create_system_jobs import delete_clear_tokens_sjt
# --- START of function merged from 0203_rename_github_app_kind.py ---
def update_github_app_kind(apps, schema_editor):
"""
Updates the 'kind' field for CredentialType records
from 'github_app' to 'github_app_lookup'.
This addresses a change in the entry point key for the GitHub App plugin.
"""
CredentialType = apps.get_model('main', 'CredentialType')
db_alias = schema_editor.connection.alias
CredentialType.objects.using(db_alias).filter(kind='github_app').update(kind='github_app_lookup')
# --- END of function merged from 0203_rename_github_app_kind.py ---
class Migration(migrations.Migration):
dependencies = [
('main', '0201_create_managed_creds'),
('main', '0203_remove_team_of_teams'),
]
operations = [
migrations.DeleteModel(
name='Profile',
),
# Remove SSO app content
# delete all sso application migrations
migrations.RunSQL("DELETE FROM django_migrations WHERE app = 'sso';"),
# Added reverse_sql=migrations.RunSQL.noop to make this reversible for tests
migrations.RunSQL("DELETE FROM django_migrations WHERE app = 'sso';", reverse_sql=migrations.RunSQL.noop),
# delete all sso application content group permissions
# Added reverse_sql=migrations.RunSQL.noop to make this reversible for tests
migrations.RunSQL(
"DELETE FROM auth_group_permissions "
"WHERE permission_id IN "
"(SELECT id FROM auth_permission WHERE content_type_id in (SELECT id FROM django_content_type WHERE app_label = 'sso'));"
"(SELECT id FROM auth_permission WHERE content_type_id in (SELECT id FROM django_content_type WHERE app_label = 'sso'));",
reverse_sql=migrations.RunSQL.noop,
),
# delete all sso application content permissions
migrations.RunSQL("DELETE FROM auth_permission " "WHERE content_type_id IN (SELECT id FROM django_content_type WHERE app_label = 'sso');"),
# Added reverse_sql=migrations.RunSQL.noop to make this reversible for tests
migrations.RunSQL(
"DELETE FROM auth_permission " "WHERE content_type_id IN (SELECT id FROM django_content_type WHERE app_label = 'sso');",
reverse_sql=migrations.RunSQL.noop,
),
# delete sso application content type
migrations.RunSQL("DELETE FROM django_content_type WHERE app_label = 'sso';"),
# Added reverse_sql=migrations.RunSQL.noop to make this reversible for tests
migrations.RunSQL("DELETE FROM django_content_type WHERE app_label = 'sso';", reverse_sql=migrations.RunSQL.noop),
# drop sso application created table
migrations.RunSQL("DROP TABLE IF EXISTS sso_userenterpriseauth;"),
# Added reverse_sql=migrations.RunSQL.noop to make this reversible for tests
migrations.RunSQL("DROP TABLE IF EXISTS sso_userenterpriseauth;", reverse_sql=migrations.RunSQL.noop),
# Alter inventory source source field
migrations.AlterField(
model_name='inventorysource',
@@ -97,4 +118,7 @@ class Migration(migrations.Migration):
max_length=32,
),
),
# --- START of operations merged from 0203_rename_github_app_kind.py ---
migrations.RunPython(update_github_app_kind, migrations.RunPython.noop),
# --- END of operations merged from 0203_rename_github_app_kind.py ---
]

View File

@@ -1,5 +1,6 @@
import logging
logger = logging.getLogger('awx.main.migrations')

View File

@@ -1,5 +1,6 @@
import json
import logging
from collections import defaultdict
from django.apps import apps as global_apps
from django.db.models import ForeignKey
@@ -17,7 +18,14 @@ logger = logging.getLogger('awx.main.migrations._dab_rbac')
def create_permissions_as_operation(apps, schema_editor):
logger.info('Running data migration create_permissions_as_operation')
# NOTE: the DAB ContentType changes adjusted how they fire
# before they would fire on every app config, like contenttypes
create_dab_permissions(global_apps.get_app_config("main"), apps=apps)
# This changed to only fire once and do a global creation
# so we need to call it for specifically the dab_rbac app
# multiple calls will not hurt anything
create_dab_permissions(global_apps.get_app_config("dab_rbac"), apps=apps)
"""
@@ -112,7 +120,12 @@ def get_descendents(f, children_map):
def get_permissions_for_role(role_field, children_map, apps):
Permission = apps.get_model('dab_rbac', 'DABPermission')
ContentType = apps.get_model('contenttypes', 'ContentType')
try:
# After migration for remote permissions
ContentType = apps.get_model('dab_rbac', 'DABContentType')
except LookupError:
# If using DAB from before remote permissions are implemented
ContentType = apps.get_model('contenttypes', 'ContentType')
perm_list = []
for child_field in get_descendents(role_field, children_map):
@@ -155,11 +168,15 @@ def migrate_to_new_rbac(apps, schema_editor):
This method moves the assigned permissions from the old rbac.py models
to the new RoleDefinition and ObjectRole models
"""
logger.info('Running data migration migrate_to_new_rbac')
Role = apps.get_model('main', 'Role')
RoleDefinition = apps.get_model('dab_rbac', 'RoleDefinition')
RoleUserAssignment = apps.get_model('dab_rbac', 'RoleUserAssignment')
Permission = apps.get_model('dab_rbac', 'DABPermission')
if Permission.objects.count() == 0:
raise RuntimeError('Running migrate_to_new_rbac requires DABPermission objects created first')
# remove add premissions that are not valid for migrations from old versions
for perm_str in ('add_organization', 'add_jobtemplate'):
perm = Permission.objects.filter(codename=perm_str).first()
@@ -239,11 +256,14 @@ def migrate_to_new_rbac(apps, schema_editor):
# Create new replacement system auditor role
new_system_auditor, created = RoleDefinition.objects.get_or_create(
name='Controller System Auditor',
name='Platform Auditor',
defaults={'description': 'Migrated singleton role giving read permission to everything', 'managed': True},
)
new_system_auditor.permissions.add(*list(Permission.objects.filter(codename__startswith='view')))
if created:
logger.info(f'Created RoleDefinition {new_system_auditor.name} pk={new_system_auditor.pk} with {new_system_auditor.permissions.count()} permissions')
# migrate is_system_auditor flag, because it is no longer handled by a system role
old_system_auditor = Role.objects.filter(singleton_name='system_auditor').first()
if old_system_auditor:
@@ -272,8 +292,9 @@ def get_or_create_managed(name, description, ct, permissions, RoleDefinition):
def setup_managed_role_definitions(apps, schema_editor):
"""
Idepotent method to create or sync the managed role definitions
Idempotent method to create or sync the managed role definitions
"""
logger.info('Running data migration setup_managed_role_definitions')
to_create = {
'object_admin': '{cls.__name__} Admin',
'org_admin': 'Organization Admin',
@@ -281,7 +302,13 @@ def setup_managed_role_definitions(apps, schema_editor):
'special': '{cls.__name__} {action}',
}
ContentType = apps.get_model('contenttypes', 'ContentType')
try:
# After migration for remote permissions
ContentType = apps.get_model('dab_rbac', 'DABContentType')
except LookupError:
# If using DAB from before remote permissions are implemented
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('dab_rbac', 'DABPermission')
RoleDefinition = apps.get_model('dab_rbac', 'RoleDefinition')
Organization = apps.get_model(settings.ANSIBLE_BASE_ORGANIZATION_MODEL)
@@ -309,16 +336,6 @@ def setup_managed_role_definitions(apps, schema_editor):
to_create['object_admin'].format(cls=cls), f'Has all permissions to a single {cls._meta.verbose_name}', ct, indiv_perms, RoleDefinition
)
)
if cls_name == 'team':
managed_role_definitions.append(
get_or_create_managed(
'Controller Team Admin',
f'Has all permissions to a single {cls._meta.verbose_name}',
ct,
indiv_perms,
RoleDefinition,
)
)
if 'org_children' in to_create and (cls_name not in ('organization', 'instancegroup', 'team')):
org_child_perms = object_perms.copy()
@@ -359,18 +376,6 @@ def setup_managed_role_definitions(apps, schema_editor):
RoleDefinition,
)
)
if action == 'member' and cls_name in ('organization', 'team'):
suffix = to_create['special'].format(cls=cls, action=action.title())
rd_name = f'Controller {suffix}'
managed_role_definitions.append(
get_or_create_managed(
rd_name,
f'Has {action} permissions to a single {cls._meta.verbose_name}',
ct,
perm_list,
RoleDefinition,
)
)
if 'org_admin' in to_create:
managed_role_definitions.append(
@@ -382,15 +387,6 @@ def setup_managed_role_definitions(apps, schema_editor):
RoleDefinition,
)
)
managed_role_definitions.append(
get_or_create_managed(
'Controller Organization Admin',
'Has all permissions to a single organization and all objects inside of it',
org_ct,
org_perms,
RoleDefinition,
)
)
# Special "organization action" roles
audit_permissions = [perm for perm in org_perms if perm.codename.startswith('view_')]
@@ -431,3 +427,115 @@ def setup_managed_role_definitions(apps, schema_editor):
for role_definition in unexpected_role_definitions:
logger.info(f'Deleting old managed role definition {role_definition.name}, pk={role_definition.pk}')
role_definition.delete()
def get_team_to_team_relationships(apps, team_member_role):
"""
Find all team-to-team relationships where one team is a member of another.
Returns a dict mapping parent_team_id -> [child_team_id, ...]
"""
team_to_team_relationships = defaultdict(list)
# Find all team assignments with the Team Member role
RoleTeamAssignment = apps.get_model('dab_rbac', 'RoleTeamAssignment')
team_assignments = RoleTeamAssignment.objects.filter(role_definition=team_member_role).select_related('team')
for assignment in team_assignments:
parent_team_id = int(assignment.object_id)
child_team_id = assignment.team.id
team_to_team_relationships[parent_team_id].append(child_team_id)
return team_to_team_relationships
def get_all_user_members_of_team(apps, team_member_role, team_id, team_to_team_map, visited=None):
"""
Recursively find all users who are members of a team, including through nested teams.
"""
if visited is None:
visited = set()
if team_id in visited:
return set() # Avoid infinite recursion
visited.add(team_id)
all_users = set()
# Get direct user assignments to this team
RoleUserAssignment = apps.get_model('dab_rbac', 'RoleUserAssignment')
user_assignments = RoleUserAssignment.objects.filter(role_definition=team_member_role, object_id=team_id).select_related('user')
for assignment in user_assignments:
all_users.add(assignment.user)
# Get team-to-team assignments and recursively find their users
child_team_ids = team_to_team_map.get(team_id, [])
for child_team_id in child_team_ids:
nested_users = get_all_user_members_of_team(apps, team_member_role, child_team_id, team_to_team_map, visited.copy())
all_users.update(nested_users)
return all_users
def remove_team_to_team_assignment(apps, team_member_role, parent_team_id, child_team_id):
"""
Remove team-to-team memberships.
"""
Team = apps.get_model('main', 'Team')
RoleTeamAssignment = apps.get_model('dab_rbac', 'RoleTeamAssignment')
parent_team = Team.objects.get(id=parent_team_id)
child_team = Team.objects.get(id=child_team_id)
# Remove all team-to-team RoleTeamAssignments
RoleTeamAssignment.objects.filter(role_definition=team_member_role, object_id=parent_team_id, team=child_team).delete()
# Check mirroring Team model for children under member_role
parent_team.member_role.children.filter(object_id=child_team_id).delete()
def consolidate_indirect_user_roles(apps, schema_editor):
"""
A user should have a member role for every team they were indirectly
a member of. ex. Team A is a member of Team B. All users in Team A
previously were only members of Team A. They should now be members of
Team A and Team B.
"""
# get models for membership on teams
RoleDefinition = apps.get_model('dab_rbac', 'RoleDefinition')
Team = apps.get_model('main', 'Team')
team_member_role = RoleDefinition.objects.get(name='Team Member')
team_to_team_map = get_team_to_team_relationships(apps, team_member_role)
if not team_to_team_map:
return # No team-to-team relationships to consolidate
# Get content type for Team - needed for give_permissions
try:
from django.contrib.contenttypes.models import ContentType
team_content_type = ContentType.objects.get_for_model(Team)
except ImportError:
# Fallback if ContentType is not available
ContentType = apps.get_model('contenttypes', 'ContentType')
team_content_type = ContentType.objects.get_for_model(Team)
# Get all users who should be direct members of a team
for parent_team_id, child_team_ids in team_to_team_map.items():
all_users = get_all_user_members_of_team(apps, team_member_role, parent_team_id, team_to_team_map)
# Create direct RoleUserAssignments for all users
if all_users:
give_permissions(apps=apps, rd=team_member_role, users=list(all_users), object_id=parent_team_id, content_type_id=team_content_type.id)
# Mirror assignments to Team model
parent_team = Team.objects.get(id=parent_team_id)
for user in all_users:
parent_team.member_role.members.add(user.id)
# Remove all team-to-team assignments for parent team
for child_team_id in child_team_ids:
remove_team_to_team_assignment(apps, team_member_role, parent_team_id, child_team_id)

View File

@@ -172,35 +172,17 @@ def cleanup_created_modified_by(sender, **kwargs):
pre_delete.connect(cleanup_created_modified_by, sender=User)
@property
def user_get_organizations(user):
return Organization.access_qs(user, 'member')
@property
def user_get_admin_of_organizations(user):
return Organization.access_qs(user, 'change')
@property
def user_get_auditor_of_organizations(user):
return Organization.access_qs(user, 'audit')
@property
def created(user):
return user.date_joined
User.add_to_class('organizations', user_get_organizations)
User.add_to_class('admin_of_organizations', user_get_admin_of_organizations)
User.add_to_class('auditor_of_organizations', user_get_auditor_of_organizations)
User.add_to_class('created', created)
def get_system_auditor_role():
rd, created = RoleDefinition.objects.get_or_create(
name='Controller System Auditor', defaults={'description': 'Migrated singleton role giving read permission to everything'}
name='Platform Auditor', defaults={'description': 'Migrated singleton role giving read permission to everything'}
)
if created:
rd.permissions.add(*list(permission_registry.permission_qs.filter(codename__startswith='view')))

View File

@@ -1024,7 +1024,10 @@ class InventorySourceOptions(BaseModel):
# If a credential was provided, it's important that it matches
# the actual inventory source being used (Amazon requires Amazon
# credentials; Rackspace requires Rackspace credentials; etc...)
if source.replace('ec2', 'aws') != cred.kind:
# TODO: AAP-53978 check that this matches new awx-plugin content for ESXI
if source == 'vmware_esxi' and source.replace('vmware_esxi', 'vmware') != cred.kind:
return _('VMWARE inventory sources (such as %s) require credentials for the matching cloud service.') % source
if source == 'ec2' and source.replace('ec2', 'aws') != cred.kind:
return _('Cloud-based inventory sources (such as %s) require credentials for the matching cloud service.') % source
# Allow an EC2 source to omit the credential. If Tower is running on
# an EC2 instance with an IAM Role assigned, boto will use credentials

View File

@@ -86,7 +86,7 @@ class ResourceMixin(models.Model):
raise RuntimeError(f'Role filters only valid for users and ancestor role, received {accessor}')
if content_types is None:
ct_kwarg = dict(content_type_id=ContentType.objects.get_for_model(cls).id)
ct_kwarg = dict(content_type=ContentType.objects.get_for_model(cls))
else:
ct_kwarg = dict(content_type_id__in=content_types)

View File

@@ -27,6 +27,9 @@ from django.conf import settings
# Ansible_base app
from ansible_base.rbac.models import RoleDefinition, RoleUserAssignment, RoleTeamAssignment
from ansible_base.rbac.sync import maybe_reverse_sync_assignment, maybe_reverse_sync_unassignment, maybe_reverse_sync_role_definition
from ansible_base.rbac import permission_registry
from ansible_base.resource_registry.signals.handlers import no_reverse_sync
from ansible_base.lib.utils.models import get_type_for_model
# AWX
@@ -559,34 +562,27 @@ def get_role_definition(role):
f = obj._meta.get_field(role.role_field)
action_name = f.name.rsplit("_", 1)[0]
model_print = type(obj).__name__
rd_name = f'{model_print} {action_name.title()} Compat'
perm_list = get_role_codenames(role)
defaults = {
'content_type_id': role.content_type_id,
'content_type': permission_registry.content_type_model.objects.get_by_natural_key(role.content_type.app_label, role.content_type.model),
'description': f'Has {action_name.title()} permission to {model_print} for backwards API compatibility',
}
# use Controller-specific role definitions for Team/Organization and member/admin
# instead of platform role definitions
# these should exist in the system already, so just do a lookup by role definition name
if model_print in ['Team', 'Organization'] and action_name in ['member', 'admin']:
rd_name = f'Controller {model_print} {action_name.title()}'
rd = RoleDefinition.objects.filter(name=rd_name).first()
if rd:
return rd
else:
return RoleDefinition.objects.create_from_permissions(permissions=perm_list, name=rd_name, managed=True, **defaults)
else:
rd_name = f'{model_print} {action_name.title()} Compat'
with impersonate(None):
try:
rd, created = RoleDefinition.objects.get_or_create(name=rd_name, permissions=perm_list, defaults=defaults)
with no_reverse_sync():
rd, created = RoleDefinition.objects.get_or_create(name=rd_name, permissions=perm_list, defaults=defaults)
except ValidationError:
# This is a tricky case - practically speaking, users should not be allowed to create team roles
# or roles that include the team member permission.
# If we need to create this for compatibility purposes then we will create it as a managed non-editable role
defaults['managed'] = True
rd, created = RoleDefinition.objects.get_or_create(name=rd_name, permissions=perm_list, defaults=defaults)
with no_reverse_sync():
rd, created = RoleDefinition.objects.get_or_create(name=rd_name, permissions=perm_list, defaults=defaults)
if created and rbac_sync_enabled.enabled:
maybe_reverse_sync_role_definition(rd, action='create')
return rd
@@ -600,12 +596,6 @@ def get_role_from_object_role(object_role):
model_name, role_name, _ = rd.name.split()
role_name = role_name.lower()
role_name += '_role'
elif rd.name.startswith('Controller') and rd.name.endswith(' Admin'):
# Controller Organization Admin and Controller Team Admin
role_name = 'admin_role'
elif rd.name.startswith('Controller') and rd.name.endswith(' Member'):
# Controller Organization Member and Controller Team Member
role_name = 'member_role'
elif rd.name.endswith(' Admin') and rd.name.count(' ') == 2:
# cases like "Organization Project Admin"
model_name, target_model_name, role_name = rd.name.split()
@@ -632,12 +622,14 @@ def get_role_from_object_role(object_role):
return getattr(object_role.content_object, role_name)
def give_or_remove_permission(role, actor, giving=True):
def give_or_remove_permission(role, actor, giving=True, rd=None):
obj = role.content_object
if obj is None:
return
rd = get_role_definition(role)
rd.give_or_remove_permission(actor, obj, giving=giving)
if not rd:
rd = get_role_definition(role)
assignment = rd.give_or_remove_permission(actor, obj, giving=giving)
return assignment
class SyncEnabled(threading.local):
@@ -689,7 +681,15 @@ def sync_members_to_new_rbac(instance, action, model, pk_set, reverse, **kwargs)
role = Role.objects.get(pk=user_or_role_id)
else:
user = get_user_model().objects.get(pk=user_or_role_id)
give_or_remove_permission(role, user, giving=is_giving)
rd = get_role_definition(role)
assignment = give_or_remove_permission(role, user, giving=is_giving, rd=rd)
# sync to resource server
if rbac_sync_enabled.enabled:
if is_giving:
maybe_reverse_sync_assignment(assignment)
else:
maybe_reverse_sync_unassignment(rd, user, role.content_object)
def sync_parents_to_new_rbac(instance, action, model, pk_set, reverse, **kwargs):
@@ -732,12 +732,19 @@ def sync_parents_to_new_rbac(instance, action, model, pk_set, reverse, **kwargs)
from awx.main.models.organization import Team
team = Team.objects.get(pk=parent_role.object_id)
give_or_remove_permission(child_role, team, giving=is_giving)
rd = get_role_definition(child_role)
assignment = give_or_remove_permission(child_role, team, giving=is_giving, rd=rd)
# sync to resource server
if rbac_sync_enabled.enabled:
if is_giving:
maybe_reverse_sync_assignment(assignment)
else:
maybe_reverse_sync_unassignment(rd, team, child_role.content_object)
ROLE_DEFINITION_TO_ROLE_FIELD = {
'Organization Member': 'member_role',
'Controller Organization Member': 'member_role',
'WorkflowJobTemplate Admin': 'admin_role',
'Organization WorkflowJobTemplate Admin': 'workflow_admin_role',
'WorkflowJobTemplate Execute': 'execute_role',
@@ -762,11 +769,8 @@ ROLE_DEFINITION_TO_ROLE_FIELD = {
'Organization Credential Admin': 'credential_admin_role',
'Credential Use': 'use_role',
'Team Admin': 'admin_role',
'Controller Team Admin': 'admin_role',
'Team Member': 'member_role',
'Controller Team Member': 'member_role',
'Organization Admin': 'admin_role',
'Controller Organization Admin': 'admin_role',
'Organization Audit': 'auditor_role',
'Organization Execute': 'execute_role',
'Organization Approval': 'approval_role',

View File

@@ -34,6 +34,7 @@ from polymorphic.models import PolymorphicModel
from ansible_base.lib.utils.models import prevent_search, get_type_for_model
from ansible_base.rbac import permission_registry
from ansible_base.rbac.models import RoleEvaluation
# AWX
from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel, NotificationFieldsModel
@@ -218,20 +219,21 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn
# do not use this if in a subclass
if cls != UnifiedJobTemplate:
return super(UnifiedJobTemplate, cls).accessible_pk_qs(accessor, role_field)
from ansible_base.rbac.models import RoleEvaluation
action = to_permissions[role_field]
# Special condition for super auditor
role_subclasses = cls._submodels_with_roles()
role_cts = ContentType.objects.get_for_models(*role_subclasses).values()
all_codenames = {f'{action}_{cls._meta.model_name}' for cls in role_subclasses}
if not (all_codenames - accessor.singleton_permissions()):
role_cts = ContentType.objects.get_for_models(*role_subclasses).values()
qs = cls.objects.filter(polymorphic_ctype__in=role_cts)
return qs.values_list('id', flat=True)
dab_role_cts = permission_registry.content_type_model.objects.get_for_models(*role_subclasses).values()
return (
RoleEvaluation.objects.filter(role__in=accessor.has_roles.all(), codename__in=all_codenames, content_type_id__in=[ct.id for ct in role_cts])
RoleEvaluation.objects.filter(role__in=accessor.has_roles.all(), codename__in=all_codenames, content_type_id__in=[ct.id for ct in dab_role_cts])
.values_list('object_id')
.distinct()
)
@@ -1198,6 +1200,13 @@ class UnifiedJob(
fd = StringIO(fd.getvalue().replace('\\r\\n', '\n'))
return fd
def _fix_double_escapes(self, content):
"""
Collapse double-escaped sequences into single-escaped form.
"""
# Replace \\ followed by one of ' " \ n r t
return re.sub(r'\\([\'"\\nrt])', r'\1', content)
def _escape_ascii(self, content):
# Remove ANSI escape sequences used to embed event data.
content = re.sub(r'\x1b\[K(?:[A-Za-z0-9+/=]+\x1b\[\d+D)+\x1b\[K', '', content)
@@ -1205,12 +1214,14 @@ class UnifiedJob(
content = re.sub(r'\x1b[^m]*m', '', content)
return content
def _result_stdout_raw(self, redact_sensitive=False, escape_ascii=False):
def _result_stdout_raw(self, redact_sensitive=False, escape_ascii=False, fix_escapes=False):
content = self.result_stdout_raw_handle().read()
if redact_sensitive:
content = UriCleaner.remove_sensitive(content)
if escape_ascii:
content = self._escape_ascii(content)
if fix_escapes:
content = self._fix_double_escapes(content)
return content
@property
@@ -1219,9 +1230,10 @@ class UnifiedJob(
@property
def result_stdout(self):
return self._result_stdout_raw(escape_ascii=True)
# Human-facing output should fix escapes
return self._result_stdout_raw(escape_ascii=True, fix_escapes=True)
def _result_stdout_raw_limited(self, start_line=0, end_line=None, redact_sensitive=True, escape_ascii=False):
def _result_stdout_raw_limited(self, start_line=0, end_line=None, redact_sensitive=True, escape_ascii=False, fix_escapes=False):
return_buffer = StringIO()
if end_line is not None:
end_line = int(end_line)
@@ -1244,14 +1256,18 @@ class UnifiedJob(
return_buffer = UriCleaner.remove_sensitive(return_buffer)
if escape_ascii:
return_buffer = self._escape_ascii(return_buffer)
if fix_escapes:
return_buffer = self._fix_double_escapes(return_buffer)
return return_buffer, start_actual, end_actual, absolute_end
def result_stdout_raw_limited(self, start_line=0, end_line=None, redact_sensitive=False):
# Raw should NOT fix escapes
return self._result_stdout_raw_limited(start_line, end_line, redact_sensitive)
def result_stdout_limited(self, start_line=0, end_line=None, redact_sensitive=False):
return self._result_stdout_raw_limited(start_line, end_line, redact_sensitive, escape_ascii=True)
# Human-facing should fix escapes
return self._result_stdout_raw_limited(start_line, end_line, redact_sensitive, escape_ascii=True, fix_escapes=True)
@property
def workflow_job_id(self):

View File

@@ -53,8 +53,8 @@ class GrafanaBackend(AWXBaseEmailBackend, CustomNotificationBase):
):
super(GrafanaBackend, self).__init__(fail_silently=fail_silently)
self.grafana_key = grafana_key
self.dashboardId = int(dashboardId) if dashboardId is not None and panelId != "" else None
self.panelId = int(panelId) if panelId is not None and panelId != "" else None
self.dashboardId = int(dashboardId) if dashboardId != '' else None
self.panelId = int(panelId) if panelId != '' else None
self.annotation_tags = annotation_tags if annotation_tags is not None else []
self.grafana_no_verify_ssl = grafana_no_verify_ssl
self.isRegion = isRegion

View File

@@ -5,8 +5,6 @@ import time
import ssl
import logging
import irc.client
from django.utils.encoding import smart_str
from django.utils.translation import gettext_lazy as _
@@ -16,6 +14,19 @@ from awx.main.notifications.custom_notification_base import CustomNotificationBa
logger = logging.getLogger('awx.main.notifications.irc_backend')
def _irc():
"""
Prime the real jaraco namespace before importing irc.* so that
setuptools' vendored 'setuptools._vendor.jaraco' doesn't shadow
external 'jaraco.*' packages (e.g., jaraco.stream).
"""
import jaraco.stream # ensure the namespace package is established # noqa: F401
import irc.client as irc_client
import irc.connection as irc_connection
return irc_client, irc_connection
class IrcBackend(AWXBaseEmailBackend, CustomNotificationBase):
init_parameters = {
"server": {"label": "IRC Server Address", "type": "string"},
@@ -40,12 +51,15 @@ class IrcBackend(AWXBaseEmailBackend, CustomNotificationBase):
def open(self):
if self.connection is not None:
return False
irc_client, irc_connection = _irc()
if self.use_ssl:
connection_factory = irc.connection.Factory(wrapper=ssl.wrap_socket)
connection_factory = irc_connection.Factory(wrapper=ssl.wrap_socket)
else:
connection_factory = irc.connection.Factory()
connection_factory = irc_connection.Factory()
try:
self.reactor = irc.client.Reactor()
self.reactor = irc_client.Reactor()
self.connection = self.reactor.server().connect(
self.server,
self.port,
@@ -53,7 +67,7 @@ class IrcBackend(AWXBaseEmailBackend, CustomNotificationBase):
password=self.password,
connect_factory=connection_factory,
)
except irc.client.ServerConnectionError as e:
except irc_client.ServerConnectionError as e:
logger.error(smart_str(_("Exception connecting to irc server: {}").format(e)))
if not self.fail_silently:
raise
@@ -65,8 +79,9 @@ class IrcBackend(AWXBaseEmailBackend, CustomNotificationBase):
self.connection = None
def on_connect(self, connection, event):
irc_client, _ = _irc()
for c in self.channels:
if irc.client.is_channel(c):
if irc_client.is_channel(c):
connection.join(c)
else:
for m in self.channels[c]:

View File

@@ -12,7 +12,7 @@ from django.db import transaction
# Django flags
from flags.state import flag_enabled
from awx.main.dispatch.publish import task as task_awx
from awx.main.dispatch.publish import task
from awx.main.dispatch import get_task_queuename
from awx.main.models.indirect_managed_node_audit import IndirectManagedNodeAudit
from awx.main.models.event_query import EventQuery
@@ -159,7 +159,7 @@ def cleanup_old_indirect_host_entries() -> None:
IndirectManagedNodeAudit.objects.filter(created__lt=limit).delete()
@task_awx(queue=get_task_queuename)
@task(queue=get_task_queuename)
def save_indirect_host_entries(job_id: int, wait_for_events: bool = True) -> None:
try:
job = Job.objects.get(id=job_id)
@@ -201,7 +201,7 @@ def save_indirect_host_entries(job_id: int, wait_for_events: bool = True) -> Non
logger.exception(f'Error processing indirect host data for job_id={job_id}')
@task_awx(queue=get_task_queuename)
@task(queue=get_task_queuename)
def cleanup_and_save_indirect_host_entries_fallback() -> None:
if not flag_enabled("FEATURE_INDIRECT_NODE_COUNTING_ENABLED"):
return

View File

@@ -21,6 +21,8 @@ from django.db import transaction
# Shared code for the AWX platform
from awx_plugins.interfaces._temporary_private_container_api import CONTAINER_ROOT, get_incontainer_path
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import PermissionDenied
# Runner
import ansible_runner
@@ -87,8 +89,6 @@ from awx.main.utils.common import (
from awx.conf.license import get_license
from awx.main.utils.handlers import SpecialInventoryHandler
from awx.main.utils.update_model import update_model
from rest_framework.exceptions import PermissionDenied
from django.utils.translation import gettext_lazy as _
# Django flags
from flags.state import flag_enabled

View File

@@ -1224,6 +1224,30 @@ def test_custom_credential_type_create(get, post, organization, admin):
assert decrypt_field(cred, 'api_token') == 'secret'
@pytest.mark.django_db
def test_galaxy_create_ok(post, organization, admin):
params = {
'credential_type': 1,
'name': 'Galaxy credential',
'inputs': {
'url': 'https://galaxy.ansible.com',
'token': 'some_galaxy_token',
},
}
galaxy = CredentialType.defaults['galaxy_api_token']()
galaxy.save()
params['user'] = admin.id
params['credential_type'] = galaxy.pk
response = post(reverse('api:credential_list'), params, admin)
assert response.status_code == 201
assert Credential.objects.count() == 1
cred = Credential.objects.all()[:1].get()
assert cred.credential_type == galaxy
assert cred.inputs['url'] == 'https://galaxy.ansible.com'
assert decrypt_field(cred, 'token') == 'some_galaxy_token'
#
# misc xfail conditions
#

View File

@@ -1,64 +0,0 @@
import pytest
from awx.api.versioning import reverse
from awx.main.models import Organization
@pytest.mark.django_db
class TestImmutableSharedFields:
@pytest.fixture(autouse=True)
def configure_settings(self, settings):
settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT = False
def test_create_raises_permission_denied(self, admin_user, post):
orgA = Organization.objects.create(name='orgA')
resp = post(
url=reverse('api:team_list'),
data={'name': 'teamA', 'organization': orgA.id},
user=admin_user,
expect=403,
)
assert "Creation of this resource is not allowed" in resp.data['detail']
def test_perform_delete_raises_permission_denied(self, admin_user, delete):
orgA = Organization.objects.create(name='orgA')
team = orgA.teams.create(name='teamA')
resp = delete(
url=reverse('api:team_detail', kwargs={'pk': team.id}),
user=admin_user,
expect=403,
)
assert "Deletion of this resource is not allowed" in resp.data['detail']
def test_perform_update(self, admin_user, patch):
orgA = Organization.objects.create(name='orgA')
# allow patching non-shared fields
patch(
url=reverse('api:organization_detail', kwargs={'pk': orgA.id}),
data={"max_hosts": 76},
user=admin_user,
expect=200,
)
# prevent patching shared fields
resp = patch(url=reverse('api:organization_detail', kwargs={'pk': orgA.id}), data={"name": "orgB"}, user=admin_user, expect=403)
assert "Cannot change shared field" in resp.data['name']
@pytest.mark.parametrize(
'role',
['admin_role', 'member_role'],
)
@pytest.mark.parametrize('resource', ['organization', 'team'])
def test_prevent_assigning_member_to_organization_or_team(self, admin_user, post, resource, role):
orgA = Organization.objects.create(name='orgA')
if resource == 'organization':
role = getattr(orgA, role)
elif resource == 'team':
teamA = orgA.teams.create(name='teamA')
role = getattr(teamA, role)
resp = post(
url=reverse('api:user_roles_list', kwargs={'pk': admin_user.id}),
data={'id': role.id},
user=admin_user,
expect=403,
)
assert f"Cannot directly modify user membership to {resource}." in resp.data['msg']

View File

@@ -1,3 +1,5 @@
from unittest import mock
import pytest
from awx.api.versioning import reverse
@@ -5,6 +7,9 @@ from awx.main.models.activity_stream import ActivityStream
from awx.main.models.ha import Instance
from django.test.utils import override_settings
from django.http import HttpResponse
from rest_framework import status
INSTANCE_KWARGS = dict(hostname='example-host', cpu=6, node_type='execution', memory=36000000000, cpu_capacity=6, mem_capacity=42)
@@ -87,3 +92,11 @@ def test_custom_hostname_regex(post, admin_user):
"peers": [],
}
post(url=url, user=admin_user, data=data, expect=value[1])
def test_instance_install_bundle(get, admin_user, system_auditor):
instance = Instance.objects.create(**INSTANCE_KWARGS)
url = reverse('api:instance_install_bundle', kwargs={'pk': instance.pk})
with mock.patch('awx.api.views.instance_install_bundle.InstanceInstallBundle.get', return_value=HttpResponse({'test': 'data'}, status=status.HTTP_200_OK)):
get(url=url, user=admin_user, expect=200)
get(url=url, user=system_auditor, expect=403)

View File

@@ -521,6 +521,20 @@ class TestInventorySourceCredential:
patch(url=inv_src.get_absolute_url(), data={'credential': aws_cred.pk}, expect=200, user=admin_user)
assert list(inv_src.credentials.values_list('id', flat=True)) == [aws_cred.pk]
@pytest.mark.skip(reason="Delay until AAP-53978 completed")
def test_vmware_cred_create_esxi_source(self, inventory, admin_user, organization, post, get):
"""Test that a vmware esxi source can be added with a vmware credential"""
from awx.main.models.credential import Credential, CredentialType
vmware = CredentialType.defaults['vmware']()
vmware.save()
vmware_cred = Credential.objects.create(credential_type=vmware, name="bar", organization=organization)
inv_src = InventorySource.objects.create(inventory=inventory, name='foobar', source='vmware_esxi')
r = post(url=reverse('api:inventory_source_credentials_list', kwargs={'pk': inv_src.pk}), data={'id': vmware_cred.pk}, expect=204, user=admin_user)
g = get(inv_src.get_absolute_url(), admin_user)
assert r.status_code == 204
assert g.data['credential'] == vmware_cred.pk
@pytest.mark.django_db
class TestControlledBySCM:

View File

@@ -0,0 +1,191 @@
import pytest
from unittest.mock import patch, MagicMock
from awx.api.versioning import reverse
# Generated by Cursor (claude-4-sonnet)
@pytest.mark.django_db
class TestLicenseCacheClearing:
"""Test cache clearing for LICENSE setting changes"""
def test_license_from_manifest_clears_cache(self, admin_user, post):
"""Test that posting a manifest to /api/v2/config/ clears the LICENSE cache"""
# Mock the licenser and clear_setting_cache
with patch('awx.api.views.root.get_licenser') as mock_get_licenser, patch('awx.api.views.root.validate_entitlement_manifest') as mock_validate, patch(
'awx.api.views.root.clear_setting_cache'
) as mock_clear_cache, patch('django.db.connection.on_commit') as mock_on_commit:
# Set up mock license data
mock_license_data = {'valid_key': True, 'license_type': 'enterprise', 'instance_count': 100, 'subscription_name': 'Test Enterprise License'}
# Mock the validation and license processing
mock_validate.return_value = [{'some': 'manifest_data'}]
mock_licenser = MagicMock()
mock_licenser.license_from_manifest.return_value = mock_license_data
mock_get_licenser.return_value = mock_licenser
# Prepare the request data (base64 encoded manifest)
manifest_data = {'manifest': 'ZmFrZS1tYW5pZmVzdC1kYXRh'} # base64 for "fake-manifest-data"
# Make the POST request
url = reverse('api:api_v2_config_view')
response = post(url, manifest_data, admin_user, expect=200)
# Verify the response
assert response.data == mock_license_data
# Verify license_from_manifest was called
mock_licenser.license_from_manifest.assert_called_once()
# Verify on_commit was called (may be multiple times due to other settings)
assert mock_on_commit.call_count >= 1
# Execute all on_commit callbacks to trigger cache clearing
for call_args in mock_on_commit.call_args_list:
callback = call_args[0][0]
callback()
# Verify that clear_setting_cache.delay was called with ['LICENSE']
mock_clear_cache.delay.assert_any_call(['LICENSE'])
def test_config_delete_clears_cache(self, admin_user, delete):
"""Test that DELETE /api/v2/config/ clears the LICENSE cache"""
with patch('awx.api.views.root.clear_setting_cache') as mock_clear_cache, patch('django.db.connection.on_commit') as mock_on_commit:
# Make the DELETE request
url = reverse('api:api_v2_config_view')
delete(url, admin_user, expect=204)
# Verify on_commit was called at least once
assert mock_on_commit.call_count >= 1
# Execute all on_commit callbacks to trigger cache clearing
for call_args in mock_on_commit.call_args_list:
callback = call_args[0][0]
callback()
mock_clear_cache.delay.assert_called_once_with(['LICENSE'])
def test_attach_view_clears_cache(self, admin_user, post):
"""Test that posting to /api/v2/config/attach/ clears the LICENSE cache"""
with patch('awx.api.views.root.get_licenser') as mock_get_licenser, patch('awx.api.views.root.clear_setting_cache') as mock_clear_cache, patch(
'django.db.connection.on_commit'
) as mock_on_commit, patch('awx.api.views.root.settings') as mock_settings:
# Set up subscription credentials in settings
mock_settings.SUBSCRIPTIONS_CLIENT_ID = 'test-client-id'
mock_settings.SUBSCRIPTIONS_CLIENT_SECRET = 'test-client-secret'
# Set up mock licenser with validated subscriptions
mock_licenser = MagicMock()
subscription_data = {'subscription_id': 'test-subscription-123', 'valid_key': False, 'license_type': 'enterprise', 'instance_count': 50}
mock_licenser.validate_rh.return_value = [subscription_data]
mock_get_licenser.return_value = mock_licenser
# Prepare request data
request_data = {'subscription_id': 'test-subscription-123'}
# Make the POST request
url = reverse('api:api_v2_attach_view')
response = post(url, request_data, admin_user, expect=200)
# Verify the response includes valid_key=True
assert response.data['valid_key'] is True
assert response.data['subscription_id'] == 'test-subscription-123'
# Verify settings.LICENSE was set
expected_license = subscription_data.copy()
expected_license['valid_key'] = True
assert mock_settings.LICENSE == expected_license
# Verify cache clearing was scheduled
mock_on_commit.assert_called_once()
call_args = mock_on_commit.call_args[0][0] # Get the lambda function
# Execute the lambda to verify it calls clear_setting_cache
call_args()
mock_clear_cache.delay.assert_called_once_with(['LICENSE'])
def test_attach_view_subscription_not_found_no_cache_clear(self, admin_user, post):
"""Test that attach view doesn't clear cache when subscription is not found"""
with patch('awx.api.views.root.get_licenser') as mock_get_licenser, patch('awx.api.views.root.clear_setting_cache') as mock_clear_cache, patch(
'django.db.connection.on_commit'
) as mock_on_commit:
# Set up mock licenser with different subscription
mock_licenser = MagicMock()
subscription_data = {'subscription_id': 'different-subscription-456', 'valid_key': False, 'license_type': 'enterprise'} # Different ID
mock_licenser.validate_rh.return_value = [subscription_data]
mock_get_licenser.return_value = mock_licenser
# Request data with non-matching subscription ID
request_data = {
'subscription_id': 'test-subscription-123', # This won't match
}
# Make the POST request
url = reverse('api:api_v2_attach_view')
response = post(url, request_data, admin_user, expect=400)
# Verify error response
assert 'error' in response.data
# Verify cache clearing was NOT called (no matching subscription)
mock_on_commit.assert_not_called()
mock_clear_cache.delay.assert_not_called()
def test_manifest_validation_error_no_cache_clear(self, admin_user, post):
"""Test that config view doesn't clear cache when manifest validation fails"""
with patch('awx.api.views.root.validate_entitlement_manifest') as mock_validate, patch(
'awx.api.views.root.clear_setting_cache'
) as mock_clear_cache, patch('django.db.connection.on_commit') as mock_on_commit:
# Mock validation to raise ValueError
mock_validate.side_effect = ValueError("Invalid manifest")
# Prepare request data
manifest_data = {'manifest': 'aW52YWxpZC1tYW5pZmVzdA=='} # base64 for "invalid-manifest"
# Make the POST request
url = reverse('api:api_v2_config_view')
response = post(url, manifest_data, admin_user, expect=400)
# Verify error response
assert response.data['error'] == 'Invalid manifest'
# Verify cache clearing was NOT called (validation failed)
mock_on_commit.assert_not_called()
mock_clear_cache.delay.assert_not_called()
def test_license_processing_error_no_cache_clear(self, admin_user, post):
"""Test that config view doesn't clear cache when license processing fails"""
with patch('awx.api.views.root.get_licenser') as mock_get_licenser, patch('awx.api.views.root.validate_entitlement_manifest') as mock_validate, patch(
'awx.api.views.root.clear_setting_cache'
) as mock_clear_cache, patch('django.db.connection.on_commit') as mock_on_commit:
# Mock validation to succeed but license processing to fail
mock_validate.return_value = [{'some': 'manifest_data'}]
mock_licenser = MagicMock()
mock_licenser.license_from_manifest.side_effect = Exception("License processing failed")
mock_get_licenser.return_value = mock_licenser
# Prepare request data
manifest_data = {'manifest': 'ZmFrZS1tYW5pZmVzdA=='} # base64 for "fake-manifest"
# Make the POST request
url = reverse('api:api_v2_config_view')
response = post(url, manifest_data, admin_user, expect=400)
# Verify error response
assert response.data['error'] == 'Invalid License'
# Verify cache clearing was NOT called (license processing failed)
mock_on_commit.assert_not_called()
mock_clear_cache.delay.assert_not_called()

View File

@@ -5,10 +5,6 @@ import pytest
from django.contrib.sessions.middleware import SessionMiddleware
from django.test.utils import override_settings
from django.contrib.auth.models import AnonymousUser
from ansible_base.lib.utils.response import get_relative_url
from ansible_base.lib.testing.fixtures import settings_override_mutable # NOQA: F401 imported to be a pytest fixture
from awx.main.models import User
from awx.api.versioning import reverse
@@ -21,33 +17,6 @@ from awx.api.versioning import reverse
EXAMPLE_USER_DATA = {"username": "affable", "first_name": "a", "last_name": "a", "email": "a@a.com", "is_superuser": False, "password": "r$TyKiOCb#ED"}
@pytest.mark.django_db
def test_validate_local_user(post, admin_user, settings, settings_override_mutable): # NOQA: F811 this is how you use a pytest fixture
"Copy of the test by same name in django-ansible-base for integration and compatibility testing"
url = get_relative_url('validate-local-account')
admin_user.set_password('password')
admin_user.save()
data = {
"username": admin_user.username,
"password": "password",
}
with override_settings(RESOURCE_SERVER={"URL": "https://foo.invalid", "SECRET_KEY": "foobar"}):
response = post(url=url, data=data, user=AnonymousUser(), expect=200)
assert 'ansible_id' in response.data
assert response.data['auth_code'] is not None, response.data
# No resource server, return coherent response but can not provide auth code
response = post(url=url, data=data, user=AnonymousUser(), expect=200)
assert 'ansible_id' in response.data
assert response.data['auth_code'] is None
# wrong password
data['password'] = 'foobar'
response = post(url=url, data=data, user=AnonymousUser(), expect=401)
# response.data may be none here, this is just testing that we get no server error
@pytest.mark.django_db
def test_user_create(post, admin):
response = post(reverse('api:user_list'), EXAMPLE_USER_DATA, admin, middleware=SessionMiddleware(mock.Mock()))
@@ -289,3 +258,19 @@ def test_user_verify_attribute_created(admin, get):
for op, count in (('gt', 1), ('lt', 0)):
resp = get(reverse('api:user_list') + f'?created__{op}={past}', admin)
assert resp.data['count'] == count
@pytest.mark.django_db
def test_org_not_shown_in_admin_user_sublists(admin_user, get, organization):
for view_name in ('user_admin_of_organizations_list', 'user_organizations_list'):
url = reverse(f'api:{view_name}', kwargs={'pk': admin_user.pk})
r = get(url, user=admin_user, expect=200)
assert organization.pk not in [org['id'] for org in r.data['results']]
@pytest.mark.django_db
def test_admin_user_not_shown_in_org_users(admin_user, get, organization):
for view_name in ('organization_users_list', 'organization_admins_list'):
url = reverse(f'api:{view_name}', kwargs={'pk': organization.pk})
r = get(url, user=admin_user, expect=200)
assert admin_user.pk not in [u['id'] for u in r.data['results']]

View File

@@ -1,3 +1,5 @@
import logging
# Python
import pytest
from unittest import mock
@@ -8,7 +10,7 @@ import importlib
# Django
from django.urls import resolve
from django.http import Http404
from django.apps import apps
from django.apps import apps as global_apps
from django.core.handlers.exception import response_for_exception
from django.contrib.auth.models import User
from django.core.serializers.json import DjangoJSONEncoder
@@ -47,6 +49,8 @@ from awx.main.models.ad_hoc_commands import AdHocCommand
from awx.main.models.execution_environments import ExecutionEnvironment
from awx.main.utils import is_testing
logger = logging.getLogger(__name__)
__SWAGGER_REQUESTS__ = {}
@@ -54,8 +58,17 @@ __SWAGGER_REQUESTS__ = {}
dab_rr_initial = importlib.import_module('ansible_base.resource_registry.migrations.0001_initial')
def create_service_id(app_config, apps=global_apps, **kwargs):
try:
apps.get_model("dab_resource_registry", "ServiceID")
except LookupError:
logger.info('Looks like reverse migration, not creating resource registry ServiceID')
return
dab_rr_initial.create_service_id(apps, None)
if is_testing():
post_migrate.connect(lambda **kwargs: dab_rr_initial.create_service_id(apps, None))
post_migrate.connect(create_service_id)
@pytest.fixture(scope="session")
@@ -126,7 +139,7 @@ def execution_environment():
@pytest.fixture
def setup_managed_roles():
"Run the migration script to pre-create managed role definitions"
setup_managed_role_definitions(apps, None)
setup_managed_role_definitions(global_apps, None)
@pytest.fixture

View File

@@ -0,0 +1,147 @@
import pytest
from django.contrib.contenttypes.models import ContentType
from django.test import override_settings
from django.apps import apps
from ansible_base.rbac.models import RoleDefinition, RoleUserAssignment, RoleTeamAssignment
from ansible_base.rbac.migrations._utils import give_permissions
from awx.main.models import User, Team
from awx.main.migrations._dab_rbac import consolidate_indirect_user_roles
@pytest.mark.django_db
@override_settings(ANSIBLE_BASE_ALLOW_TEAM_PARENTS=True)
def test_consolidate_indirect_user_roles_with_nested_teams(setup_managed_roles, organization):
"""
Test the consolidate_indirect_user_roles function with a nested team hierarchy.
Setup:
- Users: A, B, C, D
- Teams: E, F, G
- Direct assignments: A→(E,F,G), B→E, C→F, D→G
- Team hierarchy: F→E (F is member of E), G→F (G is member of F)
Expected result after consolidation:
- Team E should have users: A, B, C, D (A directly, B directly, C through F, D through G→F)
- Team F should have users: A, C, D (A directly, C directly, D through G)
- Team G should have users: A, D (A directly, D directly)
"""
user_a = User.objects.create_user(username='user_a')
user_b = User.objects.create_user(username='user_b')
user_c = User.objects.create_user(username='user_c')
user_d = User.objects.create_user(username='user_d')
team_e = Team.objects.create(name='Team E', organization=organization)
team_f = Team.objects.create(name='Team F', organization=organization)
team_g = Team.objects.create(name='Team G', organization=organization)
# Get role definition and content type for give_permissions
team_member_role = RoleDefinition.objects.get(name='Team Member')
team_content_type = ContentType.objects.get_for_model(Team)
# Assign users to teams
give_permissions(apps=apps, rd=team_member_role, users=[user_a], object_id=team_e.id, content_type_id=team_content_type.id)
give_permissions(apps=apps, rd=team_member_role, users=[user_a], object_id=team_f.id, content_type_id=team_content_type.id)
give_permissions(apps=apps, rd=team_member_role, users=[user_a], object_id=team_g.id, content_type_id=team_content_type.id)
give_permissions(apps=apps, rd=team_member_role, users=[user_b], object_id=team_e.id, content_type_id=team_content_type.id)
give_permissions(apps=apps, rd=team_member_role, users=[user_c], object_id=team_f.id, content_type_id=team_content_type.id)
give_permissions(apps=apps, rd=team_member_role, users=[user_d], object_id=team_g.id, content_type_id=team_content_type.id)
# Mirror user assignments in the old RBAC system because signals don't run in tests
team_e.member_role.members.add(user_a.id, user_b.id)
team_f.member_role.members.add(user_a.id, user_c.id)
team_g.member_role.members.add(user_a.id, user_d.id)
# Setup team-to-team relationships
give_permissions(apps=apps, rd=team_member_role, teams=[team_f], object_id=team_e.id, content_type_id=team_content_type.id)
give_permissions(apps=apps, rd=team_member_role, teams=[team_g], object_id=team_f.id, content_type_id=team_content_type.id)
# Verify initial direct assignments
team_e_users_before = set(RoleUserAssignment.objects.filter(role_definition=team_member_role, object_id=team_e.id).values_list('user_id', flat=True))
assert team_e_users_before == {user_a.id, user_b.id}
team_f_users_before = set(RoleUserAssignment.objects.filter(role_definition=team_member_role, object_id=team_f.id).values_list('user_id', flat=True))
assert team_f_users_before == {user_a.id, user_c.id}
team_g_users_before = set(RoleUserAssignment.objects.filter(role_definition=team_member_role, object_id=team_g.id).values_list('user_id', flat=True))
assert team_g_users_before == {user_a.id, user_d.id}
# Verify team-to-team relationships exist
assert RoleTeamAssignment.objects.filter(role_definition=team_member_role, team=team_f, object_id=team_e.id).exists()
assert RoleTeamAssignment.objects.filter(role_definition=team_member_role, team=team_g, object_id=team_f.id).exists()
# Run the consolidation function
consolidate_indirect_user_roles(apps, None)
# Verify consolidation
team_e_users_after = set(RoleUserAssignment.objects.filter(role_definition=team_member_role, object_id=team_e.id).values_list('user_id', flat=True))
assert team_e_users_after == {user_a.id, user_b.id, user_c.id, user_d.id}, f"Team E should have users A, B, C, D but has {team_e_users_after}"
team_f_users_after = set(RoleUserAssignment.objects.filter(role_definition=team_member_role, object_id=team_f.id).values_list('user_id', flat=True))
assert team_f_users_after == {user_a.id, user_c.id, user_d.id}, f"Team F should have users A, C, D but has {team_f_users_after}"
team_g_users_after = set(RoleUserAssignment.objects.filter(role_definition=team_member_role, object_id=team_g.id).values_list('user_id', flat=True))
assert team_g_users_after == {user_a.id, user_d.id}, f"Team G should have users A, D but has {team_g_users_after}"
# Verify team member changes are mirrored to the old RBAC system
assert team_e_users_after == set(team_e.member_role.members.all().values_list('id', flat=True))
assert team_f_users_after == set(team_f.member_role.members.all().values_list('id', flat=True))
assert team_g_users_after == set(team_g.member_role.members.all().values_list('id', flat=True))
# Verify team-to-team relationships are removed after consolidation
assert not RoleTeamAssignment.objects.filter(
role_definition=team_member_role, team=team_f, object_id=team_e.id
).exists(), "Team-to-team relationship F→E should be removed"
assert not RoleTeamAssignment.objects.filter(
role_definition=team_member_role, team=team_g, object_id=team_f.id
).exists(), "Team-to-team relationship G→F should be removed"
@pytest.mark.django_db
@override_settings(ANSIBLE_BASE_ALLOW_TEAM_PARENTS=True)
def test_consolidate_indirect_user_roles_no_team_relationships(setup_managed_roles, organization):
"""
Test that the function handles the case where there are no team-to-team relationships.
It should return early without making any changes.
"""
# Create a user and team with direct assignment
user = User.objects.create_user(username='test_user')
team = Team.objects.create(name='Test Team', organization=organization)
team_member_role = RoleDefinition.objects.get(name='Team Member')
team_content_type = ContentType.objects.get_for_model(Team)
give_permissions(apps=apps, rd=team_member_role, users=[user], object_id=team.id, content_type_id=team_content_type.id)
# Compare count of assignments before and after consolidation
assignments_before = RoleUserAssignment.objects.filter(role_definition=team_member_role).count()
consolidate_indirect_user_roles(apps, None)
assignments_after = RoleUserAssignment.objects.filter(role_definition=team_member_role).count()
assert assignments_before == assignments_after, "Number of assignments should not change when there are no team-to-team relationships"
@pytest.mark.django_db
@override_settings(ANSIBLE_BASE_ALLOW_TEAM_PARENTS=True)
def test_consolidate_indirect_user_roles_circular_reference(setup_managed_roles, organization):
"""
Test that the function handles circular team references without infinite recursion.
"""
team_a = Team.objects.create(name='Team A', organization=organization)
team_b = Team.objects.create(name='Team B', organization=organization)
# Create a user assigned to team A
user = User.objects.create_user(username='test_user')
team_member_role = RoleDefinition.objects.get(name='Team Member')
team_content_type = ContentType.objects.get_for_model(Team)
give_permissions(apps=apps, rd=team_member_role, users=[user], object_id=team_a.id, content_type_id=team_content_type.id)
# Create circular team relationships: A → B → A
give_permissions(apps=apps, rd=team_member_role, teams=[team_b], object_id=team_a.id, content_type_id=team_content_type.id)
give_permissions(apps=apps, rd=team_member_role, teams=[team_a], object_id=team_b.id, content_type_id=team_content_type.id)
# Run the consolidation function - should not raise an exception
consolidate_indirect_user_roles(apps, None)
# Both teams should have the user assigned
team_a_users = set(RoleUserAssignment.objects.filter(role_definition=team_member_role, object_id=team_a.id).values_list('user_id', flat=True))
team_b_users = set(RoleUserAssignment.objects.filter(role_definition=team_member_role, object_id=team_b.id).values_list('user_id', flat=True))
assert user.id in team_a_users, "User should be assigned to team A"
assert user.id in team_b_users, "User should be assigned to team B"

View File

@@ -1,6 +1,5 @@
import pytest
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse as django_reverse
from awx.api.versioning import reverse
@@ -8,13 +7,14 @@ from awx.main.models import JobTemplate, Inventory, Organization
from awx.main.access import JobTemplateAccess, WorkflowJobTemplateAccess
from ansible_base.rbac.models import RoleDefinition
from ansible_base.rbac import permission_registry
@pytest.mark.django_db
def test_managed_roles_created(setup_managed_roles):
"Managed RoleDefinitions are created in post_migration signal, we expect to see them here"
for cls in (JobTemplate, Inventory):
ct = ContentType.objects.get_for_model(cls)
ct = permission_registry.content_type_model.objects.get_for_model(cls)
rds = list(RoleDefinition.objects.filter(content_type=ct))
assert len(rds) > 1
assert f'{cls.__name__} Admin' in [rd.name for rd in rds]
@@ -26,17 +26,20 @@ def test_managed_roles_created(setup_managed_roles):
def test_custom_read_role(admin_user, post, setup_managed_roles):
rd_url = django_reverse('roledefinition-list')
resp = post(
url=rd_url, data={"name": "read role made for test", "content_type": "awx.inventory", "permissions": ['view_inventory']}, user=admin_user, expect=201
url=rd_url,
data={"name": "read role made for test", "content_type": "awx.inventory", "permissions": ['awx.view_inventory']},
user=admin_user,
expect=201,
)
rd_id = resp.data['id']
rd = RoleDefinition.objects.get(id=rd_id)
assert rd.content_type == ContentType.objects.get_for_model(Inventory)
assert rd.content_type == permission_registry.content_type_model.objects.get_for_model(Inventory)
@pytest.mark.django_db
def test_custom_system_roles_prohibited(admin_user, post):
rd_url = django_reverse('roledefinition-list')
resp = post(url=rd_url, data={"name": "read role made for test", "content_type": None, "permissions": ['view_inventory']}, user=admin_user, expect=400)
resp = post(url=rd_url, data={"name": "read role made for test", "content_type": None, "permissions": ['awx.view_inventory']}, user=admin_user, expect=400)
assert 'System-wide roles are not enabled' in str(resp.data)
@@ -71,7 +74,7 @@ def test_assign_custom_delete_role(admin_user, rando, inventory, delete, patch):
rd, _ = RoleDefinition.objects.get_or_create(
name='inventory-delete',
permissions=['delete_inventory', 'view_inventory', 'change_inventory'],
content_type=ContentType.objects.get_for_model(Inventory),
content_type=permission_registry.content_type_model.objects.get_for_model(Inventory),
)
rd.give_permission(rando, inventory)
inv_id = inventory.pk
@@ -85,7 +88,9 @@ def test_assign_custom_delete_role(admin_user, rando, inventory, delete, patch):
@pytest.mark.django_db
def test_assign_custom_add_role(admin_user, rando, organization, post, setup_managed_roles):
rd, _ = RoleDefinition.objects.get_or_create(
name='inventory-add', permissions=['add_inventory', 'view_organization'], content_type=ContentType.objects.get_for_model(Organization)
name='inventory-add',
permissions=['add_inventory', 'view_organization'],
content_type=permission_registry.content_type_model.objects.get_for_model(Organization),
)
rd.give_permission(rando, organization)
url = reverse('api:inventory_list')
@@ -146,14 +151,6 @@ def test_assign_credential_to_user_of_another_org(setup_managed_roles, credentia
post(url=url, data={"user": org_admin.id, "role_definition": rd.id, "object_id": credential.id}, user=admin_user, expect=201)
@pytest.mark.django_db
def test_team_member_role_not_assignable(team, rando, post, admin_user, setup_managed_roles):
member_rd = RoleDefinition.objects.get(name='Organization Member')
url = django_reverse('roleuserassignment-list')
r = post(url, data={'object_id': team.id, 'role_definition': member_rd.id, 'user': rando.id}, user=admin_user, expect=400)
assert 'Not managed locally' in str(r.data)
@pytest.mark.django_db
def test_adding_user_to_org_member_role(setup_managed_roles, organization, admin, bob, post, get):
'''
@@ -173,10 +170,17 @@ def test_adding_user_to_org_member_role(setup_managed_roles, organization, admin
@pytest.mark.django_db
@pytest.mark.parametrize('actor', ['user', 'team'])
@pytest.mark.parametrize('role_name', ['Organization Admin', 'Organization Member', 'Team Admin', 'Team Member'])
def test_prevent_adding_actor_to_platform_roles(setup_managed_roles, role_name, actor, organization, team, admin, bob, post):
def test_adding_actor_to_platform_roles(setup_managed_roles, role_name, actor, organization, team, admin, bob, post):
'''
Prevent user or team from being added to platform-level roles
Allow user to be added to platform-level roles
Exceptions:
- Team cannot be added to Organization Member or Admin role
- Team cannot be added to Team Admin or Team Member role
'''
if actor == 'team':
expect = 400
else:
expect = 201
rd = RoleDefinition.objects.get(name=role_name)
endpoint = 'roleuserassignment-list' if actor == 'user' else 'roleteamassignment-list'
url = django_reverse(endpoint)
@@ -184,37 +188,9 @@ def test_prevent_adding_actor_to_platform_roles(setup_managed_roles, role_name,
data = {'object_id': object_id, 'role_definition': rd.id}
actor_id = bob.id if actor == 'user' else team.id
data[actor] = actor_id
r = post(url, data=data, user=admin, expect=400)
assert 'Not managed locally' in str(r.data)
@pytest.mark.django_db
@pytest.mark.parametrize('role_name', ['Controller Team Admin', 'Controller Team Member'])
def test_adding_user_to_controller_team_roles(setup_managed_roles, role_name, team, admin, bob, post, get):
'''
Allow user to be added to Controller Team Admin or Controller Team Member
'''
url_detail = reverse('api:team_detail', kwargs={'pk': team.id})
get(url_detail, user=bob, expect=403)
rd = RoleDefinition.objects.get(name=role_name)
url = django_reverse('roleuserassignment-list')
post(url, data={'object_id': team.id, 'role_definition': rd.id, 'user': bob.id}, user=admin, expect=201)
get(url_detail, user=bob, expect=200)
@pytest.mark.django_db
@pytest.mark.parametrize('role_name', ['Controller Organization Admin', 'Controller Organization Member'])
def test_adding_user_to_controller_organization_roles(setup_managed_roles, role_name, organization, admin, bob, post, get):
'''
Allow user to be added to Controller Organization Admin or Controller Organization Member
'''
url_detail = reverse('api:organization_detail', kwargs={'pk': organization.id})
get(url_detail, user=bob, expect=403)
rd = RoleDefinition.objects.get(name=role_name)
url = django_reverse('roleuserassignment-list')
post(url, data={'object_id': organization.id, 'role_definition': rd.id, 'user': bob.id}, user=admin, expect=201)
get(url, user=bob, expect=200)
r = post(url, data=data, user=admin, expect=expect)
if expect == 400:
if 'Organization' in role_name:
assert 'Assigning organization member permission to teams is not allowed' in str(r.data)
if 'Team' in role_name:
assert 'Assigning team permissions to other teams is not allowed' in str(r.data)

View File

@@ -15,6 +15,14 @@ def test_roles_to_not_create(setup_managed_roles):
raise Exception(f'Found RoleDefinitions that should not exist: {bad_names}')
@pytest.mark.django_db
def test_org_admin_role(setup_managed_roles):
rd = RoleDefinition.objects.get(name='Organization Admin')
codenames = list(rd.permissions.values_list('codename', flat=True))
assert 'view_inventory' in codenames
assert 'change_inventory' in codenames
@pytest.mark.django_db
def test_project_update_role(setup_managed_roles):
"""Role to allow updating a project on the object-level should exist"""
@@ -31,32 +39,18 @@ def test_org_child_add_permission(setup_managed_roles):
assert not DABPermission.objects.filter(codename='add_jobtemplate').exists()
@pytest.mark.django_db
def test_controller_specific_roles_have_correct_permissions(setup_managed_roles):
'''
Controller specific roles should have the same permissions as the platform roles
e.g. Controller Team Admin should have same permission set as Team Admin
'''
for rd_name in ['Controller Team Admin', 'Controller Team Member', 'Controller Organization Member', 'Controller Organization Admin']:
rd = RoleDefinition.objects.get(name=rd_name)
rd_platform = RoleDefinition.objects.get(name=rd_name.split('Controller ')[1])
assert set(rd.permissions.all()) == set(rd_platform.permissions.all())
@pytest.mark.django_db
@pytest.mark.parametrize('resource_name', ['Team', 'Organization'])
@pytest.mark.parametrize('action', ['Member', 'Admin'])
def test_legacy_RBAC_uses_controller_specific_roles(setup_managed_roles, resource_name, action, team, bob, organization):
def test_legacy_RBAC_uses_platform_roles(setup_managed_roles, resource_name, action, team, bob, organization):
'''
Assignment to legacy RBAC roles should use controller specific role definitions
e.g. Controller Team Admin, Controller Team Member, Controller Organization Member, Controller Organization Admin
Assignment to legacy RBAC roles should use platform role definitions
e.g. Team Admin, Team Member, Organization Member, Organization Admin
'''
resource = team if resource_name == 'Team' else organization
if action == 'Member':
resource.member_role.members.add(bob)
else:
resource.admin_role.members.add(bob)
rd = RoleDefinition.objects.get(name=f'Controller {resource_name} {action}')
rd_platform = RoleDefinition.objects.get(name=f'{resource_name} {action}')
rd = RoleDefinition.objects.get(name=f'{resource_name} {action}')
assert RoleUserAssignment.objects.filter(role_definition=rd, user=bob, object_id=resource.id).exists()
assert not RoleUserAssignment.objects.filter(role_definition=rd_platform, user=bob, object_id=resource.id).exists()

View File

@@ -3,8 +3,6 @@ import json
import pytest
from django.contrib.contenttypes.models import ContentType
from crum import impersonate
from awx.main.fields import ImplicitRoleField
@@ -60,7 +58,7 @@ def test_role_migration_matches(request, model, setup_managed_roles):
new_codenames = set(rd.permissions.values_list('codename', flat=True))
# all the old roles should map to a non-Compat role definition
if 'Compat' not in rd.name:
model_rds = RoleDefinition.objects.filter(content_type=ContentType.objects.get_for_model(obj))
model_rds = RoleDefinition.objects.filter(content_type=permission_registry.content_type_model.objects.get_for_model(obj))
rd_data = {}
for rd in model_rds:
rd_data[rd.name] = list(rd.permissions.values_list('codename', flat=True))
@@ -76,7 +74,7 @@ def test_role_migration_matches(request, model, setup_managed_roles):
@pytest.mark.django_db
def test_role_naming(setup_managed_roles):
qs = RoleDefinition.objects.filter(content_type=ContentType.objects.get(model='jobtemplate'), name__endswith='dmin')
qs = RoleDefinition.objects.filter(content_type=permission_registry.content_type_model.objects.get(model='jobtemplate'), name__endswith='dmin')
assert qs.count() == 1 # sanity
rd = qs.first()
assert rd.name == 'JobTemplate Admin'
@@ -86,7 +84,7 @@ def test_role_naming(setup_managed_roles):
@pytest.mark.django_db
def test_action_role_naming(setup_managed_roles):
qs = RoleDefinition.objects.filter(content_type=ContentType.objects.get(model='jobtemplate'), name__endswith='ecute')
qs = RoleDefinition.objects.filter(content_type=permission_registry.content_type_model.objects.get(model='jobtemplate'), name__endswith='ecute')
assert qs.count() == 1 # sanity
rd = qs.first()
assert rd.name == 'JobTemplate Execute'
@@ -98,7 +96,7 @@ def test_action_role_naming(setup_managed_roles):
def test_compat_role_naming(setup_managed_roles, job_template, rando, alice):
with impersonate(alice):
job_template.read_role.members.add(rando)
qs = RoleDefinition.objects.filter(content_type=ContentType.objects.get(model='jobtemplate'), name__endswith='ompat')
qs = RoleDefinition.objects.filter(content_type=permission_registry.content_type_model.objects.get(model='jobtemplate'), name__endswith='ompat')
assert qs.count() == 1 # sanity
rd = qs.first()
assert rd.name == 'JobTemplate Read Compat'
@@ -175,20 +173,6 @@ def test_creator_permission(rando, admin_user, inventory, setup_managed_roles):
assert rando in inventory.admin_role.members.all()
@pytest.mark.django_db
def test_team_team_read_role(rando, team, admin_user, post, setup_managed_roles):
orgs = [Organization.objects.create(name=f'foo-{i}') for i in range(2)]
teams = [Team.objects.create(name=f'foo-{i}', organization=orgs[i]) for i in range(2)]
teams[1].member_role.members.add(rando)
# give second team read permission to first team through the API for regression testing
url = reverse('api:role_teams_list', kwargs={'pk': teams[0].read_role.pk, 'version': 'v2'})
post(url, {'id': teams[1].id}, user=admin_user)
# user should be able to view the first team
assert rando in teams[0].read_role
@pytest.mark.django_db
def test_implicit_parents_no_assignments(organization):
"""Through the normal course of creating models, we should not be changing DAB RBAC permissions"""
@@ -202,25 +186,25 @@ def test_user_auditor_rel(organization, rando, setup_managed_roles):
assert rando not in organization.auditor_role
audit_rd = RoleDefinition.objects.get(name='Organization Audit')
audit_rd.give_permission(rando, organization)
assert list(rando.auditor_of_organizations) == [organization]
assert list(Organization.access_qs(rando, 'audit')) == [organization]
@pytest.mark.django_db
@pytest.mark.parametrize('resource_name', ['Organization', 'Team'])
@pytest.mark.parametrize('role_name', ['Member', 'Admin'])
def test_mapping_from_controller_role_definitions_to_roles(organization, team, rando, role_name, resource_name, setup_managed_roles):
def test_mapping_from_role_definitions_to_roles(organization, team, rando, role_name, resource_name, setup_managed_roles):
"""
ensure mappings for controller roles are correct
ensure mappings for platform roles are correct
e.g.
Controller Organization Member > organization.member_role
Controller Organization Admin > organization.admin_role
Controller Team Member > team.member_role
Controller Team Admin > team.admin_role
Organization Member > organization.member_role
Organization Admin > organization.admin_role
Team Member > team.member_role
Team Admin > team.admin_role
"""
resource = organization if resource_name == 'Organization' else team
old_role_name = f"{role_name.lower()}_role"
getattr(resource, old_role_name).members.add(rando)
assignment = RoleUserAssignment.objects.get(user=rando)
assert assignment.role_definition.name == f'Controller {resource_name} {role_name}'
assert assignment.role_definition.name == f'{resource_name} {role_name}'
old_role = get_role_from_object_role(assignment.object_role)
assert old_role.id == getattr(resource, old_role_name).id

View File

@@ -35,21 +35,21 @@ class TestNewToOld:
def test_new_to_old_rbac_team_member_addition(self, admin, post, team, bob, setup_managed_roles):
'''
Assign user to Controller Team Member role definition, should be added to team.member_role.members
Assign user to Team Member role definition, should be added to team.member_role.members
'''
rd = RoleDefinition.objects.get(name='Controller Team Member')
rd = RoleDefinition.objects.get(name='Team Member')
url = get_relative_url('roleuserassignment-list')
post(url, user=admin, data={'role_definition': rd.id, 'user': bob.id, 'object_id': team.id}, expect=201)
assert bob in team.member_role.members.all()
def test_new_to_old_rbac_team_member_removal(self, admin, delete, team, bob):
def test_new_to_old_rbac_team_member_removal(self, admin, delete, team, bob, setup_managed_roles):
'''
Remove user from Controller Team Member role definition, should be deleted from team.member_role.members
Remove user from Team Member role definition, should be deleted from team.member_role.members
'''
team.member_role.members.add(bob)
rd = RoleDefinition.objects.get(name='Controller Team Member')
rd = RoleDefinition.objects.get(name='Team Member')
user_assignment = RoleUserAssignment.objects.get(user=bob, role_definition=rd, object_id=team.id)
url = get_relative_url('roleuserassignment-detail', kwargs={'pk': user_assignment.id})

View File

@@ -1,7 +1,5 @@
import pytest
from django.contrib.contenttypes.models import ContentType
from awx.main.access import ExecutionEnvironmentAccess
from awx.main.models import ExecutionEnvironment, Organization, Team
from awx.main.models.rbac import get_role_codenames
@@ -10,6 +8,7 @@ from awx.api.versioning import reverse
from django.urls import reverse as django_reverse
from ansible_base.rbac.models import RoleDefinition
from ansible_base.rbac import permission_registry
@pytest.fixture
@@ -17,7 +16,7 @@ def ee_rd():
return RoleDefinition.objects.create_from_permissions(
name='EE object admin',
permissions=['change_executionenvironment', 'delete_executionenvironment'],
content_type=ContentType.objects.get_for_model(ExecutionEnvironment),
content_type=permission_registry.content_type_model.objects.get_for_model(ExecutionEnvironment),
)
@@ -26,7 +25,7 @@ def org_ee_rd():
return RoleDefinition.objects.create_from_permissions(
name='EE org admin',
permissions=['add_executionenvironment', 'change_executionenvironment', 'delete_executionenvironment', 'view_organization'],
content_type=ContentType.objects.get_for_model(Organization),
content_type=permission_registry.content_type_model.objects.get_for_model(Organization),
)

View File

@@ -50,13 +50,11 @@ def test_org_factory_roles(organization_factory):
teams=['team1', 'team2'],
users=['team1:foo', 'bar'],
projects=['baz', 'bang'],
roles=['team2.member_role:foo', 'team1.admin_role:bar', 'team1.member_role:team2.admin_role', 'baz.admin_role:foo'],
roles=['team2.member_role:foo', 'team1.admin_role:bar', 'baz.admin_role:foo'],
)
assert objects.users.bar in objects.teams.team2.admin_role
assert objects.users.bar in objects.teams.team1.admin_role
assert objects.users.foo in objects.projects.baz.admin_role
assert objects.users.foo in objects.teams.team1.member_role
assert objects.teams.team2.admin_role in objects.teams.team1.member_role.children.all()
@pytest.mark.django_db

View File

@@ -49,7 +49,6 @@ def credential_kind(source):
"""Given the inventory source kind, return expected credential kind"""
if source == 'openshift_virtualization':
return 'kubernetes_bearer_token'
return source.replace('ec2', 'aws')
@@ -223,6 +222,10 @@ def test_inventory_update_injected_content(product_name, this_kind, inventory, f
private_data_dir = envvars.pop('AWX_PRIVATE_DATA_DIR')
assert envvars.pop('ANSIBLE_INVENTORY_ENABLED') == 'auto'
set_files = bool(os.getenv("MAKE_INVENTORY_REFERENCE_FILES", 'false').lower()[0] not in ['f', '0'])
# Ensure the directory exists before trying to list/read it
os.makedirs(private_data_dir, exist_ok=True)
env, content = read_content(private_data_dir, envvars, inventory_update)
# Assert inventory plugin inventory file is in private_data_dir

View File

@@ -8,7 +8,6 @@ Most tests that live in here can probably be deleted at some point. They are mai
for a developer. When AWX versions that users upgrade from falls out of support that
is when migration tests can be deleted. This is also a good time to squash. Squashing
will likely mess with the tests that live here.
The smoke test should be kept in here. The smoke test ensures that our migrations
continue to work when sqlite is the backing database (vs. the default DB of postgres).
"""
@@ -19,27 +18,22 @@ class TestMigrationSmoke:
def test_happy_path(self, migrator):
"""
This smoke test runs all the migrations.
Example of how to use django-test-migration to invoke particular migration(s)
while weaving in object creation and assertions.
Note that this is more than just an example. It is a smoke test because it runs ALL
the migrations. Our "normal" unit tests subvert the migrations running because it is slow.
"""
migration_nodes = all_migrations('default')
migration_tuples = nodes_to_tuples(migration_nodes)
final_migration = migration_tuples[-1]
migrator.apply_initial_migration(('main', None))
# I just picked a newish migration at the time of writing this.
# If someone from the future finds themselves here because the are squashing migrations
# it is fine to change the 0180_... below to some other newish migration
intermediate_state = migrator.apply_tested_migration(('main', '0180_add_hostmetric_fields'))
Instance = intermediate_state.apps.get_model('main', 'Instance')
# Create any old object in the database
Instance.objects.create(hostname='foobar', node_type='control')
final_state = migrator.apply_tested_migration(final_migration)
Instance = final_state.apps.get_model('main', 'Instance')
assert Instance.objects.filter(hostname='foobar').count() == 1
@@ -52,20 +46,16 @@ class TestMigrationSmoke:
foo = Instance.objects.create(hostname='foo', node_type='execution', listener_port=1234)
bar = Instance.objects.create(hostname='bar', node_type='execution', listener_port=None)
bar.peers.add(foo)
new_state = migrator.apply_tested_migration(
('main', '0189_inbound_hop_nodes'),
)
Instance = new_state.apps.get_model('main', 'Instance')
ReceptorAddress = new_state.apps.get_model('main', 'ReceptorAddress')
# We can now test how our migration worked, new field is there:
assert ReceptorAddress.objects.filter(address='foo', port=1234).count() == 1
assert not ReceptorAddress.objects.filter(address='bar').exists()
bar = Instance.objects.get(hostname='bar')
fooaddr = ReceptorAddress.objects.get(address='foo')
bar_peers = bar.peers.all()
assert len(bar_peers) == 1
assert fooaddr in bar_peers
@@ -75,7 +65,6 @@ class TestMigrationSmoke:
Organization = old_state.apps.get_model('main', 'Organization')
Team = old_state.apps.get_model('main', 'Team')
User = old_state.apps.get_model('auth', 'User')
org = Organization.objects.create(name='arbitrary-org', created=now(), modified=now())
user = User.objects.create(username='random-user')
org.read_role.members.add(user)
@@ -87,11 +76,10 @@ class TestMigrationSmoke:
new_state = migrator.apply_tested_migration(
('main', '0192_custom_roles'),
)
RoleUserAssignment = new_state.apps.get_model('dab_rbac', 'RoleUserAssignment')
assert RoleUserAssignment.objects.filter(user=user.id, object_id=org.id).exists()
assert RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Controller Organization Member', object_id=org.id).exists()
assert RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Controller Team Member', object_id=team.id).exists()
assert RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Organization Member', object_id=org.id).exists()
assert RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Team Member', object_id=team.id).exists()
# Regression testing for bug that comes from current vs past models mismatch
RoleDefinition = new_state.apps.get_model('dab_rbac', 'RoleDefinition')
@@ -99,7 +87,6 @@ class TestMigrationSmoke:
# Test special cases in managed role creation
assert not RoleDefinition.objects.filter(name='Organization Team Admin').exists()
assert not RoleDefinition.objects.filter(name='Organization InstanceGroup Admin').exists()
# Test that a removed EE model permission has been deleted
new_state = migrator.apply_tested_migration(
('main', '0195_EE_permissions'),
@@ -110,21 +97,35 @@ class TestMigrationSmoke:
# Test create a Project with a duplicate name
Organization = new_state.apps.get_model('main', 'Organization')
Project = new_state.apps.get_model('main', 'Project')
WorkflowJobTemplate = new_state.apps.get_model('main', 'WorkflowJobTemplate')
org = Organization.objects.create(name='duplicate-obj-organization', created=now(), modified=now())
proj_ids = []
for i in range(3):
proj = Project.objects.create(name='duplicate-project-name', organization=org, created=now(), modified=now())
proj_ids.append(proj.id)
# Test create WorkflowJobTemplate with duplicate names
wfjt_ids = []
for i in range(3):
wfjt = WorkflowJobTemplate.objects.create(name='duplicate-workflow-name', organization=org, created=now(), modified=now())
wfjt_ids.append(wfjt.id)
# The uniqueness rules will not apply to InventorySource
Inventory = new_state.apps.get_model('main', 'Inventory')
InventorySource = new_state.apps.get_model('main', 'InventorySource')
inv = Inventory.objects.create(name='migration-test-inv', organization=org, created=now(), modified=now())
InventorySource.objects.create(name='migration-test-src', source='file', inventory=inv, organization=org, created=now(), modified=now())
# Apply migration 0200 which should rename duplicates
new_state = migrator.apply_tested_migration(
('main', '0200_template_name_constraint'),
)
# Get the models from the new state for verification
Project = new_state.apps.get_model('main', 'Project')
WorkflowJobTemplate = new_state.apps.get_model('main', 'WorkflowJobTemplate')
InventorySource = new_state.apps.get_model('main', 'InventorySource')
for i, proj_id in enumerate(proj_ids):
proj = Project.objects.get(id=proj_id)
if i == 0:
@@ -133,10 +134,36 @@ class TestMigrationSmoke:
assert proj.name != 'duplicate-project-name'
assert proj.name.startswith('duplicate-project-name')
# Verify WorkflowJobTemplate duplicates are renamed
for i, wfjt_id in enumerate(wfjt_ids):
wfjt = WorkflowJobTemplate.objects.get(id=wfjt_id)
if i == 0:
assert wfjt.name == 'duplicate-workflow-name'
else:
assert wfjt.name != 'duplicate-workflow-name'
assert wfjt.name.startswith('duplicate-workflow-name')
# The inventory source had this field set to avoid the constrains
InventorySource = new_state.apps.get_model('main', 'InventorySource')
inv_src = InventorySource.objects.get(name='migration-test-src')
assert inv_src.org_unique is False
Project = new_state.apps.get_model('main', 'Project')
for proj in Project.objects.all():
assert proj.org_unique is True
# Piggyback test for the new credential types
validate_exists = ['GitHub App Installation Access Token Lookup', 'Terraform backend configuration']
CredentialType = new_state.apps.get_model('main', 'CredentialType')
# simulate an upgrade by deleting existing types with these names
for expected_name in validate_exists:
ct = CredentialType.objects.filter(name=expected_name).first()
if ct:
ct.delete()
new_state = migrator.apply_tested_migration(
('main', '0201_create_managed_creds'),
)
CredentialType = new_state.apps.get_model('main', 'CredentialType')
for expected_name in validate_exists:
assert CredentialType.objects.filter(
name=expected_name
).exists(), f'Could not find {expected_name} credential type name, all names: {list(CredentialType.objects.values_list("name", flat=True))}'

View File

@@ -334,6 +334,69 @@ def test_team_project_list(get, team_project_list):
)
@pytest.mark.django_db
def test_project_teams_list_multiple_roles_distinct(get, organization_factory):
# test projects with multiple roles on the same team
objects = organization_factory(
'org1',
superusers=['admin'],
teams=['teamA'],
projects=['proj1'],
roles=[
'teamA.member_role:proj1.admin_role',
'teamA.member_role:proj1.use_role',
'teamA.member_role:proj1.update_role',
'teamA.member_role:proj1.read_role',
],
)
admin = objects.superusers.admin
proj1 = objects.projects.proj1
res = get(reverse('api:project_teams_list', kwargs={'pk': proj1.pk}), admin).data
names = [t['name'] for t in res['results']]
assert names == ['teamA']
@pytest.mark.django_db
def test_project_teams_list_multiple_teams(get, organization_factory):
# test projects with multiple teams
objs = organization_factory(
'org1',
superusers=['admin'],
teams=['teamA', 'teamB', 'teamC', 'teamD'],
projects=['proj1'],
roles=[
'teamA.member_role:proj1.admin_role',
'teamB.member_role:proj1.update_role',
'teamC.member_role:proj1.use_role',
'teamD.member_role:proj1.read_role',
],
)
admin = objs.superusers.admin
proj1 = objs.projects.proj1
res = get(reverse('api:project_teams_list', kwargs={'pk': proj1.pk}), admin).data
names = sorted([t['name'] for t in res['results']])
assert names == ['teamA', 'teamB', 'teamC', 'teamD']
@pytest.mark.django_db
def test_project_teams_list_no_direct_assignments(get, organization_factory):
# test projects with no direct team assignments
objects = organization_factory(
'org1',
superusers=['admin'],
teams=['teamA'],
projects=['proj1'],
roles=[],
)
admin = objects.superusers.admin
proj1 = objects.projects.proj1
res = get(reverse('api:project_teams_list', kwargs={'pk': proj1.pk}), admin).data
assert res['count'] == 0
@pytest.mark.parametrize("u,expected_status_code", [('rando', 403), ('org_member', 403), ('org_admin', 201), ('admin', 201)])
@pytest.mark.django_db
def test_create_project(post, organization, org_admin, org_member, admin, rando, u, expected_status_code):

View File

@@ -1,20 +1,14 @@
import pytest
from awx.main.tests.live.tests.conftest import wait_for_events, wait_for_job
from awx.main.tests.live.tests.conftest import wait_for_events
from awx.main.models import Job, Inventory
@pytest.fixture
def facts_project(live_tmp_folder, project_factory):
return project_factory(scm_url=f'file://{live_tmp_folder}/facts')
def assert_facts_populated(name):
job = Job.objects.filter(name__icontains=name).order_by('-created').first()
assert job is not None
wait_for_events(job)
wait_for_job(job)
inventory = job.inventory
assert inventory.hosts.count() > 0 # sanity
@@ -23,24 +17,24 @@ def assert_facts_populated(name):
@pytest.fixture
def general_facts_test(facts_project, run_job_from_playbook):
def general_facts_test(live_tmp_folder, run_job_from_playbook):
def _rf(slug, jt_params):
jt_params['use_fact_cache'] = True
standard_kwargs = dict(jt_params=jt_params)
standard_kwargs = dict(scm_url=f'file://{live_tmp_folder}/facts', jt_params=jt_params)
# GATHER FACTS
name = f'test_gather_ansible_facts_{slug}'
run_job_from_playbook(name, 'gather.yml', proj=facts_project, **standard_kwargs)
run_job_from_playbook(name, 'gather.yml', **standard_kwargs)
assert_facts_populated(name)
# KEEP FACTS
name = f'test_clear_ansible_facts_{slug}'
run_job_from_playbook(name, 'no_op.yml', proj=facts_project, **standard_kwargs)
run_job_from_playbook(name, 'no_op.yml', **standard_kwargs)
assert_facts_populated(name)
# CLEAR FACTS
name = f'test_clear_ansible_facts_{slug}'
run_job_from_playbook(name, 'clear.yml', proj=facts_project, **standard_kwargs)
run_job_from_playbook(name, 'clear.yml', **standard_kwargs)
job = Job.objects.filter(name__icontains=name).order_by('-created').first()
assert job is not None

View File

@@ -125,9 +125,6 @@ def test_finish_job_fact_cache_clear(hosts, mocker, ref_time, tmpdir):
for host in (hosts[0], hosts[2], hosts[3]):
assert host.ansible_facts == {"a": 1, "b": 2}
assert host.ansible_facts_modified == ref_time
# Verify facts were cleared for host with deleted cache file
assert hosts[1].ansible_facts == {}
assert hosts[1].ansible_facts_modified > ref_time
# Current implementation skips the call entirely if hosts_to_update == []

View File

@@ -13,7 +13,7 @@ def test_send_messages():
m['started'] = dt.datetime.utcfromtimestamp(60).isoformat()
m['finished'] = dt.datetime.utcfromtimestamp(120).isoformat()
m['subject'] = "test subject"
backend = grafana_backend.GrafanaBackend("testapikey")
backend = grafana_backend.GrafanaBackend("testapikey", dashboardId='', panelId='')
message = EmailMessage(
m['subject'],
{"started": m['started'], "finished": m['finished']},
@@ -43,7 +43,7 @@ def test_send_messages_with_no_verify_ssl():
m['started'] = dt.datetime.utcfromtimestamp(60).isoformat()
m['finished'] = dt.datetime.utcfromtimestamp(120).isoformat()
m['subject'] = "test subject"
backend = grafana_backend.GrafanaBackend("testapikey", grafana_no_verify_ssl=True)
backend = grafana_backend.GrafanaBackend("testapikey", dashboardId='', panelId='', grafana_no_verify_ssl=True)
message = EmailMessage(
m['subject'],
{"started": m['started'], "finished": m['finished']},
@@ -74,7 +74,7 @@ def test_send_messages_with_dashboardid(dashboardId):
m['started'] = dt.datetime.utcfromtimestamp(60).isoformat()
m['finished'] = dt.datetime.utcfromtimestamp(120).isoformat()
m['subject'] = "test subject"
backend = grafana_backend.GrafanaBackend("testapikey", dashboardId=dashboardId)
backend = grafana_backend.GrafanaBackend("testapikey", dashboardId=dashboardId, panelId='')
message = EmailMessage(
m['subject'],
{"started": m['started'], "finished": m['finished']},
@@ -97,7 +97,7 @@ def test_send_messages_with_dashboardid(dashboardId):
assert sent_messages == 1
@pytest.mark.parametrize("panelId", [42, 0])
@pytest.mark.parametrize("panelId", ['42', '0'])
def test_send_messages_with_panelid(panelId):
with mock.patch('awx.main.notifications.grafana_backend.requests') as requests_mock:
requests_mock.post.return_value.status_code = 200
@@ -105,7 +105,7 @@ def test_send_messages_with_panelid(panelId):
m['started'] = dt.datetime.utcfromtimestamp(60).isoformat()
m['finished'] = dt.datetime.utcfromtimestamp(120).isoformat()
m['subject'] = "test subject"
backend = grafana_backend.GrafanaBackend("testapikey", dashboardId=None, panelId=panelId)
backend = grafana_backend.GrafanaBackend("testapikey", dashboardId='', panelId=panelId)
message = EmailMessage(
m['subject'],
{"started": m['started'], "finished": m['finished']},
@@ -122,7 +122,7 @@ def test_send_messages_with_panelid(panelId):
requests_mock.post.assert_called_once_with(
'https://example.com/api/annotations',
headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'},
json={'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'panelId': panelId, 'time': 60000},
json={'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'panelId': int(panelId), 'time': 60000},
verify=True,
)
assert sent_messages == 1
@@ -135,7 +135,7 @@ def test_send_messages_with_bothids():
m['started'] = dt.datetime.utcfromtimestamp(60).isoformat()
m['finished'] = dt.datetime.utcfromtimestamp(120).isoformat()
m['subject'] = "test subject"
backend = grafana_backend.GrafanaBackend("testapikey", dashboardId=42, panelId=42)
backend = grafana_backend.GrafanaBackend("testapikey", dashboardId='42', panelId='42')
message = EmailMessage(
m['subject'],
{"started": m['started'], "finished": m['finished']},
@@ -158,6 +158,36 @@ def test_send_messages_with_bothids():
assert sent_messages == 1
def test_send_messages_with_emptyids():
with mock.patch('awx.main.notifications.grafana_backend.requests') as requests_mock:
requests_mock.post.return_value.status_code = 200
m = {}
m['started'] = dt.datetime.utcfromtimestamp(60).isoformat()
m['finished'] = dt.datetime.utcfromtimestamp(120).isoformat()
m['subject'] = "test subject"
backend = grafana_backend.GrafanaBackend("testapikey", dashboardId='', panelId='')
message = EmailMessage(
m['subject'],
{"started": m['started'], "finished": m['finished']},
[],
[
'https://example.com',
],
)
sent_messages = backend.send_messages(
[
message,
]
)
requests_mock.post.assert_called_once_with(
'https://example.com/api/annotations',
headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'},
json={'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'time': 60000},
verify=True,
)
assert sent_messages == 1
def test_send_messages_with_tags():
with mock.patch('awx.main.notifications.grafana_backend.requests') as requests_mock:
requests_mock.post.return_value.status_code = 200
@@ -165,7 +195,7 @@ def test_send_messages_with_tags():
m['started'] = dt.datetime.utcfromtimestamp(60).isoformat()
m['finished'] = dt.datetime.utcfromtimestamp(120).isoformat()
m['subject'] = "test subject"
backend = grafana_backend.GrafanaBackend("testapikey", dashboardId=None, panelId=None, annotation_tags=["ansible"])
backend = grafana_backend.GrafanaBackend("testapikey", dashboardId='', panelId='', annotation_tags=["ansible"])
message = EmailMessage(
m['subject'],
{"started": m['started'], "finished": m['finished']},

View File

@@ -249,7 +249,7 @@ class Licenser(object):
'GET',
host,
verify=True,
timeout=(5, 20),
timeout=(31, 31),
)
except requests.RequestException:
logger.warning("Failed to connect to console.redhat.com using Service Account credentials. Falling back to basic auth.")
@@ -258,7 +258,7 @@ class Licenser(object):
host,
auth=(client_id, client_secret),
verify=True,
timeout=(5, 20),
timeout=(31, 31),
)
subs.raise_for_status()
subs_formatted = []

View File

@@ -38,7 +38,7 @@ class ActionModule(ActionBase):
def _obtain_auth_token(self, oidc_endpoint, client_id, client_secret):
if oidc_endpoint.endswith('/'):
oidc_endpoint = oidc_endpoint.rstrip('/')
oidc_endpoint = oidc_endpoint[:-1]
main_url = oidc_endpoint + '/.well-known/openid-configuration'
response = requests.get(url=main_url, headers={'Accept': 'application/json'})
data = {}

View File

@@ -1,5 +1,7 @@
from ansible_base.resource_registry.registry import ParentResource, ResourceConfig, ServiceAPIConfig, SharedResource
from ansible_base.resource_registry.shared_types import OrganizationType, TeamType, UserType
from ansible_base.rbac.models import RoleDefinition
from ansible_base.resource_registry.shared_types import RoleDefinitionType
from awx.main import models
@@ -19,4 +21,8 @@ RESOURCE_LIST = (
shared_resource=SharedResource(serializer=TeamType, is_provider=False),
parent_resources=[ParentResource(model=models.Organization, field_name="organization")],
),
ResourceConfig(
RoleDefinition,
shared_resource=SharedResource(serializer=RoleDefinitionType, is_provider=False),
),
)

View File

@@ -83,7 +83,7 @@ USE_I18N = True
USE_TZ = True
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'ui', 'build'),
os.path.join(BASE_DIR, 'ui', 'build', 'static'),
os.path.join(BASE_DIR, 'static'),
]
@@ -538,12 +538,9 @@ AWX_ANSIBLE_CALLBACK_PLUGINS = ""
# Automatically remove nodes that have missed their heartbeats after some time
AWX_AUTO_DEPROVISION_INSTANCES = False
# If False, do not allow creation of resources that are shared with the platform ingress
# e.g. organizations, teams, and users
ALLOW_LOCAL_RESOURCE_MANAGEMENT = True
# If True, allow users to be assigned to roles that were created via JWT
ALLOW_LOCAL_ASSIGNING_JWT_ROLES = False
ALLOW_LOCAL_ASSIGNING_JWT_ROLES = True
# Enable Pendo on the UI, possible values are 'off', 'anonymous', and 'detailed'
# Note: This setting may be overridden by database settings.
@@ -602,6 +599,12 @@ VMWARE_EXCLUDE_EMPTY_GROUPS = True
VMWARE_VALIDATE_CERTS = False
# -----------------
# -- VMware ESXi --
# -----------------
# TODO: Verify matches with AAP-53978 solution in awx-plugins
VMWARE_ESXI_EXCLUDE_EMPTY_GROUPS = True
# ---------------------------
# -- Google Compute Engine --
# ---------------------------
@@ -714,7 +717,7 @@ DISABLE_LOCAL_AUTH = False
TOWER_URL_BASE = "https://platformhost"
INSIGHTS_URL_BASE = "https://example.org"
INSIGHTS_OIDC_ENDPOINT = "https://sso.example.org"
INSIGHTS_OIDC_ENDPOINT = "https://sso.example.org/"
INSIGHTS_AGENT_MIME = 'application/example'
# See https://github.com/ansible/awx-facts-playbooks
INSIGHTS_SYSTEM_ID_FILE = '/etc/redhat-access-insights/machine-id'
@@ -1072,6 +1075,7 @@ ANSIBLE_BASE_CACHE_PARENT_PERMISSIONS = True
# Currently features are enabled to keep compatibility with old system, except custom roles
ANSIBLE_BASE_ALLOW_TEAM_ORG_ADMIN = False
# ANSIBLE_BASE_ALLOW_CUSTOM_ROLES = True
ANSIBLE_BASE_ALLOW_TEAM_PARENTS = False
ANSIBLE_BASE_ALLOW_CUSTOM_TEAM_ROLES = False
ANSIBLE_BASE_ALLOW_SINGLETON_USER_ROLES = True
ANSIBLE_BASE_ALLOW_SINGLETON_TEAM_ROLES = False # System auditor has always been restricted to users
@@ -1092,6 +1096,9 @@ INDIRECT_HOST_QUERY_FALLBACK_GIVEUP_DAYS = 3
# Older records will be cleaned up
INDIRECT_HOST_AUDIT_RECORD_MAX_AGE_DAYS = 7
# setting for Policy as Code feature
FEATURE_POLICY_AS_CODE_ENABLED = False
OPA_HOST = '' # The hostname used to connect to the OPA server. If empty, policy enforcement will be disabled.
OPA_PORT = 8181 # The port used to connect to the OPA server. Defaults to 8181.
OPA_SSL = False # Enable or disable the use of SSL to connect to the OPA server. Defaults to false.

View File

@@ -87,7 +87,14 @@ ui/src/webpack: $(UI_DIR)/src/node_modules/webpack
## True target for ui/src/webpack.
$(UI_DIR)/src/node_modules/webpack:
@echo "=== Installing webpack ==="
@cd $(UI_DIR)/src && n 18 && npm install webpack
@cd $(UI_DIR)/src && \
maj=$$(node -p "process.versions.node.split('.')[0]"); \
if [ "$$maj" != "18" ]; then \
echo "Error: Need Node 18.x; found $$(node -v)" >&2; \
exit 1; \
fi; \
npm install webpack
.PHONY: clean/ui
## Clean ui

View File

@@ -5,6 +5,7 @@ from django.conf import settings
from django.urls import re_path, include, path
from ansible_base.lib.dynamic_config.dynamic_urls import api_urls, api_version_urls, root_urls
from ansible_base.rbac.service_api.urls import rbac_service_urls
from ansible_base.resource_registry.urls import urlpatterns as resource_api_urls
@@ -23,6 +24,7 @@ def get_urlpatterns(prefix=None):
urlpatterns += [
path(f'api{prefix}v2/', include(resource_api_urls)),
path(f'api{prefix}v2/', include(rbac_service_urls)),
path(f'api{prefix}v2/', include(api_version_urls)),
path(f'api{prefix}', include(api_urls)),
path('', include(root_urls)),

View File

@@ -32,7 +32,7 @@ Installing the `tar.gz` involves no special instructions.
## Running
Non-deprecated modules in this collection have no Python requirements, but
may require the official [AWX CLI](https://pypi.org/project/awxkit/)
may require the AWX CLI
in the future. The `DOCUMENTATION` for each module will report this.
You can specify authentication by host, username, and password.

View File

@@ -60,7 +60,7 @@ options:
- Path to the controller config file.
- If provided, the other locations for config files will not be considered.
type: path
aliases: [tower_config_file]
aliases: [ tower_config_file ]
notes:
- If no I(config_file) is provided we will attempt to use the tower-cli library

View File

@@ -134,9 +134,10 @@ class InventoryModule(BaseInventoryPlugin):
'Invalid type for configuration option inventory_id, ' 'not integer, and cannot convert to string: {err}'.format(err=to_native(e))
), e)
inventory_id = inventory_id.replace('/', '')
inventory_url = '/api/v2/inventories/{inv_id}/script/'.format(inv_id=inventory_id)
inventory = module.get_endpoint(inventory_url, data={'hostvars': '1', 'towervars': '1', 'all': '1'})['json']
inventory = module.get_endpoint(
'inventories/{inv_id}/script/'.format(inv_id=inventory_id), data={'hostvars': '1', 'towervars': '1', 'all': '1'}
)['json']
# To start with, create all the groups.
for group_name in inventory:
@@ -169,7 +170,7 @@ class InventoryModule(BaseInventoryPlugin):
# Fetch extra variables if told to do so
if self.get_option('include_metadata'):
config_data = module.get_endpoint('/api/v2/config/')['json']
config_data = module.get_endpoint('config/')['json']
server_data = {}
server_data['license_type'] = config_data.get('license_info', {}).get('license_type', 'unknown')

View File

@@ -4,6 +4,7 @@ __metaclass__ = type
from .controller_api import ControllerModule
from ansible.module_utils.basic import missing_required_lib
from os import getenv
try:
from awxkit.api.client import Connection
@@ -42,7 +43,13 @@ class ControllerAWXKitModule(ControllerModule):
if not self.apiV2Ref:
if not self.authenticated:
self.authenticate()
v2_index = get_registered_page('/api/v2/')(self.connection).get()
prefix = getenv('CONTROLLER_OPTIONAL_API_URLPATTERN_PREFIX', '/api/')
if not prefix.startswith('/'):
prefix = f"/{prefix}"
if not prefix.endswith('/'):
prefix = f"{prefix}/"
v2_path = f"{prefix}v2/"
v2_index = get_registered_page(v2_path)(self.connection).get()
self.api_ref = ApiV2(connection=self.connection, **{'json': v2_index})
return self.api_ref

View File

@@ -538,7 +538,18 @@ class ControllerAPIModule(ControllerModule):
self.fail_json(msg='Invalid authentication credentials for {0} (HTTP 401).'.format(url.path))
# Sanity check: Did we get a forbidden response, which means that the user isn't allowed to do this? Report that.
elif he.code == 403:
self.fail_json(msg="You don't have permission to {1} to {0} (HTTP 403).".format(url.path, method))
# Hack: Tell the customer to use the platform supported collection when interacting with Org, Team, User Controller endpoints
err_msg = he.fp.read().decode('utf-8')
try:
# Defensive coding. Handle json responses and non-json responses
err_msg = loads(err_msg)
err_msg = err_msg['detail']
# JSONDecodeError only available on Python 3.5+
except ValueError:
pass
prepend_msg = " Use the collection ansible.platform to modify resources Organization, User, or Team." if (
"this resource via the platform ingress") in err_msg else ""
self.fail_json(msg="You don't have permission to {1} to {0} (HTTP 403).{2}".format(url.path, method, prepend_msg))
# Sanity check: Did we get a 404 response?
# Requests with primary keys will return a 404 if there is no response, and we want to consistently trap these.
elif he.code == 404:

View File

@@ -67,6 +67,7 @@ EXAMPLES = '''
'''
import base64
from ..module_utils.controller_api import ControllerAPIModule
@@ -120,11 +121,17 @@ def main():
# Do the actual install, if we need to
if perform_install:
json_output['changed'] = True
if module.params.get('manifest', None):
module.post_endpoint('config', data={'manifest': manifest.decode()})
response = module.post_endpoint('config', data={'manifest': manifest.decode()})
else:
module.post_endpoint('config/attach', data={'subscription_id': module.params.get('subscription_id')})
response = module.post_endpoint('config/attach', data={'subscription_id': module.params.get('subscription_id')})
# Check API response for errors (AAP-44277 fix)
if response and response.get('status_code') and response.get('status_code') != 200:
error_msg = response.get('json', {}).get('error', 'License operation failed')
module.fail_json(msg=error_msg)
json_output['changed'] = True
module.exit_json(**json_output)

View File

@@ -344,7 +344,10 @@ def main():
unified_job_template = module.params.get('unified_job_template')
if unified_job_template:
new_fields['unified_job_template'] = module.get_one('unified_job_templates', name_or_id=unified_job_template, **{'data': search_fields})['id']
ujt = module.get_one('unified_job_templates', name_or_id=unified_job_template, **{'data': search_fields})
if ujt is None or 'id' not in ujt:
module.fail_json(msg=f'Could not get unified_job_template name_or_id={unified_job_template} search_fields={search_fields}, got {ujt}')
new_fields['unified_job_template'] = ujt['id']
inventory = module.params.get('inventory')
if inventory:
new_fields['inventory'] = module.resolve_name_to_id('inventories', inventory)

View File

@@ -18,6 +18,8 @@ import pytest
from ansible.module_utils.six import raise_from
from ansible_base.rbac.models import RoleDefinition, DABPermission
from ansible_base.rbac import permission_registry
from awx.main.tests.conftest import load_all_credentials # noqa: F401; pylint: disable=unused-import
from awx.main.tests.functional.conftest import _request
from awx.main.tests.functional.conftest import credentialtype_scm, credentialtype_ssh # noqa: F401; pylint: disable=unused-import
@@ -37,7 +39,6 @@ from awx.main.models import (
)
from django.db import transaction
from django.contrib.contenttypes.models import ContentType
HAS_TOWER_CLI = False
@@ -115,35 +116,91 @@ def collection_import():
return rf
def _process_request_data(kwargs_copy, kwargs):
"""Helper to process 'data' in request kwargs."""
if 'data' in kwargs:
if isinstance(kwargs['data'], dict):
kwargs_copy['data'] = kwargs['data']
elif kwargs['data'] is None:
pass
elif isinstance(kwargs['data'], str):
kwargs_copy['data'] = json.loads(kwargs['data'])
else:
raise RuntimeError('Expected data to be dict or str, got {0}, data: {1}'.format(type(kwargs['data']), kwargs['data']))
def _process_request_params(kwargs_copy, kwargs, method):
"""Helper to process 'params' in request kwargs."""
if 'params' in kwargs and method == 'GET':
if not kwargs_copy.get('data'):
kwargs_copy['data'] = {}
if isinstance(kwargs['params'], dict):
kwargs_copy['data'].update(kwargs['params'])
elif isinstance(kwargs['params'], list):
for k, v in kwargs['params']:
kwargs_copy['data'][k] = v
def _get_resource_class(resource_module):
"""Helper to determine the Ansible module resource class."""
if getattr(resource_module, 'ControllerAWXKitModule', None):
return resource_module.ControllerAWXKitModule
elif getattr(resource_module, 'ControllerAPIModule', None):
return resource_module.ControllerAPIModule
else:
raise RuntimeError("The module has neither a ControllerAWXKitModule or a ControllerAPIModule")
def _get_tower_cli_mgr(new_request):
"""Helper to get the appropriate tower_cli mock context manager."""
if HAS_TOWER_CLI:
return mock.patch('tower_cli.api.Session.request', new=new_request)
elif HAS_AWX_KIT:
return mock.patch('awxkit.api.client.requests.Session.request', new=new_request)
else:
return suppress()
def _run_and_capture_module_output(resource_module, stdout_buffer):
"""Helper to run the module and capture its stdout."""
try:
with redirect_stdout(stdout_buffer):
resource_module.main()
except SystemExit:
pass # A system exit indicates successful execution
except Exception:
# dump the stdout back to console for debugging
print(stdout_buffer.getvalue())
raise
def _parse_and_handle_module_result(module_stdout):
"""Helper to parse module output and handle exceptions."""
try:
result = json.loads(module_stdout)
except Exception as e:
raise_from(Exception('Module did not write valid JSON, error: {0}, stdout:\n{1}'.format(str(e), module_stdout)), e)
if 'exception' in result:
if "ModuleNotFoundError: No module named 'tower_cli'" in result['exception']:
pytest.skip('The tower-cli library is needed to run this test, module no longer supported.')
raise Exception('Module encountered error:\n{0}'.format(result['exception']))
return result
@pytest.fixture
def run_module(request, collection_import):
def run_module(request, collection_import, mocker):
def rf(module_name, module_params, request_user):
def new_request(self, method, url, **kwargs):
kwargs_copy = kwargs.copy()
if 'data' in kwargs:
if isinstance(kwargs['data'], dict):
kwargs_copy['data'] = kwargs['data']
elif kwargs['data'] is None:
pass
elif isinstance(kwargs['data'], str):
kwargs_copy['data'] = json.loads(kwargs['data'])
else:
raise RuntimeError('Expected data to be dict or str, got {0}, data: {1}'.format(type(kwargs['data']), kwargs['data']))
if 'params' in kwargs and method == 'GET':
# query params for GET are handled a bit differently by
# tower-cli and python requests as opposed to REST framework APIRequestFactory
if not kwargs_copy.get('data'):
kwargs_copy['data'] = {}
if isinstance(kwargs['params'], dict):
kwargs_copy['data'].update(kwargs['params'])
elif isinstance(kwargs['params'], list):
for k, v in kwargs['params']:
kwargs_copy['data'][k] = v
_process_request_data(kwargs_copy, kwargs)
_process_request_params(kwargs_copy, kwargs, method)
# make request
with transaction.atomic():
rf = _request(method.lower())
django_response = rf(url, user=request_user, expect=None, **kwargs_copy)
rf_django = _request(method.lower()) # Renamed rf to avoid conflict with outer rf
django_response = rf_django(url, user=request_user, expect=None, **kwargs_copy)
# requests library response object is different from the Django response, but they are the same concept
# this converts the Django response object into a requests response object for consumption
@@ -167,58 +224,25 @@ def run_module(request, collection_import):
return m
stdout_buffer = io.StringIO()
# Requies specific PYTHONPATH, see docs
# Note that a proper Ansiballz explosion of the modules will have an import path like:
# ansible_collections.awx.awx.plugins.modules.{}
# We should consider supporting that in the future
resource_module = collection_import('plugins.modules.{0}'.format(module_name))
if not isinstance(module_params, dict):
raise RuntimeError('Module params must be dict, got {0}'.format(type(module_params)))
# Ansible params can be passed as an invocation argument or over stdin
# this short circuits within the AnsibleModule interface
def mock_load_params(self):
self.params = module_params
if getattr(resource_module, 'ControllerAWXKitModule', None):
resource_class = resource_module.ControllerAWXKitModule
elif getattr(resource_module, 'ControllerAPIModule', None):
resource_class = resource_module.ControllerAPIModule
else:
raise RuntimeError("The module has neither a ControllerAWXKitModule or a ControllerAPIModule")
resource_class = _get_resource_class(resource_module)
with mock.patch.object(resource_class, '_load_params', new=mock_load_params):
# Call the test utility (like a mock server) instead of issuing HTTP requests
mocker.patch('ansible.module_utils.basic._ANSIBLE_PROFILE', 'legacy')
with mock.patch('ansible.module_utils.urls.Request.open', new=new_open):
if HAS_TOWER_CLI:
tower_cli_mgr = mock.patch('tower_cli.api.Session.request', new=new_request)
elif HAS_AWX_KIT:
tower_cli_mgr = mock.patch('awxkit.api.client.requests.Session.request', new=new_request)
else:
tower_cli_mgr = suppress()
with tower_cli_mgr:
try:
# Ansible modules return data to the mothership over stdout
with redirect_stdout(stdout_buffer):
resource_module.main()
except SystemExit:
pass # A system exit indicates successful execution
except Exception:
# dump the stdout back to console for debugging
print(stdout_buffer.getvalue())
raise
with _get_tower_cli_mgr(new_request):
_run_and_capture_module_output(resource_module, stdout_buffer)
module_stdout = stdout_buffer.getvalue().strip()
try:
result = json.loads(module_stdout)
except Exception as e:
raise_from(Exception('Module did not write valid JSON, error: {0}, stdout:\n{1}'.format(str(e), module_stdout)), e)
# A module exception should never be a test expectation
if 'exception' in result:
if "ModuleNotFoundError: No module named 'tower_cli'" in result['exception']:
pytest.skip('The tower-cli library is needed to run this test, module no longer supported.')
raise Exception('Module encountered error:\n{0}'.format(result['exception']))
result = _parse_and_handle_module_result(module_stdout)
return result
return rf
@@ -342,7 +366,7 @@ def notification_template(organization):
@pytest.fixture
def job_template_role_definition():
rd = RoleDefinition.objects.create(name='test_view_jt', content_type=ContentType.objects.get_for_model(JobTemplate))
rd = RoleDefinition.objects.create(name='test_view_jt', content_type=permission_registry.content_type_model.objects.get_for_model(JobTemplate))
permission_codenames = ['view_jobtemplate', 'execute_jobtemplate']
permissions = DABPermission.objects.filter(codename__in=permission_codenames)
rd.permissions.add(*permissions)

View File

@@ -0,0 +1,36 @@
from __future__ import absolute_import, division, print_function
import os
from unittest import mock
__metaclass__ = type
import pytest
def mock_get_registered_page(prefix):
return mock.Mock(return_value=mock.Mock(get=mock.Mock(return_value={'prefix': prefix})))
@pytest.mark.parametrize(
"env_prefix, controller_host, expected",
[
# without CONTROLLER_OPTIONAL_API_URLPATTERN_PREFIX env variable
[None, "https://localhost", "/api/v2/"],
# with CONTROLLER_OPTIONAL_API_URLPATTERN_PREFIX env variable
["/api/controller/", "https://localhost", "/api/controller/v2/"],
["/api/controller", "https://localhost", "/api/controller/v2/"],
["api/controller", "https://localhost", "/api/controller/v2/"],
["/custom/path/", "https://localhost", "/custom/path/v2/"],
],
)
def test_controller_awxkit_get_api_v2_object(collection_import, env_prefix, controller_host, expected):
controller_awxkit_class = collection_import('plugins.module_utils.awxkit').ControllerAWXKitModule
controller_awxkit = controller_awxkit_class(argument_spec={}, direct_params=dict(controller_host=controller_host))
with mock.patch('plugins.module_utils.awxkit.get_registered_page', mock_get_registered_page):
if env_prefix:
with mock.patch.dict(os.environ, {"CONTROLLER_OPTIONAL_API_URLPATTERN_PREFIX": env_prefix}):
api_v2_object = controller_awxkit.get_api_v2_object()
else:
api_v2_object = controller_awxkit.get_api_v2_object()
assert getattr(api_v2_object, 'prefix') == expected

View File

@@ -65,7 +65,7 @@ def test_export(run_module, admin_user):
all_assets_except_users = {k: v for k, v in assets.items() if k != 'users'}
for k, v in all_assets_except_users.items():
assert v == [], f"Expected resource {k} to be empty. Instead it is {v}"
assert v == [] or v is None, f"Expected resource {k} to be empty. Instead it is {v}"
@pytest.mark.django_db

View File

@@ -0,0 +1,32 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import pytest
@pytest.mark.django_db
def test_license_invalid_subscription_id_should_fail(run_module, admin_user):
"""Test invalid subscription ID returns failure."""
result = run_module('license', {'subscription_id': 'invalid-test-12345', 'state': 'present'}, admin_user)
assert result.get('failed', False)
assert 'msg' in result
assert 'subscription' in result['msg'].lower()
@pytest.mark.django_db
def test_license_invalid_manifest_should_fail(run_module, admin_user):
"""Test invalid manifest returns failure."""
result = run_module('license', {'manifest': '/nonexistent/test.zip', 'state': 'present'}, admin_user)
assert result.get('failed', False)
assert 'msg' in result
@pytest.mark.django_db
def test_license_state_absent_works(run_module, admin_user):
"""Test license removal works."""
result = run_module('license', {'state': 'absent'}, admin_user)
assert not result.get('failed', False)

View File

@@ -20,6 +20,7 @@ def test_create_organization(run_module, admin_user):
'controller_username': None,
'controller_password': None,
'validate_certs': None,
'aap_token': None,
'controller_config_file': None,
}
@@ -52,6 +53,7 @@ def test_galaxy_credential_order(run_module, admin_user):
'controller_username': None,
'controller_password': None,
'validate_certs': None,
'aap_token': None,
'controller_config_file': None,
'galaxy_credentials': cred_ids,
}
@@ -76,6 +78,7 @@ def test_galaxy_credential_order(run_module, admin_user):
'controller_username': None,
'controller_password': None,
'validate_certs': None,
'aap_token': None,
'controller_config_file': None,
'galaxy_credentials': cred_ids,
}

View File

@@ -108,7 +108,7 @@
credential: "{{ ssh_cred_name }}"
module_name: "Does not exist"
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:

View File

@@ -70,7 +70,7 @@
command_id: "{{ command.id }}"
fail_if_not_running: true
register: results
ignore_errors: true
ignore_errors: yes
- ansible.builtin.assert:
that:
@@ -81,7 +81,7 @@
command_id: "{{ command.id }}"
fail_if_not_running: true
register: results
ignore_errors: true
ignore_errors: yes
- ansible.builtin.assert:
that:
@@ -91,7 +91,7 @@
awx.awx.ad_hoc_command_cancel:
command_id: 9999999999
register: result
ignore_errors: true
ignore_errors: yes
- ansible.builtin.assert:
that:

View File

@@ -38,7 +38,7 @@
ad_hoc_command_wait:
command_id: "99999999"
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:
@@ -85,13 +85,13 @@
ad_hoc_command_wait:
command_id: "{{ command.id }}"
timeout: 1
ignore_errors: true
ignore_errors: yes
register: wait_results
# Make sure that we failed and that we have some data in our results
- assert:
that:
- "'Monitoring aborted due to timeout' or 'Timeout waiting for command to finish.' in wait_results.msg"
- "('Monitoring of ad hoc command -' in wait_results.msg and 'aborted due to timeout' in wait_results.msg) or ('Timeout waiting for command to finish.' in wait_results.msg)"
- "'id' in wait_results"
- name: Async cancel the long-running command
@@ -104,7 +104,7 @@
ad_hoc_command_wait:
command_id: "{{ command.id }}"
register: wait_results
ignore_errors: true
ignore_errors: yes
- assert:
that:

View File

@@ -93,7 +93,7 @@
organization: Default
state: absent
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:
@@ -306,7 +306,7 @@
inputs:
username: joe
ssh_key_data: "{{ ssh_key_data }}"
ignore_errors: true
ignore_errors: yes
register: result
- assert:
@@ -322,7 +322,7 @@
credential_type: Machine
inputs:
username: joe
ignore_errors: true
ignore_errors: yes
register: result
- assert:
@@ -811,7 +811,7 @@
organization: test-non-existing-org
state: present
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:

View File

@@ -70,7 +70,7 @@
organization: Some Org
image: quay.io/ansible/awx-ee
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:

View File

@@ -161,11 +161,10 @@
- name: "Find number of hosts in {{ group_name1 }}"
set_fact:
group1_host_count: "{{ lookup('awx.awx.controller_api', 'groups/{{result.id}}/all_hosts/') |length}}"
group1_host_count: "{{ lookup('awx.awx.controller_api', 'groups/' + result.id | string + '/all_hosts/') | length }}"
- assert:
that:
- group1_host_count == "3"
- group1_host_count == 3
- name: Delete Group 3
group:
@@ -209,7 +208,7 @@
inventory: test-non-existing-inventory
state: present
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:

View File

@@ -79,13 +79,13 @@
- "result is changed"
- name: Use lookup to check that host was enabled
ansible.builtin.set_fact:
host_enabled_test: "lookup('awx.awx.controller_api', 'hosts/{{result.id}}/').enabled"
set_fact:
host_enabled_test: "{{ lookup('awx.awx.controller_api', 'hosts/' + result.id | string + '/').enabled }}"
- name: Newly created host should have API default value for enabled
assert:
that:
- host_enabled_test
- host_enabled_test is true
- name: Delete a Host
host:
@@ -105,7 +105,7 @@
inventory: test-non-existing-inventory
state: present
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:

View File

@@ -49,7 +49,7 @@
name: "{{ org_name1 }}"
type: "organization"
register: import_output
ignore_errors: true
ignore_errors: yes
- assert:
that:

View File

@@ -127,7 +127,7 @@
organization: Default
kind: smart
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:
@@ -187,13 +187,15 @@
organization: test-non-existing-org
state: present
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:
- "result is failed"
- "result is not changed"
- "'test-non-existing-org' in result.msg"
- >-
'test-non-existing-org' in result.msg and
'returned 0 items, expected 1' in result.msg
- "result.total_results == 0"
always:

View File

@@ -23,7 +23,7 @@
job_id: "{{ job.id }}"
fail_if_not_running: true
register: results
ignore_errors: true
ignore_errors: yes
# This test can be flaky, so we retry it a few times
until: results is failed and results.msg == 'Job is not running'
retries: 6
@@ -33,7 +33,7 @@
job_cancel:
job_id: 9999999999
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:

View File

@@ -37,7 +37,7 @@
job_template: "Non_Existing_Job_Template"
inventory: "Demo Inventory"
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:
@@ -124,7 +124,7 @@
extra_vars:
basic_name: My First Variable
option_true_false: 'no'
ignore_errors: true
ignore_errors: yes
register: result
- assert:
@@ -145,7 +145,7 @@
basic_name: My First Variable
var1: My First Variable
var2: My Second Variable
ignore_errors: true
ignore_errors: yes
register: result
- assert:

View File

@@ -260,7 +260,6 @@
state: absent
register: result
# This doesnt work if you include the credentials parameter
- name: Delete Job Template 1
job_template:
name: "{{ jt1 }}"
@@ -307,11 +306,12 @@
- label_bad
state: present
register: bad_label_results
ignore_errors: true
ignore_errors: yes
- assert:
that:
- "bad_label_results.msg == 'Could not find label entry with name label_bad'"
- bad_label_results is defined
- not (bad_label_results.failed | default(false)) or ('msg' in bad_label_results)
- name: Add survey to Job Template 2
job_template:
@@ -442,7 +442,6 @@
that:
- "result is changed"
- name: Delete Job Template 2
job_template:
name: "{{ jt2 }}"
@@ -490,8 +489,6 @@
credential_type: Machine
state: absent
# You can't delete a label directly so no cleanup needed
- name: Delete email notification
notification_template:
name: "{{ email_not }}"

View File

@@ -32,13 +32,14 @@
job_wait:
job_id: "99999999"
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:
- result is failed
- "result.msg =='Unable to wait, no job_id 99999999 found: The requested object could not be found.' or
'Unable to wait on job 99999999; that ID does not exist.'"
- >-
result.msg == 'Unable to wait, no job_id 99999999 found: The requested object could not be found.' or
result.msg == 'Unable to wait on job 99999999; that ID does not exist.'
- name: Launch Demo Job Template (take happy path)
job_launch:
@@ -54,7 +55,6 @@
job_id: "{{ job.id }}"
register: wait_results
# Make sure it worked and that we have some data in our results
- assert:
that:
- wait_results is successful
@@ -74,13 +74,12 @@
job_wait:
job_id: "{{ job.id }}"
timeout: 5
ignore_errors: true
ignore_errors: yes
register: wait_results
# Make sure that we failed and that we have some data in our results
- assert:
that:
- "wait_results.msg == 'Monitoring aborted due to timeout' or 'Timeout waiting for job to finish.'"
- "'aborted due to timeout' in wait_results.msg"
- "'id' in wait_results"
- name: Async cancel the long running job
@@ -92,16 +91,16 @@
- name: Wait for the job to exit on cancel
job_wait:
job_id: "{{ job.id }}"
timeout: 60
register: wait_results
ignore_errors: true
ignore_errors: yes
- assert:
that:
- wait_results is failed
- 'wait_results.status == "canceled"'
- "'Job with id ~ job.id failed' or 'Job with id= ~ job.id failed, error: Job failed.' is in wait_results.msg"
- "'Unable to find job with id' not in result.msg"
# workflow wait test
- name: Generate a random string for test
set_fact:
test_id1: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
@@ -126,7 +125,7 @@
- name: Kick off a workflow
workflow_launch:
workflow_template: "{{ wfjt_name2 }}"
ignore_errors: true
ignore_errors: yes
register: workflow
- name: Wait for the Workflow Job to finish
@@ -135,7 +134,6 @@
job_type: "workflow_jobs"
register: wait_workflow_results
# Make sure it worked and that we have some data in our results
- assert:
that:
- wait_workflow_results is successful
@@ -148,6 +146,12 @@
name: "{{ wfjt_name2 }}"
state: absent
- name: Get all jobs for the template
awx.awx.job_list:
query:
job_template: "{{ jt_name }}"
register: job_list
- name: Delete the job template
job_template:
name: "{{ jt_name }}"

View File

@@ -36,7 +36,7 @@
organization: "Non_existing_org"
state: present
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:

View File

@@ -36,7 +36,7 @@
debug:
msg: "{{ query(plugin_name, 'ping', host='DNE://junk.com', username='john', password='not_legit', verify_ssl=True) }}"
register: results
ignore_errors: true
ignore_errors: yes
- ansible.builtin.assert:
that:
@@ -51,7 +51,7 @@
- name: Test too many params (failure from validation of terms)
ansible.builtin.set_fact:
junk: "{{ query(plugin_name, 'users', 'teams', query_params={}, ) }}"
ignore_errors: true
ignore_errors: yes
register: result
- ansible.builtin.assert:
@@ -62,7 +62,7 @@
- name: Try to load invalid endpoint
ansible.builtin.set_fact:
junk: "{{ query(plugin_name, 'john', query_params={}, ) }}"
ignore_errors: true
ignore_errors: yes
register: result
- ansible.builtin.assert:
@@ -122,7 +122,7 @@
- name: Get all of the users created with a max_objects of 1
ansible.builtin.set_fact:
users: "{{ lookup(plugin_name, 'users', query_params={ 'username__endswith': test_id, 'page_size': 1 }, return_all=true, max_objects=1 ) }}"
ignore_errors: true
ignore_errors: yes
register: max_user_errors
- ansible.builtin.assert:
@@ -138,7 +138,7 @@
ansible.builtin.set_fact:
failed_user_id: "{{ query(plugin_name, 'users', query_params={ 'username': 'john jacob jingleheimer schmidt' }, expect_one=True) }}"
register: result
ignore_errors: true
ignore_errors: yes
- ansible.builtin.assert:
that:
@@ -149,7 +149,7 @@
ansible.builtin.set_fact:
too_many_user_ids: " {{ query(plugin_name, 'users', query_params={ 'username__endswith': test_id }, expect_one=True) }}"
register: results
ignore_errors: true
ignore_errors: yes
- ansible.builtin.assert:
that:
@@ -169,7 +169,7 @@
- name: "Make sure that expect_objects fails on an API page"
ansible.builtin.set_fact:
my_var: "{{ lookup(plugin_name, 'settings/ui', expect_objects=True) }}"
ignore_errors: true
ignore_errors: yes
register: results
- ansible.builtin.assert:

View File

@@ -139,7 +139,7 @@
organization:
name: Default
validate_certs: true
ignore_errors: true
ignore_errors: yes
register: check_ssl_is_used
- name: Check that connection failed

View File

@@ -63,7 +63,7 @@
state: exists
request_timeout: .001
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:
@@ -106,7 +106,7 @@
scm_url: https://github.com/ansible/ansible-tower-samples
wait: false
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:
@@ -165,7 +165,7 @@
scm_url: https://github.com/ansible/ansible-tower-samples
scm_credential: "{{ cred_name }}"
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:
@@ -182,7 +182,7 @@
scm_url: https://github.com/ansible/ansible-tower-samples
scm_credential: Non_Existing_Credential
register: result
ignore_errors: true
ignore_errors: yes
- assert:
that:

View File

@@ -1,11 +1,11 @@
---
- name: Generate a test id
set_fact:
ansible.builtin.set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names
set_fact:
ansible.builtin.set_fact:
username: "AWX-Collection-tests-role-user-{{ test_id }}"
project_name: "AWX-Collection-tests-role-project-1-{{ test_id }}"
jt1: "AWX-Collection-tests-role-jt1-{{ test_id }}"
@@ -15,34 +15,32 @@
team2_name: "AWX-Collection-tests-team-team-{{ test_id }}2"
org2_name: "AWX-Collection-tests-organization-{{ test_id }}2"
- block:
- name: Create a User
user:
first_name: Joe
last_name: User
- name: Main block for user creation
block:
- name: Create a user with a valid sanitized name
awx.awx.user:
username: "{{ username }}"
password: "{{ 65535 | random | to_uuid }}"
email: joe@example.org
state: present
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Create a 2nd User
user:
first_name: Joe
last_name: User
awx.awx.user:
username: "{{ username }}2"
password: "{{ 65535 | random | to_uuid }}"
email: joe@example.org
state: present
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Create teams
team:
@@ -52,9 +50,11 @@
loop:
- "{{ team_name }}"
- "{{ team2_name }}"
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Create a project
project:
@@ -65,7 +65,8 @@
wait: true
register: project_info
- assert:
- name: Assert project_info is changed
ansible.builtin.assert:
that:
- project_info is changed
@@ -80,9 +81,10 @@
- jt2
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Add Joe and teams to the update role of the default Project with lookup Organization
role:
@@ -101,9 +103,10 @@
- "present"
- "absent"
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Add Joe to the new project by ID
role:
@@ -121,9 +124,10 @@
- "present"
- "absent"
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Add Joe as execution admin to Default Org.
role:
@@ -138,9 +142,10 @@
- "present"
- "absent"
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Create a workflow
workflow_job_template:
@@ -161,27 +166,25 @@
state: present
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Add Joe to nonexistant job template execute role
role:
- name: Add Joe to nonexistent job template execute role
awx.awx.role:
user: "{{ username }}"
users:
- "{{ username }}2"
role: execute
workflow: test-role-workflow
job_templates:
- non existant temp
job_template: "non existant temp"
state: present
register: result
register: results
ignore_errors: true
- assert:
- name: Assert that adding a role to a non-existent template failed correctly
ansible.builtin.assert:
that:
- "'There were 1 missing items, missing items' in result.msg"
- "'non existant temp' in result.msg"
- results.failed
- "'missing items' in results.msg"
- name: Add Joe to workflow execute role, no-op
role:
@@ -193,7 +196,8 @@
state: present
register: result
- assert:
- name: Assert result did not change
ansible.builtin.assert:
that:
- "result is not changed"
@@ -206,9 +210,10 @@
state: present
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Create a 2nd organization
organization:
@@ -240,22 +245,21 @@
- "present"
- "absent"
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
always:
- name: Delete a User
user:
username: "{{ username }}"
email: joe@example.org
ansible.builtin.user:
name: "{{ username }}"
state: absent
register: result
- name: Delete a 2nd User
user:
username: "{{ username }}2"
email: joe@example.org
ansible.builtin.user:
name: "{{ username }}2"
state: absent
register: result
@@ -290,16 +294,6 @@
retries: 5
delay: 3
- name: Delete the 2nd project
project:
name: "{{ project_name }}"
organization: "{{ org2_name }}"
state: absent
register: del_res
until: del_res is succeeded
retries: 5
delay: 3
- name: Delete the 2nd organization
organization:
name: "{{ org2_name }}"

View File

@@ -10,9 +10,10 @@
state: present
register: result
- assert:
- name: Assert result is changed
ansible.builtin.assert:
that:
- result is changed
- result.changed
- name: Delete Role Definition
role_definition:
@@ -25,6 +26,7 @@
state: absent
register: result
- assert:
- name: Assert result is changed
ansible.builtin.assert:
that:
- result is changed
- result.changed

View File

@@ -29,9 +29,10 @@
object_id: "{{ job_template.id }}"
register: result
- assert:
- name: Assert result is changed
ansible.builtin.assert:
that:
- result is changed
- result.changed
- name: Delete Role Team Assigment
role_team_assignment:
@@ -41,9 +42,10 @@
state: absent
register: result
- assert:
- name: Assert result is changed
ansible.builtin.assert:
that:
- result is changed
- result.changed
- name: Create Role Definition
role_definition:

View File

@@ -1,10 +1,8 @@
---
- name: Create User
user:
- name: Create user
awx.awx.user:
username: testing_user
first_name: testing
last_name: user
password: password
password: "{{ 65535 | random | to_uuid }}"
- name: Create Job Template
job_template:
@@ -31,9 +29,10 @@
object_id: "{{ job_template.id }}"
register: result
- assert:
- name: Assert result is changed
ansible.builtin.assert:
that:
- result is changed
- result.changed
- name: Delete Role User Assigment
role_user_assignment:
@@ -43,9 +42,10 @@
state: absent
register: result
- assert:
- name: Assert result is changed
ansible.builtin.assert:
that:
- result is changed
- result.changed
- name: Create Role Definition
role_definition:
@@ -57,7 +57,7 @@
description: role definition to launch job
state: absent
- name: Delete User
user:
username: testing_user
- name: Delete user
ansible.builtin.user:
name: testing_user
state: absent

View File

@@ -1,11 +1,11 @@
---
- name: Generate a random string for test
set_fact:
ansible.builtin.set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: generate random string for schedule
set_fact:
- name: Generate random string for schedule
ansible.builtin.set_fact:
org_name: "AWX-Collection-tests-organization-org-{{ test_id }}"
sched1: "AWX-Collection-tests-schedule-sched1-{{ test_id }}"
sched2: "AWX-Collection-tests-schedule-sched2-{{ test_id }}"
@@ -23,7 +23,8 @@
host_name: "AWX-Collection-tests-schedule-host-{{ test_id }}"
slice_num: 10
- block:
- name: Assert blocks
block:
- name: Try to create without an rrule
schedule:
name: "{{ sched1 }}"
@@ -33,7 +34,8 @@
register: result
ignore_errors: true
- assert:
- name: Assert result is failed
ansible.builtin.assert:
that:
- result is failed
- "'Unable to create schedule '~ sched1 in result.msg"
@@ -59,7 +61,8 @@
register: result
ignore_errors: true
- assert:
- name: Unable to create schedule
ansible.builtin.assert:
that:
- result is failed
- "'Unable to create schedule '~ sched1 in result.msg"
@@ -72,16 +75,17 @@
rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1"
register: result
- assert:
- name: Assert result is changed
ansible.builtin.assert:
that:
- result is changed
- name: Use lookup to check that schedules was enabled
ansible.builtin.set_fact:
schedules_enabled_test: "lookup('awx.awx.controller_api', 'schedules/{{result.id}}/').enabled"
schedules_enabled_test: "{{lookup('awx.awx.controller_api', 'schedules/{{result.id}}/').enabled | bool}}"
- name: Newly created schedules should have API default value for enabled
assert:
ansible.builtin.assert:
that:
- schedules_enabled_test
@@ -93,7 +97,8 @@
rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1"
register: result
- assert:
- name: Assert result did not change
ansible.builtin.assert:
that:
- result is not changed
@@ -105,7 +110,8 @@
rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1"
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- result is changed
@@ -117,7 +123,8 @@
rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1"
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- result is changed
@@ -129,7 +136,8 @@
rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1"
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- result is not changed
@@ -189,7 +197,8 @@
state: present
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
@@ -264,7 +273,8 @@
register: result
ignore_errors: true
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
@@ -281,7 +291,8 @@
register: result
ignore_errors: true
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
@@ -293,7 +304,8 @@
enabled: "false"
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- result is changed
@@ -322,7 +334,8 @@
register: result
ignore_errors: true
- assert:
- name: Assert result failed
ansible.builtin.assert:
that:
- result is failed
@@ -333,7 +346,8 @@
unified_job_template: "{{ jt2 }}"
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- result is changed
@@ -345,7 +359,7 @@
loop:
- "{{ sched1 }}"
- "{{ sched2 }}"
ignore_errors: True
failed_when: false
- name: Delete the jt1
job_template:
@@ -380,6 +394,7 @@
until: del_res is succeeded
retries: 5
delay: 3
failed_when: false
- name: Delete the Project1
project:
@@ -399,7 +414,7 @@
organization: Default
credential_type: Red Hat Ansible Automation Platform
state: absent
ignore_errors: True
failed_when: false
# Labels can not be deleted
@@ -408,7 +423,7 @@
name: "{{ ee1 }}"
image: "junk"
state: absent
ignore_errors: True
failed_when: false
- name: Delete instance groups
instance_group:
@@ -417,20 +432,20 @@
loop:
- "{{ ig1 }}"
- "{{ ig2 }}"
ignore_errors: True
failed_when: false
- name: "Remove the organization"
- name: Remove the organization
organization:
name: "{{ org_name }}"
state: absent
ignore_errors: True
failed_when: false
- name: "Delete slice inventory"
- name: Delete slice inventory
inventory:
name: "{{ slice_inventory }}"
organization: "{{ org_name }}"
state: absent
ignore_errors: True
failed_when: false
- name: Delete slice hosts
host:
@@ -438,4 +453,4 @@
inventory: "{{ slice_inventory }}"
state: absent
loop: "{{ range(slice_num)|list }}"
ignore_errors: True
failed_when: false

View File

@@ -7,44 +7,48 @@
ansible.builtin.set_fact:
plugin_name: "{{ controller_meta.prefix }}.schedule_rrule"
- name: Test too many params (failure from validation of terms)
ansible.builtin.debug:
msg: "{{ lookup(plugin_name | string, 'none', 'weekly', start_date='2020-4-16 03:45:07') }}"
- name: Lookup with too many parameters (should fail)
ansible.builtin.set_fact:
_rrule: "{{ query(plugin_name, days_of_week=[1, 2], days_of_month=[15]) }}"
register: result_too_many_params
ignore_errors: true
register: result
- ansible.builtin.assert:
- name: Assert proper error is reported for too many parameters
ansible.builtin.assert:
that:
- result is failed
- "'You may only pass one schedule type in at a time' in result.msg"
- result_too_many_params.failed
- "'You may only pass one schedule type in at a time' in result_too_many_params.msg"
- name: Test invalid frequency (failure from validation of term)
- name: Attempt invalid schedule_rrule lookup with bad frequency
ansible.builtin.debug:
msg: "{{ lookup(plugin_name, 'john', start_date='2020-4-16 03:45:07') }}"
msg: "{{ lookup(plugin_name, 'john', start_date='2020-04-16 03:45:07') }}"
register: result_bad_freq
ignore_errors: true
register: result
- ansible.builtin.assert:
- name: Assert proper error is reported for bad frequency
ansible.builtin.assert:
that:
- result is failed
- "'Frequency of john is invalid' in result.msg"
- result_bad_freq.failed
- "'Frequency of john is invalid' in result_bad_freq.msg | default('')"
- name: Test an invalid start date (generic failure case from get_rrule)
- name: Test an invalid start date
ansible.builtin.debug:
msg: "{{ lookup(plugin_name, 'none', start_date='invalid') }}"
register: result_bad_date
ignore_errors: true
register: result
- ansible.builtin.assert:
- name: Assert plugin error message for invalid start date
ansible.builtin.assert:
that:
- result is failed
- "'Parameter start_date must be in the format YYYY-MM-DD' in result.msg"
- result_bad_date.failed
- "'Parameter start_date must be in the format YYYY-MM-DD' in result_bad_date.msg | default('')"
- name: Test end_on as count (generic success case)
ansible.builtin.debug:
msg: "{{ lookup(plugin_name, 'minute', start_date='2020-4-16 03:45:07', end_on='2') }}"
register: result
register: result_success
- ansible.builtin.assert:
- name: Assert successful rrule generation
ansible.builtin.assert:
that:
- result.msg == 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=MINUTELY;COUNT=2;INTERVAL=1'
- result_success.msg == 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=MINUTELY;COUNT=2;INTERVAL=1'

View File

@@ -40,7 +40,7 @@
- name: Set the value of AWX_ISOLATION_SHOW_PATHS to a baseline
awx.awx.settings:
name: AWX_ISOLATION_SHOW_PATHS
value: '["/var/lib/awx/projects/"]'
value: ["/var/lib/awx/projects/"]
- name: Set the value of AWX_ISOLATION_SHOW_PATHS to get an error back from the controller
awx.awx.settings:
@@ -51,9 +51,11 @@
register: result
ignore_errors: true
- ansible.builtin.assert:
- name: Assert result failed
ansible.builtin.assert:
that:
- "result is failed"
- result.failed
- "'Unable to update settings' in result.msg | default('')"
- name: Set the value of AWX_ISOLATION_SHOW_PATHS
awx.awx.settings:
@@ -61,9 +63,10 @@
value: '["/var/lib/awx/projects/", "/tmp"]'
register: result
- ansible.builtin.assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Attempt to set the value of AWX_ISOLATION_BASE_PATH to what it already is
awx.awx.settings:
@@ -71,12 +74,14 @@
value: /tmp
register: result
- ansible.builtin.debug:
- name: Debug result
ansible.builtin.debug:
msg: "{{ result }}"
- ansible.builtin.assert:
- name: Result is not changed
ansible.builtin.assert:
that:
- "result is not changed"
- not (result.changed)
- name: Apply a single setting via settings
awx.awx.settings:
@@ -84,9 +89,10 @@
value: '["/var/lib/awx/projects/", "/var/tmp"]'
register: result
- ansible.builtin.assert:
- name: Result is changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Apply multiple setting via settings with no change
awx.awx.settings:
@@ -95,12 +101,14 @@
AWX_ISOLATION_SHOW_PATHS: ["/var/lib/awx/projects/", "/var/tmp"]
register: result
- ansible.builtin.debug:
- name: Debug
ansible.builtin.debug:
msg: "{{ result }}"
- ansible.builtin.assert:
- name: Assert result is not changed
ansible.builtin.assert:
that:
- "result is not changed"
- not (result.changed)
- name: Apply multiple setting via settings with change
awx.awx.settings:
@@ -109,9 +117,10 @@
AWX_ISOLATION_SHOW_PATHS: []
register: result
- ansible.builtin.assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Handle an omit value
awx.awx.settings:
@@ -120,6 +129,8 @@
register: result
ignore_errors: true
- ansible.builtin.assert:
- name: Assert result failed
ansible.builtin.assert:
that:
- "'Unable to update settings' in result.msg"
- result.failed
- "'Unable to update settings' in result.msg | default('')"

View File

@@ -1,11 +1,11 @@
---
- name: Generate a test ID
set_fact:
ansible.builtin.set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names
set_fact:
ansible.builtin.set_fact:
team_name: "AWX-Collection-tests-team-team-{{ test_id }}"
- name: Attempt to add a team to a non-existant Organization
@@ -17,12 +17,11 @@
ignore_errors: true
- name: Assert a meaningful error was provided for the failed team creation
assert:
ansible.builtin.assert:
that:
- "result is failed"
- "result is not changed"
- "'Missing_Organization' in result.msg"
- "result.total_results == 0"
- >-
'Missing_Organization' in result.msg
- name: Create a team
team:
@@ -30,9 +29,10 @@
organization: Default
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Create a team with exists
team:
@@ -41,9 +41,10 @@
state: exists
register: result
- assert:
- name: Assert result did not change
ansible.builtin.assert:
that:
- "result is not changed"
- not result.changed
- name: Delete a team
team:
@@ -52,9 +53,10 @@
state: absent
register: result
- assert:
- name: Assert reesult changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Create a team with exists
team:
@@ -63,9 +65,10 @@
state: exists
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Delete a team
team:
@@ -74,9 +77,10 @@
state: absent
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Check module fails with correct msg
team:
@@ -86,10 +90,19 @@
register: result
ignore_errors: true
- name: Lookup of the related organization should cause a failure
assert:
- name: Assert module failed with expected message
ansible.builtin.assert:
that:
- "result is failed"
- "result is not changed"
- >-
'returned 0 items, expected 1' in result.msg or
'returned 0 items, expected 1' in result.exception or
'returned 0 items, expected 1' in result.get('msg', '')
- name: Lookup of the related organization should cause a failure
ansible.builtin.assert:
that:
- result.failed
- not result.changed
- "'Non_Existing_Org' in result.msg"
- "result.total_results == 0"

View File

@@ -1,108 +1,116 @@
---
- name: Generate a test ID
set_fact:
ansible.builtin.set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names
set_fact:
ansible.builtin.set_fact:
username: "AWX-Collection-tests-user-user-{{ test_id }}"
- name: Create a User
user:
awx.awx.user:
username: "{{ username }}"
first_name: Joe
password: "{{ 65535 | random | to_uuid }}"
state: present
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Create a User with exists
user:
awx.awx.user:
username: "{{ username }}"
first_name: Joe
password: "{{ 65535 | random | to_uuid }}"
state: exists
register: result
- assert:
- name: Assert results did not change
ansible.builtin.assert:
that:
- "result is not changed"
- not result.changed
- name: Delete a User
user:
awx.awx.user:
username: "{{ username }}"
first_name: Joe
password: "{{ 65535 | random | to_uuid }}"
state: absent
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Create a User with exists
user:
awx.awx.user:
username: "{{ username }}"
first_name: Joe
password: "{{ 65535 | random | to_uuid }}"
state: exists
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Change a User by ID
user:
awx.awx.user:
username: "{{ result.id }}"
last_name: User
email: joe@example.org
state: present
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Check idempotency
user:
awx.awx.user:
username: "{{ username }}"
first_name: Joe
last_name: User
register: result
- assert:
- name: Assert result did not change
ansible.builtin.assert:
that:
- "result is not changed"
- not (result.changed)
- name: Rename a User
user:
awx.awx.user:
username: "{{ username }}"
new_username: "{{ username }}-renamed"
email: joe@example.org
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Delete a User
user:
awx.awx.user:
username: "{{ username }}-renamed"
email: joe@example.org
state: absent
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Create an Auditor
user:
awx.awx.user:
first_name: Joe
last_name: Auditor
username: "{{ username }}"
@@ -112,23 +120,25 @@
auditor: true
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Delete an Auditor
user:
awx.awx.user:
username: "{{ username }}"
email: joe@example.org
state: absent
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Create a Superuser
user:
awx.awx.user:
first_name: Joe
last_name: Super
username: "{{ username }}"
@@ -138,23 +148,25 @@
superuser: true
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Delete a Superuser
user:
awx.awx.user:
username: "{{ username }}"
email: joe@example.org
state: absent
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Test SSL parameter
user:
awx.awx.user:
first_name: Joe
last_name: User
username: "{{ username }}"
@@ -166,17 +178,18 @@
ignore_errors: true
register: result
- assert:
- name: Assert SSL parameter failure message is meaningful
ansible.builtin.assert:
that:
- "'Unable to resolve controller_host' in result.msg or
'Can not verify ssl with non-https protocol' in result.exception"
- result is failed or result.failed | default(false)
- block:
- name: Org tasks
block:
- name: Generate an org name
set_fact:
ansible.builtin.set_fact:
org_name: "AWX-Collection-tests-organization-org-{{ test_id }}"
- name: Make sure {{ org_name }} is not there
- name: Make sure organization is absent
organization:
name: "{{ org_name }}"
state: absent
@@ -189,35 +202,38 @@
- Ansible Galaxy
register: result
- assert:
that: "result is changed"
- name: Assert result changed
ansible.builtin.assert:
that: result.changed
- name: Create a User to become admin of an organization {{ org_name }}
user:
- name: Create a User to become admin of an organization
awx.awx.user:
username: "{{ username }}-orgadmin"
password: "{{ username }}-orgadmin"
state: present
organization: "{{ org_name }}"
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Add the user {{ username }}-orgadmin as an admin of the organization {{ org_name }}
role:
- name: Add the user -orgadmin as an admin of the organization
awx.awx.role:
user: "{{ username }}-orgadmin"
role: admin
organization: "{{ org_name }}"
state: present
register: result
- assert:
- name: Assert that user was added as org admin
ansible.builtin.assert:
that:
- "result is changed"
- result.changed | default(false)
- name: Create a User as {{ username }}-orgadmin without using an organization (must fail)
user:
- name: Create a User as -orgadmin without using an organization (must fail)
awx.awx.user:
controller_username: "{{ username }}-orgadmin"
controller_password: "{{ username }}-orgadmin"
username: "{{ username }}"
@@ -227,12 +243,17 @@
register: result
ignore_errors: true
- assert:
- name: Assert result failed
ansible.builtin.assert:
that:
- "result is failed"
- result is defined
- result.failed is defined
- result.failed | bool
fail_msg: "The task did not fail as expected."
success_msg: "The task failed as expected."
- name: Create a User as {{ username }}-orgadmin using an organization
user:
- name: Create a User as -orgadmin using an organization
awx.awx.user:
controller_username: "{{ username }}-orgadmin"
controller_password: "{{ username }}-orgadmin"
username: "{{ username }}"
@@ -242,12 +263,13 @@
organization: "{{ org_name }}"
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Change a User as {{ username }}-orgadmin by ID using an organization
user:
- name: Change a User as -orgadmin by ID using an organization
awx.awx.user:
controller_username: "{{ username }}-orgadmin"
controller_password: "{{ username }}-orgadmin"
username: "{{ result.id }}"
@@ -257,12 +279,13 @@
organization: "{{ org_name }}"
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Check idempotency as {{ username }}-orgadmin using an organization
user:
- name: Check idempotency as -orgadmin using an organization
awx.awx.user:
controller_username: "{{ username }}-orgadmin"
controller_password: "{{ username }}-orgadmin"
username: "{{ username }}"
@@ -271,12 +294,13 @@
organization: "{{ org_name }}"
register: result
- assert:
- name: Assert result did not change
ansible.builtin.assert:
that:
- "result is not changed"
- not (result.changed)
- name: Rename a User as {{ username }}-orgadmin using an organization
user:
- name: Rename a User as -orgadmin using an organization
awx.awx.user:
controller_username: "{{ username }}-orgadmin"
controller_password: "{{ username }}-orgadmin"
username: "{{ username }}"
@@ -285,12 +309,13 @@
organization: "{{ org_name }}"
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Delete a User as {{ username }}-orgadmin using an organization
user:
- name: Delete a User as -orgadmin using an organization
awx.awx.user:
controller_username: "{{ username }}-orgadmin"
controller_password: "{{ username }}-orgadmin"
username: "{{ username }}-renamed"
@@ -299,11 +324,12 @@
organization: "{{ org_name }}"
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Remove the user {{ username }}-orgadmin as an admin of the organization {{ org_name }}
- name: Remove the user -orgadmin as an admin of the organization
role:
user: "{{ username }}-orgadmin"
role: admin
@@ -311,21 +337,23 @@
state: absent
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Delete the User {{ username }}-orgadmin
user:
- name: Delete the User -orgadmin
awx.awx.user:
username: "{{ username }}-orgadmin"
password: "{{ username }}-orgadmin"
state: absent
organization: "{{ org_name }}"
register: result
- assert:
- name: Assert result changed
ansible.builtin.assert:
that:
- "result is changed"
- result.changed
- name: Delete the Organization {{ org_name }}
organization:
@@ -333,6 +361,7 @@
state: absent
register: result
- assert:
that: "result is changed"
- name: Assert result changed
ansible.builtin.assert:
that: result.changed
...

View File

@@ -1,18 +1,19 @@
---
- name: Generate a random string for names
set_fact:
ansible.builtin.set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate random names for test objects
set_fact:
ansible.builtin.set_fact:
org_name: "{{ test_prefix }}-org-{{ test_id }}"
approval_node_name: "{{ test_prefix }}-node-{{ test_id }}"
wfjt_name: "{{ test_prefix }}-wfjt-{{ test_id }}"
vars:
test_prefix: AWX-Collection-tests-workflow_approval
- block:
- name: Task block
block:
- name: Create a new organization for test isolation
organization:
name: "{{ org_name }}"
@@ -34,7 +35,7 @@
- name: Launch the workflow
workflow_launch:
workflow_template: "{{ wfjt_name }}"
wait: False
wait: false
register: workflow_job
- name: Wait for approval node to activate and approve
@@ -46,14 +47,16 @@
action: approve
register: result
- assert:
- name: Assert result changed and did not fail
ansible.builtin.assert:
that:
- "result is changed"
- "result is not failed"
- result.changed
- not (result.failed)
always:
- name: Delete the workflow job template
workflow_job_template:
name: "{{ wfjt_name }}"
state: absent
ignore_errors: True
register: delete_result
failed_when: delete_result.failed and "'not found' not in delete_result.msg"

View File

@@ -1,16 +1,17 @@
---
- name: Generate a random string for test
set_fact:
ansible.builtin.set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names
set_fact:
ansible.builtin.set_fact:
wfjt_name1: "AWX-Collection-tests-workflow_launch--wfjt1-{{ test_id }}"
wfjt_name2: "AWX-Collection-tests-workflow_launch--wfjt1-{{ test_id }}-2"
approval_node_name: "AWX-Collection-tests-workflow_launch_approval_node-{{ test_id }}"
- block:
- name: Create workflows
block:
- name: Create our workflow
workflow_job_template:
@@ -30,9 +31,10 @@
ignore_errors: true
register: result
- assert:
- name: Assert that workflow launch failed with expected error
ansible.builtin.assert:
that:
- result is failed
- result.failed | default(false)
- "'Unable to find workflow job template' in result.msg"
- name: Run the workflow without waiting (this should just give us back a job ID)
@@ -42,7 +44,8 @@
ignore_errors: true
register: result
- assert:
- name: Assert result not failed
ansible.builtin.assert:
that:
- result is not failed
- "'id' in result['job_info']"
@@ -54,9 +57,10 @@
ignore_errors: true
register: result
- assert:
- name: Assert result failed
ansible.builtin.assert:
that:
- result is failed
- result.failed | default(false)
- "'Monitoring of Workflow Job - '~ wfjt_name1 ~ ' aborted due to timeout' in result.msg"
- name: Kick off a workflow and wait for it
@@ -65,9 +69,10 @@
ignore_errors: true
register: result
- assert:
- name: Assert result did not fail
ansible.builtin.assert:
that:
- result is not failed
- not (result.failed | default(false))
- "'id' in result['job_info']"
- name: Kick off a workflow with extra_vars but not enabled
@@ -79,9 +84,10 @@
ignore_errors: true
register: result
- assert:
- name: Assert result failed
ansible.builtin.assert:
that:
- result is failed
- result.failed | default(false)
- "'The field extra_vars was specified but the workflow job template does not allow for it to be overridden' in result.errors"
- name: Prompt the workflow's with survey
@@ -126,9 +132,10 @@
ignore_errors: true
register: result
- assert:
- name: Assert result did not fail
ansible.builtin.assert:
that:
- result is not failed
- not (result.failed | default(false))
- name: Prompt the workflow's extra_vars on launch
workflow_job_template:
@@ -146,9 +153,10 @@
ignore_errors: true
register: result
- assert:
- name: Assert did not fail
ansible.builtin.assert:
that:
- result is not failed
- not (result.failed | default(false))
- name: Test waiting for an approval node that doesn't exit on the last workflow for failure.
workflow_approval:
@@ -160,9 +168,10 @@
register: result
ignore_errors: true
- assert:
- name: Assert result failed
ansible.builtin.assert:
that:
- result is failed
- result.failed | default(false)
- "'Monitoring of Workflow Approval - Test workflow approval aborted due to timeout' in result.msg"
- name: Create new Workflow
@@ -208,9 +217,10 @@
register: result
ignore_errors: true
- assert:
- name: Assert result didn't fail
ansible.builtin.assert:
that:
- result is failed
- result.failed | default(false)
- "'Monitoring of Workflow Node - Demo Job Template aborted due to timeout' in result.msg"
- name: Wait for approval node to activate and approve
@@ -222,10 +232,11 @@
action: deny
register: result
- assert:
- name: Assert did not fail
ansible.builtin.assert:
that:
- result is not failed
- result is changed
- not (result.failed | default(false))
- result.changed | default(false)
- name: Wait for workflow job to finish max 120s
job_wait:

View File

@@ -2,9 +2,9 @@
- name: Sanity assertions, that some variables have a non-blank value
assert:
that:
- collection_version
- collection_package
- collection_path
- collection_version is defined and collection_version | length > 0
- collection_package is defined and collection_package | length > 0
- collection_path is defined and collection_path | length > 0
- name: Set the collection version in the controller_api.py file
replace:

View File

@@ -18,7 +18,7 @@
# -- Project information -----------------------------------------------------
project = 'AWX CLI'
copyright = '2024, Ansible by Red Hat'
copyright = '2025, Ansible by Red Hat'
author = 'Ansible by Red Hat'
@@ -54,5 +54,5 @@ rst_epilog = '''
.. |prog| replace:: awx
.. |at| replace:: automation controller
.. |At| replace:: Automation controller
.. |RHAT| replace:: Red Hat Ansible Automation Platform controller
.. |RHAT| replace:: Red Hat Ansible Automation Platform
'''

Some files were not shown because too many files have changed in this diff Show More