From 1f90a7e04aa0eff341091034bf38055a59ab136b Mon Sep 17 00:00:00 2001 From: rohitthakur2590 Date: Wed, 3 Jun 2026 16:19:03 +0530 Subject: [PATCH 1/3] [AAP-75240] Fix role_team_assignment content_type mismatch causing 400/500 errors and enable resource-level type support Signed-off-by: rohitthakur2590 --- plugins/action/role_team_assignment.py | 69 +++++++ plugins/modules/role_team_assignment.py | 18 +- .../api/v1/role_team_assignment.py | 20 +- .../role_team_assignments_test/tasks/main.yml | 184 +++++++++++++++++- 4 files changed, 282 insertions(+), 9 deletions(-) diff --git a/plugins/action/role_team_assignment.py b/plugins/action/role_team_assignment.py index 90016c79..694d60e4 100644 --- a/plugins/action/role_team_assignment.py +++ b/plugins/action/role_team_assignment.py @@ -10,6 +10,44 @@ from ansible_collections.ansible.platform.plugins.action.base_action import BaseResourceActionPlugin from ansible_collections.ansible.platform.plugins.plugin_utils.ansible_models.role_team_assignment import AnsibleRoleTeamAssignment +# Maps the suffix of a role definition's content_type to the Gateway API +# endpoint used for name-based object lookup. +# e.g. "awx.project" → suffix "project" → endpoint "projects" +_CONTENT_TYPE_ENDPOINT_MAP = { + "organization": "organizations", + "team": "teams", + # Controller (awx) + "project": "projects", + "inventory": "inventories", + "credential": "credentials", + "jobtemplate": "job_templates", + "workflowjobtemplate": "workflow_job_templates", + "executionenvironment": "execution_environments", + "instancegroup": "instance_groups", + "notificationtemplate": "notification_templates", + # EDA (eda) + "activation": "activations", + "edacredential": "eda_credentials", + "eventstream": "event_streams", + "decisionenvironment": "decision_environments", + "credentialinputsource": "credential_input_sources", + # Hub (galaxy) + "namespace": "namespaces", + "collectionremote": "collection_remotes", + "ansiblerepository": "ansible_repositories", + "containernamespace": "container_namespaces", + "containerrepository": "container_repositories", +} + + +def _get_expected_endpoint(content_type): + """Derive the expected lookup endpoint from a role definition's content_type.""" + raw = (content_type or "").strip() + if not raw: + return None + suffix = raw.split(".")[-1] if "." in raw else raw + return _CONTENT_TYPE_ENDPOINT_MAP.get(suffix, "{0}s".format(suffix)) + class ActionModule(BaseResourceActionPlugin): MODULE_NAME = "role_team_assignment" @@ -78,6 +116,20 @@ def run(self, tmp=None, task_vars=None): # ---- single-object path: standard run logic ------------------- return self._run_standard(result, manager, argspec, validated_params, state) + # ---- resolve role_definition content_type for type validation ----- + role_def_name = validated_params.get("role_definition", "") + _role_def_obj = None + try: + _role_def_obj = manager.execute( + operation="find", + module_name="role_definition", + ansible_data={"name": role_def_name}, + ) + except Exception: + pass + _role_content_type = (_role_def_obj or {}).get("content_type") if _role_def_obj else None + _expected_endpoint = _get_expected_endpoint(_role_content_type) + # ---- multi-object path: iterate over assignment_objects ----------- # Base data shared across all assignments (role + team, no object_id) _skip = self._AUTH_PARAMS | { @@ -102,6 +154,23 @@ def run(self, tmp=None, task_vars=None): elif obj.get("object_ansible_id"): per_obj["object_ansible_id"] = str(obj["object_ansible_id"]) elif obj.get("name") and obj.get("type"): + # Validate that the user-provided type matches what the + # role's content_type expects. Mismatches cause the Gateway + # API to reject the assignment with 400/500. + if _expected_endpoint and obj["type"] != _expected_endpoint: + raise AnsibleError( + "Role '{role}' has content_type that requires type '{expected}' " + "for name-based lookup, but assignment_objects specifies " + "type '{provided}'. To grant access to all {resource}s within " + "an organization, use the organization-scoped variant of this " + "role. To target a specific {resource}, use " + "type '{expected}' with the resource name.".format( + role=role_def_name, + expected=_expected_endpoint, + provided=obj["type"], + resource=_expected_endpoint.rstrip("s"), + ) + ) try: oid = manager.lookup_resource_id(obj["type"], "name", obj["name"]) per_obj["object_id"] = str(oid) # CRITICAL: Must be string diff --git a/plugins/modules/role_team_assignment.py b/plugins/modules/role_team_assignment.py index 6fb5331f..c5e797a8 100644 --- a/plugins/modules/role_team_assignment.py +++ b/plugins/modules/role_team_assignment.py @@ -22,13 +22,14 @@ remove those permissions. - Not all role assignments are valid. See Limitations below. notes: - - This module is subject to limitations of the RBAC system in AAP 2.6. - Global roles (e.g. Platform Auditor) cannot be assigned to teams. - - Team roles cannot be assigned to another team - (Team Admin to Team is not supported). - Organization Member role cannot be assigned to teams. - - Only resource-scoped organization roles such as Organization Inventory Admin - and Organization Credential Admin can be meaningfully assigned to teams. + - The C(type) field in C(assignment_objects) must match the resource type + expected by the role definition's C(content_type). For example, a role + with C(content_type=awx.project) requires C(type=projects). Using + C(type=organizations) for such a role will result in an error. Use the + organization-scoped variant of the role (e.g. "Organization Project Admin") + to grant access to all resources of that type within an organization. - Attempting unsupported role assignments will result in errors. options: role_definition: @@ -65,8 +66,11 @@ required: false type: description: - - The object type used for name lookup. - - Supported values are C(organizations) and C(teams). + - The resource type endpoint used for name-based lookup. + - Must match the content_type of the role definition. + - Examples - C(organizations), C(teams), C(projects), + C(inventories), C(credentials), C(job_templates), + C(activations), C(event_streams), C(decision_environments). type: str required: false object_id: diff --git a/plugins/plugin_utils/api/v1/role_team_assignment.py b/plugins/plugin_utils/api/v1/role_team_assignment.py index b02fb6bf..6c53b5b3 100644 --- a/plugins/plugin_utils/api/v1/role_team_assignment.py +++ b/plugins/plugin_utils/api/v1/role_team_assignment.py @@ -84,7 +84,25 @@ def _get(key): if isinstance(object_id, int) or str(object_id).isdigit(): api_data["object_id"] = str(object_id) elif manager: - for endpoint in ("organizations", "teams"): + # Try all common resource endpoints — no longer limited to + # organizations/teams. The action plugin resolves name→id via + # the caller-provided type; this fallback is for the single-object + # path where no explicit type is given (object_id passed as a name). + _fallback_endpoints = ( + "organizations", + "teams", + "projects", + "inventories", + "credentials", + "job_templates", + "workflow_job_templates", + "activations", + "event_streams", + "decision_environments", + "eda_credentials", + "namespaces", + ) + for endpoint in _fallback_endpoints: resolved = _resolve_fk(manager, endpoint, "name", object_id) if resolved is not None: api_data["object_id"] = str(resolved) diff --git a/tests/integration/targets/role_team_assignments_test/tasks/main.yml b/tests/integration/targets/role_team_assignments_test/tasks/main.yml index 8603f2e5..0293d1b0 100644 --- a/tests/integration/targets/role_team_assignments_test/tasks/main.yml +++ b/tests/integration/targets/role_team_assignments_test/tasks/main.yml @@ -106,6 +106,170 @@ state: present register: custom_role + # -------------------------------------------------------------------------- + # Resource-level role definitions for type-mismatch tests + # -------------------------------------------------------------------------- + - name: Create project-scoped custom role (content_type awx.project) + ansible.platform.role_definition: + name: "{{ custom_role_name }}-project" + description: "Project-scoped role for content_type mismatch tests" + content_type: "awx.project" + permissions: + - "awx.view_project" + state: present + register: custom_project_role + + - name: Create activation-scoped custom role (content_type eda.activation) + ansible.platform.role_definition: + name: "{{ custom_role_name }}-activation" + description: "Activation-scoped role for content_type mismatch tests" + content_type: "eda.activation" + permissions: + - "eda.view_activation" + state: present + register: custom_activation_role + + # -------------------------------------------------------------------------- + # Root cause: resource-level role + type:organizations caused 400/500. + # Fix: module now validates type matches content_type and raises a clear error. + # -------------------------------------------------------------------------- + + # TEST 1: Type mismatch — project role + type:organizations must give clear error + - name: "BUG#165 | project-scoped role with type:organizations must fail with clear error" + ansible.platform.role_team_assignment: + role_definition: "{{ custom_role_name }}-project" + team: "{{ team1.name }}" + assignment_objects: + - name: "{{ org1.name }}" + type: organizations + state: present + register: mismatch_project_org + ignore_errors: true + + - name: "BUG#165 | Assert error message tells user the correct type to use" + ansible.builtin.assert: + that: + - mismatch_project_org is failed + - "'projects' in mismatch_project_org.msg" + - "'organizations' in mismatch_project_org.msg" + success_msg: > + BUG FIX CONFIRMED: module rejects project role + type:organizations with + a clear error pointing to 'projects' as the expected type. + fail_msg: > + BUG NOT FIXED: either the assignment was silently accepted (no error) + or the error message does not mention the correct type 'projects'. + Result: {{ mismatch_project_org | to_nice_json }} + + # TEST 2: Type mismatch — activation role + type:organizations must give clear error + - name: "BUG#165 | activation-scoped role with type:organizations must fail with clear error" + ansible.platform.role_team_assignment: + role_definition: "{{ custom_role_name }}-activation" + team: "{{ team1.name }}" + assignment_objects: + - name: "{{ org1.name }}" + type: organizations + state: present + register: mismatch_activation_org + ignore_errors: true + + - name: "BUG#165 | Assert activation mismatch error mentions activations" + ansible.builtin.assert: + that: + - mismatch_activation_org is failed + - "'activations' in mismatch_activation_org.msg" + - "'organizations' in mismatch_activation_org.msg" + success_msg: "BUG FIX CONFIRMED: activation role + type:organizations correctly rejected." + fail_msg: "Activation mismatch not caught. Result: {{ mismatch_activation_org | to_nice_json }}" + + # TEST 3: Correct org-level role assignment still works (no regression) + - name: "BUG#165 | org-scoped role with type:organizations must still work (regression check)" + ansible.platform.role_team_assignment: + role_definition: "{{ custom_role_name }}" + team: "{{ team1.name }}" + assignment_objects: + - name: "{{ org1.name }}" + type: organizations + state: present + register: correct_org_assignment + + - name: "BUG#165 | Assert org-level assignment succeeds" + ansible.builtin.assert: + that: + - correct_org_assignment is not failed + success_msg: "No regression: org-scoped role + type:organizations still works correctly." + fail_msg: "REGRESSION: org-scoped role assignment is broken. {{ correct_org_assignment.msg | default('') }}" + + # TEST 4: Idempotency — re-running org assignment produces no change + - name: "BUG#165 | Idempotency — re-run org assignment" + ansible.platform.role_team_assignment: + role_definition: "{{ custom_role_name }}" + team: "{{ team1.name }}" + assignment_objects: + - name: "{{ org1.name }}" + type: organizations + state: present + register: idempotent_check + + - name: "BUG#165 | Assert idempotency" + ansible.builtin.assert: + that: + - idempotent_check is not failed + - idempotent_check is not changed + success_msg: "Idempotency confirmed — second run made no changes." + fail_msg: "Idempotency broken — second run changed state or failed." + + # TEST 5: object_ansible_id path bypasses type check and still works + - name: "BUG#165 | Assignment via object_ansible_id must work regardless of content_type" + ansible.platform.role_team_assignment: + role_definition: "{{ custom_role_name }}" + team: "{{ team2.name }}" + assignment_objects: + - object_ansible_id: "{{ org2.organization.ansible_id | default(omit) }}" + state: present + register: ansible_id_assignment + when: org2.organization.ansible_id is defined + ignore_errors: true + + # TEST 6: state:absent removes assignment correctly + - name: "BUG#165 | state:absent removes the assignment" + ansible.platform.role_team_assignment: + role_definition: "{{ custom_role_name }}" + team: "{{ team1.name }}" + assignment_objects: + - name: "{{ org1.name }}" + type: organizations + state: absent + register: absent_assignment + + - name: "BUG#165 | Assert assignment was removed" + ansible.builtin.assert: + that: + - absent_assignment is not failed + - absent_assignment is changed + success_msg: "state:absent removed assignment correctly." + fail_msg: "state:absent failed or made no change. {{ absent_assignment.msg | default('') }}" + + # TEST 7: Multi-org assignment — single task, multiple objects + - name: "BUG#165 | Multi-org assignment in one task" + ansible.platform.role_team_assignment: + role_definition: "{{ custom_role_name }}" + team: "{{ team3.name }}" + assignment_objects: + - name: "{{ org3.name }}" + type: organizations + - name: "{{ org4.name }}" + type: organizations + state: present + register: multi_org_assignment + + - name: "BUG#165 | Assert multi-org assignment succeeded" + ansible.builtin.assert: + that: + - multi_org_assignment is not failed + - multi_org_assignment.assignments | length == 2 + success_msg: "Multi-org assignment: both orgs assigned in one task." + fail_msg: "Multi-org assignment failed. {{ multi_org_assignment | to_nice_json }}" + # 1. Assign Org Admin role to Team1 on Org1 (Global role can't be assigned) - name: Assign Org Admin to Team1 on Org1 ansible.platform.role_team_assignment: @@ -258,7 +422,7 @@ - "{{ org3.name }}" - "{{ org4.name }}" - - name: Delete custom role + - name: Delete custom org-scoped role ansible.platform.role_definition: name: "{{ custom_role_name }}" content_type: "shared.organization" @@ -270,4 +434,22 @@ - role_delete.failed - "'Not found' not in role_delete.msg" - "'does not exist' not in role_delete.msg" + + - name: Delete custom project-scoped role (bug#165 test) + ansible.platform.role_definition: + name: "{{ custom_role_name }}-project" + content_type: "awx.project" + permissions: + - "awx.view_project" + state: absent + failed_when: false + + - name: Delete custom activation-scoped role (bug#165 test) + ansible.platform.role_definition: + name: "{{ custom_role_name }}-activation" + content_type: "eda.activation" + permissions: + - "eda.view_activation" + state: absent + failed_when: false ... From 7396b7c0602482d2c066bf9309a5e08e7e57ebe2 Mon Sep 17 00:00:00 2001 From: rohitthakur2590 Date: Thu, 4 Jun 2026 19:17:50 +0530 Subject: [PATCH 2/3] update rta and rua Signed-off-by: rohitthakur2590 --- plugins/action/role_team_assignment.py | 47 ++++++++++++++++- plugins/modules/role_team_assignment.py | 69 ++++++++++++++++++++++--- 2 files changed, 108 insertions(+), 8 deletions(-) diff --git a/plugins/action/role_team_assignment.py b/plugins/action/role_team_assignment.py index 694d60e4..1710e8b3 100644 --- a/plugins/action/role_team_assignment.py +++ b/plugins/action/role_team_assignment.py @@ -39,6 +39,46 @@ "containerrepository": "container_repositories", } +# Maps the user-facing type name in assignment_objects to the full API path +# used for name-based resource lookup. +# +# Gateway resources use short names (platform_manager._build_url prepends +# /api/gateway/v1/ automatically). EDA and Hub resources must use full +# paths because the Gateway does NOT proxy those endpoints under /gateway/v1/. +# +# Confirmed via spike (AAPRFE-2614): +# /api/gateway/v1/activations/ → 404 +# /api/eda/v1/activations/ → 200 ← correct path +# +# Controller paths use /api/controller/v2/ — add as verified. +# Hub paths use /api/hub/v3/ — add as verified. +_SERVICE_LOOKUP_PATH_MAP = { + # Gateway — short names, prefixed automatically by _build_url + "organizations": "organizations", + "teams": "teams", + # Controller (awx) — full path required + "projects": "/api/controller/v2/projects/", + "inventories": "/api/controller/v2/inventories/", + "credentials": "/api/controller/v2/credentials/", + "job_templates": "/api/controller/v2/job_templates/", + "workflow_job_templates": "/api/controller/v2/workflow_job_templates/", + "execution_environments": "/api/controller/v2/execution_environments/", + "instance_groups": "/api/controller/v2/instance_groups/", + "notification_templates": "/api/controller/v2/notification_templates/", + # EDA — full path required (confirmed working) + "activations": "/api/eda/v1/activations/", + "eda_credentials": "/api/eda/v1/eda-credentials/", + "event_streams": "/api/eda/v1/event-streams/", + "decision_environments": "/api/eda/v1/decision-environments/", + "credential_input_sources": "/api/eda/v1/credential-input-sources/", + # Hub (galaxy) — full path required + "namespaces": "/api/hub/v3/namespaces/", + "collection_remotes": "/api/hub/v3/remotes/", + "ansible_repositories": "/api/hub/v3/ansible/repositories/", + "container_namespaces": "/api/hub/v3/container-namespaces/", + "container_repositories": "/api/hub/v3/container/repositories/", +} + def _get_expected_endpoint(content_type): """Derive the expected lookup endpoint from a role definition's content_type.""" @@ -171,8 +211,13 @@ def run(self, tmp=None, task_vars=None): resource=_expected_endpoint.rstrip("s"), ) ) + # Use the service-specific API path for name resolution. + # EDA and Hub resources are not exposed under /api/gateway/v1/, + # so we must call their own service APIs directly. + # _build_url passes /api/... paths through unchanged. + _lookup_path = _SERVICE_LOOKUP_PATH_MAP.get(obj["type"], obj["type"]) try: - oid = manager.lookup_resource_id(obj["type"], "name", obj["name"]) + oid = manager.lookup_resource_id(_lookup_path, "name", obj["name"]) per_obj["object_id"] = str(oid) # CRITICAL: Must be string except Exception: per_obj["object_id"] = str(obj["name"]) diff --git a/plugins/modules/role_team_assignment.py b/plugins/modules/role_team_assignment.py index c5e797a8..6a035cc8 100644 --- a/plugins/modules/role_team_assignment.py +++ b/plugins/modules/role_team_assignment.py @@ -117,28 +117,83 @@ """ EXAMPLES = """ -- name: Assign role to a team against multiple organizations by name +# ── Organization-level roles (content_type shared.organization) ─────────────── + +- name: Assign org-level role to a team against multiple organizations ansible.platform.role_team_assignment: role_definition: Organization Inventory Admin team: "APAC-BLR" assignment_objects: - name: "org-emea" - type: "organizations" + type: organizations - name: "org-apac" - type: "organizations" + type: organizations state: present register: result -- name: Assign role using object_ansible_id +# ── Controller resource-level roles ────────────────────────────────────────── + +- name: Assign Project Admin to a team on a specific project (content_type awx.project) ansible.platform.role_team_assignment: - role_definition: Organization Inventory Admin - team: "APAC-BLR" + role_definition: Project Admin + team: "dev-team" + assignment_objects: + - name: "Demo Project" + type: projects + state: present + +- name: Assign JobTemplate Execute to a team on a specific job template + ansible.platform.role_team_assignment: + role_definition: JobTemplate Execute + team: "ops-team" + assignment_objects: + - name: "Deploy to Production" + type: job_templates + state: present + +# ── EDA resource-level roles ────────────────────────────────────────────────── + +- name: Assign Activation Admin to a team on a specific EDA activation (content_type eda.activation) + ansible.platform.role_team_assignment: + role_definition: Activation Admin + team: "eda-operators" + assignment_objects: + - name: "prod-alert-activation" + type: activations + state: present + +- name: Assign Event Stream Admin to a team on a specific event stream + ansible.platform.role_team_assignment: + role_definition: Event Stream Admin + team: "eda-team" + assignment_objects: + - name: "kafka-prod-stream" + type: event_streams + state: present + +# ── Hub resource-level roles ────────────────────────────────────────────────── + +- name: Assign namespace owner role to a team on a Hub namespace (content_type galaxy.namespace) + ansible.platform.role_team_assignment: + role_definition: galaxy.collection_namespace_owner + team: "hub-publishers" + assignment_objects: + - name: "my_namespace" + type: namespaces + state: present + +# ── Using object IDs directly (works for all resource types) ────────────────── + +- name: Assign role using object_ansible_id (UUID — no name lookup needed) + ansible.platform.role_team_assignment: + role_definition: Activation Admin + team: "eda-operators" assignment_objects: - object_ansible_id: "c891b9f7-cc08-4b62-9843-c9ebfda362a8" state: present register: result -- name: Assign role using direct object_id +- name: Assign role using direct numeric object_id ansible.platform.role_team_assignment: role_definition: Organization Inventory Admin team: "APAC-BLR" From d4d3914acbf34be953fcdda1245ed337168fcd5b Mon Sep 17 00:00:00 2001 From: Rohit Thakur Date: Wed, 17 Jun 2026 15:31:24 +0530 Subject: [PATCH 3/3] remove multiend points changes Signed-off-by: Rohit Thakur --- plugins/action/role_team_assignment.py | 47 +----------- plugins/modules/role_team_assignment.py | 71 ++----------------- .../api/v1/role_team_assignment.py | 25 ++----- 3 files changed, 14 insertions(+), 129 deletions(-) diff --git a/plugins/action/role_team_assignment.py b/plugins/action/role_team_assignment.py index 1710e8b3..694d60e4 100644 --- a/plugins/action/role_team_assignment.py +++ b/plugins/action/role_team_assignment.py @@ -39,46 +39,6 @@ "containerrepository": "container_repositories", } -# Maps the user-facing type name in assignment_objects to the full API path -# used for name-based resource lookup. -# -# Gateway resources use short names (platform_manager._build_url prepends -# /api/gateway/v1/ automatically). EDA and Hub resources must use full -# paths because the Gateway does NOT proxy those endpoints under /gateway/v1/. -# -# Confirmed via spike (AAPRFE-2614): -# /api/gateway/v1/activations/ → 404 -# /api/eda/v1/activations/ → 200 ← correct path -# -# Controller paths use /api/controller/v2/ — add as verified. -# Hub paths use /api/hub/v3/ — add as verified. -_SERVICE_LOOKUP_PATH_MAP = { - # Gateway — short names, prefixed automatically by _build_url - "organizations": "organizations", - "teams": "teams", - # Controller (awx) — full path required - "projects": "/api/controller/v2/projects/", - "inventories": "/api/controller/v2/inventories/", - "credentials": "/api/controller/v2/credentials/", - "job_templates": "/api/controller/v2/job_templates/", - "workflow_job_templates": "/api/controller/v2/workflow_job_templates/", - "execution_environments": "/api/controller/v2/execution_environments/", - "instance_groups": "/api/controller/v2/instance_groups/", - "notification_templates": "/api/controller/v2/notification_templates/", - # EDA — full path required (confirmed working) - "activations": "/api/eda/v1/activations/", - "eda_credentials": "/api/eda/v1/eda-credentials/", - "event_streams": "/api/eda/v1/event-streams/", - "decision_environments": "/api/eda/v1/decision-environments/", - "credential_input_sources": "/api/eda/v1/credential-input-sources/", - # Hub (galaxy) — full path required - "namespaces": "/api/hub/v3/namespaces/", - "collection_remotes": "/api/hub/v3/remotes/", - "ansible_repositories": "/api/hub/v3/ansible/repositories/", - "container_namespaces": "/api/hub/v3/container-namespaces/", - "container_repositories": "/api/hub/v3/container/repositories/", -} - def _get_expected_endpoint(content_type): """Derive the expected lookup endpoint from a role definition's content_type.""" @@ -211,13 +171,8 @@ def run(self, tmp=None, task_vars=None): resource=_expected_endpoint.rstrip("s"), ) ) - # Use the service-specific API path for name resolution. - # EDA and Hub resources are not exposed under /api/gateway/v1/, - # so we must call their own service APIs directly. - # _build_url passes /api/... paths through unchanged. - _lookup_path = _SERVICE_LOOKUP_PATH_MAP.get(obj["type"], obj["type"]) try: - oid = manager.lookup_resource_id(_lookup_path, "name", obj["name"]) + oid = manager.lookup_resource_id(obj["type"], "name", obj["name"]) per_obj["object_id"] = str(oid) # CRITICAL: Must be string except Exception: per_obj["object_id"] = str(obj["name"]) diff --git a/plugins/modules/role_team_assignment.py b/plugins/modules/role_team_assignment.py index 6a035cc8..24311524 100644 --- a/plugins/modules/role_team_assignment.py +++ b/plugins/modules/role_team_assignment.py @@ -66,11 +66,9 @@ required: false type: description: - - The resource type endpoint used for name-based lookup. + - The object type used for name lookup. - Must match the content_type of the role definition. - - Examples - C(organizations), C(teams), C(projects), - C(inventories), C(credentials), C(job_templates), - C(activations), C(event_streams), C(decision_environments). + - Supported values are C(organizations) and C(teams). type: str required: false object_id: @@ -117,9 +115,7 @@ """ EXAMPLES = """ -# ── Organization-level roles (content_type shared.organization) ─────────────── - -- name: Assign org-level role to a team against multiple organizations +- name: Assign role to a team against multiple organizations by name ansible.platform.role_team_assignment: role_definition: Organization Inventory Admin team: "APAC-BLR" @@ -131,69 +127,16 @@ state: present register: result -# ── Controller resource-level roles ────────────────────────────────────────── - -- name: Assign Project Admin to a team on a specific project (content_type awx.project) - ansible.platform.role_team_assignment: - role_definition: Project Admin - team: "dev-team" - assignment_objects: - - name: "Demo Project" - type: projects - state: present - -- name: Assign JobTemplate Execute to a team on a specific job template - ansible.platform.role_team_assignment: - role_definition: JobTemplate Execute - team: "ops-team" - assignment_objects: - - name: "Deploy to Production" - type: job_templates - state: present - -# ── EDA resource-level roles ────────────────────────────────────────────────── - -- name: Assign Activation Admin to a team on a specific EDA activation (content_type eda.activation) +- name: Assign role using object_ansible_id ansible.platform.role_team_assignment: - role_definition: Activation Admin - team: "eda-operators" - assignment_objects: - - name: "prod-alert-activation" - type: activations - state: present - -- name: Assign Event Stream Admin to a team on a specific event stream - ansible.platform.role_team_assignment: - role_definition: Event Stream Admin - team: "eda-team" - assignment_objects: - - name: "kafka-prod-stream" - type: event_streams - state: present - -# ── Hub resource-level roles ────────────────────────────────────────────────── - -- name: Assign namespace owner role to a team on a Hub namespace (content_type galaxy.namespace) - ansible.platform.role_team_assignment: - role_definition: galaxy.collection_namespace_owner - team: "hub-publishers" - assignment_objects: - - name: "my_namespace" - type: namespaces - state: present - -# ── Using object IDs directly (works for all resource types) ────────────────── - -- name: Assign role using object_ansible_id (UUID — no name lookup needed) - ansible.platform.role_team_assignment: - role_definition: Activation Admin - team: "eda-operators" + role_definition: Organization Inventory Admin + team: "APAC-BLR" assignment_objects: - object_ansible_id: "c891b9f7-cc08-4b62-9843-c9ebfda362a8" state: present register: result -- name: Assign role using direct numeric object_id +- name: Assign role using direct object_id ansible.platform.role_team_assignment: role_definition: Organization Inventory Admin team: "APAC-BLR" diff --git a/plugins/plugin_utils/api/v1/role_team_assignment.py b/plugins/plugin_utils/api/v1/role_team_assignment.py index 6c53b5b3..b3585031 100644 --- a/plugins/plugin_utils/api/v1/role_team_assignment.py +++ b/plugins/plugin_utils/api/v1/role_team_assignment.py @@ -84,25 +84,12 @@ def _get(key): if isinstance(object_id, int) or str(object_id).isdigit(): api_data["object_id"] = str(object_id) elif manager: - # Try all common resource endpoints — no longer limited to - # organizations/teams. The action plugin resolves name→id via - # the caller-provided type; this fallback is for the single-object - # path where no explicit type is given (object_id passed as a name). - _fallback_endpoints = ( - "organizations", - "teams", - "projects", - "inventories", - "credentials", - "job_templates", - "workflow_job_templates", - "activations", - "event_streams", - "decision_environments", - "eda_credentials", - "namespaces", - ) - for endpoint in _fallback_endpoints: + # Fallback name→id lookup for the single-object path where + # object_id is passed as a name with no explicit type. + # Scoped to Gateway resources only (organizations, teams). + # Resource-level name resolution (EDA/Hub/Controller) is + # handled in the action plugin via type + assignment_objects. + for endpoint in ("organizations", "teams"): resolved = _resolve_fk(manager, endpoint, "name", object_id) if resolved is not None: api_data["object_id"] = str(resolved)