mirror of
https://github.com/ansible/awx.git
synced 2026-02-12 15:14:45 -03:30
AAP-62657 Implement logic to extract and populate JWT claims from Controller Jobs (#16259)
* AAP-62657 Add populate_claims_for_workload function and unit tests * Update safe_get helper function * Trigger CI rebuild to pick up latest django-ansible-base * Trigger CI after org visibility update * Retrigger CI * Rename workload to job, refine safe_get helper function * Update test_jobs to use job fixture * Retrigger CI * Create fresh job, removed launched_by since this is read-only property * Retrigger CI after runner issues * Retrigger CI after runner issues * Add unit tests for other workload types * Update CLAIM_LAUNCHED_BY_USER_NAME and CLAIM_LAUNCHED_BY_USER_ID, with CLAIM_LAUNCHED_BY_NAME and CLAIM_LAUNCHED_BY_ID * Generate claims with a more static schema try to operate directly on object when possible For cases where field is valid for the type, but null value still add the field, so blank and null values appear * Allow unified related items to be omittied --------- Co-authored-by: AlanCoding <arominge@redhat.com>
This commit is contained in:
@@ -84,6 +84,7 @@ from awx.main.utils.common import (
|
||||
create_partition,
|
||||
ScheduleWorkflowManager,
|
||||
ScheduleTaskManager,
|
||||
getattr_dne,
|
||||
)
|
||||
from awx.conf.license import get_license
|
||||
from awx.main.utils.handlers import SpecialInventoryHandler
|
||||
@@ -92,9 +93,76 @@ from awx.main.utils.update_model import update_model
|
||||
# Django flags
|
||||
from flags.state import flag_enabled
|
||||
|
||||
# Workload Identity
|
||||
from ansible_base.lib.workload_identity.controller import AutomationControllerJobScope
|
||||
|
||||
logger = logging.getLogger('awx.main.tasks.jobs')
|
||||
|
||||
|
||||
def populate_claims_for_workload(unified_job) -> dict:
|
||||
"""
|
||||
Extract JWT claims from a Controller workload for the aap_controller_automation_job scope.
|
||||
"""
|
||||
|
||||
# Related objects in the UnifiedJob model, applies to all job types
|
||||
organization = getattr_dne(unified_job, 'organization')
|
||||
ujt = getattr_dne(unified_job, 'unified_job_template')
|
||||
instance_group = getattr_dne(unified_job, 'instance_group')
|
||||
|
||||
claims = {
|
||||
AutomationControllerJobScope.CLAIM_JOB_ID: unified_job.id,
|
||||
AutomationControllerJobScope.CLAIM_JOB_NAME: unified_job.name,
|
||||
AutomationControllerJobScope.CLAIM_LAUNCH_TYPE: unified_job.launch_type,
|
||||
}
|
||||
|
||||
# Related objects in the UnifiedJob model, applies to all job types
|
||||
# null cases are omitted because of OIDC
|
||||
if organization := getattr_dne(unified_job, 'organization'):
|
||||
claims[AutomationControllerJobScope.CLAIM_ORGANIZATION_NAME] = organization.name
|
||||
claims[AutomationControllerJobScope.CLAIM_ORGANIZATION_ID] = organization.id
|
||||
|
||||
if ujt := getattr_dne(unified_job, 'unified_job_template'):
|
||||
claims[AutomationControllerJobScope.CLAIM_UNIFIED_JOB_TEMPLATE_NAME] = ujt.name
|
||||
claims[AutomationControllerJobScope.CLAIM_UNIFIED_JOB_TEMPLATE_ID] = ujt.id
|
||||
|
||||
if instance_group := getattr_dne(unified_job, 'instance_group'):
|
||||
claims[AutomationControllerJobScope.CLAIM_INSTANCE_GROUP_NAME] = instance_group.name
|
||||
claims[AutomationControllerJobScope.CLAIM_INSTANCE_GROUP_ID] = instance_group.id
|
||||
|
||||
# Related objects on concrete models, may not be valid for type of unified_job
|
||||
if inventory := getattr_dne(unified_job, 'inventory', None):
|
||||
claims[AutomationControllerJobScope.CLAIM_INVENTORY_NAME] = inventory.name
|
||||
claims[AutomationControllerJobScope.CLAIM_INVENTORY_ID] = inventory.id
|
||||
|
||||
if execution_environment := getattr_dne(unified_job, 'execution_environment', None):
|
||||
claims[AutomationControllerJobScope.CLAIM_EXECUTION_ENVIRONMENT_NAME] = execution_environment.name
|
||||
claims[AutomationControllerJobScope.CLAIM_EXECUTION_ENVIRONMENT_ID] = execution_environment.id
|
||||
|
||||
if project := getattr_dne(unified_job, 'project', None):
|
||||
claims[AutomationControllerJobScope.CLAIM_PROJECT_NAME] = project.name
|
||||
claims[AutomationControllerJobScope.CLAIM_PROJECT_ID] = project.id
|
||||
|
||||
if jt := getattr_dne(unified_job, 'job_template', None):
|
||||
claims[AutomationControllerJobScope.CLAIM_JOB_TEMPLATE_NAME] = jt.name
|
||||
claims[AutomationControllerJobScope.CLAIM_JOB_TEMPLATE_ID] = jt.id
|
||||
|
||||
# Only valid for job templates
|
||||
if hasattr(unified_job, 'playbook'):
|
||||
claims[AutomationControllerJobScope.CLAIM_PLAYBOOK_NAME] = unified_job.playbook
|
||||
|
||||
# Not valid for inventory updates and system jobs
|
||||
if hasattr(unified_job, 'job_type'):
|
||||
claims[AutomationControllerJobScope.CLAIM_JOB_TYPE] = unified_job.job_type
|
||||
|
||||
launched_by: dict = unified_job.launched_by
|
||||
if 'name' in launched_by:
|
||||
claims[AutomationControllerJobScope.CLAIM_LAUNCHED_BY_NAME] = launched_by['name']
|
||||
if 'id' in launched_by:
|
||||
claims[AutomationControllerJobScope.CLAIM_LAUNCHED_BY_ID] = launched_by['id']
|
||||
|
||||
return claims
|
||||
|
||||
|
||||
def with_path_cleanup(f):
|
||||
@functools.wraps(f)
|
||||
def _wrapped(self, *args, **kwargs):
|
||||
|
||||
@@ -18,8 +18,17 @@ from awx.main.models import (
|
||||
Job,
|
||||
Organization,
|
||||
Project,
|
||||
JobTemplate,
|
||||
UnifiedJobTemplate,
|
||||
InstanceGroup,
|
||||
ExecutionEnvironment,
|
||||
ProjectUpdate,
|
||||
InventoryUpdate,
|
||||
InventorySource,
|
||||
AdHocCommand,
|
||||
)
|
||||
from awx.main.tasks import jobs
|
||||
from ansible_base.lib.workload_identity.controller import AutomationControllerJobScope
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -188,3 +197,233 @@ def test_invalid_host_facts(mock_facts_settings, bulk_update_sorted_by_id, priva
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
if failures:
|
||||
pytest.fail(f" {len(failures)} facts cleared failures : {','.join(failures)}")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"job_attrs,expected_claims",
|
||||
[
|
||||
(
|
||||
{
|
||||
'id': 100,
|
||||
'name': 'Test Job',
|
||||
'job_type': 'run',
|
||||
'launch_type': 'manual',
|
||||
'playbook': 'site.yml',
|
||||
'organization': Organization(id=1, name='Test Org'),
|
||||
'inventory': Inventory(id=2, name='Test Inventory'),
|
||||
'project': Project(id=3, name='Test Project'),
|
||||
'execution_environment': ExecutionEnvironment(id=4, name='Test EE'),
|
||||
'job_template': JobTemplate(id=5, name='Test Job Template'),
|
||||
'unified_job_template': UnifiedJobTemplate(pk=6, id=6, name='Test Unified Job Template'),
|
||||
'instance_group': InstanceGroup(id=7, name='Test Instance Group'),
|
||||
},
|
||||
{
|
||||
AutomationControllerJobScope.CLAIM_JOB_ID: 100,
|
||||
AutomationControllerJobScope.CLAIM_JOB_NAME: 'Test Job',
|
||||
AutomationControllerJobScope.CLAIM_JOB_TYPE: 'run',
|
||||
AutomationControllerJobScope.CLAIM_LAUNCH_TYPE: 'manual',
|
||||
AutomationControllerJobScope.CLAIM_PLAYBOOK_NAME: 'site.yml',
|
||||
AutomationControllerJobScope.CLAIM_ORGANIZATION_NAME: 'Test Org',
|
||||
AutomationControllerJobScope.CLAIM_ORGANIZATION_ID: 1,
|
||||
AutomationControllerJobScope.CLAIM_INVENTORY_NAME: 'Test Inventory',
|
||||
AutomationControllerJobScope.CLAIM_INVENTORY_ID: 2,
|
||||
AutomationControllerJobScope.CLAIM_EXECUTION_ENVIRONMENT_NAME: 'Test EE',
|
||||
AutomationControllerJobScope.CLAIM_EXECUTION_ENVIRONMENT_ID: 4,
|
||||
AutomationControllerJobScope.CLAIM_PROJECT_NAME: 'Test Project',
|
||||
AutomationControllerJobScope.CLAIM_PROJECT_ID: 3,
|
||||
AutomationControllerJobScope.CLAIM_JOB_TEMPLATE_NAME: 'Test Job Template',
|
||||
AutomationControllerJobScope.CLAIM_JOB_TEMPLATE_ID: 5,
|
||||
AutomationControllerJobScope.CLAIM_UNIFIED_JOB_TEMPLATE_NAME: 'Test Unified Job Template',
|
||||
AutomationControllerJobScope.CLAIM_UNIFIED_JOB_TEMPLATE_ID: 6,
|
||||
AutomationControllerJobScope.CLAIM_INSTANCE_GROUP_NAME: 'Test Instance Group',
|
||||
AutomationControllerJobScope.CLAIM_INSTANCE_GROUP_ID: 7,
|
||||
},
|
||||
),
|
||||
(
|
||||
{'id': 100, 'name': 'Test', 'job_type': 'run', 'launch_type': 'manual', 'organization': Organization(id=1, name='')},
|
||||
{
|
||||
AutomationControllerJobScope.CLAIM_JOB_ID: 100,
|
||||
AutomationControllerJobScope.CLAIM_JOB_NAME: 'Test',
|
||||
AutomationControllerJobScope.CLAIM_JOB_TYPE: 'run',
|
||||
AutomationControllerJobScope.CLAIM_LAUNCH_TYPE: 'manual',
|
||||
AutomationControllerJobScope.CLAIM_ORGANIZATION_ID: 1,
|
||||
AutomationControllerJobScope.CLAIM_ORGANIZATION_NAME: '',
|
||||
AutomationControllerJobScope.CLAIM_PLAYBOOK_NAME: '',
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_populate_claims_for_workload(job_attrs, expected_claims):
|
||||
job = Job()
|
||||
|
||||
for attr, value in job_attrs.items():
|
||||
setattr(job, attr, value)
|
||||
|
||||
claims = jobs.populate_claims_for_workload(job)
|
||||
assert claims == expected_claims
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"workload_attrs,expected_claims",
|
||||
[
|
||||
(
|
||||
{
|
||||
'id': 200,
|
||||
'name': 'Git Sync',
|
||||
'job_type': 'check',
|
||||
'launch_type': 'sync',
|
||||
'organization': Organization(id=1, name='Test Org'),
|
||||
'project': Project(pk=3, id=3, name='Test Project'),
|
||||
'unified_job_template': Project(pk=3, id=3, name='Test Project'),
|
||||
'execution_environment': ExecutionEnvironment(id=4, name='Test EE'),
|
||||
'instance_group': InstanceGroup(id=7, name='Test Instance Group'),
|
||||
},
|
||||
{
|
||||
AutomationControllerJobScope.CLAIM_JOB_ID: 200,
|
||||
AutomationControllerJobScope.CLAIM_JOB_NAME: 'Git Sync',
|
||||
AutomationControllerJobScope.CLAIM_JOB_TYPE: 'check',
|
||||
AutomationControllerJobScope.CLAIM_LAUNCH_TYPE: 'sync',
|
||||
AutomationControllerJobScope.CLAIM_LAUNCHED_BY_NAME: 'Test Project',
|
||||
AutomationControllerJobScope.CLAIM_LAUNCHED_BY_ID: 3,
|
||||
AutomationControllerJobScope.CLAIM_ORGANIZATION_NAME: 'Test Org',
|
||||
AutomationControllerJobScope.CLAIM_ORGANIZATION_ID: 1,
|
||||
AutomationControllerJobScope.CLAIM_PROJECT_NAME: 'Test Project',
|
||||
AutomationControllerJobScope.CLAIM_PROJECT_ID: 3,
|
||||
AutomationControllerJobScope.CLAIM_UNIFIED_JOB_TEMPLATE_NAME: 'Test Project',
|
||||
AutomationControllerJobScope.CLAIM_UNIFIED_JOB_TEMPLATE_ID: 3,
|
||||
AutomationControllerJobScope.CLAIM_EXECUTION_ENVIRONMENT_NAME: 'Test EE',
|
||||
AutomationControllerJobScope.CLAIM_EXECUTION_ENVIRONMENT_ID: 4,
|
||||
AutomationControllerJobScope.CLAIM_INSTANCE_GROUP_NAME: 'Test Instance Group',
|
||||
AutomationControllerJobScope.CLAIM_INSTANCE_GROUP_ID: 7,
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
'id': 201,
|
||||
'name': 'Minimal Project Update',
|
||||
'job_type': 'run',
|
||||
'launch_type': 'manual',
|
||||
},
|
||||
{
|
||||
AutomationControllerJobScope.CLAIM_JOB_ID: 201,
|
||||
AutomationControllerJobScope.CLAIM_JOB_NAME: 'Minimal Project Update',
|
||||
AutomationControllerJobScope.CLAIM_JOB_TYPE: 'run',
|
||||
AutomationControllerJobScope.CLAIM_LAUNCH_TYPE: 'manual',
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_populate_claims_for_project_update(workload_attrs, expected_claims):
|
||||
project_update = ProjectUpdate()
|
||||
for attr, value in workload_attrs.items():
|
||||
setattr(project_update, attr, value)
|
||||
|
||||
claims = jobs.populate_claims_for_workload(project_update)
|
||||
assert claims == expected_claims
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"workload_attrs,expected_claims",
|
||||
[
|
||||
(
|
||||
{
|
||||
'id': 300,
|
||||
'name': 'AWS Sync',
|
||||
'launch_type': 'scheduled',
|
||||
'organization': Organization(id=1, name='Test Org'),
|
||||
'inventory': Inventory(id=2, name='AWS Inventory'),
|
||||
'unified_job_template': InventorySource(pk=8, id=8, name='AWS Source'),
|
||||
'execution_environment': ExecutionEnvironment(id=4, name='Test EE'),
|
||||
'instance_group': InstanceGroup(id=7, name='Test Instance Group'),
|
||||
},
|
||||
{
|
||||
AutomationControllerJobScope.CLAIM_JOB_ID: 300,
|
||||
AutomationControllerJobScope.CLAIM_JOB_NAME: 'AWS Sync',
|
||||
AutomationControllerJobScope.CLAIM_LAUNCH_TYPE: 'scheduled',
|
||||
AutomationControllerJobScope.CLAIM_ORGANIZATION_NAME: 'Test Org',
|
||||
AutomationControllerJobScope.CLAIM_ORGANIZATION_ID: 1,
|
||||
AutomationControllerJobScope.CLAIM_INVENTORY_NAME: 'AWS Inventory',
|
||||
AutomationControllerJobScope.CLAIM_INVENTORY_ID: 2,
|
||||
AutomationControllerJobScope.CLAIM_UNIFIED_JOB_TEMPLATE_NAME: 'AWS Source',
|
||||
AutomationControllerJobScope.CLAIM_UNIFIED_JOB_TEMPLATE_ID: 8,
|
||||
AutomationControllerJobScope.CLAIM_EXECUTION_ENVIRONMENT_NAME: 'Test EE',
|
||||
AutomationControllerJobScope.CLAIM_EXECUTION_ENVIRONMENT_ID: 4,
|
||||
AutomationControllerJobScope.CLAIM_INSTANCE_GROUP_NAME: 'Test Instance Group',
|
||||
AutomationControllerJobScope.CLAIM_INSTANCE_GROUP_ID: 7,
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
'id': 301,
|
||||
'name': 'Minimal Inventory Update',
|
||||
'launch_type': 'manual',
|
||||
},
|
||||
{
|
||||
AutomationControllerJobScope.CLAIM_JOB_ID: 301,
|
||||
AutomationControllerJobScope.CLAIM_JOB_NAME: 'Minimal Inventory Update',
|
||||
AutomationControllerJobScope.CLAIM_LAUNCH_TYPE: 'manual',
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_populate_claims_for_inventory_update(workload_attrs, expected_claims):
|
||||
inventory_update = InventoryUpdate()
|
||||
for attr, value in workload_attrs.items():
|
||||
setattr(inventory_update, attr, value)
|
||||
|
||||
claims = jobs.populate_claims_for_workload(inventory_update)
|
||||
assert claims == expected_claims
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"workload_attrs,expected_claims",
|
||||
[
|
||||
(
|
||||
{
|
||||
'id': 400,
|
||||
'name': 'Ping All Hosts',
|
||||
'job_type': 'run',
|
||||
'launch_type': 'manual',
|
||||
'organization': Organization(id=1, name='Test Org'),
|
||||
'inventory': Inventory(id=2, name='Test Inventory'),
|
||||
'execution_environment': ExecutionEnvironment(id=4, name='Test EE'),
|
||||
'instance_group': InstanceGroup(id=7, name='Test Instance Group'),
|
||||
},
|
||||
{
|
||||
AutomationControllerJobScope.CLAIM_JOB_ID: 400,
|
||||
AutomationControllerJobScope.CLAIM_JOB_NAME: 'Ping All Hosts',
|
||||
AutomationControllerJobScope.CLAIM_JOB_TYPE: 'run',
|
||||
AutomationControllerJobScope.CLAIM_LAUNCH_TYPE: 'manual',
|
||||
AutomationControllerJobScope.CLAIM_ORGANIZATION_NAME: 'Test Org',
|
||||
AutomationControllerJobScope.CLAIM_ORGANIZATION_ID: 1,
|
||||
AutomationControllerJobScope.CLAIM_INVENTORY_NAME: 'Test Inventory',
|
||||
AutomationControllerJobScope.CLAIM_INVENTORY_ID: 2,
|
||||
AutomationControllerJobScope.CLAIM_EXECUTION_ENVIRONMENT_NAME: 'Test EE',
|
||||
AutomationControllerJobScope.CLAIM_EXECUTION_ENVIRONMENT_ID: 4,
|
||||
AutomationControllerJobScope.CLAIM_INSTANCE_GROUP_NAME: 'Test Instance Group',
|
||||
AutomationControllerJobScope.CLAIM_INSTANCE_GROUP_ID: 7,
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
'id': 401,
|
||||
'name': 'Minimal Ad Hoc',
|
||||
'job_type': 'run',
|
||||
'launch_type': 'manual',
|
||||
},
|
||||
{
|
||||
AutomationControllerJobScope.CLAIM_JOB_ID: 401,
|
||||
AutomationControllerJobScope.CLAIM_JOB_NAME: 'Minimal Ad Hoc',
|
||||
AutomationControllerJobScope.CLAIM_JOB_TYPE: 'run',
|
||||
AutomationControllerJobScope.CLAIM_LAUNCH_TYPE: 'manual',
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_populate_claims_for_adhoc_command(workload_attrs, expected_claims):
|
||||
adhoc_command = AdHocCommand()
|
||||
for attr, value in workload_attrs.items():
|
||||
setattr(adhoc_command, attr, value)
|
||||
|
||||
claims = jobs.populate_claims_for_workload(adhoc_command)
|
||||
assert claims == expected_claims
|
||||
|
||||
@@ -1000,9 +1000,15 @@ def getattrd(obj, name, default=NoDefaultProvided):
|
||||
raise
|
||||
|
||||
|
||||
def getattr_dne(obj, name, notfound=ObjectDoesNotExist):
|
||||
empty = object()
|
||||
|
||||
|
||||
def getattr_dne(obj, name, default=empty, notfound=ObjectDoesNotExist):
|
||||
try:
|
||||
return getattr(obj, name)
|
||||
if default is empty:
|
||||
return getattr(obj, name)
|
||||
else:
|
||||
return getattr(obj, name, default)
|
||||
except notfound:
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user