├── ansible ├── roles │ ├── opa │ │ ├── templates │ │ │ ├── namespace.yaml.j2 │ │ │ ├── service.yaml.j2 │ │ │ ├── route.yaml.j2 │ │ │ └── deployment.yaml.j2 │ │ ├── defaults │ │ │ └── main.yml │ │ └── tasks │ │ │ ├── main.yml │ │ │ └── mtls.yml │ └── opa-policies │ │ ├── defaults │ │ └── main.yml │ │ └── tasks │ │ └── main.yml ├── vars │ └── main.yml ├── requirements.yml ├── inventory.yml ├── deploy-opa-mtls.yml ├── load-opa-policies-mtls.yml └── README.md ├── aap_policy_examples ├── allowed_false.rego ├── superuser_allowed_false.rego ├── global_credential_allowed_false.rego ├── restrict_inv_use_to_org.rego ├── maintenance_window.rego ├── jt_naming_validation.rego ├── project_scm_branch.rego ├── github_repo_validation.rego ├── extra_vars_allowlist.rego ├── extra_vars_validation.rego ├── mismatch_prefix_allowed_false.rego └── team_based_extra_vars_restriction.rego ├── .gitignore ├── test_aap_policy_examples ├── allowed_false_test.rego ├── superuser_allowed_false_test.rego ├── maintenance_window_test.rego ├── global_credential_allowed_false_test.rego ├── extra_vars_allowlist_test.rego ├── extra_vars_validation_test.rego ├── mismatch_prefix_allowed_false_test.rego └── team_based_extra_vars_restriction_test.rego ├── .github └── workflows │ └── test.yml ├── POLICY_OUTPUT_DATA.md ├── LICENSE ├── openshift └── opa-deployment.yaml ├── docs ├── Enabling Policy as Code feature.md ├── Deploy OPA server with Podman.md ├── Deploy OPA server on OpenShift.md ├── Configuring OPA Server Connection.md └── Associating policy with AAP resources.md ├── 2.Prevent job execution by platform admin.md ├── 7b.Only allow certain Git branches.md ├── 7a.Only allow approved Github repos.md ├── 3.Prevent job execution during maintenance window.md ├── 9.Restrict Inventory use to an organization.md ├── 1.Prevent job execution at different policy enforcement points.md ├── tools ├── sync_policy_docs.py └── sync_policies.py ├── 8.Enforce Job Template Naming Standards.md ├── Makefile ├── 4.Prevent job execution using credential with no Organization.md ├── 6a.Prevent job execution using extra_vars with non approved keys.md ├── README.md ├── 6b.Prevent job execution using extra_vars with non approved values.md ├── 6c.Prevent job execution based on user limitations for extra vars.md ├── 5.Prevent job execution using mismatching resources.md └── POLICY_INPUT_DATA.md /ansible/roles/opa/templates/namespace.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: "{{ opa_namespace }}" -------------------------------------------------------------------------------- /ansible/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Custom variables can be added here 3 | # These will override the defaults in roles/opa/defaults/main.yml -------------------------------------------------------------------------------- /ansible/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - name: kubernetes.core 4 | version: ">=5.1.0" 5 | - name: community.crypto 6 | version: ">=2.0.0" -------------------------------------------------------------------------------- /ansible/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | all: 3 | hosts: 4 | localhost: 5 | ansible_connection: local 6 | kubeconfig: /Users/haoli/cluster-info/controller-dev/auth/kubeconfig -------------------------------------------------------------------------------- /aap_policy_examples/allowed_false.rego: -------------------------------------------------------------------------------- 1 | package aap_policy_examples 2 | 3 | import rego.v1 4 | 5 | allowed_false := { 6 | "allowed": false, 7 | "violations": ["No job execution is allowed"], 8 | } 9 | -------------------------------------------------------------------------------- /ansible/deploy-opa-mtls.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Deploy OPA Server on OpenShift with mTLS 3 | hosts: localhost 4 | gather_facts: false 5 | vars_files: 6 | - vars/main.yml 7 | 8 | roles: 9 | - role: opa 10 | -------------------------------------------------------------------------------- /ansible/load-opa-policies-mtls.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Load OPA Policies onto OPA Server 3 | hosts: localhost 4 | gather_facts: false 5 | vars_files: 6 | - vars/main.yml 7 | 8 | roles: 9 | - role: opa-policies 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binary directory 2 | bin/ 3 | 4 | # macOS system files 5 | .DS_Store 6 | 7 | # Editor directories and files 8 | .idea/ 9 | .vscode/ 10 | *.swp 11 | *.swo 12 | 13 | # Generated certificates 14 | ansible/certificates/ 15 | -------------------------------------------------------------------------------- /ansible/roles/opa/templates/service.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: "{{ opa_deployment_name }}" 5 | namespace: "{{ opa_namespace }}" 6 | spec: 7 | selector: 8 | app: "{{ opa_deployment_name }}" 9 | ports: 10 | - name: https 11 | port: 8181 12 | targetPort: 8181 13 | type: ClusterIP -------------------------------------------------------------------------------- /aap_policy_examples/superuser_allowed_false.rego: -------------------------------------------------------------------------------- 1 | package aap_policy_examples 2 | 3 | import rego.v1 4 | 5 | default superuser_allowed_false := { 6 | "allowed": true, 7 | "violations": [], 8 | } 9 | 10 | superuser_allowed_false := { 11 | "allowed": false, 12 | "violations": ["SuperUser is not allow to launch jobs"], 13 | } if { 14 | input.created_by.is_superuser 15 | } 16 | -------------------------------------------------------------------------------- /ansible/roles/opa/templates/route.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: route.openshift.io/v1 2 | kind: Route 3 | metadata: 4 | name: "{{ opa_deployment_name }}" 5 | namespace: "{{ opa_namespace }}" 6 | spec: 7 | host: "{{ route_host }}" 8 | to: 9 | kind: Service 10 | name: "{{ opa_deployment_name }}" 11 | port: 12 | targetPort: https 13 | tls: 14 | termination: passthrough -------------------------------------------------------------------------------- /ansible/roles/opa-policies/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # OPA Configuration 3 | opa_deployment_name: "opa-mtls" 4 | opa_namespace: "opa" 5 | 6 | # TLS Configuration 7 | certificates_dir: "{{ playbook_dir }}/certificates" 8 | 9 | # Directory where policy files are stored 10 | policies_dir: "{{ playbook_dir }}/../aap_policy_examples" 11 | 12 | # Pattern to match policy files 13 | policy_pattern: "*.rego" 14 | -------------------------------------------------------------------------------- /test_aap_policy_examples/allowed_false_test.rego: -------------------------------------------------------------------------------- 1 | package test_aap_policy_examples 2 | 3 | import data.aap_policy_examples 4 | 5 | test_allowed_false_always_false if { 6 | aap_policy_examples.allowed_false.allowed == false 7 | } 8 | 9 | test_allowed_false_has_violation if { 10 | aap_policy_examples.allowed_false.violations[0] == "No job execution is allowed" 11 | } 12 | 13 | test_allowed_false_single_violation if { 14 | count(aap_policy_examples.allowed_false.violations) == 1 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: OPA Policy Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Run OPA Tests 16 | run: make test 17 | 18 | - name: Check Formatting 19 | run: make fmt-check 20 | 21 | - name: Validate Policies 22 | run: make check 23 | 24 | - name: Generate Coverage Report 25 | run: make test-coverage 26 | -------------------------------------------------------------------------------- /ansible/roles/opa/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # OPA Configuration 3 | opa_deployment_name: "opa-mtls" 4 | opa_namespace: "opa" 5 | opa_image: "openpolicyagent/opa:latest-static" 6 | 7 | # TLS Configuration 8 | ca_cert_validity_days: 365 9 | server_cert_validity_days: 365 10 | client_cert_validity_days: 365 11 | certificates_dir: "{{ playbook_dir }}/certificates" 12 | 13 | # Kubernetes Configuration 14 | # Use environment variable K8S_AUTH_KUBECONFIG or set this variable 15 | kubeconfig: "{{ lookup('env', 'K8S_AUTH_KUBECONFIG') | default('~/.kube/config', true) }}" 16 | 17 | # Route Configuration 18 | create_route: true -------------------------------------------------------------------------------- /POLICY_OUTPUT_DATA.md: -------------------------------------------------------------------------------- 1 | # Expected output from OPA policy evaluation 2 | 3 | This document describes the expected output from OPA policy query 4 | 5 | ## Expected Fields 6 | 7 | ### `allowed` 8 | - **Type:** Boolean 9 | - **Description:** Indicates whether the action is permitted. 10 | 11 | ### `violations` 12 | - **Type:** List of Strings 13 | - **Description:** Reasons why the action is not allowed. 14 | 15 | ## Example output expected from OPA policy query 16 | 17 | ```json 18 | { 19 | "allowed": false, 20 | "violations": [ 21 | "No job execution is allowed", 22 | ... 23 | ], 24 | ... 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /aap_policy_examples/global_credential_allowed_false.rego: -------------------------------------------------------------------------------- 1 | package aap_policy_examples 2 | 3 | # Find credentials with no organization 4 | violating_credentials := {cred.name | cred := input.credentials[_]; cred.organization == null} 5 | 6 | default global_credential_allowed_false := { 7 | "allowed": true, 8 | "violations": [], 9 | } 10 | 11 | # If any credential is violating, deny access and return violations 12 | global_credential_allowed_false := { 13 | "allowed": false, 14 | "violations": [sprintf("Credential used in job execution does not belong to any org. Violating credentials: [%s]", [concat(", ", violating_credentials)])], 15 | } if { 16 | count(violating_credentials) > 0 17 | } 18 | -------------------------------------------------------------------------------- /aap_policy_examples/restrict_inv_use_to_org.rego: -------------------------------------------------------------------------------- 1 | package aap_policy_examples 2 | 3 | import rego.v1 4 | 5 | # Default policy response indicating allowed status with no violations 6 | default organization_inventory_validation := { 7 | "allowed": true, 8 | "violations": [], 9 | } 10 | 11 | # Validate that only "Default" can use "Demo Inventory" 12 | organization_inventory_validation := result if { 13 | # Extract values from input 14 | inventory_name := object.get(input, ["inventory", "name"], "") 15 | org_name := object.get(input, ["organization", "name"], "") 16 | 17 | # Check if inventory is "Demo Inventory" 18 | inventory_name == "Demo Inventory" 19 | 20 | # Check if organization is not "Default" 21 | org_name != "Default" 22 | 23 | result := { 24 | "allowed": false, 25 | "violations": ["Only the 'Default' organization should use the 'Demo Inventory' inventory"], 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test_aap_policy_examples/superuser_allowed_false_test.rego: -------------------------------------------------------------------------------- 1 | package test_aap_policy_examples 2 | 3 | import data.aap_policy_examples 4 | 5 | test_superuser_blocked if { 6 | test_input := {"created_by": {"is_superuser": true}} 7 | aap_policy_examples.superuser_allowed_false.allowed == false with input as test_input 8 | } 9 | 10 | test_superuser_violation_message if { 11 | test_input := {"created_by": {"is_superuser": true}} 12 | aap_policy_examples.superuser_allowed_false.violations[0] == "SuperUser is not allow to launch jobs" with input as test_input 13 | } 14 | 15 | test_non_superuser_allowed if { 16 | test_input := {"created_by": {"is_superuser": false}} 17 | aap_policy_examples.superuser_allowed_false.allowed == true with input as test_input 18 | } 19 | 20 | test_non_superuser_no_violations if { 21 | test_input := {"created_by": {"is_superuser": false}} 22 | count(aap_policy_examples.superuser_allowed_false.violations) == 0 with input as test_input 23 | } 24 | -------------------------------------------------------------------------------- /aap_policy_examples/maintenance_window.rego: -------------------------------------------------------------------------------- 1 | package aap_policy_examples 2 | 3 | # Define maintenance window in UTC 4 | maintenance_start_hour := 12 # 12:00 UTC (5 PM EST) 5 | 6 | maintenance_end_hour := 4 # 04:00 UTC (9 AM EST) 7 | 8 | # Extract the job creation timestamp (which is in UTC) 9 | created_clock := time.clock(time.parse_rfc3339_ns(input.created)) # returns [hour, minute, second] 10 | 11 | created_hour_utc := created_clock[0] 12 | 13 | # Check if job was created within the maintenance window (UTC) 14 | is_maintenance_time if { 15 | created_hour_utc >= maintenance_start_hour # After 12:00 UTC 16 | } 17 | 18 | is_maintenance_time if { 19 | created_hour_utc <= maintenance_end_hour # Before or at 04:00 UTC 20 | } 21 | 22 | default maintenance_window := { 23 | "allowed": true, 24 | "violations": [], 25 | } 26 | 27 | maintenance_window := { 28 | "allowed": false, 29 | "violations": ["No job execution allowed outside of maintenance window"], 30 | } if { 31 | not is_maintenance_time 32 | } 33 | -------------------------------------------------------------------------------- /aap_policy_examples/jt_naming_validation.rego: -------------------------------------------------------------------------------- 1 | package aap_policy_examples 2 | 3 | import rego.v1 4 | 5 | # Default policy response indicating allowed status with no violations 6 | default jt_naming_validation := { 7 | "allowed": true, 8 | "violations": [], 9 | } 10 | 11 | # Validate that job template name has correct organization and project name prefixes 12 | jt_naming_validation := result if { 13 | # Extract values from input 14 | org_name := object.get(input, ["organization", "name"], "") 15 | project_name := object.get(input, ["project", "name"], "") 16 | jt_name := object.get(input, ["job_template", "name"], "") 17 | 18 | # Construct the expected prefix 19 | expected_prefix := concat("_", [org_name, project_name]) 20 | 21 | # Check if job template name starts with expected prefix 22 | not startswith(jt_name, expected_prefix) 23 | 24 | result := { 25 | "allowed": false, 26 | "violations": [sprintf("Job template naming for '%v' does not comply with standards", [jt_name])] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /aap_policy_examples/project_scm_branch.rego: -------------------------------------------------------------------------------- 1 | package aap_policy_examples 2 | 3 | import rego.v1 4 | 5 | # Define allowed values for project.scm_branch 6 | valid_project_scm_branch_values := ["main", "v1"] 7 | 8 | # Default policy response indicating allowed status with no violations 9 | default project_scm_branch_validation := { 10 | "allowed": true, 11 | "violations": [], 12 | } 13 | 14 | # Evaluate branch_validation to check if project.scm_branch value is allowed 15 | project_scm_branch_validation := result if { 16 | # Extract project.scm_branch from input 17 | branch := object.get(input, ["project", "scm_branch"], "") 18 | 19 | # Check if branch value is not in the allowed list 20 | not allowed_branch(branch) 21 | 22 | result := { 23 | "allowed": false, 24 | "violations": [sprintf("Invalid branch: %v. Only named 'main' or 'v1' branches are allowed.", [branch])], 25 | } 26 | } 27 | 28 | # Check if a given branch value is allowed 29 | allowed_branch(branch) if { 30 | some allowed_value in valid_project_scm_branch_values 31 | branch == allowed_value 32 | } 33 | -------------------------------------------------------------------------------- /ansible/roles/opa-policies/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure policies directory exists 3 | file: 4 | path: "{{ policies_dir }}" 5 | state: directory 6 | mode: '0755' 7 | 8 | - name: Find all policy files 9 | find: 10 | paths: "{{ policies_dir }}" 11 | patterns: "{{ policy_pattern }}" 12 | register: policy_files 13 | 14 | - name: Get OPA route hostname 15 | kubernetes.core.k8s_info: 16 | kind: Route 17 | name: "{{ opa_deployment_name }}" 18 | namespace: "{{ opa_namespace }}" 19 | register: opa_route 20 | 21 | - name: Load each policy into OPA 22 | uri: 23 | url: "https://{{ opa_route.resources[0].spec.host }}/v1/policies/{{ item.path | basename }}" 24 | method: PUT 25 | body: "{{ lookup('file', item.path) }}" 26 | body_format: raw 27 | client_cert: "{{ certificates_dir }}/client.crt" 28 | client_key: "{{ certificates_dir }}/client.key" 29 | ca_path: "{{ certificates_dir }}/ca.crt" 30 | status_code: [200, 201] 31 | loop: "{{ policy_files.files }}" 32 | loop_control: 33 | label: "{{ item.path | basename }}" 34 | -------------------------------------------------------------------------------- /ansible/roles/opa/templates/deployment.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: "{{ opa_deployment_name }}" 5 | namespace: "{{ opa_namespace }}" 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: "{{ opa_deployment_name }}" 11 | template: 12 | metadata: 13 | labels: 14 | app: "{{ opa_deployment_name }}" 15 | spec: 16 | containers: 17 | - name: opa-server 18 | image: "{{ opa_image }}" 19 | args: 20 | - "run" 21 | - "--server" 22 | - "--authentication=tls" 23 | - "--tls-cert-file=/etc/opa/certs/server.crt" 24 | - "--tls-private-key-file=/etc/opa/certs/server.key" 25 | - "--tls-ca-cert-file=/etc/opa/certs/ca.crt" 26 | - "--addr=:8181" 27 | ports: 28 | - containerPort: 8181 29 | name: https 30 | protocol: TCP 31 | volumeMounts: 32 | - name: opa-certs 33 | mountPath: /etc/opa/certs 34 | volumes: 35 | - name: opa-certs 36 | secret: 37 | secretName: "{{ opa_deployment_name }}-certs" -------------------------------------------------------------------------------- /aap_policy_examples/github_repo_validation.rego: -------------------------------------------------------------------------------- 1 | package aap_policy_examples 2 | 3 | import rego.v1 4 | 5 | # Define list of allowed GitHub repositories 6 | allowed_github_repos := [ 7 | "organization/repo1", 8 | "organization/repo2" 9 | ] 10 | 11 | # Default policy response indicating allowed status with no violations 12 | default github_repo_validation := { 13 | "allowed": true, 14 | "violations": [], 15 | } 16 | 17 | # Validate that the GitHub repository is in the whitelist 18 | github_repo_validation := result if { 19 | # Extract SCM URL from input 20 | scm_url := object.get(input, ["project", "scm_url"], "") 21 | 22 | # Extract repository path from URL 23 | parts := split(scm_url, "/") 24 | count_parts := count(parts) 25 | org := parts[count_parts-2] 26 | repo_name := trim_suffix(parts[count_parts-1], ".git") 27 | repo_path := concat("/", [org, repo_name]) 28 | 29 | # Check if repo path is not in the whitelist 30 | not repo_path in allowed_github_repos 31 | 32 | result := { 33 | "allowed": false, 34 | "violations": [sprintf("Repository '%v' is not in the allowed list", [repo_path])], 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /aap_policy_examples/extra_vars_allowlist.rego: -------------------------------------------------------------------------------- 1 | package aap_policy_examples 2 | 3 | import rego.v1 4 | 5 | # Define the allowed keys for extra_vars 6 | allowed_extra_var_keys := ["allowed"] 7 | 8 | # Default policy result: allowed (no violations) 9 | default extra_vars_allowlist := { 10 | "allowed": true, 11 | "violations": [], 12 | } 13 | 14 | # Evaluate extra_vars_allowlist, checking if provided extra_vars contain any keys not allowed 15 | extra_vars_allowlist := result if { 16 | # Extract extra_vars from input, defaulting to empty object if missing 17 | input_extra_var_keys := object.get(input, ["extra_vars"], {}) 18 | 19 | # Identify keys in extra_vars that are not in the allowed list 20 | violating_keys := [key | input_extra_var_keys[key]; not allowed_key(key)] 21 | 22 | # If violating keys are found, construct result indicating disallowed status and violations 23 | count(violating_keys) > 0 24 | 25 | result := { 26 | "allowed": false, 27 | "violations": [sprintf("Following extra_vars are not allowed: %v. Allowed keys: %v", [violating_keys, allowed_extra_var_keys])], 28 | } 29 | } 30 | 31 | # Helper function: Checks if a given key is in the allowed_extra_var_keys list 32 | allowed_key(key) if { 33 | allowed_extra_var_keys[_] == key 34 | } 35 | -------------------------------------------------------------------------------- /test_aap_policy_examples/maintenance_window_test.rego: -------------------------------------------------------------------------------- 1 | package test_aap_policy_examples 2 | 3 | import data.aap_policy_examples 4 | 5 | test_maintenance_window_allowed if { 6 | test_input := {"created": "2025-02-27T13:00:00Z"} # 13:00 UTC (within window) 7 | aap_policy_examples.maintenance_window.allowed == true with input as test_input 8 | } 9 | 10 | test_maintenance_window_blocked if { 11 | test_input := {"created": "2025-02-27T10:00:00Z"} # 10:00 UTC (outside window) 12 | aap_policy_examples.maintenance_window.allowed == false with input as test_input 13 | } 14 | 15 | test_maintenance_window_violation_message if { 16 | test_input := {"created": "2025-02-27T10:00:00Z"} 17 | aap_policy_examples.maintenance_window.violations[0] == "No job execution allowed outside of maintenance window" with input as test_input 18 | } 19 | 20 | test_maintenance_window_edge_case_start if { 21 | test_input := {"created": "2025-02-27T12:00:00Z"} # Exactly at start time 22 | aap_policy_examples.maintenance_window.allowed == true with input as test_input 23 | } 24 | 25 | test_maintenance_window_edge_case_end if { 26 | test_input := {"created": "2025-02-27T04:00:00Z"} # Exactly at end time 27 | aap_policy_examples.maintenance_window.allowed == true with input as test_input 28 | } 29 | -------------------------------------------------------------------------------- /aap_policy_examples/extra_vars_validation.rego: -------------------------------------------------------------------------------- 1 | package aap_policy_examples 2 | 3 | import rego.v1 4 | 5 | # Define allowed values for specific keys in extra_vars 6 | valid_extra_var_values := {"extra_var_key": ["allowed_value1", "allowed_value2"]} 7 | 8 | # Default policy response indicating allowed status with no violations 9 | default extra_vars_validation := { 10 | "allowed": true, 11 | "violations": [], 12 | } 13 | 14 | # Evaluate extra_vars_validation to check if extra_vars values are allowed 15 | extra_vars_validation := result if { 16 | # Extract extra_vars from input, defaulting to empty object if missing 17 | input_extra_vars := object.get(input, ["extra_vars"], {}) 18 | 19 | # Identify keys with disallowed values 20 | violating_keys := [key | 21 | valid_extra_var_values[key] 22 | not allowed_value(key, input_extra_vars[key]) 23 | ] 24 | 25 | # Check if there are any violations 26 | count(violating_keys) > 0 27 | 28 | result := { 29 | "allowed": false, 30 | "violations": [sprintf("extra_vars contain disallowed values for keys: %v. Allowed values: %v", [violating_keys, valid_extra_var_values])], 31 | } 32 | } 33 | 34 | # Check if a given value for a key is allowed 35 | allowed_value(key, value) if { 36 | valid_extra_var_values[key][_] == value 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 19 | NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /openshift/opa-deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: opa 6 | labels: 7 | app: opa 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: opa 13 | template: 14 | metadata: 15 | labels: 16 | app: opa 17 | name: opa 18 | spec: 19 | containers: 20 | - name: opa 21 | image: openpolicyagent/opa:${OPA_VERSION}-static 22 | ports: 23 | - name: http 24 | containerPort: 8181 25 | args: 26 | - "run" 27 | - "--ignore=.*" # exclude hidden dirs created by Kubernetes 28 | - "--server" 29 | - "--log-level=debug" 30 | - "--addr=:8181" 31 | --- 32 | kind: Service 33 | apiVersion: v1 34 | metadata: 35 | name: opa 36 | labels: 37 | app: opa 38 | spec: 39 | type: ClusterIP 40 | selector: 41 | app: opa 42 | ports: 43 | - name: http 44 | protocol: TCP 45 | port: 8181 46 | targetPort: 8181 47 | --- 48 | apiVersion: route.openshift.io/v1 49 | kind: Route 50 | metadata: 51 | labels: 52 | app: opa 53 | name: opa 54 | spec: 55 | port: 56 | targetPort: http 57 | to: 58 | kind: Service 59 | name: opa 60 | weight: 100 61 | wildcardPolicy: None -------------------------------------------------------------------------------- /ansible/roles/opa/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create OPA namespace 3 | kubernetes.core.k8s: 4 | state: present 5 | kubeconfig: "{{ kubeconfig }}" 6 | definition: "{{ lookup('template', 'namespace.yaml.j2') | from_yaml }}" 7 | 8 | - name: Get OpenShift ingress domain 9 | shell: oc get ingresscontroller -n openshift-ingress-operator default -o jsonpath='{.status.domain}' --kubeconfig {{ kubeconfig }} 10 | register: ingress_domain 11 | when: create_route | default(true) | bool 12 | 13 | - name: Set route hostname 14 | set_fact: 15 | route_host: "{{ opa_deployment_name }}-{{ opa_namespace }}.{{ ingress_domain.stdout }}" 16 | when: create_route | default(true) | bool 17 | 18 | - name: Include mTLS tasks 19 | include_tasks: mtls.yml 20 | 21 | - name: Create OPA Deployment 22 | kubernetes.core.k8s: 23 | state: present 24 | kubeconfig: "{{ kubeconfig }}" 25 | definition: "{{ lookup('template', 'deployment.yaml.j2') | from_yaml }}" 26 | 27 | - name: Create OPA Service 28 | kubernetes.core.k8s: 29 | state: present 30 | kubeconfig: "{{ kubeconfig }}" 31 | definition: "{{ lookup('template', 'service.yaml.j2') | from_yaml }}" 32 | 33 | - name: Create OPA Route 34 | kubernetes.core.k8s: 35 | state: present 36 | kubeconfig: "{{ kubeconfig }}" 37 | definition: "{{ lookup('template', 'route.yaml.j2') | from_yaml }}" 38 | when: create_route | default(true) | bool -------------------------------------------------------------------------------- /test_aap_policy_examples/global_credential_allowed_false_test.rego: -------------------------------------------------------------------------------- 1 | package test_aap_policy_examples 2 | 3 | import data.aap_policy_examples 4 | 5 | test_global_credential_blocked if { 6 | test_input := {"credentials": [{ 7 | "name": "global_cred", 8 | "organization": null, 9 | }]} 10 | aap_policy_examples.global_credential_allowed_false.allowed == false with input as test_input 11 | } 12 | 13 | test_global_credential_violation_message if { 14 | test_input := {"credentials": [{ 15 | "name": "global_cred", 16 | "organization": null, 17 | }]} 18 | aap_policy_examples.global_credential_allowed_false.violations[0] == "Credential used in job execution does not belong to any org. Violating credentials: [global_cred]" with input as test_input 19 | } 20 | 21 | test_org_credential_allowed if { 22 | test_input := {"credentials": [{ 23 | "name": "org_cred", 24 | "organization": "org1", 25 | }]} 26 | aap_policy_examples.global_credential_allowed_false.allowed == true with input as test_input 27 | } 28 | 29 | test_org_credential_no_violations if { 30 | test_input := {"credentials": [{ 31 | "name": "org_cred", 32 | "organization": "org1", 33 | }]} 34 | count(aap_policy_examples.global_credential_allowed_false.violations) == 0 with input as test_input 35 | } 36 | 37 | test_multiple_credentials_mixed if { 38 | test_input := {"credentials": [ 39 | { 40 | "name": "org_cred", 41 | "organization": "org1", 42 | }, 43 | { 44 | "name": "global_cred", 45 | "organization": null, 46 | }, 47 | ]} 48 | aap_policy_examples.global_credential_allowed_false.allowed == false with input as test_input 49 | } 50 | -------------------------------------------------------------------------------- /docs/Enabling Policy as Code feature.md: -------------------------------------------------------------------------------- 1 | # Enabling Policy as Code Feature in Ansible Automation Platform 2 | 3 | This document describes how to enable feature flags in Ansible Automation Platform (AAP) for different installation types. 4 | 5 | ## OpenShift Installation 6 | 7 | For OpenShift installations, you need to modify the `AnsibleAutomationPlatform` custom resource. Add the following to the `spec` section: 8 | 9 | ```yaml 10 | spec: 11 | feature_flags: 12 | FEATURE_POLICY_AS_CODE_ENABLED: True 13 | ``` 14 | 15 | After applying the changes, wait for the operator to complete the update process. The operator will automatically handle the necessary service restarts and configuration updates. 16 | 17 | ## RPM Installation 18 | 19 | For RPM-based installations, modify your inventory file used by the installer and add: 20 | 21 | ```yaml 22 | feature_flags: 23 | FEATURE_POLICY_AS_CODE_ENABLED: True 24 | ``` 25 | 26 | After modifying the inventory file, you will need to rerun the installer to apply the changes. 27 | 28 | ## Containerized Installation 29 | 30 | For containerized installations, modify your inventory file used by the installer and add: 31 | 32 | ```yaml 33 | feature_flags: 34 | FEATURE_POLICY_AS_CODE_ENABLED: True 35 | ``` 36 | 37 | After modifying the inventory file, you will need to rerun the installer to apply the changes. 38 | 39 | ## Verifying Feature Flag Status 40 | 41 | To verify that the feature flag is enabled, you can check the feature flags state endpoint: 42 | 43 | ``` 44 | https:///api/controller/v2/feature_flags_state/ 45 | ``` 46 | 47 | This endpoint will return a JSON response containing the current state of all feature flags, including `FEATURE_POLICY_AS_CODE_ENABLED`. -------------------------------------------------------------------------------- /aap_policy_examples/mismatch_prefix_allowed_false.rego: -------------------------------------------------------------------------------- 1 | package aap_policy_examples 2 | 3 | prefix_delimiter := "_" 4 | 5 | # job_template_prefix extracts the substring before the first prefix_delimiter in `input.job_template.name`. 6 | job_template_prefix := jtp if { 7 | parts := split(input.job_template.name, prefix_delimiter) 8 | jtp := parts[0] 9 | } 10 | 11 | # inventory_prefix extracts the substring before the first prefix_delimiter in `input.inventory.name`. 12 | inventory_prefix := inv_pref if { 13 | parts := split(input.inventory.name, prefix_delimiter) 14 | inv_pref := parts[0] 15 | } 16 | 17 | # project_prefix extracts the substring before the first prefix_delimiter in `input.project.name`. 18 | project_prefix := proj_pref if { 19 | parts := split(input.project.name, prefix_delimiter) 20 | proj_pref := parts[0] 21 | } 22 | 23 | # credentials_prefixes is a list of prefix values from each credential's name. 24 | credentials_prefixes := [cprefix | 25 | cred := input.credentials[_] # iterate over credentials 26 | parts := split(cred.name, prefix_delimiter) # split name 27 | cprefix := parts[0] # grab the first part 28 | ] 29 | 30 | # mismatch is true if either: 31 | # 1. The project prefix != job template prefix, OR 32 | # 2. The inventory prefix != job template prefix OR 33 | # 3. Any credential's prefix != job template prefix. 34 | mismatch if { 35 | project_prefix != job_template_prefix 36 | } 37 | 38 | mismatch if { 39 | inventory_prefix != job_template_prefix 40 | } 41 | 42 | mismatch if { 43 | some cp in credentials_prefixes 44 | cp != job_template_prefix 45 | } 46 | 47 | default mismatch_prefix_allowed_false := { 48 | "allowed": true, 49 | "violations": [], 50 | } 51 | 52 | mismatch_prefix_allowed_false := { 53 | "allowed": false, 54 | "violations": ["Mismatch prefix between Inventory, Credentials and Project detected."], 55 | } if { 56 | mismatch 57 | } 58 | -------------------------------------------------------------------------------- /aap_policy_examples/team_based_extra_vars_restriction.rego: -------------------------------------------------------------------------------- 1 | package aap_policy_examples 2 | 3 | import rego.v1 4 | 5 | # Define allowed values for specific keys in extra_vars based on teams 6 | valid_extra_var_values_by_team := { 7 | "dev_team": {"environment": ["dev", "staging"]}, 8 | "prod_team": {"environment": ["prod", "staging"]}, 9 | } 10 | 11 | # Default response allowing extra_vars unless violations occur 12 | default team_based_extra_vars_restriction := { 13 | "allowed": true, 14 | "violations": [], 15 | } 16 | 17 | # Evaluate extra_vars against allowed values considering team memberships 18 | team_based_extra_vars_restriction := result if { 19 | # Extract extra_vars from input 20 | input_extra_vars := object.get(input, ["extra_vars"], {}) 21 | 22 | # Extract user's team names 23 | user_teams := {team | team := input.created_by.teams[_].name} 24 | 25 | violating_keys := [key | 26 | allowed_vals := allowed_values_for_key_and_teams(key, user_teams) 27 | input_value := input_extra_vars[key] 28 | allowed_values_for_key_and_teams(key, user_teams) 29 | not allowed_value(input_value, allowed_vals) 30 | ] 31 | 32 | count(violating_keys) > 0 33 | 34 | result := { 35 | "allowed": false, 36 | "violations": [sprintf("extra_vars contain disallowed values for keys: %v. Allowed extra_vars for your teams (%v): %v", [violating_keys, user_teams, allowed_values_for_user_teams(user_teams)])], 37 | } 38 | } 39 | 40 | # Retrieve allowed values for a specific key based on user's teams 41 | allowed_values_for_key_and_teams(key, teams) := values if { 42 | values := {val | team := teams[_]; val := valid_extra_var_values_by_team[team][key][_]; valid_extra_var_values_by_team[team][key]} 43 | } 44 | 45 | # Retrieve all allowed values based on user's teams 46 | allowed_values_for_user_teams(teams) := team_values if { 47 | team_values := {team: valid_extra_var_values_by_team[team] | team := teams[_]; valid_extra_var_values_by_team[team]} 48 | } 49 | 50 | # Check if given value is in allowed values set 51 | allowed_value(value, allowed_values) if { 52 | allowed_values[_] == value 53 | } 54 | -------------------------------------------------------------------------------- /test_aap_policy_examples/extra_vars_allowlist_test.rego: -------------------------------------------------------------------------------- 1 | package test_aap_policy_examples 2 | 3 | import data.aap_policy_examples 4 | import rego.v1 5 | 6 | # Test case: Empty extra_vars should be allowed 7 | test_allow_empty_extra_vars if { 8 | aap_policy_examples.extra_vars_allowlist.allowed == true 9 | aap_policy_examples.extra_vars_allowlist.violations == [] 10 | } 11 | 12 | # Test case: Allowed extra_vars key should be allowed 13 | test_allow_valid_extra_vars if { 14 | test_input := {"extra_vars": {"allowed": "some_value"}} 15 | aap_policy_examples.extra_vars_allowlist.allowed == true with input as test_input 16 | aap_policy_examples.extra_vars_allowlist.violations == [] with input as test_input 17 | } 18 | 19 | # Test case: Disallowed extra_vars key should be rejected 20 | test_reject_disallowed_extra_vars if { 21 | test_input := {"extra_vars": {"disallowed": "some_value"}} 22 | aap_policy_examples.extra_vars_allowlist.allowed == false with input as test_input 23 | aap_policy_examples.extra_vars_allowlist.violations == ["Following extra_vars are not allowed: [\"disallowed\"]. Allowed keys: [\"allowed\"]"] with input as test_input 24 | } 25 | 26 | # Test case: Multiple disallowed extra_vars keys should be rejected 27 | test_reject_multiple_disallowed_extra_vars if { 28 | test_input := {"extra_vars": {"disallowed1": "value1", "disallowed2": "value2"}} 29 | aap_policy_examples.extra_vars_allowlist.allowed == false with input as test_input 30 | aap_policy_examples.extra_vars_allowlist.violations == ["Following extra_vars are not allowed: [\"disallowed1\", \"disallowed2\"]. Allowed keys: [\"allowed\"]"] with input as test_input 31 | } 32 | 33 | # Test case: Mix of allowed and disallowed keys should be rejected 34 | test_reject_mixed_extra_vars if { 35 | test_input := {"extra_vars": {"allowed": "value1", "disallowed": "value2"}} 36 | aap_policy_examples.extra_vars_allowlist.allowed == false with input as test_input 37 | aap_policy_examples.extra_vars_allowlist.violations == ["Following extra_vars are not allowed: [\"disallowed\"]. Allowed keys: [\"allowed\"]"] with input as test_input 38 | } 39 | -------------------------------------------------------------------------------- /docs/Deploy OPA server with Podman.md: -------------------------------------------------------------------------------- 1 | # Quick Start Guide 2 | 3 | This guide will help you set up and run the OPA (Open Policy Agent) server using Podman for use with Ansible Automation Platform (AAP). **This setup is intended for testing and development purposes only.** 4 | 5 | ## ⚠️ Security Warning 6 | 7 | This setup runs an OPA server with: 8 | - No authentication (AuthN) 9 | - No authorization (AuthZ) 10 | - No TLS/HTTPS 11 | - No access controls 12 | 13 | **DO NOT use this configuration in production environments.** This setup is designed for local development and testing only. 14 | 15 | ## Prerequisites 16 | 17 | - Podman installed on your system 18 | - Make installed on your system 19 | - Network connectivity between your AAP instance and the OPA server 20 | 21 | ## Running OPA Server with Podman 22 | 23 | 1. First, ensure that your OPA server will be accessible from your AAP instance. The OPA server needs to be reachable via HTTP/HTTPS. 24 | 25 | 2. Run the OPA server using Podman: 26 | 27 | ```bash 28 | make container/run-opa-server 29 | ``` 30 | 31 | This command will: 32 | - Run the OPA server in a Podman container 33 | - Make the server accessible on port 8181 34 | - Mount your policies directory (`aap_policy_examples`) into the container 35 | - Enable file watching for automatic policy updates 36 | - Use the latest OPA version by default 37 | 38 | ## Verifying the Setup 39 | 40 | 1. Test the OPA server's health endpoint: 41 | ```bash 42 | curl http://localhost:8181/health 43 | ``` 44 | 45 | You should receive a response indicating the server is healthy. 46 | 47 | ## Important Notes 48 | 49 | - Make sure your AAP instance can reach the OPA server's address and port 50 | - This setup is for development and testing purposes only 51 | - For production environments, you must: 52 | - Implement proper authentication 53 | - Configure authorization controls 54 | - Enable HTTPS/TLS 55 | - Set up appropriate firewall rules 56 | - Consider using a reverse proxy 57 | - Implement proper access controls 58 | 59 | For more detailed information about OPA configuration and policy management, refer to the [official OPA documentation](https://www.openpolicyagent.org/docs/latest/). -------------------------------------------------------------------------------- /test_aap_policy_examples/extra_vars_validation_test.rego: -------------------------------------------------------------------------------- 1 | package test_aap_policy_examples 2 | 3 | import data.aap_policy_examples 4 | import rego.v1 5 | 6 | # Test case: Empty extra_vars should be allowed 7 | test_allow_empty_extra_vars if { 8 | aap_policy_examples.extra_vars_validation.allowed == true 9 | aap_policy_examples.extra_vars_validation.violations == [] 10 | } 11 | 12 | # Test case: Valid extra_vars value should be allowed 13 | test_allow_valid_extra_vars if { 14 | test_input := {"extra_vars": {"extra_var_key": "allowed_value1"}} 15 | aap_policy_examples.extra_vars_validation.allowed == true with input as test_input 16 | aap_policy_examples.extra_vars_validation.violations == [] with input as test_input 17 | } 18 | 19 | # Test case: Another valid extra_vars value should be allowed 20 | test_allow_another_valid_extra_vars if { 21 | test_input := {"extra_vars": {"extra_var_key": "allowed_value2"}} 22 | aap_policy_examples.extra_vars_validation.allowed == true with input as test_input 23 | aap_policy_examples.extra_vars_validation.violations == [] with input as test_input 24 | } 25 | 26 | # Test case: Invalid extra_vars value should be rejected 27 | test_reject_invalid_extra_vars if { 28 | test_input := {"extra_vars": {"extra_var_key": "invalid_value"}} 29 | aap_policy_examples.extra_vars_validation.allowed == false with input as test_input 30 | aap_policy_examples.extra_vars_validation.violations == ["extra_vars contain disallowed values for keys: [\"extra_var_key\"]. Allowed values: {\"extra_var_key\": [\"allowed_value1\", \"allowed_value2\"]}"] with input as test_input 31 | } 32 | 33 | # Test case: Non-whitelisted key should be allowed (since it's not in valid_extra_var_values) 34 | test_allow_non_whitelisted_key if { 35 | test_input := {"extra_vars": {"other_key": "any_value"}} 36 | aap_policy_examples.extra_vars_validation.allowed == true with input as test_input 37 | aap_policy_examples.extra_vars_validation.violations == [] with input as test_input 38 | } 39 | 40 | # Test case: Multiple valid extra_vars should be allowed 41 | test_allow_multiple_valid_extra_vars if { 42 | test_input := {"extra_vars": {"extra_var_key": "allowed_value1", "other_key": "any_value"}} 43 | aap_policy_examples.extra_vars_validation.allowed == true with input as test_input 44 | aap_policy_examples.extra_vars_validation.violations == [] with input as test_input 45 | } 46 | -------------------------------------------------------------------------------- /2.Prevent job execution by platform admin.md: -------------------------------------------------------------------------------- 1 | # 2. Prevent job execution by platform admin 2 | 3 | In the previous example we looked at a simple policy that does not take in any data. In this example we will demonstrate how to use data provided from Ansible Automation Platform to craft a policy that makes decisions based on the situation. 4 | 5 | 6 | Example policy [aap_policy_examples/superuser_allowed_false.rego](aap_policy_examples/superuser_allowed_false.rego): 7 | 8 | ```rego 9 | package aap_policy_examples 10 | 11 | import rego.v1 12 | 13 | default superuser_allowed_false := { 14 | "allowed": true, 15 | "violations": [], 16 | } 17 | 18 | superuser_allowed_false := { 19 | "allowed": false, 20 | "violations": ["System/Platform Administrator is not allow to launch jobs"], 21 | } if { 22 | input.created_by.is_superuser 23 | } 24 | ``` 25 | 26 | Example input provided by Ansible Automation Platform during query: 27 | 28 | ```json 29 | { 30 | "id": 785, 31 | "name": "Demo Job Template", 32 | "created": "2025-02-27T20:32:14.874821Z", 33 | "created_by": { 34 | "id": 1, 35 | "username": "admin", 36 | "is_superuser": true 37 | }, 38 | ... 39 | } 40 | ``` 41 | 42 | Example output from policy query: 43 | 44 | ```json 45 | { 46 | "allowed": false, 47 | "violations": [ 48 | "SuperUser is not allow to launch jobs", 49 | ], 50 | } 51 | ``` 52 | 53 | This policy will use the `created_by.is_superuser` data from the input to decide if the job execution is `allowed`. In this case, if the job is created by a superuser (platform admin) the job execution will be prevented. 54 | 55 | When applied at different enforcement points, this policy prevents job execution accordingly: 56 | 57 | - Job Template: All jobs launched from the template by a superuser will ERROR and playbook execution will be prevented. 58 | - Inventory: All jobs launched by a superuser using the inventory will ERROR and playbook execution will be prevented. 59 | - Organization: All jobs launched by a superuser using a job template that belongs to the organization will ERROR and playbook execution will be prevented. 60 | 61 | The Policy as Code feature in Ansible Automation Platform is capable of providing all relevant data around the job launch. For more details about the `input` data that Ansible Automation Platform provides for the OPA policy query see [policy input data](POLICY_OUTPUT_DATA.md). 62 | -------------------------------------------------------------------------------- /test_aap_policy_examples/mismatch_prefix_allowed_false_test.rego: -------------------------------------------------------------------------------- 1 | package test_aap_policy_examples 2 | 3 | import data.aap_policy_examples 4 | 5 | test_matching_prefixes_allowed if { 6 | test_input := { 7 | "job_template": {"name": "prod_Job Template"}, 8 | "inventory": {"name": "prod_Inventory"}, 9 | "project": {"name": "prod_Project"}, 10 | "credentials": [{"name": "prod_Credential"}], 11 | } 12 | aap_policy_examples.mismatch_prefix_allowed_false.allowed == true with input as test_input 13 | } 14 | 15 | test_mismatched_inventory_prefix_blocked if { 16 | test_input := { 17 | "job_template": {"name": "prod_Job Template"}, 18 | "inventory": {"name": "dev_Inventory"}, 19 | "project": {"name": "prod_Project"}, 20 | "credentials": [{"name": "prod_Credential"}], 21 | } 22 | aap_policy_examples.mismatch_prefix_allowed_false.allowed == false with input as test_input 23 | } 24 | 25 | test_mismatched_project_prefix_blocked if { 26 | test_input := { 27 | "job_template": {"name": "prod_Job Template"}, 28 | "inventory": {"name": "prod_Inventory"}, 29 | "project": {"name": "dev_Project"}, 30 | "credentials": [{"name": "prod_Credential"}], 31 | } 32 | aap_policy_examples.mismatch_prefix_allowed_false.allowed == false with input as test_input 33 | } 34 | 35 | test_mismatched_credential_prefix_blocked if { 36 | test_input := { 37 | "job_template": {"name": "prod_Job Template"}, 38 | "inventory": {"name": "prod_Inventory"}, 39 | "project": {"name": "prod_Project"}, 40 | "credentials": [{"name": "dev_Credential"}], 41 | } 42 | aap_policy_examples.mismatch_prefix_allowed_false.allowed == false with input as test_input 43 | } 44 | 45 | test_violation_message if { 46 | test_input := { 47 | "job_template": {"name": "prod_Job Template"}, 48 | "inventory": {"name": "dev_Inventory"}, 49 | "project": {"name": "prod_Project"}, 50 | "credentials": [{"name": "prod_Credential"}], 51 | } 52 | aap_policy_examples.mismatch_prefix_allowed_false.violations[0] == "Mismatch prefix between Inventory, Credentials and Project detected." with input as test_input 53 | } 54 | 55 | test_multiple_credentials_all_matching if { 56 | test_input := { 57 | "job_template": {"name": "prod_Job Template"}, 58 | "inventory": {"name": "prod_Inventory"}, 59 | "project": {"name": "prod_Project"}, 60 | "credentials": [ 61 | {"name": "prod_Credential1"}, 62 | {"name": "prod_Credential2"}, 63 | ], 64 | } 65 | aap_policy_examples.mismatch_prefix_allowed_false.allowed == true with input as test_input 66 | } 67 | -------------------------------------------------------------------------------- /docs/Deploy OPA server on OpenShift.md: -------------------------------------------------------------------------------- 1 | # Quick Start Guide 2 | 3 | This guide will help you set up and run the OPA (Open Policy Agent) server on OpenShift Container Platform (OCP) for use with Ansible Automation Platform (AAP). **This setup is intended for testing and development purposes only.** 4 | 5 | ## ⚠️ Security Warning 6 | 7 | This setup runs an OPA server with: 8 | - No authentication (AuthN) 9 | - No authorization (AuthZ) 10 | - No TLS/HTTPS 11 | - No access controls 12 | 13 | **DO NOT use this configuration in production environments.** This setup is designed for development and testing only. 14 | 15 | ## Prerequisites 16 | 17 | - OpenShift CLI (`oc`) installed and configured 18 | - Access to an OpenShift cluster with appropriate permissions 19 | - Network connectivity between your AAP instance and the OPA server 20 | - Make installed on your system 21 | 22 | ## Running OPA Server on OpenShift 23 | 24 | 1. First, ensure that your OPA server will be accessible from your AAP instance. The OPA server needs to be reachable via HTTP/HTTPS. 25 | 26 | 2. Create a new project for OPA (optional, but recommended): 27 | ```bash 28 | oc new-project opa-server 29 | ``` 30 | 31 | 3. Deploy the OPA server using the provided OpenShift manifests: 32 | ```bash 33 | make openshift/deploy-opa-server 34 | ``` 35 | 36 | This command will: 37 | - Create necessary OpenShift resources (Deployment, Service, Route) 38 | - Mount your policies directory (`aap_policy_examples`) into the container 39 | - Enable file watching for automatic policy updates 40 | - Use the latest OPA version by default 41 | - Create a Route for external access 42 | 43 | ## Verifying the Setup 44 | 45 | 1. Get the OPA server's route URL: 46 | ```bash 47 | oc get route opa -o jsonpath='{.spec.host}' 48 | ``` 49 | 50 | 2. Test the OPA server's health endpoint: 51 | ```bash 52 | curl http://$(oc get route opa -o jsonpath='{.spec.host}')/health 53 | ``` 54 | 55 | You should receive a response indicating the server is healthy. 56 | 57 | ## Loading Policies 58 | 59 | The OPA server is configured to automatically load policies from the mounted `aap_policy_examples` directory. However, you can also manually load or update policies using the provided make target. 60 | 61 | 1. Load all policies from the `aap_policy_examples` directory: 62 | ```bash 63 | make openshift/load-policies 64 | ``` 65 | 66 | This command will: 67 | - Get the OPA server's route URL 68 | - Load all `.rego` files from the `aap_policy_examples` directory 69 | - Verify that each policy was loaded successfully 70 | 71 | 2. To verify a specific policy was loaded, you can use: 72 | ```bash 73 | make openshift/verify-policy POLICY_NAME=your-policy-name 74 | ``` 75 | 76 | ## Important Notes 77 | 78 | - Make sure your AAP instance can reach the OPA server's route URL 79 | - This setup is for development and testing purposes only 80 | - For production environments, you must: 81 | - Implement proper authentication 82 | - Configure authorization controls 83 | - Enable HTTPS/TLS 84 | - Set up appropriate network policies 85 | - Consider using a reverse proxy 86 | - Implement proper access controls 87 | - Configure resource limits and requests 88 | - Set up proper monitoring and logging 89 | 90 | For more detailed information about OPA configuration and policy management, refer to the [official OPA documentation](https://www.openpolicyagent.org/docs/latest/). -------------------------------------------------------------------------------- /7b.Only allow certain Git branches.md: -------------------------------------------------------------------------------- 1 | # 7a. Only allow approved Github source branches 2 | 3 | ## Overview 4 | 5 | This example demonstrates how to prevent the use of unapproved Github repo branches for AAP Project automation content. 6 | 7 | ## Example Policy [aap_policy_examples/project_scm_branch.rego](aap_policy_examples/project_scm_branch.rego): 8 | 9 | The following policy (`project_scm_branch.rego`) blocks job execution and is best applied to the Organization policy level: 10 | 11 | ```rego 12 | package aap_policy_examples 13 | 14 | import rego.v1 15 | 16 | # Define allowed values for project.scm_branch 17 | valid_project_scm_branch_values := ["main", "v1"] 18 | 19 | # Default policy response indicating allowed status with no violations 20 | default project_scm_branch_validation := { 21 | "allowed": true, 22 | "violations": [], 23 | } 24 | 25 | # Evaluate branch_validation to check if project.scm_branch value is allowed 26 | project_scm_branch_validation := result if { 27 | # Extract project.scm_branch from input 28 | branch := object.get(input, ["project", "scm_branch"], "") 29 | 30 | # Check if branch value is not in the allowed list 31 | not allowed_branch(branch) 32 | 33 | result := { 34 | "allowed": false, 35 | "violations": [sprintf("Invalid branch: %v. Only named 'main' or 'v1' branches are allowed.", [branch])], 36 | } 37 | } 38 | 39 | # Check if a given branch value is allowed 40 | allowed_branch(branch) if { 41 | some allowed_value in valid_project_scm_branch_values 42 | branch == allowed_value 43 | } 44 | ``` 45 | 46 | ## Enforcement Behavior 47 | 48 | When applied at different enforcement points, this policy prevents job execution accordingly: 49 | 50 | - **Job Template:** All jobs launched from the template will **ERROR**, preventing playbook execution. 51 | - **Inventory:** All jobs using the inventory will **ERROR**, preventing playbook, preventing playbook execution. 52 | - **Organization:** All jobs launch from job template that belongs to the organization will **ERROR**, preventing playbook execution. 53 | 54 | It is recommended to use this at the **Organization** policy enforcement point. 55 | 56 | ## Try It Yourself 57 | 58 | Take advantage of the [Rego playground](https://play.openpolicyagent.org/p/xzsz9n3yeP)! 59 | 60 | ## Real World Use Case: Blocking Unapproved Automation Content / Untrusted Automation Supply Chain 61 | 62 | ### Scenario 63 | 64 | Your company policy mandates the use of only certain approved Github repo branches as these are validated using SDLC controls and mechanisms. This could be used to stop the use of 'devel' and other branches not suitable for Production environments. This policy allows you to police where automation content can be pulled from. 65 | 66 | ## Impact of Policy Enforcement in Ansible Automation Platform 67 | 68 | By applying the Block Policy, Ansible Automation Platform completely prevents automation execution at the specified enforcement points. 69 | This ensures that no changes can be made to critical environments during security incidents, audits, or compliance reviews. 70 | 71 | ## How AAP Enforces the Policy 72 | 73 | Running an automation Job Template when this policy is in place will result in this type of error when an unapproved repo tries to be used: 74 | 75 | ``` 76 | This job cannot be executed due to a policy violation or error. See the following details: 77 | {'Violations': {'Organization': ["Invalid branch: . Only named 'main' or 'v1' " 78 | 'branches are allowed.']}} 79 | ``` 80 | -------------------------------------------------------------------------------- /docs/Configuring OPA Server Connection.md: -------------------------------------------------------------------------------- 1 | # Configuring OPA Server Connection in Ansible Automation Platform 2 | 3 | After enabling the Policy as Code feature, you can configure the connection to your OPA server through the AAP user interface. This document describes how to set up the OPA server connection. 4 | 5 | ## Accessing Policy Settings 6 | 7 | 1. Log in to your Ansible Automation Platform web interface as System Admin 8 | 2. Navigate to **Settings** in the left navigation menu 9 | 3. Select **Policy** from the settings submenu 10 | 4. Click the **Edit** button in the top right corner to modify the policy settings 11 | 12 | ## Available Configuration Options 13 | 14 | ### Basic Settings 15 | 16 | - **OPA server hostname**: The hostname of your OPA server 17 | - Example: `opa-awx.apps.controller-dev.testing.ansible.com` 18 | - If left empty, policy enforcement will be disabled 19 | 20 | - **OPA server port**: The port number on which the OPA server is listening 21 | - Default: `8181` 22 | 23 | - **OPA authentication type**: The authentication method to use when connecting to OPA 24 | - Available options: 25 | - `None`: No authentication 26 | - `Token`: Bearer token authentication 27 | - `Certificate`: Client certificate (mTLS) authentication 28 | 29 | ### Authentication Settings 30 | 31 | #### Token Authentication 32 | When **OPA authentication type** is set to `Token`: 33 | - **OPA authentication token**: The bearer token used for authentication 34 | 35 | Note: If custom headers include an authorization header, it will be overridden by this token 36 | 37 | #### Certificate Authentication 38 | When **OPA authentication type** is set to `Certificate`: 39 | - **OPA client certificate content**: The content of the client certificate file for mTLS authentication 40 | - **OPA client key content**: The content of the client private key for mTLS authentication 41 | - **OPA CA certificate content**: The content of the CA certificate for mTLS authentication 42 | 43 | Note: All three certificate fields are required for certificate authentication 44 | 45 | ### Advanced Settings 46 | 47 | - **OPA request timeout**: The timeout duration for OPA requests in seconds 48 | - Default: `1.5` 49 | 50 | - **OPA request retry count**: Number of times to retry failed OPA requests 51 | - Default: `2` 52 | 53 | ### Security Options 54 | 55 | - **Use SSL for OPA connection**: Enable/disable SSL for secure communication 56 | - Default: `Disabled` 57 | 58 | ### Custom Headers 59 | 60 | - **OPA custom authentication headers**: Optional custom headers for authentication 61 | - Format: YAML or JSON 62 | - Default: Empty dictionary (`{}`) 63 | 64 | Note: If using token authentication, any authorization header here will be overridden by the token setting 65 | 66 | ## After Configuration 67 | 68 | Once you have configured these settings: 69 | 1. Save your changes using the Save button 70 | 2. The system will validate the connection to your OPA server 71 | 3. If successful, AAP will begin using this OPA server for policy decisions 72 | 73 | ## Troubleshooting 74 | 75 | If you encounter connection issues: 76 | 1. Verify the hostname and port are correct 77 | 2. Ensure the OPA server is accessible from your AAP instance 78 | 3. Check if any firewalls are blocking the connection 79 | 4. Verify SSL settings if enabled 80 | 5. Review the authentication configuration: 81 | - For token authentication: Verify the token is valid and properly formatted 82 | - For certificate authentication: Ensure all certificate and key contents are properly provided 83 | - For custom headers: Verify the YAML/JSON format is correct -------------------------------------------------------------------------------- /7a.Only allow approved Github repos.md: -------------------------------------------------------------------------------- 1 | # 7a. Only allow approved Github source repositories 2 | 3 | ## Overview 4 | 5 | This example demonstrates how to prevent the use of unapproved Github repos for AAP Project automation content. 6 | 7 | ## Example Policy [aap_policy_examples/github_repo_validation.rego](aap_policy_examples/github_repo_validation.rego): 8 | 9 | The following policy (`github_repo_validation.rego`) blocks job execution and is best applied to the Organization policy level: 10 | 11 | ```rego 12 | package aap_policy_examples 13 | 14 | import rego.v1 15 | 16 | # Define list of allowed GitHub repositories 17 | allowed_github_repos := [ 18 | "organization/repo1", 19 | "organization/repo2" 20 | ] 21 | 22 | # Default policy response indicating allowed status with no violations 23 | default github_repo_validation := { 24 | "allowed": true, 25 | "violations": [], 26 | } 27 | 28 | # Validate that the GitHub repository is in the whitelist 29 | github_repo_validation := result if { 30 | # Extract SCM URL from input 31 | scm_url := object.get(input, ["project", "scm_url"], "") 32 | 33 | # Extract repository path from URL 34 | parts := split(scm_url, "/") 35 | count_parts := count(parts) 36 | org := parts[count_parts-2] 37 | repo_name := trim_suffix(parts[count_parts-1], ".git") 38 | repo_path := concat("/", [org, repo_name]) 39 | 40 | # Check if repo path is not in the whitelist 41 | not repo_path in allowed_github_repos 42 | 43 | result := { 44 | "allowed": false, 45 | "violations": [sprintf("Repository '%v' is not in the allowed list", [repo_path])], 46 | } 47 | } 48 | ``` 49 | 50 | ## Enforcement Behavior 51 | 52 | When applied at different enforcement points, this policy prevents job execution accordingly: 53 | 54 | - **Job Template:** All jobs launched from the template will **ERROR**, preventing playbook execution. 55 | - **Inventory:** All jobs using the inventory will **ERROR**, preventing playbook, preventing playbook execution. 56 | - **Organization:** All jobs launch from job template that belongs to the organization will **ERROR**, preventing playbook execution. 57 | 58 | It is recommended to use this at the **Organization policy** enforcement point. 59 | 60 | ## Try It Yourself 61 | 62 | Take advantage of the [Rego playground](https://play.openpolicyagent.org/p/W9MkwQ5Idm)! 63 | 64 | ## Real World Use Case: Blocking Unapproved Automation Content / Untrusted Automation Supply Chain 65 | 66 | ### Scenario 67 | 68 | Your company policy mandates the use of only certain approved Github repos as these are validated using SDLC controls and mechanisms. Personal Github accounts may not be allowed as they do not undergone the same stringent checks. This policy allows you to police where automation content can be pulled from. 69 | 70 | ## Impact of Policy Enforcement in Ansible Automation Platform 71 | 72 | By applying the Block Policy, Ansible Automation Platform completely prevents automation execution at the specified enforcement points. 73 | This ensures that no changes can be made to critical environments during security incidents, audits, or compliance reviews. 74 | 75 | ## How AAP Enforces the Policy 76 | 77 | Running an automation Job Template when this policy is in place will result in this type of error when an unapproved repo tries to be used: 78 | 79 | ``` 80 | This job cannot be executed due to a policy violation or error. See the following details: 81 | {'Violations': {'Organization': ["Repository 'ansible/ansible-tower-samples' is not in the " 82 | 'allowed list']}} 83 | ``` 84 | -------------------------------------------------------------------------------- /3.Prevent job execution during maintenance window.md: -------------------------------------------------------------------------------- 1 | # 3. Preventing Job Execution by Maintenance Window 2 | 3 | In this example, we'll demonstrate how to use Ansible Automation Platform to create a policy that makes decisions based on when a job is launched, specifically blocking execution if it is initiated during a timeframe, e.g. 5PM to 9AM EST. 4 | 5 | ## Example Policy [aap_policy_examples/maintenance_window.rego](aap_policy_examples/maintenance_window.rego): 6 | 7 | The following policy (`aap_policy_examples/maintenance_window.rego`) prevents users from launching a job during restricted hours: 8 | 9 | ```rego 10 | package aap_policy_examples 11 | 12 | # Define maintenance window in UTC 13 | maintenance_start_hour := 12 # 12:00 UTC (5 PM EST) 14 | 15 | maintenance_end_hour := 4 # 04:00 UTC (9 AM EST) 16 | 17 | # Extract the job creation timestamp (which is in UTC) 18 | created_clock := time.clock(time.parse_rfc3339_ns(input.created)) # returns [hour, minute, second] 19 | 20 | created_hour_utc := created_clock[0] 21 | 22 | # Check if job was created within the maintenance window (UTC) 23 | is_maintenance_time if { 24 | created_hour_utc >= maintenance_start_hour # After 12:00 UTC 25 | } 26 | 27 | is_maintenance_time if { 28 | created_hour_utc <= maintenance_end_hour # Before or at 04:00 UTC 29 | } 30 | 31 | default maintenance_window := { 32 | "allowed": true, 33 | "violations": [], 34 | } 35 | 36 | maintenance_window := { 37 | "allowed": false, 38 | "violations": ["No job execution allowed outside of maintenance window"], 39 | } if { 40 | not is_maintenance_time 41 | } 42 | ``` 43 | The output of this policy checks whether the job was initiated during a maintenance window and returns `"allowed": false` with a violation message. 44 | 45 | Example output from policy query: 46 | 47 | ```json 48 | { 49 | "allowed": false, 50 | "violations": [ 51 | "No job execution allowed outside of maintenance window" 52 | ] 53 | } 54 | ``` 55 | 56 | ## Enforcement Behavior 57 | 58 | This policy uses the `is_maintenance_time` function to determine whether job execution is allowed. If a job is created outside the defined maintenance window, the job will result in an error, preventing playbook execution. 59 | 60 | ## Real World Use Case: Enforcing Maintenance Windows in Production Environments 61 | 62 | ### Scenario 63 | 64 | In production environments, it's often necessary to limit when automation can be run. For example, jobs that restart services, deploy updates, or perform infrastructure changes could impact system stability during business hours. 65 | 66 | Organizations may enforce maintenance windows to reduce risk and ensure proper change control. This policy supports that operational model by automatically blocking job execution outside defined timeframes. 67 | 68 | By enforcing maintenance windows, teams can avoid unintentional disruptions and align automation workflows within acceptable timeframes. 69 | 70 | ## Impact of Policy Enforcement in Ansible Automation Platform 71 | 72 | This policy adds an automated safeguard that blocks jobs launched outside of an approved time window. When applied to a Job Template, it ensures that playbooks only run during designated maintenance hours. 73 | 74 | ## How AAP Enforces the Policy 75 | 76 | If a user attempts to launch a job outside the approved timeframe, the job will ERROR, and provide a message that the job execution was attempted during a maintenance window. 77 | 78 | ## Why This Matters 79 | 80 | - Ensures jobs only run during approved timeframes. 81 | - Prevents accidental job launches during business critical periods. 82 | - Helps maintain service uptime and user confidence in automation reliability. 83 | -------------------------------------------------------------------------------- /9.Restrict Inventory use to an organization.md: -------------------------------------------------------------------------------- 1 | # 1. Restrict use of an Inventory to a particular Organization 2 | 3 | ## Overview 4 | 5 | Ansible Automation Platform allows users to enforce policies at multiple enforcement points, including: 6 | 7 | * **Organization Level:** Affects all job templates within an Organization. 8 | * **Inventory Level:** Affects all jobs using a specified Inventory. 9 | * **Job Template Level:** Affects jobs launched from a specific Job Template. 10 | 11 | This example demonstrates how to restrict the use of a certain Inventory to a particular Organization: 12 | 13 | ## Example Policy [aap_policy_examples/restrict_inv_use_to_org.rego](aap_policy_examples/restrict_inv_use_to_org.rego): 14 | 15 | The following policy (`restrict_inv_use_to_org.rego`) blocks job execution entirely unless it's the 'Default' organization using the 'Demo Inventory' inventory. 16 | 17 | ```rego 18 | package aap_policy_examples 19 | 20 | import rego.v1 21 | 22 | # Default policy response indicating allowed status with no violations 23 | default organization_inventory_validation := { 24 | "allowed": true, 25 | "violations": [], 26 | } 27 | 28 | # Validate that only "Default" can use "Demo Inventory" 29 | organization_inventory_validation := result if { 30 | # Extract values from input 31 | inventory_name := object.get(input, ["inventory", "name"], "") 32 | org_name := object.get(input, ["organization", "name"], "") 33 | 34 | # Check if inventory is "Demo Inventory" 35 | inventory_name == "Demo Inventory" 36 | 37 | # Check if organization is not "Default" 38 | org_name != "Default" 39 | 40 | result := { 41 | "allowed": false, 42 | "violations": ["Only the 'Default' organization should use the 'Demo Inventory' inventory"], 43 | } 44 | } 45 | ``` 46 | 47 | ## Enforcement Behavior 48 | 49 | When applied at different enforcement points, this policy prevents job execution accordingly: 50 | 51 | - **Job Template:** All jobs launched from the template will **ERROR**, preventing playbook execution. 52 | - **Inventory:** All jobs using the inventory will **ERROR**, preventing playbook, preventing playbook execution. 53 | - **Organization:** All jobs launch from job template that belongs to the organization will **ERROR**, preventing playbook execution. 54 | 55 | This policy is recommended to be used at the **Inventory** policy enforcement level. 56 | 57 | ## Try It Yourself 58 | 59 | Take advantage of the [Rego playground](https://play.openpolicyagent.org/p/KGShnmQ2zc)! 60 | 61 | ## Real World Use Case: Lock down the use of an Inventory to a particular Organization 62 | 63 | ### Scenario 64 | 65 | AAP ships with and by default installs a Demo Organization, Inventory and Credential. This should not be used for anything other than initial testing. This policy lets you check that someone in another Organization (MyOrg\_not\_Default) does not try to use the Demo Inventory. 66 | 67 | ## Impact of Policy Enforcement in Ansible Automation Platform 68 | 69 | By applying the Block Policy, Ansible Automation Platform completely prevents automation execution at the specified enforcement points. 70 | This ensures that no changes can be made to critical environments during security incidents, audits, or compliance reviews. 71 | 72 | ## How AAP Enforces the Policy 73 | 74 | Running an automation Job Template when this policy is in place will result in this type of error: 75 | 76 | ``` 77 | This job cannot be executed due to a policy violation or error. See the following details: 78 | {'Violations': {'Inventory': ["Only the 'Default' organization should use the " 79 | "'Demo Inventory' inventory"]}} 80 | ``` 81 | -------------------------------------------------------------------------------- /1.Prevent job execution at different policy enforcement points.md: -------------------------------------------------------------------------------- 1 | # 1. Preventing Job Execution at Different Policy Enforcement Points 2 | 3 | ## Overview 4 | 5 | Ansible Automation Platform allows users to enforce policies at multiple enforcement points, including: 6 | 7 | * **Organization Level:** Affects all job templates within an Organization. 8 | * **Inventory Level:** Affects all jobs using a specified Inventory. 9 | * **Job Template Level:** Affects jobs launched from a specific Job Template. 10 | 11 | This example demonstrates how to prevent job execution using Open Policy Agent (OPA) and Ansible Automation Platform's Policy as Code feature. 12 | 13 | ## Example Policy [aap_policy_examples/allowed_false.rego](aap_policy_examples/allowed_false.rego): 14 | 15 | The following policy (`aap_policy_examples/allowed_false.rego`) blocks job execution entirely: 16 | 17 | 18 | ```rego 19 | package aap_policy_examples 20 | 21 | import rego.v1 22 | 23 | allowed_false := { 24 | "allowed": false, 25 | "violations": ["No job execution is allowed"], 26 | } 27 | ``` 28 | 29 | This policy does not take any input. It simply returns `"allowed": false` with a violation message explaining why the job is blocked. 30 | 31 | ```json 32 | { 33 | "allowed": false, 34 | "violations": ["No job execution is allowed"] 35 | } 36 | ``` 37 | 38 | ## Enforcement Behavior 39 | 40 | When applied at different enforcement points, this policy prevents job execution accordingly: 41 | 42 | 43 | - **Job Template:** All jobs launched from the template will **ERROR**, preventing playbook execution. 44 | - **Inventory:** All jobs using the inventory will **ERROR**, preventing playbook, preventing playbook execution. 45 | - **Organization:** All jobs launch from job template that belongs to the organization will **ERROR**, preventing playbook execution. 46 | 47 | ## Real World Use Case: Blocking Automation During a Security Incident 48 | 49 | ### Scenario 50 | 51 | A security team's monitoring tool has detected unusual access patterns within their production systems. 52 | While the security team investigated the matter, they applied the Block Policy to all their Ansible Job Templates. 53 | This prevented any automated changes from being made on their systems that could have worsened the situation. 54 | 55 | ## Impact of Policy Enforcement in Ansible Automation Platform 56 | 57 | By applying the Block Policy, Ansible Automation Platform completely prevents automation execution at the specified enforcement points. 58 | This ensures that no changes can be made to critical environments during security incidents, audits, or compliance reviews. 59 | 60 | ## How AAP Enforces the Policy 61 | 62 | When the Block Policy (`allowed_false.rego`) is enforced: 63 | 64 | - 🚫 **Job Templates Fail Immediately** – Any playbook execution triggered by an affected job template will **ERROR**. 65 | - 🚫 **Inventories Cannot Be Used** – Jobs attempting to use an affected inventory will be blocked and **ERROR**. 66 | - 🚫 **Organization-Level Restrictions Apply** – All jobs launched from job templates that belongs to a restricted organization will **ERROR**. 67 | 68 | ## Why This Matters 69 | 70 | - **Rapid Response**: Teams can enforce immediate security controls to their automation jobs. 71 | - **Security & Compliance**: Prevent unauthorized automation during incidents or policy violations. 72 | - **Flexibility**: Policies can be applied, updated, and revoked based on security requirements. 73 | 74 | 75 | With Ansible Automation Platform's Policy as Code, organizations can ensure that automation doesn't become a liability during critical incidents, instead, it works in tandem with your policies to protect infrastructure from unintended or malicious changes. -------------------------------------------------------------------------------- /ansible/roles/opa/tasks/mtls.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create certificates directory 3 | file: 4 | path: "{{ certificates_dir }}" 5 | state: directory 6 | mode: '0755' 7 | 8 | - name: Generate CA private key 9 | community.crypto.openssl_privatekey: 10 | path: "{{ certificates_dir }}/ca.key" 11 | size: 2048 12 | 13 | - name: Generate CA certificate 14 | community.crypto.openssl_csr: 15 | path: "{{ certificates_dir }}/ca.csr" 16 | privatekey_path: "{{ certificates_dir }}/ca.key" 17 | common_name: "OPA-CA" 18 | basic_constraints_critical: true 19 | basic_constraints: 20 | - "CA:TRUE" 21 | 22 | - name: Generate self-signed CA certificate 23 | community.crypto.x509_certificate: 24 | path: "{{ certificates_dir }}/ca.crt" 25 | privatekey_path: "{{ certificates_dir }}/ca.key" 26 | csr_path: "{{ certificates_dir }}/ca.csr" 27 | provider: selfsigned 28 | selfsigned_not_before: "-1d" 29 | selfsigned_not_after: "+{{ ca_cert_validity_days }}d" 30 | 31 | - name: Generate server private key 32 | community.crypto.openssl_privatekey: 33 | path: "{{ certificates_dir }}/server.key" 34 | size: 2048 35 | 36 | - name: Set subject alternative names for server certificate 37 | set_fact: 38 | server_sans: 39 | - "DNS:{{ opa_deployment_name }}" 40 | - "DNS:{{ opa_deployment_name }}.{{ opa_namespace }}" 41 | - "DNS:{{ opa_deployment_name }}.{{ opa_namespace }}.svc" 42 | - "DNS:{{ opa_deployment_name }}.{{ opa_namespace }}.svc.cluster.local" 43 | - "DNS:{{ route_host }}" 44 | 45 | - name: Generate server CSR 46 | community.crypto.openssl_csr: 47 | path: "{{ certificates_dir }}/server.csr" 48 | privatekey_path: "{{ certificates_dir }}/server.key" 49 | common_name: "{{ opa_deployment_name }}" 50 | subject_alt_name: "{{ server_sans }}" 51 | 52 | - name: Generate server certificate 53 | community.crypto.x509_certificate: 54 | path: "{{ certificates_dir }}/server.crt" 55 | privatekey_path: "{{ certificates_dir }}/server.key" 56 | csr_path: "{{ certificates_dir }}/server.csr" 57 | provider: ownca 58 | ownca_path: "{{ certificates_dir }}/ca.crt" 59 | ownca_privatekey_path: "{{ certificates_dir }}/ca.key" 60 | ownca_not_before: "-1d" 61 | ownca_not_after: "+{{ server_cert_validity_days }}d" 62 | 63 | - name: Generate client private key 64 | community.crypto.openssl_privatekey: 65 | path: "{{ certificates_dir }}/client.key" 66 | size: 2048 67 | 68 | - name: Generate client CSR 69 | community.crypto.openssl_csr: 70 | path: "{{ certificates_dir }}/client.csr" 71 | privatekey_path: "{{ certificates_dir }}/client.key" 72 | common_name: "opa-client" 73 | 74 | - name: Generate client certificate 75 | community.crypto.x509_certificate: 76 | path: "{{ certificates_dir }}/client.crt" 77 | privatekey_path: "{{ certificates_dir }}/client.key" 78 | csr_path: "{{ certificates_dir }}/client.csr" 79 | provider: ownca 80 | ownca_path: "{{ certificates_dir }}/ca.crt" 81 | ownca_privatekey_path: "{{ certificates_dir }}/ca.key" 82 | ownca_not_before: "-1d" 83 | ownca_not_after: "+{{ client_cert_validity_days }}d" 84 | 85 | - name: Create Kubernetes secret for OPA certificates 86 | kubernetes.core.k8s: 87 | state: present 88 | kubeconfig: "{{ kubeconfig }}" 89 | definition: 90 | apiVersion: v1 91 | kind: Secret 92 | metadata: 93 | name: "{{ opa_deployment_name }}-certs" 94 | namespace: "{{ opa_namespace }}" 95 | type: Opaque 96 | data: 97 | ca.crt: "{{ lookup('file', certificates_dir + '/ca.crt') | b64encode }}" 98 | server.crt: "{{ lookup('file', certificates_dir + '/server.crt') | b64encode }}" 99 | server.key: "{{ lookup('file', certificates_dir + '/server.key') | b64encode }}" -------------------------------------------------------------------------------- /tools/sync_policy_docs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Policy Documentation Sync Tool 5 | 6 | This script helps maintain synchronization between policy implementations and their documentation. 7 | It updates the policy content embedded in markdown files with the current content from the actual 8 | policy files in the aap_policy_examples directory. 9 | 10 | Usage: 11 | make tools/sync-markdown 12 | 13 | The script expects: 14 | 1. Policy files to be in the aap_policy_examples/ directory 15 | 2. Markdown files to reference policies using the format [aap_policy_examples/policy_name] 16 | 3. Policy content to be embedded in markdown files using rego code blocks (```rego) 17 | 18 | Example markdown format: 19 | ## Example Policy [aap_policy_examples/allowed_false.rego](aap_policy_examples/allowed_false.rego): 20 | 21 | ```rego 22 | package aap_policy_examples 23 | ... 24 | ``` 25 | """ 26 | 27 | import os 28 | import re 29 | from pathlib import Path 30 | 31 | def read_policy_file(policy_path): 32 | """ 33 | Read content from a policy file. 34 | 35 | Args: 36 | policy_path (Path): Path to the policy file 37 | 38 | Returns: 39 | str: The content of the policy file, stripped of leading/trailing whitespace 40 | """ 41 | with open(policy_path, 'r') as f: 42 | return f.read().strip() 43 | 44 | def update_markdown_file(markdown_file, policy_name, policy_content): 45 | """ 46 | Update the markdown file with the policy content from the policy file. 47 | 48 | Args: 49 | markdown_file (str): Path to the markdown file to update 50 | policy_name (str): Name of the policy file (e.g., 'allowed_false.rego') 51 | policy_content (str): Current content of the policy file 52 | 53 | Returns: 54 | bool: True if the markdown file was updated, False if no reference to the policy was found 55 | """ 56 | with open(markdown_file, 'r') as f: 57 | content = f.read() 58 | 59 | # Find the policy reference in the markdown 60 | policy_ref_pattern = r'\[aap_policy_examples/' + re.escape(policy_name) + r'\]' 61 | if not re.search(policy_ref_pattern, content): 62 | return False 63 | 64 | # Replace the content in the rego code block 65 | # The pattern matches the opening ```rego, any content, and the closing ``` 66 | pattern = r'(```rego\n).*?(\n```)' 67 | replacement = f'\\1{policy_content}\\2' 68 | new_content = re.sub(pattern, replacement, content, flags=re.DOTALL) 69 | 70 | with open(markdown_file, 'w') as f: 71 | f.write(new_content) 72 | return True 73 | 74 | def main(): 75 | """ 76 | Main function that orchestrates the sync process. 77 | 78 | The function: 79 | 1. Finds all markdown files in the current directory 80 | 2. Gets all policy files from aap_policy_examples/ 81 | 3. For each policy file, attempts to update its content in any markdown files that reference it 82 | """ 83 | # Get all markdown files in the current directory 84 | markdown_files = [f for f in os.listdir('.') if f.endswith('.md')] 85 | policy_dir = Path('aap_policy_examples') 86 | 87 | # Get all policy files 88 | policy_files = list(policy_dir.glob('*.rego')) 89 | 90 | for md_file in markdown_files: 91 | print(f"\nProcessing '{md_file}'...") 92 | for policy_file in policy_files: 93 | policy_name = policy_file.name 94 | policy_content = read_policy_file(policy_file) 95 | 96 | if update_markdown_file(md_file, policy_name, policy_content): 97 | print(f"✓ Updated {policy_name} in '{md_file}'") 98 | 99 | if __name__ == '__main__': 100 | main() -------------------------------------------------------------------------------- /8.Enforce Job Template Naming Standards.md: -------------------------------------------------------------------------------- 1 | # 1. Enforce Job Template Naming Standards 2 | 3 | ## Overview 4 | 5 | Ansible Automation Platform allows users to enforce policies at multiple enforcement points, including: 6 | 7 | * **Organization Level:** Affects all job templates within an Organization. 8 | * **Inventory Level:** Affects all jobs using a specified Inventory. 9 | * **Job Template Level:** Affects jobs launched from a specific Job Template. 10 | 11 | This example demonstrates how to prevent job execution using Open Policy Agent (OPA) and Ansible Automation Platform’s Policy as Code feature. 12 | 13 | ## Example Policy [aap_policy_examples/jt_naming_validation.rego](aap_policy_examples/jt_naming_validation.rego): 14 | 15 | The following policy (`aap_policy_examples/allowed_false.rego`) blocks job execution entirely unless the Job Template name matches our standards. In this particular rule will enforce the convention '**OrganizationName_ProjectName_JobTemplateName**': 16 | 17 | ```rego 18 | package aap_policy_examples 19 | 20 | import rego.v1 21 | 22 | # Default policy response indicating allowed status with no violations 23 | default jt_naming_validation := { 24 | "allowed": true, 25 | "violations": [], 26 | } 27 | 28 | # Validate that job template name has correct organization and project name prefixes 29 | jt_naming_validation := result if { 30 | # Extract values from input 31 | org_name := object.get(input, ["organization", "name"], "") 32 | project_name := object.get(input, ["project", "name"], "") 33 | jt_name := object.get(input, ["job_template", "name"], "") 34 | 35 | # Construct the expected prefix 36 | expected_prefix := concat("_", [org_name, project_name]) 37 | 38 | # Check if job template name starts with expected prefix 39 | not startswith(jt_name, expected_prefix) 40 | 41 | result := { 42 | "allowed": false, 43 | "violations": [sprintf("Job template naming for '%v' does not comply with standards", [jt_name])] 44 | } 45 | } 46 | ``` 47 | 48 | ## Enforcement Behavior 49 | 50 | When applied at different enforcement points, this policy prevents job execution accordingly: 51 | 52 | 53 | - **Job Template:** All jobs launched from the template will **ERROR**, preventing playbook execution. 54 | - **Inventory:** All jobs using the inventory will **ERROR**, preventing playbook, preventing playbook execution. 55 | - **Organization:** All jobs launch from job template that belongs to the organization will **ERROR**, preventing playbook execution. 56 | 57 | ## Try It Yourself 58 | 59 | Take advantage of the [Rego playground](https://play.openpolicyagent.org/p/xuafwiSmYM)! 60 | 61 | ## Real World Use Case: Use standard naming conventions for ease of identification and Configuration-as-Code initiatives 62 | 63 | ### Scenario 64 | 65 | Without enforcing naming standards it will be hard to use initiatives such as Configuration-as-Code (CaC or CasC) to simply platform management and automation content seeding. It will also be much easier to identify content relevant to different environments such as Development or Production. 66 | 67 | ## Impact of Policy Enforcement in Ansible Automation Platform 68 | 69 | By applying the Block Policy, Ansible Automation Platform completely prevents automation execution at the specified enforcement points. 70 | This ensures that no changes can be made to critical environments during security incidents, audits, or compliance reviews. 71 | 72 | ## How AAP Enforces the Policy 73 | 74 | Running an automation Job Template when this policy is in place will result in this type of error when an unapproved repo tries to be used: 75 | 76 | ``` 77 | This job cannot be executed due to a policy violation or error. See the following details: 78 | {'Violations': {'Job template': ["Job template naming for 'PhilFooOrg Phils " 79 | "Random Ansible Job Template with PaC' does " 80 | 'not comply with standards']}} 81 | ``` 82 | -------------------------------------------------------------------------------- /tools/sync_policies.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Policy Documentation Sync Tool 5 | 6 | This script helps maintain synchronization between policy implementations and their documentation. 7 | It updates the policy content embedded in markdown files with the current content from the actual 8 | policy files in the aap_policy_examples directory. 9 | 10 | Usage: 11 | make tools/sync-markdown 12 | 13 | The script expects: 14 | 1. Policy files to be in the aap_policy_examples/ directory 15 | 2. Markdown files to reference policies using the format [aap_policy_examples/policy_name] 16 | 3. Policy content to be embedded in markdown files using rego code blocks (```rego) 17 | 18 | Example markdown format: 19 | ## Example Policy [aap_policy_examples/allowed_false.rego](aap_policy_examples/allowed_false.rego): 20 | 21 | ```rego 22 | package aap_policy_examples 23 | ... 24 | ``` 25 | """ 26 | 27 | import os 28 | import re 29 | from pathlib import Path 30 | 31 | def read_policy_file(policy_path): 32 | """ 33 | Read content from a policy file. 34 | 35 | Args: 36 | policy_path (Path): Path to the policy file 37 | 38 | Returns: 39 | str: The content of the policy file, stripped of leading/trailing whitespace 40 | """ 41 | with open(policy_path, 'r') as f: 42 | return f.read().strip() 43 | 44 | def update_markdown_file(markdown_file, policy_name, policy_content): 45 | """ 46 | Update the markdown file with the policy content from the policy file. 47 | 48 | Args: 49 | markdown_file (str): Path to the markdown file to update 50 | policy_name (str): Name of the policy file (e.g., 'allowed_false.rego') 51 | policy_content (str): Current content of the policy file 52 | 53 | Returns: 54 | bool: True if the markdown file was updated, False if no reference to the policy was found 55 | """ 56 | with open(markdown_file, 'r') as f: 57 | content = f.read() 58 | 59 | # Find the policy reference in the markdown 60 | policy_ref_pattern = r'\[aap_policy_examples/' + re.escape(policy_name) + r'\]' 61 | if not re.search(policy_ref_pattern, content): 62 | print(f"Warning: No reference to policy {policy_name} found in {markdown_file}") 63 | return False 64 | 65 | # Replace the content in the rego code block 66 | # The pattern matches the opening ```rego, any content, and the closing ``` 67 | pattern = r'(```rego\n).*?(\n```)' 68 | replacement = f'\\1{policy_content}\\2' 69 | new_content = re.sub(pattern, replacement, content, flags=re.DOTALL) 70 | 71 | with open(markdown_file, 'w') as f: 72 | f.write(new_content) 73 | return True 74 | 75 | def main(): 76 | """ 77 | Main function that orchestrates the sync process. 78 | 79 | The function: 80 | 1. Finds all markdown files in the current directory 81 | 2. Gets all policy files from aap_policy_examples/ 82 | 3. For each policy file, attempts to update its content in any markdown files that reference it 83 | """ 84 | # Get all markdown files in the current directory 85 | markdown_files = [f for f in os.listdir('.') if f.endswith('.md')] 86 | policy_dir = Path('aap_policy_examples') 87 | 88 | # Get all policy files 89 | policy_files = list(policy_dir.glob('*.rego')) 90 | 91 | for md_file in markdown_files: 92 | print(f"\nProcessing {md_file}...") 93 | for policy_file in policy_files: 94 | policy_name = policy_file.name 95 | policy_content = read_policy_file(policy_file) 96 | 97 | if update_markdown_file(md_file, policy_name, policy_content): 98 | print(f"✓ Updated {policy_name} in {md_file}") 99 | # else: 100 | # print(f"✗ No reference to {policy_name} found in {md_file}") 101 | 102 | if __name__ == '__main__': 103 | main() -------------------------------------------------------------------------------- /test_aap_policy_examples/team_based_extra_vars_restriction_test.rego: -------------------------------------------------------------------------------- 1 | package test_aap_policy_examples 2 | 3 | import data.aap_policy_examples 4 | import rego.v1 5 | 6 | # Test case: Empty extra_vars should be allowed 7 | test_allow_empty_extra_vars if { 8 | test_input := { 9 | "extra_vars": {}, 10 | "created_by": {"teams": [{"name": "dev_team"}]}, 11 | } 12 | aap_policy_examples.team_based_extra_vars_restriction.allowed == true with input as test_input 13 | aap_policy_examples.team_based_extra_vars_restriction.violations == [] with input as test_input 14 | } 15 | 16 | # Test case: Dev team member should be allowed to use dev environment 17 | test_allow_dev_team_dev_environment if { 18 | test_input := { 19 | "extra_vars": {"environment": "dev"}, 20 | "created_by": {"teams": [{"name": "dev_team"}]}, 21 | } 22 | aap_policy_examples.team_based_extra_vars_restriction.allowed == true with input as test_input 23 | aap_policy_examples.team_based_extra_vars_restriction.violations == [] with input as test_input 24 | } 25 | 26 | # Test case: Dev team member should be allowed to use staging environment 27 | test_allow_dev_team_staging_environment if { 28 | test_input := { 29 | "extra_vars": {"environment": "staging"}, 30 | "created_by": {"teams": [{"name": "dev_team"}]}, 31 | } 32 | aap_policy_examples.team_based_extra_vars_restriction.allowed == true with input as test_input 33 | aap_policy_examples.team_based_extra_vars_restriction.violations == [] with input as test_input 34 | } 35 | 36 | # Test case: Prod team member should be allowed to use prod environment 37 | test_allow_prod_team_prod_environment if { 38 | test_input := { 39 | "extra_vars": {"environment": "prod"}, 40 | "created_by": {"teams": [{"name": "prod_team"}]}, 41 | } 42 | aap_policy_examples.team_based_extra_vars_restriction.allowed == true with input as test_input 43 | aap_policy_examples.team_based_extra_vars_restriction.violations == [] with input as test_input 44 | } 45 | 46 | # Test case: Dev team member should not be allowed to use prod environment 47 | test_reject_dev_team_prod_environment if { 48 | test_input := { 49 | "extra_vars": {"environment": "prod"}, 50 | "created_by": {"teams": [{"name": "dev_team"}]}, 51 | } 52 | aap_policy_examples.team_based_extra_vars_restriction.allowed == false with input as test_input 53 | aap_policy_examples.team_based_extra_vars_restriction.violations == ["extra_vars contain disallowed values for keys: [\"environment\"]. Allowed extra_vars for your teams ({\"dev_team\"}): {\"dev_team\": {\"environment\": [\"dev\", \"staging\"]}}"] with input as test_input 54 | } 55 | 56 | # Test case: Prod team member should not be allowed to use dev environment 57 | test_reject_prod_team_dev_environment if { 58 | test_input := { 59 | "extra_vars": {"environment": "dev"}, 60 | "created_by": {"teams": [{"name": "prod_team"}]}, 61 | } 62 | aap_policy_examples.team_based_extra_vars_restriction.allowed == false with input as test_input 63 | aap_policy_examples.team_based_extra_vars_restriction.violations == ["extra_vars contain disallowed values for keys: [\"environment\"]. Allowed extra_vars for your teams ({\"prod_team\"}): {\"prod_team\": {\"environment\": [\"prod\", \"staging\"]}}"] with input as test_input 64 | } 65 | 66 | # Test case: User in multiple teams should have access to environments from all teams 67 | test_allow_multiple_teams_environment if { 68 | test_input := { 69 | "extra_vars": {"environment": "staging"}, 70 | "created_by": {"teams": [{"name": "dev_team"}, {"name": "prod_team"}]}, 71 | } 72 | aap_policy_examples.team_based_extra_vars_restriction.allowed == true with input as test_input 73 | aap_policy_examples.team_based_extra_vars_restriction.violations == [] with input as test_input 74 | } 75 | 76 | # Test case: Non-team member should not be allowed to use any environment 77 | test_reject_non_team_member if { 78 | test_input := { 79 | "extra_vars": {"environment": "dev"}, 80 | "created_by": {"teams": [{"name": "other_team"}]}, 81 | } 82 | aap_policy_examples.team_based_extra_vars_restriction.allowed == false with input as test_input 83 | aap_policy_examples.team_based_extra_vars_restriction.violations == ["extra_vars contain disallowed values for keys: [\"environment\"]. Allowed extra_vars for your teams ({\"other_team\"}): {}"] with input as test_input 84 | } 85 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Default target 2 | .DEFAULT_GOAL := help 3 | 4 | # Build and Environment Variables 5 | OS := $(shell uname -s | tr '[:upper:]' '[:lower:]') 6 | ARCH := $(shell uname -m | sed 's/x86_64/amd64/') 7 | OPA_VERSION := latest 8 | CONTAINER_RUNTIME ?= podman 9 | 10 | # Directory Variables 11 | TEST_DIR := test_aap_policy_examples 12 | POLICY_DIR := aap_policy_examples 13 | TOOLS_DIR := tools 14 | 15 | # OPA Binary Management 16 | OPA := $(shell pwd)/bin/opa 17 | 18 | .PHONY: opa 19 | opa: ## Download OPA locally if necessary 20 | ifeq (,$(wildcard $(OPA))) 21 | ifeq (,$(shell which opa 2>/dev/null)) 22 | @{ \ 23 | set -e ;\ 24 | mkdir -p $(dir $(OPA)) ;\ 25 | curl -L -o $(OPA) https://openpolicyagent.org/downloads/$(OPA_VERSION)/opa_$(OS)_$(ARCH)_static ;\ 26 | chmod +x $(OPA) ;\ 27 | } 28 | else 29 | $(eval OPA := $(shell which opa)) 30 | endif 31 | endif 32 | 33 | # Testing Targets 34 | .PHONY: test 35 | test: opa ## Run all OPA tests 36 | @echo "Running all OPA tests..." 37 | @$(OPA) test $(TEST_DIR) $(POLICY_DIR) --verbose 38 | 39 | .PHONY: test-% 40 | test-%: opa ## Run tests for a specific policy (e.g., make test-allowed_false) 41 | @echo "Running tests for $*..." 42 | @$(OPA) test $(TEST_DIR)/$*_test.rego $(POLICY_DIR)/$*.rego --verbose 43 | 44 | .PHONY: test-coverage 45 | test-coverage: opa ## Run tests with coverage report 46 | @echo "Running tests with coverage report..." 47 | @$(OPA) test $(TEST_DIR) $(POLICY_DIR) --coverage --format=json 48 | 49 | # Code Quality Targets 50 | .PHONY: fmt 51 | fmt: opa ## Format all rego files 52 | @echo "Formatting all rego files..." 53 | @$(OPA) fmt -w $(TEST_DIR) $(POLICY_DIR) 54 | 55 | .PHONY: fmt-check 56 | fmt-check: opa ## Check if rego files are formatted correctly 57 | @echo "Checking rego file formatting..." 58 | @$(OPA) fmt -d $(TEST_DIR) $(POLICY_DIR) 59 | 60 | .PHONY: check 61 | check: opa ## Validate all rego files 62 | @echo "Validating rego files..." 63 | @$(OPA) check $(TEST_DIR) $(POLICY_DIR) 64 | 65 | # Containerized OPA Server 66 | .PHONY: container/run-opa-server 67 | container/run-opa-server: ## Run OPA server in container with file watching 68 | @echo "Starting OPA server with file watching..." 69 | @$(CONTAINER_RUNTIME) run \ 70 | --rm \ 71 | -p 8181:8181 \ 72 | -v $(shell pwd)/$(POLICY_DIR):/policies \ 73 | openpolicyagent/opa:$(OPA_VERSION) \ 74 | run --server --addr=0.0.0.0:8181 --watch /policies 75 | 76 | # OpenShift Deployment of OPA Server 77 | .PHONY: openshift/deploy-opa-server 78 | openshift/deploy-opa-server: ## Deploy OPA server to OpenShift 79 | @echo "Deploying OPA server to OpenShift..." 80 | @OPA_VERSION=$(OPA_VERSION) envsubst < openshift/opa-deployment.yaml | oc apply -f - 81 | 82 | .PHONY: openshift/undeploy-opa-server 83 | openshift/undeploy-opa-server: ## Remove OPA server from OpenShift 84 | @echo "Removing OPA server from OpenShift..." 85 | @OPA_VERSION=$(OPA_VERSION) envsubst < openshift/opa-deployment.yaml | oc delete -f - 86 | 87 | .PHONY: openshift/load-policies 88 | openshift/load-policies: ## Load all policies onto the deployed OPA server 89 | @echo "Loading policies onto OPA server..." 90 | @OPA_URL=$$(oc get route opa -o jsonpath='{.spec.host}') && \ 91 | echo "OPA URL: $$OPA_URL" && \ 92 | for policy in $(POLICY_DIR)/*.rego; do \ 93 | policy_name=$$(basename "$$policy" .rego); \ 94 | echo "Policy file: $$policy"; \ 95 | echo "Policy name: $$policy_name"; \ 96 | if curl -s -w "%{http_code}" -X PUT "http://$$OPA_URL/v1/policies/$$policy_name" \ 97 | -H "Content-Type: text/plain" \ 98 | --data-binary @$$policy | grep -q "200"; then \ 99 | echo " ✓ Successfully loaded policy"; \ 100 | else \ 101 | echo " ✗ Failed to load policy"; \ 102 | fi; \ 103 | done 104 | 105 | # Maintenance Targets 106 | .PHONY: clean 107 | clean: ## Clean up generated files 108 | @echo "Cleaning up..." 109 | @find . -type f -name "*.rego.bak" -delete 110 | 111 | .PHONY: tools/sync_policy_docs 112 | tools/sync_policy_docs: ## Sync policy documentation with actual policy files 113 | @echo "Syncing policy documentation with actual policy files..." 114 | @python3 $(TOOLS_DIR)/sync_policy_docs.py 115 | 116 | # Help and Documentation 117 | .PHONY: help 118 | help: ## Show this help message 119 | @echo "Usage: make [target]" 120 | @echo "" 121 | @echo "Targets:" 122 | @awk '/^[a-zA-Z\-_0-9%]+:/ { \ 123 | helpMessage = match($$0, /^([^:]+):[^#]*## (.+)/); \ 124 | if (helpMessage) { \ 125 | helpCommand = substr($$0, 1, index($$0, ":")-1); \ 126 | helpMessage = substr($$0, index($$0, "##") + 3); \ 127 | printf " %-20s %s\n", helpCommand, helpMessage; \ 128 | } \ 129 | }' $(MAKEFILE_LIST) | sort 130 | -------------------------------------------------------------------------------- /docs/Associating policy with AAP resources.md: -------------------------------------------------------------------------------- 1 | # Associating Policies with AAP Resources 2 | 3 | ## Table of Contents 4 | - [Overview](#overview) 5 | - [Understanding OPA Packages and Rules](#understanding-opa-packages-and-rules) 6 | - [Associating Policies with AAP Resources](#associating-policies-with-aap-resources) 7 | - [Effects of Policy Association](#effects-of-policy-association) 8 | 9 | ## Overview 10 | 11 | Ansible Automation Platform (AAP) allows you to associate Open Policy Agent (OPA) policies with various resources to enforce security and compliance controls. This guide explains how to associate policies with AAP resources and their effects. 12 | 13 | ## Understanding OPA Packages and Rules 14 | 15 | ### OPA Package Structure 16 | 17 | An OPA policy is organized in packages, which are namespaced collections of rules. The basic structure of an OPA policy looks like this: 18 | 19 | ```rego 20 | package aap_policy_examples # Package name 21 | 22 | import rego.v1 # Import required for Rego v1 syntax 23 | 24 | # Rules define the policy logic 25 | allowed := { 26 | "allowed": true, 27 | "violations": [] 28 | } 29 | ``` 30 | 31 | ### Key Components 32 | 33 | 1. **Package Declaration**: Defines the namespace for your policy 34 | 2. **Rules**: Define the policy logic and return a decision object 35 | 36 | Note that these components comprise the OPA policy name, which is formatted as `{package}/{rule}`. You will enter the OPA policy name when configuring enforcement points. 37 | 38 | ## Associating Policies with AAP Resources 39 | 40 | ### Available Enforcement Points 41 | 42 | You can create an enforcement point by associating a policy with the following AAP resources: 43 | 44 | 1. **Organizations** 45 | - Affects all job templates within an Organization 46 | - Provides broad control over automation within organizational boundaries 47 | 48 | 2. **Inventories** 49 | - Affects all jobs using a specified Inventory 50 | - Controls access to specific infrastructure resources 51 | 52 | 3. **Job Templates** 53 | - Affects jobs launched from a specific Job Template 54 | - Provides granular control over specific automation tasks 55 | 56 | ### How to Associate a Policy with a Resource 57 | 58 | #### 1. Job Template Level 59 | 60 | To associate a policy with a Job Template: 61 | 1. Navigate to **Templates** in the AAP UI 62 | 2. Select or create a Job Template 63 | 3. In the Job Template edit form, locate the **OPA policy** field 64 | 4. Enter the policy in the format `{package}/{rule}` 65 | - Example: `aap_policy_examples/allowed_false` 66 | 5. Click "Save job template" to apply the policy 67 | 68 | #### 2. Inventory Level 69 | 70 | To associate a policy with an Inventory: 71 | 1. Navigate to **Inventories** under Infrastructure 72 | 2. Select or create an Inventory 73 | 3. In the Inventory edit form, find the **OPA policy** field 74 | 4. Enter the policy in the format `{package}/{rule}` 75 | 5. The policy will be enforced for all jobs using this inventory 76 | 77 | #### 3. Organization Level 78 | 79 | To associate a policy with an Organization: 80 | 1. Navigate to **Organizations** under Access Management 81 | 2. Select or create an Organization 82 | 3. In the Organization edit form, locate the **OPA policy** field 83 | 4. Enter the policy in the format `{package}/{rule}` 84 | 5. The policy will affect all job templates within the organization 85 | 86 | Note: For all resources, the OPA policy field format must follow the pattern `{package}/{rule}`. This is a required format and will be validated by the UI. 87 | 88 | ## Effects of Policy Association 89 | 90 | ### Job Execution Behavior 91 | 92 | Policy evaluation is integrated into the job lifecycle as a dedicated phase called `evaluate_policy`. Here's how it works: 93 | 94 | 1. **Job Launch Sequence**: 95 | - User initiates a job launch 96 | - Before playbook execution begins, the job enters the `evaluate_policy` phase 97 | 98 | 2. **Policy Collection**: 99 | During the `evaluate_policy` phase, AAP gathers all relevant policies from: 100 | - The organization that owns the job template 101 | - The inventory being used in the job 102 | - The job template the job was launched from 103 | 104 | 3. **Policy Evaluation**: 105 | - AAP sends the collected policies to the configured OPA server for evaluation 106 | - Each policy is evaluated against the job context 107 | - The job will be blocked if any policy evaluation: 108 | - Returns `"allowed": false`, or 109 | - Fails to evaluate 110 | 111 | 4. **Job State Transition**: 112 | - If all policies allow the job: 113 | - The job proceeds to playbook execution 114 | - If any policy blocks the job: 115 | - The job transitions to "Error" state 116 | - Playbook execution is prevented 117 | - Error messages from policy violations are recorded -------------------------------------------------------------------------------- /4.Prevent job execution using credential with no Organization.md: -------------------------------------------------------------------------------- 1 | # 4. Preventing Job Execution Using Credentials Without an Organization 2 | 3 | ## Overview 4 | 5 | Ansible Automation Platform includes a powerful RBAC system that enables robust and highly customizable control over access. However, no matter how robust and customizable our RBAC system is, there will be unique needs required by customers. 6 | 7 | For example, Ansible Automation Controller allows you to create a credential that does not belong to any organization, and this credential can be assigned to used by any user. For some organizations, these global credentials that can be used by any user present a security risk or compliance concern in environments with strict boundaries. 8 | 9 | Currently, there is no method within our RBAC system to prevent the use of these global credentials within a specific organization, but with Policy Enforcement we can restrict access with ease. 10 | 11 | ## Example Policy [aap_policy_examples/global_credential_allowed_false.rego](aap_policy_examples/global_credential_allowed_false.rego) 12 | 13 | The following policy identifies and blocks the use of credentials that do not belong to any Organization: 14 | 15 | ```rego 16 | package aap_policy_examples 17 | 18 | # Find credentials with no organization 19 | violating_credentials := {cred.name | cred := input.credentials[_]; cred.organization == null} 20 | 21 | default global_credential_allowed_false := { 22 | "allowed": true, 23 | "violations": [], 24 | } 25 | 26 | # If any credential is violating, deny access and return violations 27 | global_credential_allowed_false := { 28 | "allowed": false, 29 | "violations": [sprintf("Credential used in job execution does not belong to any org. Violating credentials: [%s]", [concat(", ", violating_credentials)])], 30 | } if { 31 | count(violating_credentials) > 0 32 | } 33 | ``` 34 | 35 | ### Input: 36 | 37 | ```json 38 | { 39 | "credential": [ 40 | { 41 | "name": "Demo Credential", 42 | "description": "", 43 | "organization": null, 44 | "credential_type": 1, 45 | "managed": false, 46 | "inputs": { 47 | "username": "admin" 48 | } 49 | } 50 | ] 51 | } 52 | ``` 53 | 54 | ### Output: 55 | 56 | ```json 57 | { 58 | "allowed": false, 59 | "violations": [ 60 | "Credential used in job execution does not belong to any org" 61 | ] 62 | } 63 | ``` 64 | 65 | ## Enforcement Behavior 66 | 67 | When this policy is applied to a Job Template, it prevents job execution for that organization if any unscoped (global) credentials with no ties to any organization are used. 68 | 69 | - ✅ Credentials tied to the Organization: **Allowed** 70 | - 🚫 Credentials with `organization: null`: **Blocked/ERROR** 71 | 72 | Jobs using a violating credential will immediately **ERROR**, and the playbook will not run. 73 | 74 | ## Real World Use Case: Enforcing Credential Boundaries 75 | 76 | ### Scenario 77 | 78 | An enterprise uses Ansible Automation Platform to manage automation across multiple internal teams, each represented by a separate Organization within the platform. For example: 79 | 80 | - `Infrastructure Team` 81 | - `Security Team` 82 | - `DevOps Team` 83 | 84 | Each team operates independently, with its own credentials, inventories, and job templates. RBAC ensures users can only see resources tied to their Organization. 85 | 86 | However, during a security review, the platform team discovers that several credentials were created without an assigned Organization (`organization: null`). These global credentials are not restricted by RBAC and could be used across any Organization if referenced by a job template or workflow. 87 | 88 | To eliminate this risk, the platform engineering team enforces a Policy Enforcement rule that blocks job execution when unscoped (global) credentials are detected. This ensures that all credentials must be explicitly assigned to an Organization in order to be used in automation jobs. 89 | 90 | ## Impact of Policy Enforcement in Ansible Automation Platform 91 | 92 | When the policy is applied: 93 | 94 | - 🚫 **Jobs referencing global credentials will error** – preventing unauthorized use. 95 | - ✅ **Jobs using credentials tied to an organization continue to run** – preserving normal automation flow for compliant configurations. 96 | - ✅ **Clear violation messages are surfaced** – allowing teams to quickly identify and fix unscoped credentials. 97 | 98 | This policy gives platform administrators confidence that no credential can bypass organization-level isolation, even if mistakenly created without an organization 99 | 100 | ## Why This Matters 101 | 102 | - **Security** – Prevents accidental or unintended use of global credentials across boundaries that may not be privy to all organizations. 103 | - **Enforced Governance** – Ensures credentials are owned and managed by the appropriate teams. 104 | - **Audit Readiness** – Demonstrates credential ownership and scope compliance during audits. 105 | - **RBAC Enhancement** – Fills a critical gap not covered by Ansible Automation Platform's built-in access control. 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /ansible/README.md: -------------------------------------------------------------------------------- 1 | # OPA Server Deployment Playbook 2 | 3 | This Ansible playbook deploys an OpenPolicyAgent (OPA) server on OpenShift with mTLS support and provides tools for loading and managing OPA policies. 4 | 5 | > ⚠️ **Warning: Development and Testing Only** 6 | > 7 | > This OPA server deployment is designed for development and testing purposes only. Please note: 8 | > - Policies loaded onto this OPA server are non-persistent 9 | > - If the OPA pod restarts, all loaded policies will be lost 10 | > - You will need to reload policies after any pod restart 11 | > - This setup is not suitable for production use 12 | > 13 | > For production deployments, please refer to the [official OPA documentation](https://www.openpolicyagent.org/docs/latest/deployment/) 14 | 15 | ## Table of Contents 16 | 17 | - [Prerequisites](#prerequisites) 18 | - [Installation](#installation) 19 | - [Configuration](#configuration) 20 | - [Usage](#usage) 21 | - [Deploying OPA Server](#deploying-opa-server) 22 | - [Loading OPA Policies](#loading-opa-policies) 23 | - [mTLS Support](#mtls-support) 24 | - [Accessing OPA Server](#accessing-opa-server-via-openshift-route) 25 | - [Basic Health Check](#basic-health-check) 26 | - [Policy Validation](#validating-policy-loading) 27 | 28 | ## Prerequisites 29 | 30 | - Ansible 2.9+ 31 | - OpenShift CLI (oc) 32 | - Access to an OpenShift cluster 33 | - Python kubernetes module (`pip install kubernetes`) 34 | - Python cryptography module (`pip install cryptography`) 35 | - OpenSSL 36 | 37 | ## Installation 38 | 39 | 1. Install the required Ansible collections: 40 | 41 | ```bash 42 | ansible-galaxy collection install -r requirements.yml 43 | ``` 44 | 45 | ## Configuration 46 | 47 | The playbook can be configured by modifying the variables in `roles/opa/defaults/main.yml` or by creating a custom vars file in `vars/main.yml`. 48 | 49 | Key configuration options: 50 | 51 | - `opa_namespace`: Namespace where OPA will be deployed 52 | - `opa_image`: OPA container image to use 53 | - `certificates_dir`: Directory for storing generated certificates 54 | - `kubeconfig`: Path to your kubeconfig file (defaults to `~/.kube/config` or `K8S_AUTH_KUBECONFIG` environment variable) 55 | - `create_route`: Whether to create an OpenShift route (default: `true`) 56 | 57 | ## Usage 58 | 59 | ### Deploying OPA Server 60 | 61 | 1. Set your kubeconfig path in one of these ways: 62 | - Set the `K8S_AUTH_KUBECONFIG` environment variable to the file path of your KUBECONFIG file 63 | - Set `kubeconfig` variable to the file path of your KUBECONFIG file 64 | - Create a custom vars file with your `kubeconfig` path 65 | 66 | 2. Run the deployment playbook: 67 | 68 | ```bash 69 | ansible-playbook deploy-opa-mtls.yml 70 | ``` 71 | 72 | ### Loading OPA Policies 73 | 74 | To load policies onto your OPA server, use the provided playbook: 75 | 76 | ```bash 77 | ansible-playbook load-opa-policies-mtls.yml 78 | ``` 79 | 80 | This playbook will: 81 | - Load all policies from the repository onto your OPA server 82 | - Use mTLS for secure communication with the OPA server 83 | - Configure the policies using variables defined in `vars/main.yml` 84 | 85 | ## mTLS Support 86 | 87 | The playbook automatically: 88 | 89 | 1. Generates a CA certificate and key 90 | 2. Generates server certificates and keys 91 | 3. Generates client certificates and keys 92 | 4. Creates a Kubernetes secret with the certificates 93 | 5. Configures the OPA server to use mTLS 94 | 95 | The certificates will be stored in the directory specified by `certificates_dir` (default: `certificates/` in the playbook directory). 96 | 97 | ## Accessing OPA Server via OpenShift Route 98 | 99 | The OPA server is exposed externally through an OpenShift route. To access it using curl: 100 | 101 | 1. Get the route URL: 102 | ```bash 103 | oc get route opa-mtls -n opa -o jsonpath='{.spec.host}' 104 | ``` 105 | 106 | ### Basic Health Check 107 | 108 | ```bash 109 | curl --cacert certificates/ca.crt \ 110 | --cert certificates/client.crt \ 111 | --key certificates/client.key \ 112 | https://$(oc get route opa-mtls -n opa -o jsonpath='{.spec.host}')/health 113 | ``` 114 | 115 | ### Validating Policy Loading 116 | 117 | To validate that the policies were loaded successfully, you can use the following commands with your mTLS certificates: 118 | 119 | 1. List all loaded policies: 120 | ```bash 121 | curl --cacert certificates/ca.crt \ 122 | --cert certificates/client.crt \ 123 | --key certificates/client.key \ 124 | https://$(oc get route opa-mtls -n opa -o jsonpath='{.spec.host}')/v1/policies | jq 125 | ``` 126 | 127 | 2. Test a specific policy: 128 | ```bash 129 | curl --cacert certificates/ca.crt \ 130 | --cert certificates/client.crt \ 131 | --key certificates/client.key \ 132 | -X POST \ 133 | -H "Content-Type: application/json" \ 134 | -d '{"input": {"example": "test"}}' \ 135 | https://$(oc get route opa-mtls -n opa -o jsonpath='{.spec.host}')/v1/data/aap_policy_examples/allowed_false 136 | ``` 137 | 138 | Replace `aap_policy_examples/allowed_false` and the input data with the specific policy and test data you want to validate. 139 | -------------------------------------------------------------------------------- /6a.Prevent job execution using extra_vars with non approved keys.md: -------------------------------------------------------------------------------- 1 | # Validating `extra_vars` Structure: Limit Inputs to Approved Variables 2 | 3 | ## Overview 4 | 5 | In many organizations, the person who launches automation jobs is not necessarily same individual who created or maintains the underlying Job Templates. Ansible Automation Platform allows users to specify `extra_vars` at job launch that enables customization of Job Templates. However, without guardrails, these variables can be freely defined by anyone launching the job. 6 | 7 | This example demonstrates how to use Policy as Code to enforce an allowlist of approved `extra_vars` keys. This ensures administrators can control what variables are permitted while still allowing flexibility in job customization. 8 | 9 | ## Example Policy [aap_policy_examples/extra_vars_allowlist.rego](aap_policy_examples/extra_vars_allowlist.rego) 10 | 11 | The following policy defines a list of allowed variable keys and blocks job execution if any other variables are included in the `extra_vars`. 12 | 13 | ```rego 14 | package aap_policy_examples 15 | 16 | import rego.v1 17 | 18 | # Define the allowed keys for extra_vars 19 | allowed_extra_var_keys := ["allowed"] 20 | 21 | # Default policy result: allowed (no violations) 22 | default extra_vars_allowlist := { 23 | "allowed": true, 24 | "violations": [], 25 | } 26 | 27 | # Evaluate extra_vars_allowlist, checking if provided extra_vars contain any keys not allowed 28 | extra_vars_allowlist := result if { 29 | # Extract extra_vars from input, defaulting to empty object if missing 30 | input_extra_var_keys := object.get(input, ["extra_vars"], {}) 31 | 32 | # Identify keys in extra_vars that are not in the allowed list 33 | violating_keys := [key | input_extra_var_keys[key]; not allowed_key(key)] 34 | 35 | # If violating keys are found, construct result indicating disallowed status and violations 36 | count(violating_keys) > 0 37 | 38 | result := { 39 | "allowed": false, 40 | "violations": [sprintf("Following extra_vars are not allowed: %v. Allowed keys: %v", [violating_keys, allowed_extra_var_keys])], 41 | } 42 | } 43 | 44 | # Helper function: Checks if a given key is in the allowed_extra_var_keys list 45 | allowed_key(key) if { 46 | allowed_extra_var_keys[_] == key 47 | } 48 | ``` 49 | 50 | ## Sample Input and Output 51 | 52 | Let's say a user launches a job with the following `extra_vars`: 53 | 54 | ```json 55 | { 56 | "extra_vars": { 57 | "env": "prod", 58 | "debug": true, 59 | "allowed": true 60 | } 61 | } 62 | ``` 63 | 64 | Since only specific keys are allowed, the job will be rejected if it includes unapproved variables. In the example above, the keys `env` and `debug` are not permitted and will trigger a policy violation since the built in policy did not allowlist either of those two variables. 65 | 66 | Expected policy decision output: 67 | 68 | ```json 69 | { 70 | "allowed": false, 71 | "violations": [ 72 | "Following extra_vars are not allowed: [\"env\", \"debug\"]. Allowed keys: [\"allowed\"]" 73 | ] 74 | } 75 | ``` 76 | 77 | ## Enforcement Behavior 78 | 79 | This policy is enforced at the Job Template level. Any job launched from a job template with this policy attached will fail if disallowed `extra_vars` are provided. 80 | 81 | #TODO: Need to add Rego link 82 | 83 | ## Real World Use Case: Enforcing Safe `extra_vars` at Launch 84 | 85 | ### Scenario 86 | 87 | A platform team maintains job templates that support limited customization through a small set of predefined `extra_vars`, such as `target_environment` and `rollback`. These variables are explicitly handled in the Job Template to modify behavior. For example, selecting whether a deployment should target an on-prem or a cloud environment, or trigger a rollback to a previously working version. 88 | 89 | To prevent jobs from running with unsupported variables, the team enforces a policy that restricts which `extra_vars` can be passed at launch. If a user includes a variable outside the approved list due to a typo or misuse, the job is blocked before execution. A message is displayed in the Ansible Automation Platform output of the job identifying the policy violation and which variables are allowed. 90 | 91 | This approach ensures that job runs with the intended variable allowlist that reduces the risk of misconfiguration and helps maintain predictable automation across teams and environments. 92 | 93 | ## Impact of Policy Enforcement in Ansible Automation Platform 94 | 95 | By enforcing structure on `extra_vars`, teams benefit from: 96 | 97 | - Controlled job customization with clearly defined variables 98 | - Fewer misconfigurations and errors at runtime due to misuse of `extra_vars` 99 | - A consistent automation experience across environments 100 | 101 | ## How AAP Enforces the Policy 102 | 103 | Once the allowlist policy is in place: 104 | 105 | - Jobs using unapproved `extra_vars` are rejected before any execution begins 106 | - Only jobs that use the allowed variables can proceed 107 | - Violation messages provide specific feedback on what variables were denied and which are allowed 108 | 109 | ## Why This Matters 110 | 111 | - **Security**: Prevents arbitrary or malicious input from altering job behavior 112 | - **Consistency**: Enforces naming and input standards across teams 113 | - **Clarity**: Gives users immediate feedback when unsupported variables are used 114 | 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example OPA Policies for Ansible Automation Platform 2 | 3 | This repository contains example policies and use cases demonstrating how to use Policy as Code feature in Ansible Automation Platform (AAP). These examples will guide you through implementing various policy enforcement scenarios using Open Policy Agent (OPA). 4 | 5 | ## Overview 6 | 7 | Policy as Code allows you to define and enforce policies across your Ansible Automation Platform using OPA and the Rego language. This repository provides practical examples of common policy enforcement scenarios. 8 | 9 | ## Prerequisites 10 | 11 | - Ansible Automation Platform 2.5 or later with the `FEATURE_POLICY_AS_CODE_ENABLED` feature flag set to `True` 12 | - see [Enabling Policy as Code Feature](docs/Enabling%20Policy%20as%20Code%20feature.md) for more information 13 | - An OPA server that's reachable from your AAP deployment 14 | - See [Deploy OPA server on OpenShift](docs/Deploy%20OPA%20server%20on%20OpenShift.md) for development and testing setup 15 | - See [Deploy OPA server with Podman](docs/Deploy%20OPA%20server%20with%20Podman.md) for development and testing setup 16 | - Configured AAP with settings required for connecting to your OPA server 17 | - see [Configuring OPA Server Connection](docs/Configuring%20OPA%20Server%20Connection.md) for more information 18 | - General knowledge around OPA and the Rego language 19 | - see [Official OPA Documentation](https://www.openpolicyagent.org/docs/latest/) for more information 20 | 21 | For detailed setup instructions, see "Setting up Policy as Code for Ansible Automation Platform" in the official documentation. 22 | 23 | ## Repository Structure 24 | 25 | ``` 26 | . 27 | ├── aap_policy_examples/ # Example policy implementations 28 | ├── example_input_data/ # Sample input data for testing 29 | ├── test_aap_policy_examples/ # Test cases and validation 30 | ├── openshift/ # OpenShift-specific configurations 31 | ├── tools/ # Utility scripts and tools 32 | ├── bin/ # Binary and executable files 33 | ├── .github/ # GitHub-specific configurations 34 | ├── POLICY_INPUT_DATA.md # Documentation of input data structure 35 | └── POLICY_OUTPUT_DATA.md # Documentation of output data structure 36 | ``` 37 | 38 | ## Example Policies 39 | 40 | The repository includes several example policies demonstrating different use cases: 41 | 42 | 1. [Prevent job execution at various policy enforcement points](1.Prevent%20job%20execution%20at%20different%20policy%20enforcement%20points.md) 43 | 2. [Prevent job execution by platform admin](2.Prevent%20job%20execution%20by%20platform%20admin.md) 44 | 3. [Prevent job execution during maintenance window](3.Prevent%20job%20execution%20during%20maintenance%20window.md) 45 | 4. [Prevent job execution using credential with no Organization](4.Prevent%20job%20execution%20using%20credential%20with%20no%20Organization.md) 46 | 5. [Prevent job execution using mismatching resources](5.Prevent%20job%20execution%20using%20mismatching%20resources.md) 47 | 6. Enforce extra_vars based policies 48 | - [Prevent job execution using extra vars with non approved vars](6a.Prevent%20job%20execution%20using%20extra%20vars%20with%20non%20approved%20vars.md) - Validate keys for extra_vars 49 | - [Prevent job execution using extra vars with non approved values](6b.Prevent%20job%20execution%20using%20extra%20vars%20with%20non%20approved%20values.md) - Validate values for extra_vars 50 | - [Prevent job execution based on user limitations for extra vars](6c.Prevent%20job%20execution%20based%20on%20user%20limitations%20for%20extra%20vars.md) - Team-based access control on extra_vars 51 | 7. Source code controls 52 | - [Only allow approved Github source repos](7a.Only%20allow%20approved%20Github%20repos.md) - Only allow approved source repos 53 | - [Only allow approved Github source repo branches](7b.Only%20allow%20certain%20Git%20branches.md) - Only allow approved source repo branches 54 | 8. [Enforce Naming Standards](8.Enforce%20Job%20Template%20Naming%20Standards.md) - ensure Job Template name conforms to our standards 55 | 9. [Restrict usage of an Inventory to an Organization](9.Restrict%20Inventory%20use%20to%20an%20organization.md) - restrict inventory usage by organization 56 | 57 | Each policy example includes: 58 | - Detailed explanation of the use case 59 | - Example Rego policy implementation 60 | - Sample input and output data 61 | - Testing instructions 62 | 63 | ## Getting Started 64 | 65 | 1. Clone this repository 66 | 2. Review the example policies in the `aap_policy_examples/` directory 67 | 3. Use the provided test cases in `test_aap_policy_examples/` to validate your policies 68 | 4. Customize the policies according to your needs 69 | 70 | ## Testing 71 | 72 | The repository includes test cases and example input data to help you validate your policies. See the `test_aap_policy_examples/` directory for more details. 73 | 74 | ## Documentation 75 | 76 | - [POLICY_INPUT_DATA.md](POLICY_INPUT_DATA.md): Contains detailed information about the input data structure used by the policies 77 | - [POLICY_OUTPUT_DATA.md](POLICY_OUTPUT_DATA.md): Contains detailed information about the output data structure 78 | - [Associating Policies with AAP Resources](docs/Associating%20policy%20with%20AAP%20resources.md): Guide on how to associate OPA policies with AAP resources 79 | 80 | ## Contributing 81 | 82 | Contributions are welcome! Please feel free to submit a Pull Request. 83 | 84 | ## License 85 | 86 | This project is dedicated to the public domain under The Unlicense. See the LICENSE file for details. 87 | 88 | The Unlicense is a template for disclaiming copyright monopoly interest in software you've written; in other words, it is a template for dedicating your software to the public domain. 89 | -------------------------------------------------------------------------------- /6b.Prevent job execution using extra_vars with non approved values.md: -------------------------------------------------------------------------------- 1 | # Preventing unauthorized `extra_vars` values from executing a Job Template 2 | 3 | ## Overview 4 | 5 | In many organizations, the person who launches automation jobs is not 6 | necessarily the same individual who created or maintains the underlying Job 7 | Templates. While Ansible Automation Platform allows users to supply `extra_vars` 8 | at launch time, there's often a need to validate not just the presence of 9 | certain variables, but also the specific values those variables are allowed to accept. 10 | 11 | This example demonstrates how to use the Policy as Code feature to enforce a set of approved values for given `extra_vars` variables. 12 | 13 | ## Example Policy [aap_policy_examples/extra_vars_validation.rego](aap_policy_examples/extra_vars_validation.rego) 14 | 15 | The following policy defines which values are valid for specific `extra_vars` variables. If a job is launched with any value outside the allowed list, it will be blocked and an appropriate violation message will be returned. 16 | 17 | ```rego 18 | package aap_policy_examples 19 | 20 | import rego.v1 21 | 22 | # Define allowed values for specific keys in extra_vars 23 | valid_extra_var_values := {"extra_var_key": ["allowed_value1", "allowed_value2"]} 24 | 25 | # Default policy response indicating allowed status with no violations 26 | default extra_vars_validation := { 27 | "allowed": true, 28 | "violations": [], 29 | } 30 | 31 | # Evaluate extra_vars_validation to check if extra_vars values are allowed 32 | extra_vars_validation := result if { 33 | # Extract extra_vars from input, defaulting to empty object if missing 34 | input_extra_vars := object.get(input, ["extra_vars"], {}) 35 | 36 | # Identify keys with disallowed values 37 | violating_keys := [key | 38 | valid_extra_var_values[key] 39 | not allowed_value(key, input_extra_vars[key]) 40 | ] 41 | 42 | # Check if there are any violations 43 | count(violating_keys) > 0 44 | 45 | result := { 46 | "allowed": false, 47 | "violations": [sprintf("extra_vars contain disallowed values for keys: %v. Allowed values: %v", [violating_keys, valid_extra_var_values])], 48 | } 49 | } 50 | 51 | # Check if a given value for a key is allowed 52 | allowed_value(key, value) if { 53 | valid_extra_var_values[key][_] == value 54 | } 55 | ``` 56 | 57 | ## Sample Input and Output 58 | 59 | Let's say a user launches a job with the following `extra_vars`: 60 | 61 | ```json 62 | { 63 | "extra_vars": { 64 | "extra_var_key": "unauthorized_value" 65 | } 66 | } 67 | ``` 68 | 69 | The policy is configured to only allow the following values for the `extra_var_key`: 70 | 71 | ```json 72 | { 73 | "extra_var_key": ["allowed_value1", "allowed_value2"] 74 | } 75 | ``` 76 | 77 | Because `"unauthorized_value"` is not in that allowed list, the job will be blocked. 78 | 79 | Expected policy decision output: 80 | 81 | ```json 82 | { 83 | "allowed": false, 84 | "violations": [ 85 | "extra_vars contain disallowed values for keys: [\"extra_var_key\"]. Allowed values: {\"extra_var_key\": [\"allowed_value1\", \"allowed_value2\"]}" 86 | ] 87 | } 88 | ``` 89 | 90 | 91 | ## Enforcement Behavior 92 | 93 | This policy is enforced at the Job Template level. Any job launched from a job 94 | template with this policy attached will fail if disallowed `extra_vars` values 95 | provided are not part of the `valid_extra_var_values` variable in the policy. 96 | 97 | ## Use Case: Enforcing Allowed Values within `extra_vars` Variables 98 | 99 | A platform team supports job templates that accept certain `extra_vars` to 100 | control behavior at runtime. For example, users may provide an input like 101 | `deployment_type`, referring to dev or production environments, to control how 102 | aggressively changes are implemented. 103 | 104 | To reduce risk and prevent unintended behavior, the team defines an explicit 105 | list of allowed values for each key—such as `deployment_type: ["dev", "prod"]`. 106 | If a user provides a value not on that list (e.g., `deployment_type: 107 | "staging"`), the job is rejected before it runs. 108 | 109 | By enforcing approved values per key, the team ensures only known, supported options are used—preserving consistency and reducing the potential for errors or misuse. 110 | 111 | ## Impact of Policy Enforcement in Ansible Automation Platform 112 | 113 | By enforcing validation, teams can: 114 | 115 | - Prevent unsupported or unstable configurations from being introduced into automation 116 | - Ensure only prescribed variable values are used within playbooks 117 | - Reduce debugging time caused by unexpected or invalid user provided values 118 | 119 | This gives automation authors more confidence that job behavior will remain predictable, regardless of who launches the job or what values they attempt to pass in. 120 | 121 | ## How AAP Enforces the Policy 122 | 123 | When the policy is attached to a Job Template: 124 | 125 | - The `extra_vars` values provided at job launch are evaluated against the policy's allowed value lists 126 | - Jobs are immediately blocked if a variable contains a value outside its defined list 127 | - A clear violation message is returned, identifying which variable(s) failed validation and what values are allowed 128 | 129 | This enforcement happens before any playbook execution begins, ensuring violations are caught early in the job lifecycle. 130 | 131 | ## Why This Matters 132 | 133 | Unlike variable restrictions, value validation enforces the correctness of what 134 | users are trying to do, not just what they're allowed to modify. It protects 135 | automation logic that depends on trusted variables by making sure only supported 136 | values are used within those variables. 137 | 138 | This is especially important when Job Templates branch behavior based on 139 | `extra_vars`. A single incorrect value could trigger the wrong deployment 140 | strategy, bypass a safety step, or produce an unpredictable result.. 141 | 142 | -------------------------------------------------------------------------------- /6c.Prevent job execution based on user limitations for extra vars.md: -------------------------------------------------------------------------------- 1 | # Enforcing team based access to `extra_vars` values 2 | 3 | ## Overview 4 | In many organizations, responsibilities are divided among teams, each responsible for a specific environment such as development or production. While Ansible Automation Platform allows users to provide `extra_vars` at job launch, it becomes critical to ensure those values are appropriate for the user's team to prevent unauthorized or unsafe deployments. 5 | 6 | This example demonstrates how to use Policy as Code to enforce `extra_vars` value restrictions based on what team the user belongs too. 7 | 8 | ## Example Policy [aap_policy_examples/team_based_extra_vars_restriction.rego](aap_policy_examples/team_based_extra_vars_restriction.rego) 9 | 10 | The following policy ensures that only approved `extra_vars` values are accepted for users based on what team they're are on. If a user supplies a value outside of what their team is allowed to use, the job is blocked and a detailed violation message is returned. 11 | 12 | ```rego 13 | package aap_policy_examples 14 | 15 | import rego.v1 16 | 17 | # Define allowed values for specific keys in extra_vars based on teams 18 | valid_extra_var_values_by_team := { 19 | "dev_team": {"environment": ["dev", "staging"]}, 20 | "prod_team": {"environment": ["prod", "staging"]}, 21 | } 22 | 23 | # Default response allowing extra_vars unless violations occur 24 | default team_based_extra_vars_restriction := { 25 | "allowed": true, 26 | "violations": [], 27 | } 28 | 29 | # Evaluate extra_vars against allowed values considering team memberships 30 | team_based_extra_vars_restriction := result if { 31 | # Extract extra_vars from input 32 | input_extra_vars := object.get(input, ["extra_vars"], {}) 33 | 34 | # Extract user's team names 35 | user_teams := {team | team := input.created_by.teams[_].name} 36 | 37 | violating_keys := [key | 38 | allowed_vals := allowed_values_for_key_and_teams(key, user_teams) 39 | input_value := input_extra_vars[key] 40 | allowed_values_for_key_and_teams(key, user_teams) 41 | not allowed_value(input_value, allowed_vals) 42 | ] 43 | 44 | count(violating_keys) > 0 45 | 46 | result := { 47 | "allowed": false, 48 | "violations": [sprintf("extra_vars contain disallowed values for keys: %v. Allowed extra_vars for your teams (%v): %v", [violating_keys, user_teams, allowed_values_for_user_teams(user_teams)])], 49 | } 50 | } 51 | 52 | # Retrieve allowed values for a specific key based on user's teams 53 | allowed_values_for_key_and_teams(key, teams) := values if { 54 | values := {val | team := teams[_]; val := valid_extra_var_values_by_team[team][key][_]; valid_extra_var_values_by_team[team][key]} 55 | } 56 | 57 | # Retrieve all allowed values based on user's teams 58 | allowed_values_for_user_teams(teams) := team_values if { 59 | team_values := {team: valid_extra_var_values_by_team[team] | team := teams[_]; valid_extra_var_values_by_team[team]} 60 | } 61 | 62 | # Check if given value is in allowed values set 63 | allowed_value(value, allowed_values) if { 64 | allowed_values[_] == value 65 | } 66 | ``` 67 | 68 | ## Sample Input and Output 69 | 70 | Suppose a user from the `dev_team` attempts to launch a job with the following `extra_vars`: 71 | 72 | ```json 73 | { 74 | "extra_vars": { 75 | "environment": "prod" 76 | }, 77 | "created_by": { 78 | "teams": [ 79 | { "name": "dev_team" } 80 | ] 81 | } 82 | } 83 | ``` 84 | 85 | The policy only allows `dev_team` members to use `["dev", "staging"]` for the `environment` variable. 86 | 87 | Because `prod` is not an approved value for that team, the job is blocked and fails. 88 | 89 | **Expected policy decision output:** 90 | 91 | ```json 92 | { 93 | "allowed": false, 94 | "violations": [ 95 | "extra_vars contain disallowed values for keys: [\"environment\"]. Allowed extra_vars for your teams ([\"dev_team\"]): {\"dev_team\": {\"environment\": [\"dev\", \"staging\"]}}" 96 | ] 97 | } 98 | ``` 99 | 100 | ## Enforcement Behavior 101 | 102 | This policy is attached to a Job Template. During launch, it checks if the user belongs to a team and verifies that the provided `extra_vars` are within the set of allowed values for their teams. If any value is not allowed, the job is blocked with a clear violation message. 103 | 104 | ## Use Case: Team Based `extra_vars` Control 105 | 106 | An enterprise automation team supports Job Templates shared across multiple departments. Each department has access only to specific environments. To enforce this, the platform team implements a policy that only allows certain `extra_vars` values depending on what team the user belongs too. 107 | 108 | For instance, the `dev_team` can deploy to `dev` or `staging` environments, while the `prod_team` can deploy to `prod`. This prevents accidental or unauthorized changes across environments. 109 | 110 | ## Impact of Policy Enforcement in Ansible Automation Platform 111 | 112 | By tying `extra_vars` validation to a user's team membership, platform teams can: 113 | 114 | - Ensure separation of duties and environment access 115 | - Prevent accidental deployment to production by unauthorized teams 116 | 117 | ## How AAP Enforces the Policy 118 | 119 | When this policy is attached to a Job Template: 120 | 121 | - It checks each variable within `extra_vars` against the team's allowed values 122 | - Jobs are blocked and fail immediately if any value is unauthorized 123 | - A message explains the violation, including the allowed values for the user's team 124 | 125 | This enforcement occurs **before** the playbook runs, helping catch misconfigurations early. 126 | 127 | 128 | ## Why This Matters 129 | 130 | Enforcing approved variable values based on team ownership improves both security and stability. It ensures that automation jobs only execute in contexts explicitly approved for the user running them. This is especially useful when different teams manage different lifecycle environments, reducing the chance of critical mistakes and maintaining environment integrity. 131 | -------------------------------------------------------------------------------- /5.Prevent job execution using mismatching resources.md: -------------------------------------------------------------------------------- 1 | # 5. Preventing Job Execution Using Mismatched Credential, Inventory, and Project Prefixes 2 | 3 | ## Overview 4 | 5 | Ansible Automation Platform provides powerful tools for orchestrating and scaling automation across teams and environments. As automation grows, maintaining consistency and preventing misconfiguration becomes increasingly important. 6 | 7 | In some cases, teams adopt naming conventions to clearly separate resources, for example, using prefixes like `prod_` or `dev_` in job templates, inventories, credentials, and projects. These naming patterns help reinforce environment boundaries and reduce the chance of accidental misuse. 8 | 9 | With Policy Enforcement, organizations can take this one step further by ensuring these naming conventions exist at execution time. 10 | 11 | ## Example Policy [aap_policy_examples/mismatch_prefix_allowed_false.rego](aap_policy_examples/mismatch_prefix_allowed_false.rego) 12 | 13 | The policy below inspects the job template's name prefix and compares it with the prefix of: 14 | 15 | - The job template name 16 | - The inventory name 17 | - The project name 18 | - Each credential used 19 | 20 | If any of them don't match the job template's prefix, the job is blocked. 21 | 22 | ```rego 23 | package aap_policy_examples 24 | 25 | prefix_delimiter := "_" 26 | 27 | # job_template_prefix extracts the substring before the first prefix_delimiter in `input.job_template.name`. 28 | job_template_prefix := jtp if { 29 | parts := split(input.job_template.name, prefix_delimiter) 30 | jtp := parts[0] 31 | } 32 | 33 | # inventory_prefix extracts the substring before the first prefix_delimiter in `input.inventory.name`. 34 | inventory_prefix := inv_pref if { 35 | parts := split(input.inventory.name, prefix_delimiter) 36 | inv_pref := parts[0] 37 | } 38 | 39 | # project_prefix extracts the substring before the first prefix_delimiter in `input.project.name`. 40 | project_prefix := proj_pref if { 41 | parts := split(input.project.name, prefix_delimiter) 42 | proj_pref := parts[0] 43 | } 44 | 45 | # credentials_prefixes is a list of prefix values from each credential's name. 46 | credentials_prefixes := [cprefix | 47 | cred := input.credentials[_] # iterate over credentials 48 | parts := split(cred.name, prefix_delimiter) # split name 49 | cprefix := parts[0] # grab the first part 50 | ] 51 | 52 | # mismatch is true if either: 53 | # 1. The project prefix != job template prefix, OR 54 | # 2. The inventory prefix != job template prefix OR 55 | # 3. Any credential's prefix != job template prefix. 56 | mismatch if { 57 | project_prefix != job_template_prefix 58 | } 59 | 60 | mismatch if { 61 | inventory_prefix != job_template_prefix 62 | } 63 | 64 | mismatch if { 65 | some cp in credentials_prefixes 66 | cp != job_template_prefix 67 | } 68 | 69 | default mismatch_prefix_allowed_false := { 70 | "allowed": true, 71 | "violations": [], 72 | } 73 | 74 | mismatch_prefix_allowed_false := { 75 | "allowed": false, 76 | "violations": ["Mismatch prefix between Inventory, Credentials and Project detected."], 77 | } if { 78 | mismatch 79 | } 80 | ``` 81 | 82 | ## Input 83 | 84 | ```json 85 | { 86 | "id": 785, 87 | "name": "demo_Job Template", 88 | "credentials": [ 89 | { 90 | "name": "demo_Credential", 91 | "description": "", 92 | "organization": null, 93 | "credential_type": 1, 94 | "managed": false, 95 | "inputs": { 96 | "username": "admin" 97 | }, 98 | "kind": "ssh", 99 | "cloud": false, 100 | "kubernetes": false 101 | } 102 | ], 103 | "project": { 104 | "id": 6, 105 | "name": "demo_Project", 106 | "status": "successful", 107 | "scm_type": "git", 108 | "scm_url": "https://github.com/ansible/ansible-tower-samples", 109 | "scm_branch": "", 110 | "scm_refspec": "", 111 | "scm_clean": false, 112 | "scm_track_submodules": false, 113 | "scm_delete_on_update": false 114 | }, 115 | "inventory": { 116 | "name": "prod_Inventory", 117 | "description": "", 118 | "has_active_failures": false, 119 | "total_hosts": 1, 120 | "hosts_with_active_failures": 0, 121 | "total_groups": 0, 122 | "has_inventory_sources": false, 123 | "total_inventory_sources": 0, 124 | "kind": "" 125 | }, 126 | } 127 | ``` 128 | 129 | ## Output 130 | 131 | ```json 132 | { 133 | "allowed": false, 134 | "violations": [ 135 | "Mismatch prefix between Inventory, Credentials and Project detected." 136 | ] 137 | } 138 | ``` 139 | 140 | ## Enforcement Behavior 141 | 142 | When this policy is enforced, the job will **ERROR** if any of the following are true: 143 | 144 | - The **inventory** name prefix does not match the job template name prefix 145 | - The **project** name prefix does not match the job template name prefix 146 | - One or more **credentials** have a name prefix that doesn't match 147 | 148 | This policy is applied within the Job Template, ensuring that every job execution strictly follows naming conventions. 149 | 150 | ## Real World Use Case: Preventing Environment Crossovers 151 | 152 | ### Scenario 153 | 154 | A platform team manages automation across different environments — `prod`, `dev`, and `qa` — within the same Ansible Automation Platform instance. Each job template, inventory, project, and credential is named using a prefix (e.g., `prod_`, `dev_`) to denote its environment. 155 | 156 | During a production incident, a developer accidentally launched a job template using a `dev_` project with a `prod_` inventory and credential. The automation went through — but made unintended changes to a live system. 157 | 158 | To prevent this from happening again, the team enforced a policy that checks for **naming consistency across job resources**. Any job that combines mismatching prefixes is now automatically blocked before execution. 159 | 160 | ## Impact of Policy Enforcement in Ansible Automation Platform 161 | 162 | With the policy in place: 163 | 164 | - 🚫 **Mismatched environments are blocked** – accidental combinations of unrelated resource sets are caught at execution time. 165 | - ✅ **Consistent naming is enforced** – only resources with the same prefix can be used together. 166 | - ✅ **Clear violation messages help users self-correct** – reducing support overhead and errors. 167 | 168 | ## Why This Matters 169 | 170 | - **Prevent Human Error** – Automate the guardrails to reduce human errors. 171 | - **Enforce Standards** – Naming conventions become enforceable, not just suggested. 172 | - **Reduce Risk** – Ensure production inventories are only used with production-configured jobs. 173 | 174 | 175 | With Policy Enforcement at Runtime in Ansible Automation Platform, you can go beyond access control and enforce context aware policies protecting your infrastructure with smart, simple rules. 176 | 177 | -------------------------------------------------------------------------------- /POLICY_INPUT_DATA.md: -------------------------------------------------------------------------------- 1 | # Input data for OPA policy from job execution 2 | 3 | This document describes the input data that Ansible Automation Platform will provide to OPA server when querying policies 4 | 5 | ## Fields 6 | 7 | ### `id` 8 | - **Type:** Integer 9 | - **Description:** Unique identifier for the job. 10 | 11 | ### `name` 12 | - **Type:** String 13 | - **Description:** Name of the job template. 14 | 15 | ### `created` 16 | - **Type:** Datetime (ISO 8601) 17 | - **Description:** Timestamp indicating when the job was created. 18 | 19 | ### `created_by` 20 | - **Type:** Object 21 | - **Description:** Information about the user who created the job. 22 | - `id` (Integer): Unique identifier for the user. 23 | - `username` (String): Username of the creator. 24 | - `is_superuser` (Boolean): Indicates if the user is a superuser. 25 | 26 | ### `credentials` 27 | - **Type:** List of Objects 28 | - **Description:** Credentials associated with the job execution. 29 | - `id` (Integer): Unique identifier for the credential. 30 | - `name` (String): Name of the credential. 31 | - `description` (String): Description of the credential. 32 | - `organization` (Integer or Null): Organization identifier associated with the credential. 33 | - `credential_type` (Integer): Type identifier for the credential. 34 | - `managed` (Boolean): Indicates if the credential is managed internally. 35 | - `kind` (String): Credential type (`ssh`, `cloud`, `kubernetes`, etc.). 36 | - `cloud` (Boolean): Indicates if the credential is for cloud services. 37 | - `kubernetes` (Boolean): Indicates if the credential is for Kubernetes services. 38 | 39 | ### `execution_environment` 40 | - **Type:** Object 41 | - **Description:** Details about the execution environment used for the job. 42 | - `id` (Integer): Unique identifier for the execution environment. 43 | - `name` (String): Name of the execution environment. 44 | - `image` (String): Container image used for execution. 45 | - `pull` (String): Pull policy for the execution environment. 46 | 47 | ### `extra_vars` 48 | - **Type:** JSON 49 | - **Description:** Extra variables provided for the job execution. 50 | 51 | ### `forks` 52 | - **Type:** Integer 53 | - **Description:** Number of parallel processes used for the job execution. 54 | 55 | ### `hosts_count` 56 | - **Type:** Integer 57 | - **Description:** Number of hosts targeted by the job. 58 | 59 | ### `instance_group` 60 | - **Type:** Object 61 | - **Description:** Information about the instance group handling the job. 62 | - `id` (Integer): Unique identifier for the instance group. 63 | - `name` (String): Name of the instance group. 64 | - `capacity` (Integer): Available capacity in the group. 65 | - `jobs_running` (Integer): Number of currently running jobs. 66 | - `jobs_total` (Integer): Total jobs handled by the group. 67 | - `max_concurrent_jobs` (Integer): Maximum concurrent jobs allowed. 68 | - `max_forks` (Integer): Maximum forks allowed. 69 | 70 | ### `inventory` 71 | - **Type:** Object 72 | - **Description:** Inventory details used in the job execution. 73 | - `id` (Integer): Unique identifier for the inventory. 74 | - `name` (String): Name of the inventory. 75 | - `description` (String): Description of the inventory. 76 | - `kind` (String): Inventory type. 77 | - `total_hosts` (Integer): Total number of hosts in the inventory. 78 | - `total_groups` (Integer): Total number of groups in the inventory. 79 | - `has_inventory_sources` (Boolean): Indicates if the inventory has external sources. 80 | - `total_inventory_sources` (Integer): Number of external inventory sources. 81 | - `has_active_failures` (Boolean): Indicates if there are active failures in the inventory. 82 | - `hosts_with_active_failures` (Integer): Number of hosts with active failures. 83 | - `inventory_sources` (Array): External inventory sources associated with the inventory. 84 | 85 | ### `job_template` 86 | - **Type:** Object 87 | - **Description:** Information about the job template. 88 | - `id` (Integer): Unique identifier for the job template. 89 | - `name` (String): Name of the job template. 90 | - `job_type` (String): Type of job (e.g., `run`). 91 | 92 | ### `job_type` 93 | - **Type:** Choice (String) 94 | - **Description:** Type of job execution. 95 | - Allowed values: 96 | - `run`: Run 97 | - `check`: Check 98 | - `scan`: Scan 99 | 100 | ### `job_type_name` 101 | - **Type:** String 102 | - **Description:** Human-readable name for the job type. 103 | 104 | ### `labels` 105 | - **Type:** List of Objects 106 | - **Description:** Labels associated with the job. 107 | - `id` (Integer): Unique identifier for the label. 108 | - `name` (String): Name of the label. 109 | - `organization` (Object): Organization associated with the label. 110 | - `id` (Integer): Unique identifier of the organization. 111 | - `name` (String): Name of the organization. 112 | 113 | ### `launch_type` 114 | - **Type:** Choice (String) 115 | - **Description:** How the job was launched. 116 | - Allowed values: 117 | - `manual`: Manual 118 | - `relaunch`: Relaunch 119 | - `callback`: Callback 120 | - `scheduled`: Scheduled 121 | - `dependency`: Dependency 122 | - `workflow`: Workflow 123 | - `webhook`: Webhook 124 | - `sync`: Sync 125 | - `scm`: SCM Update 126 | 127 | ### `limit` 128 | - **Type:** String 129 | - **Description:** Limit applied to the job execution. 130 | 131 | ### `launched_by` 132 | - **Type:** Object 133 | - **Description:** Information about the user who launched the job. 134 | - `id` (Integer): Unique identifier for the user. 135 | - `name` (String): Name of the user. 136 | - `type` (String): Type of user (`user`, `system`, etc.). 137 | - `url` (String): API URL for the user. 138 | 139 | ### `organization` 140 | - **Type:** Object 141 | - **Description:** Information about the organization associated with the job. 142 | - `id` (Integer): Unique identifier for the organization. 143 | - `name` (String): Name of the organization. 144 | 145 | ### `playbook` 146 | - **Type:** String 147 | - **Description:** The playbook used in the job execution. 148 | 149 | ### `project` 150 | - **Type:** Object 151 | - **Description:** Details about the project associated with the job. 152 | - `id` (Integer): Unique identifier for the project. 153 | - `name` (String): Name of the project. 154 | - `status` (Choice - String): Status of the project. 155 | - `successful`: Successful 156 | - `failed`: Failed 157 | - `error`: Error 158 | - `scm_type` (String): Source control type (`git`, `svn`, etc.). 159 | - `scm_url` (String): URL of the source control repository. 160 | - `scm_branch` (String): Branch used in the repository. 161 | - `scm_refspec` (String): RefSpec for the repository. 162 | - `scm_clean` (Boolean): Whether SCM is cleaned before updates. 163 | - `scm_track_submodules` (Boolean): Whether submodules are tracked. 164 | - `scm_delete_on_update` (Boolean): Whether SCM deletes files on update. 165 | 166 | ### `scm_branch` 167 | - **Type:** String 168 | - **Description:** Specific branch to use for SCM. 169 | 170 | ### `scm_revision` 171 | - **Type:** String 172 | - **Description:** SCM revision used for the job. 173 | 174 | ### `workflow_job` 175 | - **Type:** Object 176 | - **Description:** Workflow job details, if the job is part of a workflow. 177 | - `id` (Integer): Unique identifier for the workflow job. 178 | - `name` (String): Name of the workflow job. 179 | 180 | ### `workflow_job_template` 181 | - **Type:** Object 182 | - **Description:** Workflow job template details. 183 | - `id` (Integer): Unique identifier for the workflow job template. 184 | - `name` (String): Name of the workflow job template. 185 | - `job_type` (String or Null): Type of job within the workflow context. 186 | 187 | ## Example input data from demo job template launch 188 | 189 | ```json 190 | { 191 | "id": 70, 192 | "name": "Demo Job Template", 193 | "created": "2025-03-19T19:07:03.329426Z", 194 | "created_by": { 195 | "id": 1, 196 | "username": "admin", 197 | "is_superuser": true, 198 | "teams": [] 199 | }, 200 | "credentials": [ 201 | { 202 | "id": 3, 203 | "name": "Example Machine Credential", 204 | "description": "", 205 | "organization": null, 206 | "credential_type": 1, 207 | "managed": false, 208 | "kind": "ssh", 209 | "cloud": false, 210 | "kubernetes": false 211 | } 212 | ], 213 | "execution_environment": { 214 | "id": 2, 215 | "name": "Default execution environment", 216 | "image": "registry.redhat.io/ansible-automation-platform-25/ee-supported-rhel8@sha256:b9f60d9ebbbb5fdc394186574b95dea5763b045ceff253815afeb435c626914d", 217 | "pull": "" 218 | }, 219 | "extra_vars": { 220 | "example": "value" 221 | }, 222 | "forks": 0, 223 | "hosts_count": 0, 224 | "instance_group": { 225 | "id": 2, 226 | "name": "default", 227 | "capacity": 0, 228 | "jobs_running": 1, 229 | "jobs_total": 38, 230 | "max_concurrent_jobs": 0, 231 | "max_forks": 0 232 | }, 233 | "inventory": { 234 | "id": 1, 235 | "name": "Demo Inventory", 236 | "description": "", 237 | "kind": "", 238 | "total_hosts": 1, 239 | "total_groups": 0, 240 | "has_inventory_sources": false, 241 | "total_inventory_sources": 0, 242 | "has_active_failures": false, 243 | "hosts_with_active_failures": 0, 244 | "inventory_sources": [] 245 | }, 246 | "job_template": { 247 | "id": 7, 248 | "name": "Demo Job Template", 249 | "job_type": "run" 250 | }, 251 | "job_type": "run", 252 | "job_type_name": "job", 253 | "labels": [ 254 | { 255 | "id": 1, 256 | "name": "Demo label", 257 | "organization": { 258 | "id": 1, 259 | "name": "Default" 260 | } 261 | } 262 | ], 263 | "launch_type": "workflow", 264 | "limit": "", 265 | "launched_by": { 266 | "id": 1, 267 | "name": "admin", 268 | "type": "user", 269 | "url": "/api/v2/users/1/" 270 | }, 271 | "organization": { 272 | "id": 1, 273 | "name": "Default" 274 | }, 275 | "playbook": "hello_world.yml", 276 | "project": { 277 | "id": 6, 278 | "name": "Demo Project", 279 | "status": "successful", 280 | "scm_type": "git", 281 | "scm_url": "https://github.com/ansible/ansible-tower-samples", 282 | "scm_branch": "", 283 | "scm_refspec": "", 284 | "scm_clean": false, 285 | "scm_track_submodules": false, 286 | "scm_delete_on_update": false 287 | }, 288 | "scm_branch": "", 289 | "scm_revision": "", 290 | "workflow_job": { 291 | "id": 69, 292 | "name": "Demo Workflow" 293 | }, 294 | "workflow_job_template": { 295 | "id": 10, 296 | "name": "Demo Workflow", 297 | "job_type": null 298 | } 299 | } 300 | ``` 301 | --------------------------------------------------------------------------------