mirror of
https://github.com/ansible/awx.git
synced 2026-06-20 22:27:42 -02:30
Compare commits
70 Commits
24.1.0
...
x-request-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19e3cba35c | ||
|
|
e4646ae611 | ||
|
|
c11ff49a56 | ||
|
|
7dc77546f4 | ||
|
|
f5f85666c8 | ||
|
|
47a061eb39 | ||
|
|
51bcf82cf4 | ||
|
|
c760577855 | ||
|
|
814ceb0d06 | ||
|
|
f178c84728 | ||
|
|
c0f71801f6 | ||
|
|
4e8e1398d7 | ||
|
|
3d6a8fd4ef | ||
|
|
e873bb1304 | ||
|
|
672f1eb745 | ||
|
|
199507c6f1 | ||
|
|
a176c04c14 | ||
|
|
e3af658f82 | ||
|
|
e8a3b96482 | ||
|
|
c015e8413e | ||
|
|
390c2d8907 | ||
|
|
97605c5f19 | ||
|
|
818c326160 | ||
|
|
c98727d83e | ||
|
|
a138a92e67 | ||
|
|
7aed19ffda | ||
|
|
3bb559dd09 | ||
|
|
389a729b75 | ||
|
|
2f3c9122fd | ||
|
|
733478ee19 | ||
|
|
41c6337fc1 | ||
|
|
7446da1c2f | ||
|
|
c79fca5ceb | ||
|
|
dc5f43927a | ||
|
|
35a5a81e19 | ||
|
|
9dcc11d54c | ||
|
|
74ce21fa54 | ||
|
|
eb93660b36 | ||
|
|
f50e597548 | ||
|
|
817c3b36b9 | ||
|
|
1859a6ae69 | ||
|
|
0645d342dd | ||
|
|
61ec03e540 | ||
|
|
09f0a366bf | ||
|
|
778961d31e | ||
|
|
f962c88df3 | ||
|
|
8db3ffe719 | ||
|
|
cc5d4dd119 | ||
|
|
86204cf23b | ||
|
|
468949b899 | ||
|
|
f1d9966224 | ||
|
|
b022b50966 | ||
|
|
e2f4213839 | ||
|
|
ae1235b223 | ||
|
|
c061f59f1c | ||
|
|
3edaaebba2 | ||
|
|
7cdf1c7f96 | ||
|
|
d558204192 | ||
|
|
d06ce8f911 | ||
|
|
4b6f7e0ebe | ||
|
|
370c567be1 | ||
|
|
9be64f3de5 | ||
|
|
30500e5a95 | ||
|
|
bb323c5710 | ||
|
|
7571df49d5 | ||
|
|
1559c21033 | ||
|
|
d9b81731e9 | ||
|
|
2034cca3a9 | ||
|
|
0b5e59d9cb | ||
|
|
f48b2d1ae5 |
2
.github/actions/run_awx_devel/action.yml
vendored
2
.github/actions/run_awx_devel/action.yml
vendored
@@ -71,7 +71,7 @@ runs:
|
|||||||
id: data
|
id: data
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
AWX_IP=$(docker inspect -f '{{.NetworkSettings.Networks._sources_awx.IPAddress}}' tools_awx_1)
|
AWX_IP=$(docker inspect -f '{{.NetworkSettings.Networks.awx.IPAddress}}' tools_awx_1)
|
||||||
ADMIN_TOKEN=$(docker exec -i tools_awx_1 awx-manage create_oauth2_token --user admin)
|
ADMIN_TOKEN=$(docker exec -i tools_awx_1 awx-manage create_oauth2_token --user admin)
|
||||||
echo "ip=$AWX_IP" >> $GITHUB_OUTPUT
|
echo "ip=$AWX_IP" >> $GITHUB_OUTPUT
|
||||||
echo "admin_token=$ADMIN_TOKEN" >> $GITHUB_OUTPUT
|
echo "admin_token=$ADMIN_TOKEN" >> $GITHUB_OUTPUT
|
||||||
|
|||||||
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
@@ -66,6 +66,8 @@ jobs:
|
|||||||
awx-operator:
|
awx-operator:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
env:
|
||||||
|
DEBUG_OUTPUT_DIR: /tmp/awx_operator_molecule_test
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout awx
|
- name: Checkout awx
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@@ -94,11 +96,11 @@ jobs:
|
|||||||
- name: Build AWX image
|
- name: Build AWX image
|
||||||
working-directory: awx
|
working-directory: awx
|
||||||
run: |
|
run: |
|
||||||
ansible-playbook -v tools/ansible/build.yml \
|
VERSION=`make version-for-buildyml` make awx-kube-build
|
||||||
-e headless=yes \
|
env:
|
||||||
-e awx_image=awx \
|
COMPOSE_TAG: ci
|
||||||
-e awx_image_tag=ci \
|
DEV_DOCKER_TAG_BASE: local
|
||||||
-e ansible_python_interpreter=$(which python3)
|
HEADLESS: yes
|
||||||
|
|
||||||
- name: Run test deployment with awx-operator
|
- name: Run test deployment with awx-operator
|
||||||
working-directory: awx-operator
|
working-directory: awx-operator
|
||||||
@@ -109,8 +111,17 @@ jobs:
|
|||||||
make kustomize
|
make kustomize
|
||||||
KUSTOMIZE_PATH=$(readlink -f bin/kustomize) molecule -v test -s kind -- --skip-tags=replicas
|
KUSTOMIZE_PATH=$(readlink -f bin/kustomize) molecule -v test -s kind -- --skip-tags=replicas
|
||||||
env:
|
env:
|
||||||
AWX_TEST_IMAGE: awx
|
AWX_TEST_IMAGE: local/awx
|
||||||
AWX_TEST_VERSION: ci
|
AWX_TEST_VERSION: ci
|
||||||
|
AWX_EE_TEST_IMAGE: quay.io/ansible/awx-ee:latest
|
||||||
|
STORE_DEBUG_OUTPUT: true
|
||||||
|
|
||||||
|
- name: Upload debug output
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: awx-operator-debug-output
|
||||||
|
path: ${{ env.DEBUG_OUTPUT_DIR }}
|
||||||
|
|
||||||
collection-sanity:
|
collection-sanity:
|
||||||
name: awx_collection sanity
|
name: awx_collection sanity
|
||||||
|
|||||||
40
.github/workflows/promote.yml
vendored
40
.github/workflows/promote.yml
vendored
@@ -7,7 +7,11 @@ env:
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag_name:
|
||||||
|
description: 'Name for the tag of the release.'
|
||||||
|
required: true
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
|
||||||
@@ -17,6 +21,16 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
steps:
|
steps:
|
||||||
|
- name: Set GitHub Env vars for workflow_dispatch event
|
||||||
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
|
run: |
|
||||||
|
echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set GitHub Env vars if release event
|
||||||
|
if: ${{ github.event_name == 'release' }}
|
||||||
|
run: |
|
||||||
|
echo "TAG_NAME=${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout awx
|
- name: Checkout awx
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
@@ -43,16 +57,18 @@ jobs:
|
|||||||
- name: Build collection and publish to galaxy
|
- name: Build collection and publish to galaxy
|
||||||
env:
|
env:
|
||||||
COLLECTION_NAMESPACE: ${{ env.collection_namespace }}
|
COLLECTION_NAMESPACE: ${{ env.collection_namespace }}
|
||||||
COLLECTION_VERSION: ${{ github.event.release.tag_name }}
|
COLLECTION_VERSION: ${{ env.TAG_NAME }}
|
||||||
COLLECTION_TEMPLATE_VERSION: true
|
COLLECTION_TEMPLATE_VERSION: true
|
||||||
run: |
|
run: |
|
||||||
make build_collection
|
make build_collection
|
||||||
if [ "$(curl -L --head -sw '%{http_code}' https://galaxy.ansible.com/download/${{ env.collection_namespace }}-awx-${{ github.event.release.tag_name }}.tar.gz | tail -1)" == "302" ] ; then \
|
curl_with_redirects=$(curl --head -sLw '%{http_code}' https://galaxy.ansible.com/download/${{ env.collection_namespace }}-awx-${{ env.TAG_NAME }}.tar.gz | tail -1)
|
||||||
echo "Galaxy release already done"; \
|
curl_without_redirects=$(curl --head -sw '%{http_code}' https://galaxy.ansible.com/download/${{ env.collection_namespace }}-awx-${{ env.TAG_NAME }}.tar.gz | tail -1)
|
||||||
else \
|
if [[ "$curl_with_redirects" == "302" ]] || [[ "$curl_without_redirects" == "302" ]]; then
|
||||||
|
echo "Galaxy release already done";
|
||||||
|
else
|
||||||
ansible-galaxy collection publish \
|
ansible-galaxy collection publish \
|
||||||
--token=${{ secrets.GALAXY_TOKEN }} \
|
--token=${{ secrets.GALAXY_TOKEN }} \
|
||||||
awx_collection_build/${{ env.collection_namespace }}-awx-${{ github.event.release.tag_name }}.tar.gz; \
|
awx_collection_build/${{ env.collection_namespace }}-awx-${{ env.TAG_NAME }}.tar.gz;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Set official pypi info
|
- name: Set official pypi info
|
||||||
@@ -64,6 +80,8 @@ jobs:
|
|||||||
if: ${{ github.repository_owner != 'ansible' }}
|
if: ${{ github.repository_owner != 'ansible' }}
|
||||||
|
|
||||||
- name: Build awxkit and upload to pypi
|
- name: Build awxkit and upload to pypi
|
||||||
|
env:
|
||||||
|
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ env.TAG_NAME }}
|
||||||
run: |
|
run: |
|
||||||
git reset --hard
|
git reset --hard
|
||||||
cd awxkit && python3 setup.py sdist bdist_wheel
|
cd awxkit && python3 setup.py sdist bdist_wheel
|
||||||
@@ -84,14 +102,14 @@ jobs:
|
|||||||
- name: Re-tag and promote awx image
|
- name: Re-tag and promote awx image
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create \
|
docker buildx imagetools create \
|
||||||
ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} \
|
ghcr.io/${{ github.repository }}:${{ env.TAG_NAME }} \
|
||||||
--tag quay.io/${{ github.repository }}:${{ github.event.release.tag_name }}
|
--tag quay.io/${{ github.repository }}:${{ env.TAG_NAME }}
|
||||||
docker buildx imagetools create \
|
docker buildx imagetools create \
|
||||||
ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} \
|
ghcr.io/${{ github.repository }}:${{ env.TAG_NAME }} \
|
||||||
--tag quay.io/${{ github.repository }}:latest
|
--tag quay.io/${{ github.repository }}:latest
|
||||||
|
|
||||||
- name: Re-tag and promote awx-ee image
|
- name: Re-tag and promote awx-ee image
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create \
|
docker buildx imagetools create \
|
||||||
ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }} \
|
ghcr.io/${{ github.repository_owner }}/awx-ee:${{ env.TAG_NAME }} \
|
||||||
--tag quay.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }}
|
--tag quay.io/${{ github.repository_owner }}/awx-ee:${{ env.TAG_NAME }}
|
||||||
|
|||||||
92
.github/workflows/stage.yml
vendored
92
.github/workflows/stage.yml
vendored
@@ -49,13 +49,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: awx
|
path: awx
|
||||||
|
|
||||||
- name: Get python version from Makefile
|
- name: Checkout awx-operator
|
||||||
run: echo py_version=`make PYTHON_VERSION` >> $GITHUB_ENV
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install python ${{ env.py_version }}
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.py_version }}
|
repository: ${{ github.repository_owner }}/awx-operator
|
||||||
|
path: awx-operator
|
||||||
|
|
||||||
- name: Checkout awx-logos
|
- name: Checkout awx-logos
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@@ -63,57 +61,85 @@ jobs:
|
|||||||
repository: ansible/awx-logos
|
repository: ansible/awx-logos
|
||||||
path: awx-logos
|
path: awx-logos
|
||||||
|
|
||||||
- name: Checkout awx-operator
|
- name: Get python version from Makefile
|
||||||
uses: actions/checkout@v3
|
working-directory: awx
|
||||||
|
run: echo py_version=`make PYTHON_VERSION` >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install python ${{ env.py_version }}
|
||||||
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
repository: ${{ github.repository_owner }}/awx-operator
|
python-version: ${{ env.py_version }}
|
||||||
path: awx-operator
|
|
||||||
|
|
||||||
- name: Install playbook dependencies
|
- name: Install playbook dependencies
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install docker
|
python3 -m pip install docker
|
||||||
|
|
||||||
- name: Build and stage AWX
|
|
||||||
working-directory: awx
|
|
||||||
run: |
|
|
||||||
ansible-playbook -v tools/ansible/build.yml \
|
|
||||||
-e registry=ghcr.io \
|
|
||||||
-e registry_username=${{ github.actor }} \
|
|
||||||
-e registry_password=${{ secrets.GITHUB_TOKEN }} \
|
|
||||||
-e awx_image=${{ github.repository }} \
|
|
||||||
-e awx_version=${{ github.event.inputs.version }} \
|
|
||||||
-e ansible_python_interpreter=$(which python3) \
|
|
||||||
-e push=yes \
|
|
||||||
-e awx_official=yes
|
|
||||||
|
|
||||||
- name: Log into registry ghcr.io
|
- name: Log into registry ghcr.io
|
||||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Log into registry quay.io
|
- name: Copy logos for inclusion in sdist for official build
|
||||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
|
working-directory: awx
|
||||||
|
run: |
|
||||||
|
cp ../awx-logos/awx/ui/client/assets/* awx/ui/public/static/media/
|
||||||
|
|
||||||
|
- name: Setup node and npm
|
||||||
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
node-version: '16.13.1'
|
||||||
username: ${{ secrets.QUAY_USER }}
|
|
||||||
password: ${{ secrets.QUAY_TOKEN }}
|
- name: Prebuild UI for awx image (to speed up build process)
|
||||||
|
working-directory: awx
|
||||||
|
run: |
|
||||||
|
sudo apt-get install gettext
|
||||||
|
make ui-release
|
||||||
|
make ui-next
|
||||||
|
|
||||||
|
- name: Set build env variables
|
||||||
|
run: |
|
||||||
|
echo "DEV_DOCKER_TAG_BASE=ghcr.io/${OWNER,,}" >> $GITHUB_ENV
|
||||||
|
echo "COMPOSE_TAG=${{ github.event.inputs.version }}" >> $GITHUB_ENV
|
||||||
|
echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
|
||||||
|
echo "AWX_TEST_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
|
||||||
|
echo "AWX_TEST_IMAGE=ghcr.io/${OWNER,,}/awx" >> $GITHUB_ENV
|
||||||
|
echo "AWX_EE_TEST_IMAGE=ghcr.io/${OWNER,,}/awx-ee:${{ github.event.inputs.version }}" >> $GITHUB_ENV
|
||||||
|
echo "AWX_OPERATOR_TEST_IMAGE=ghcr.io/${OWNER,,}/awx-operator:${{ github.event.inputs.operator_version }}" >> $GITHUB_ENV
|
||||||
|
env:
|
||||||
|
OWNER: ${{ github.repository_owner }}
|
||||||
|
|
||||||
|
- name: Build and stage AWX
|
||||||
|
working-directory: awx
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDX_PUSH: true
|
||||||
|
HEADLESS: false
|
||||||
|
PLATFORMS: linux/amd64,linux/arm64
|
||||||
|
run: |
|
||||||
|
make awx-kube-buildx
|
||||||
|
|
||||||
- name: tag awx-ee:latest with version input
|
- name: tag awx-ee:latest with version input
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create \
|
docker buildx imagetools create \
|
||||||
quay.io/ansible/awx-ee:latest \
|
quay.io/ansible/awx-ee:latest \
|
||||||
--tag ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }}
|
--tag ${AWX_EE_TEST_IMAGE}
|
||||||
|
|
||||||
- name: Stage awx-operator image
|
- name: Stage awx-operator image
|
||||||
working-directory: awx-operator
|
working-directory: awx-operator
|
||||||
run: |
|
run: |
|
||||||
BUILD_ARGS="--build-arg DEFAULT_AWX_VERSION=${{ github.event.inputs.version}} \
|
BUILD_ARGS="--build-arg DEFAULT_AWX_VERSION=${{ github.event.inputs.version}} \
|
||||||
--build-arg OPERATOR_VERSION=${{ github.event.inputs.operator_version }}" \
|
--build-arg OPERATOR_VERSION=${{ github.event.inputs.operator_version }}" \
|
||||||
IMG=ghcr.io/${{ github.repository_owner }}/awx-operator:${{ github.event.inputs.operator_version }} \
|
IMG=${AWX_OPERATOR_TEST_IMAGE} \
|
||||||
make docker-buildx
|
make docker-buildx
|
||||||
|
|
||||||
|
- name: Pulling images for test deployment with awx-operator
|
||||||
|
# awx operator molecue test expect to kind load image and buildx exports image to registry and not local
|
||||||
|
run: |
|
||||||
|
docker pull ${AWX_OPERATOR_TEST_IMAGE}
|
||||||
|
docker pull ${AWX_EE_TEST_IMAGE}
|
||||||
|
docker pull ${AWX_TEST_IMAGE}:${AWX_TEST_VERSION}
|
||||||
|
|
||||||
- name: Run test deployment with awx-operator
|
- name: Run test deployment with awx-operator
|
||||||
working-directory: awx-operator
|
working-directory: awx-operator
|
||||||
run: |
|
run: |
|
||||||
@@ -122,10 +148,6 @@ jobs:
|
|||||||
sudo rm -f $(which kustomize)
|
sudo rm -f $(which kustomize)
|
||||||
make kustomize
|
make kustomize
|
||||||
KUSTOMIZE_PATH=$(readlink -f bin/kustomize) molecule test -s kind
|
KUSTOMIZE_PATH=$(readlink -f bin/kustomize) molecule test -s kind
|
||||||
env:
|
|
||||||
AWX_TEST_IMAGE: ${{ github.repository }}
|
|
||||||
AWX_TEST_VERSION: ${{ github.event.inputs.version }}
|
|
||||||
AWX_EE_TEST_IMAGE: ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }}
|
|
||||||
|
|
||||||
- name: Create draft release for AWX
|
- name: Create draft release for AWX
|
||||||
working-directory: awx
|
working-directory: awx
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ If any of those items are missing your pull request will still get the `needs_tr
|
|||||||
Currently you can expect awxbot to add common labels such as `state:needs_triage`, `type:bug`, `component:docs`, etc...
|
Currently you can expect awxbot to add common labels such as `state:needs_triage`, `type:bug`, `component:docs`, etc...
|
||||||
These labels are determined by the template data. Please use the template and fill it out as accurately as possible.
|
These labels are determined by the template data. Please use the template and fill it out as accurately as possible.
|
||||||
|
|
||||||
The `state:needs_triage` label will will remain on your pull request until a person has looked at it.
|
The `state:needs_triage` label will remain on your pull request until a person has looked at it.
|
||||||
|
|
||||||
You can also expect the bot to CC maintainers of specific areas of the code, this will notify them that there is a pull request by placing a comment on the pull request.
|
You can also expect the bot to CC maintainers of specific areas of the code, this will notify them that there is a pull request by placing a comment on the pull request.
|
||||||
The comment will look something like `CC @matburt @wwitzel3 ...`.
|
The comment will look something like `CC @matburt @wwitzel3 ...`.
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
PYTHON := $(notdir $(shell for i in python3.11 python3; do command -v $$i; done|sed 1q))
|
PYTHON := $(notdir $(shell for i in python3.11 python3; do command -v $$i; done|sed 1q))
|
||||||
SHELL := bash
|
SHELL := bash
|
||||||
DOCKER_COMPOSE ?= docker-compose
|
DOCKER_COMPOSE ?= docker compose
|
||||||
OFFICIAL ?= no
|
OFFICIAL ?= no
|
||||||
NODE ?= node
|
NODE ?= node
|
||||||
NPM_BIN ?= npm
|
NPM_BIN ?= npm
|
||||||
@@ -616,7 +616,7 @@ docker-clean:
|
|||||||
-$(foreach image_id,$(shell docker images --filter=reference='*/*/*awx_devel*' --filter=reference='*/*awx_devel*' --filter=reference='*awx_devel*' -aq),docker rmi --force $(image_id);)
|
-$(foreach image_id,$(shell docker images --filter=reference='*/*/*awx_devel*' --filter=reference='*/*awx_devel*' --filter=reference='*awx_devel*' -aq),docker rmi --force $(image_id);)
|
||||||
|
|
||||||
docker-clean-volumes: docker-compose-clean docker-compose-container-group-clean
|
docker-clean-volumes: docker-compose-clean docker-compose-container-group-clean
|
||||||
docker volume rm -f tools_var_lib_awx tools_awx_db tools_vault_1 tools_ldap_1 tools_grafana_storage tools_prometheus_storage $(docker volume ls --filter name=tools_redis_socket_ -q)
|
docker volume rm -f tools_var_lib_awx tools_awx_db tools_awx_db_15 tools_vault_1 tools_ldap_1 tools_grafana_storage tools_prometheus_storage $(shell docker volume ls --filter name=tools_redis_socket_ -q)
|
||||||
|
|
||||||
docker-refresh: docker-clean docker-compose
|
docker-refresh: docker-clean docker-compose
|
||||||
|
|
||||||
|
|||||||
@@ -30,11 +30,15 @@ from rest_framework.permissions import IsAuthenticated
|
|||||||
from rest_framework.renderers import StaticHTMLRenderer
|
from rest_framework.renderers import StaticHTMLRenderer
|
||||||
from rest_framework.negotiation import DefaultContentNegotiation
|
from rest_framework.negotiation import DefaultContentNegotiation
|
||||||
|
|
||||||
|
# django-ansible-base
|
||||||
from ansible_base.rest_filters.rest_framework.field_lookup_backend import FieldLookupBackend
|
from ansible_base.rest_filters.rest_framework.field_lookup_backend import FieldLookupBackend
|
||||||
from ansible_base.lib.utils.models import get_all_field_names
|
from ansible_base.lib.utils.models import get_all_field_names
|
||||||
|
from ansible_base.rbac.models import RoleEvaluation, RoleDefinition
|
||||||
|
from ansible_base.rbac.permission_registry import permission_registry
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.models import UnifiedJob, UnifiedJobTemplate, User, Role, Credential, WorkflowJobTemplateNode, WorkflowApprovalTemplate
|
from awx.main.models import UnifiedJob, UnifiedJobTemplate, User, Role, Credential, WorkflowJobTemplateNode, WorkflowApprovalTemplate
|
||||||
|
from awx.main.models.rbac import give_creator_permissions
|
||||||
from awx.main.access import optimize_queryset
|
from awx.main.access import optimize_queryset
|
||||||
from awx.main.utils import camelcase_to_underscore, get_search_fields, getattrd, get_object_or_400, decrypt_field, get_awx_version
|
from awx.main.utils import camelcase_to_underscore, get_search_fields, getattrd, get_object_or_400, decrypt_field, get_awx_version
|
||||||
from awx.main.utils.licensing import server_product_name
|
from awx.main.utils.licensing import server_product_name
|
||||||
@@ -91,7 +95,9 @@ class LoggedLoginView(auth_views.LoginView):
|
|||||||
ret = super(LoggedLoginView, self).post(request, *args, **kwargs)
|
ret = super(LoggedLoginView, self).post(request, *args, **kwargs)
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
logger.info(smart_str(u"User {} logged in from {}".format(self.request.user.username, request.META.get('REMOTE_ADDR', None))))
|
logger.info(smart_str(u"User {} logged in from {}".format(self.request.user.username, request.META.get('REMOTE_ADDR', None))))
|
||||||
ret.set_cookie('userLoggedIn', 'true', secure=getattr(settings, 'SESSION_COOKIE_SECURE', False))
|
ret.set_cookie(
|
||||||
|
'userLoggedIn', 'true', secure=getattr(settings, 'SESSION_COOKIE_SECURE', False), samesite=getattr(settings, 'USER_COOKIE_SAMESITE', 'Lax')
|
||||||
|
)
|
||||||
ret.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid'))
|
ret.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid'))
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
@@ -472,7 +478,11 @@ class ListAPIView(generics.ListAPIView, GenericAPIView):
|
|||||||
|
|
||||||
class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
|
class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
|
||||||
# Base class for a list view that allows creating new objects.
|
# Base class for a list view that allows creating new objects.
|
||||||
pass
|
def perform_create(self, serializer):
|
||||||
|
super().perform_create(serializer)
|
||||||
|
if serializer.Meta.model in permission_registry.all_registered_models:
|
||||||
|
if self.request and self.request.user:
|
||||||
|
give_creator_permissions(self.request.user, serializer.instance)
|
||||||
|
|
||||||
|
|
||||||
class ParentMixin(object):
|
class ParentMixin(object):
|
||||||
@@ -792,6 +802,7 @@ class RetrieveUpdateDestroyAPIView(RetrieveUpdateAPIView, DestroyAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class ResourceAccessList(ParentMixin, ListAPIView):
|
class ResourceAccessList(ParentMixin, ListAPIView):
|
||||||
|
deprecated = True
|
||||||
serializer_class = ResourceAccessListElementSerializer
|
serializer_class = ResourceAccessListElementSerializer
|
||||||
ordering = ('username',)
|
ordering = ('username',)
|
||||||
|
|
||||||
@@ -799,6 +810,15 @@ class ResourceAccessList(ParentMixin, ListAPIView):
|
|||||||
obj = self.get_parent_object()
|
obj = self.get_parent_object()
|
||||||
|
|
||||||
content_type = ContentType.objects.get_for_model(obj)
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
|
|
||||||
|
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||||
|
ancestors = set(RoleEvaluation.objects.filter(content_type_id=content_type.id, object_id=obj.id).values_list('role_id', flat=True))
|
||||||
|
qs = User.objects.filter(has_roles__in=ancestors) | User.objects.filter(is_superuser=True)
|
||||||
|
auditor_role = RoleDefinition.objects.filter(name="System Auditor").first()
|
||||||
|
if auditor_role:
|
||||||
|
qs |= User.objects.filter(role_assignments__role_definition=auditor_role)
|
||||||
|
return qs.distinct()
|
||||||
|
|
||||||
roles = set(Role.objects.filter(content_type=content_type, object_id=obj.id))
|
roles = set(Role.objects.filter(content_type=content_type, object_id=obj.id))
|
||||||
|
|
||||||
ancestors = set()
|
ancestors = set()
|
||||||
@@ -958,7 +978,7 @@ class CopyAPIView(GenericAPIView):
|
|||||||
None, None, self.model, obj, request.user, create_kwargs=create_kwargs, copy_name=serializer.validated_data.get('name', '')
|
None, None, self.model, obj, request.user, create_kwargs=create_kwargs, copy_name=serializer.validated_data.get('name', '')
|
||||||
)
|
)
|
||||||
if hasattr(new_obj, 'admin_role') and request.user not in new_obj.admin_role.members.all():
|
if hasattr(new_obj, 'admin_role') and request.user not in new_obj.admin_role.members.all():
|
||||||
new_obj.admin_role.members.add(request.user)
|
give_creator_permissions(request.user, new_obj)
|
||||||
if sub_objs:
|
if sub_objs:
|
||||||
permission_check_func = None
|
permission_check_func = None
|
||||||
if hasattr(type(self), 'deep_copy_permission_check_func'):
|
if hasattr(type(self), 'deep_copy_permission_check_func'):
|
||||||
|
|||||||
@@ -43,11 +43,14 @@ from rest_framework.utils.serializer_helpers import ReturnList
|
|||||||
# Django-Polymorphic
|
# Django-Polymorphic
|
||||||
from polymorphic.models import PolymorphicModel
|
from polymorphic.models import PolymorphicModel
|
||||||
|
|
||||||
|
# django-ansible-base
|
||||||
from ansible_base.lib.utils.models import get_type_for_model
|
from ansible_base.lib.utils.models import get_type_for_model
|
||||||
|
from ansible_base.rbac.models import RoleEvaluation, ObjectRole
|
||||||
|
from ansible_base.rbac import permission_registry
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.access import get_user_capabilities
|
from awx.main.access import get_user_capabilities
|
||||||
from awx.main.constants import ACTIVE_STATES, CENSOR_VALUE
|
from awx.main.constants import ACTIVE_STATES, CENSOR_VALUE, org_role_to_permission
|
||||||
from awx.main.models import (
|
from awx.main.models import (
|
||||||
ActivityStream,
|
ActivityStream,
|
||||||
AdHocCommand,
|
AdHocCommand,
|
||||||
@@ -102,7 +105,7 @@ from awx.main.models import (
|
|||||||
CLOUD_INVENTORY_SOURCES,
|
CLOUD_INVENTORY_SOURCES,
|
||||||
)
|
)
|
||||||
from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES
|
from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES
|
||||||
from awx.main.models.rbac import role_summary_fields_generator, RoleAncestorEntry
|
from awx.main.models.rbac import role_summary_fields_generator, give_creator_permissions, get_role_codenames, to_permissions, get_role_from_object_role
|
||||||
from awx.main.fields import ImplicitRoleField
|
from awx.main.fields import ImplicitRoleField
|
||||||
from awx.main.utils import (
|
from awx.main.utils import (
|
||||||
get_model_for_type,
|
get_model_for_type,
|
||||||
@@ -191,6 +194,7 @@ SUMMARIZABLE_FK_FIELDS = {
|
|||||||
'webhook_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
'webhook_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
||||||
'approved_or_denied_by': ('id', 'username', 'first_name', 'last_name'),
|
'approved_or_denied_by': ('id', 'username', 'first_name', 'last_name'),
|
||||||
'credential_type': DEFAULT_SUMMARY_FIELDS,
|
'credential_type': DEFAULT_SUMMARY_FIELDS,
|
||||||
|
'resource': ('ansible_id', 'resource_type'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2762,13 +2766,26 @@ class ResourceAccessListElementSerializer(UserSerializer):
|
|||||||
team_content_type = ContentType.objects.get_for_model(Team)
|
team_content_type = ContentType.objects.get_for_model(Team)
|
||||||
content_type = ContentType.objects.get_for_model(obj)
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
|
|
||||||
def get_roles_on_resource(parent_role):
|
reversed_org_map = {}
|
||||||
"Returns a string list of the roles a parent_role has for current obj."
|
for k, v in org_role_to_permission.items():
|
||||||
return list(
|
reversed_org_map[v] = k
|
||||||
RoleAncestorEntry.objects.filter(ancestor=parent_role, content_type_id=content_type.id, object_id=obj.id)
|
reversed_role_map = {}
|
||||||
.values_list('role_field', flat=True)
|
for k, v in to_permissions.items():
|
||||||
.distinct()
|
reversed_role_map[v] = k
|
||||||
)
|
|
||||||
|
def get_roles_from_perms(perm_list):
|
||||||
|
"""given a list of permission codenames return a list of role names"""
|
||||||
|
role_names = set()
|
||||||
|
for codename in perm_list:
|
||||||
|
action = codename.split('_', 1)[0]
|
||||||
|
if action in reversed_role_map:
|
||||||
|
role_names.add(reversed_role_map[action])
|
||||||
|
elif codename in reversed_org_map:
|
||||||
|
if isinstance(obj, Organization):
|
||||||
|
role_names.add(reversed_org_map[codename])
|
||||||
|
if 'view_organization' not in role_names:
|
||||||
|
role_names.add('read_role')
|
||||||
|
return list(role_names)
|
||||||
|
|
||||||
def format_role_perm(role):
|
def format_role_perm(role):
|
||||||
role_dict = {'id': role.id, 'name': role.name, 'description': role.description}
|
role_dict = {'id': role.id, 'name': role.name, 'description': role.description}
|
||||||
@@ -2785,13 +2802,21 @@ class ResourceAccessListElementSerializer(UserSerializer):
|
|||||||
else:
|
else:
|
||||||
# Singleton roles should not be managed from this view, as per copy/edit rework spec
|
# Singleton roles should not be managed from this view, as per copy/edit rework spec
|
||||||
role_dict['user_capabilities'] = {'unattach': False}
|
role_dict['user_capabilities'] = {'unattach': False}
|
||||||
return {'role': role_dict, 'descendant_roles': get_roles_on_resource(role)}
|
|
||||||
|
model_name = content_type.model
|
||||||
|
if isinstance(obj, Organization):
|
||||||
|
descendant_perms = [codename for codename in get_role_codenames(role) if codename.endswith(model_name) or codename.startswith('add_')]
|
||||||
|
else:
|
||||||
|
descendant_perms = [codename for codename in get_role_codenames(role) if codename.endswith(model_name)]
|
||||||
|
|
||||||
|
return {'role': role_dict, 'descendant_roles': get_roles_from_perms(descendant_perms)}
|
||||||
|
|
||||||
def format_team_role_perm(naive_team_role, permissive_role_ids):
|
def format_team_role_perm(naive_team_role, permissive_role_ids):
|
||||||
ret = []
|
ret = []
|
||||||
|
team = naive_team_role.content_object
|
||||||
team_role = naive_team_role
|
team_role = naive_team_role
|
||||||
if naive_team_role.role_field == 'admin_role':
|
if naive_team_role.role_field == 'admin_role':
|
||||||
team_role = naive_team_role.content_object.member_role
|
team_role = team.member_role
|
||||||
for role in team_role.children.filter(id__in=permissive_role_ids).all():
|
for role in team_role.children.filter(id__in=permissive_role_ids).all():
|
||||||
role_dict = {
|
role_dict = {
|
||||||
'id': role.id,
|
'id': role.id,
|
||||||
@@ -2811,10 +2836,87 @@ class ResourceAccessListElementSerializer(UserSerializer):
|
|||||||
else:
|
else:
|
||||||
# Singleton roles should not be managed from this view, as per copy/edit rework spec
|
# Singleton roles should not be managed from this view, as per copy/edit rework spec
|
||||||
role_dict['user_capabilities'] = {'unattach': False}
|
role_dict['user_capabilities'] = {'unattach': False}
|
||||||
ret.append({'role': role_dict, 'descendant_roles': get_roles_on_resource(team_role)})
|
|
||||||
|
descendant_perms = list(
|
||||||
|
RoleEvaluation.objects.filter(role__in=team.has_roles.all(), object_id=obj.id, content_type_id=content_type.id)
|
||||||
|
.values_list('codename', flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
ret.append({'role': role_dict, 'descendant_roles': get_roles_from_perms(descendant_perms)})
|
||||||
|
return ret
|
||||||
|
|
||||||
|
gfk_kwargs = dict(content_type_id=content_type.id, object_id=obj.id)
|
||||||
|
direct_permissive_role_ids = Role.objects.filter(**gfk_kwargs).values_list('id', flat=True)
|
||||||
|
|
||||||
|
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||||
|
ret['summary_fields']['direct_access'] = []
|
||||||
|
ret['summary_fields']['indirect_access'] = []
|
||||||
|
|
||||||
|
new_roles_seen = set()
|
||||||
|
all_team_roles = set()
|
||||||
|
all_permissive_role_ids = set()
|
||||||
|
for evaluation in RoleEvaluation.objects.filter(role__in=user.has_roles.all(), **gfk_kwargs).prefetch_related('role'):
|
||||||
|
new_role = evaluation.role
|
||||||
|
if new_role.id in new_roles_seen:
|
||||||
|
continue
|
||||||
|
new_roles_seen.add(new_role.id)
|
||||||
|
old_role = get_role_from_object_role(new_role)
|
||||||
|
all_permissive_role_ids.add(old_role.id)
|
||||||
|
|
||||||
|
if int(new_role.object_id) == obj.id and new_role.content_type_id == content_type.id:
|
||||||
|
ret['summary_fields']['direct_access'].append(format_role_perm(old_role))
|
||||||
|
elif new_role.content_type_id == team_content_type.id:
|
||||||
|
all_team_roles.add(old_role)
|
||||||
|
else:
|
||||||
|
ret['summary_fields']['indirect_access'].append(format_role_perm(old_role))
|
||||||
|
|
||||||
|
# Lazy role creation gives us a big problem, where some intermediate roles are not easy to find
|
||||||
|
# like when a team has indirect permission, so here we get all roles the users teams have
|
||||||
|
# these contribute to all potential permission-granting roles of the object
|
||||||
|
user_teams_qs = permission_registry.team_model.objects.filter(member_roles__in=ObjectRole.objects.filter(users=user))
|
||||||
|
team_obj_roles = ObjectRole.objects.filter(teams__in=user_teams_qs)
|
||||||
|
for evaluation in RoleEvaluation.objects.filter(role__in=team_obj_roles, **gfk_kwargs).prefetch_related('role'):
|
||||||
|
new_role = evaluation.role
|
||||||
|
if new_role.id in new_roles_seen:
|
||||||
|
continue
|
||||||
|
new_roles_seen.add(new_role.id)
|
||||||
|
old_role = get_role_from_object_role(new_role)
|
||||||
|
all_permissive_role_ids.add(old_role.id)
|
||||||
|
|
||||||
|
# In DAB RBAC, superuser is strictly a user flag, and global roles are not in the RoleEvaluation table
|
||||||
|
if user.is_superuser:
|
||||||
|
ret['summary_fields'].setdefault('indirect_access', [])
|
||||||
|
all_role_names = [field.name for field in obj._meta.get_fields() if isinstance(field, ImplicitRoleField)]
|
||||||
|
ret['summary_fields']['indirect_access'].append(
|
||||||
|
{
|
||||||
|
"role": {
|
||||||
|
"id": None,
|
||||||
|
"name": _("System Administrator"),
|
||||||
|
"description": _("Can manage all aspects of the system"),
|
||||||
|
"user_capabilities": {"unattach": False},
|
||||||
|
},
|
||||||
|
"descendant_roles": all_role_names,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif user.is_system_auditor:
|
||||||
|
ret['summary_fields'].setdefault('indirect_access', [])
|
||||||
|
ret['summary_fields']['indirect_access'].append(
|
||||||
|
{
|
||||||
|
"role": {
|
||||||
|
"id": None,
|
||||||
|
"name": _("System Auditor"),
|
||||||
|
"description": _("Can view all aspects of the system"),
|
||||||
|
"user_capabilities": {"unattach": False},
|
||||||
|
},
|
||||||
|
"descendant_roles": ["read_role"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ret['summary_fields']['direct_access'].extend([y for x in (format_team_role_perm(r, all_permissive_role_ids) for r in all_team_roles) for y in x])
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
direct_permissive_role_ids = Role.objects.filter(content_type=content_type, object_id=obj.id).values_list('id', flat=True)
|
|
||||||
all_permissive_role_ids = Role.objects.filter(content_type=content_type, object_id=obj.id).values_list('ancestors__id', flat=True)
|
all_permissive_role_ids = Role.objects.filter(content_type=content_type, object_id=obj.id).values_list('ancestors__id', flat=True)
|
||||||
|
|
||||||
direct_access_roles = user.roles.filter(id__in=direct_permissive_role_ids).all()
|
direct_access_roles = user.roles.filter(id__in=direct_permissive_role_ids).all()
|
||||||
@@ -3083,7 +3185,7 @@ class CredentialSerializerCreate(CredentialSerializer):
|
|||||||
credential = super(CredentialSerializerCreate, self).create(validated_data)
|
credential = super(CredentialSerializerCreate, self).create(validated_data)
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
credential.admin_role.members.add(user)
|
give_creator_permissions(user, credential)
|
||||||
if team:
|
if team:
|
||||||
if not credential.organization or team.organization.id != credential.organization.id:
|
if not credential.organization or team.organization.id != credential.organization.id:
|
||||||
raise serializers.ValidationError({"detail": _("Credential organization must be set and match before assigning to a team")})
|
raise serializers.ValidationError({"detail": _("Credential organization must be set and match before assigning to a team")})
|
||||||
|
|||||||
@@ -2,32 +2,21 @@
|
|||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import NoReverseMatch
|
|
||||||
|
|
||||||
from rest_framework.reverse import _reverse
|
from rest_framework.reverse import reverse as drf_reverse
|
||||||
from rest_framework.versioning import URLPathVersioning as BaseVersioning
|
from rest_framework.versioning import URLPathVersioning as BaseVersioning
|
||||||
|
|
||||||
|
|
||||||
def drf_reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
def is_optional_api_urlpattern_prefix_request(request):
|
||||||
"""
|
|
||||||
Copy and monkey-patch `rest_framework.reverse.reverse` to prevent adding unwarranted
|
|
||||||
query string parameters.
|
|
||||||
"""
|
|
||||||
scheme = getattr(request, 'versioning_scheme', None)
|
|
||||||
if scheme is not None:
|
|
||||||
try:
|
|
||||||
url = scheme.reverse(viewname, args, kwargs, request, format, **extra)
|
|
||||||
except NoReverseMatch:
|
|
||||||
# In case the versioning scheme reversal fails, fallback to the
|
|
||||||
# default implementation
|
|
||||||
url = _reverse(viewname, args, kwargs, request, format, **extra)
|
|
||||||
else:
|
|
||||||
url = _reverse(viewname, args, kwargs, request, format, **extra)
|
|
||||||
|
|
||||||
if settings.OPTIONAL_API_URLPATTERN_PREFIX and request:
|
if settings.OPTIONAL_API_URLPATTERN_PREFIX and request:
|
||||||
if request.path.startswith(f"/api/{settings.OPTIONAL_API_URLPATTERN_PREFIX}"):
|
if request.path.startswith(f"/api/{settings.OPTIONAL_API_URLPATTERN_PREFIX}"):
|
||||||
url = url.replace('/api', f"/api/{settings.OPTIONAL_API_URLPATTERN_PREFIX}")
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def transform_optional_api_urlpattern_prefix_url(request, url):
|
||||||
|
if is_optional_api_urlpattern_prefix_request(request):
|
||||||
|
url = url.replace('/api', f"/api/{settings.OPTIONAL_API_URLPATTERN_PREFIX}")
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ from oauth2_provider.models import get_access_token_model
|
|||||||
import pytz
|
import pytz
|
||||||
from wsgiref.util import FileWrapper
|
from wsgiref.util import FileWrapper
|
||||||
|
|
||||||
|
# django-ansible-base
|
||||||
|
from ansible_base.rbac.models import RoleEvaluation, ObjectRole
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.tasks.system import send_notifications, update_inventory_computed_fields
|
from awx.main.tasks.system import send_notifications, update_inventory_computed_fields
|
||||||
from awx.main.access import get_user_queryset
|
from awx.main.access import get_user_queryset
|
||||||
@@ -87,6 +90,7 @@ from awx.api.generics import (
|
|||||||
from awx.api.views.labels import LabelSubListCreateAttachDetachView
|
from awx.api.views.labels import LabelSubListCreateAttachDetachView
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
from awx.main import models
|
from awx.main import models
|
||||||
|
from awx.main.models.rbac import get_role_definition
|
||||||
from awx.main.utils import (
|
from awx.main.utils import (
|
||||||
camelcase_to_underscore,
|
camelcase_to_underscore,
|
||||||
extract_ansible_vars,
|
extract_ansible_vars,
|
||||||
@@ -536,6 +540,7 @@ class InstanceGroupAccessList(ResourceAccessList):
|
|||||||
|
|
||||||
|
|
||||||
class InstanceGroupObjectRolesList(SubListAPIView):
|
class InstanceGroupObjectRolesList(SubListAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.Role
|
model = models.Role
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
parent_model = models.InstanceGroup
|
parent_model = models.InstanceGroup
|
||||||
@@ -724,6 +729,7 @@ class TeamUsersList(BaseUsersList):
|
|||||||
|
|
||||||
|
|
||||||
class TeamRolesList(SubListAttachDetachAPIView):
|
class TeamRolesList(SubListAttachDetachAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.Role
|
model = models.Role
|
||||||
serializer_class = serializers.RoleSerializerWithParentAccess
|
serializer_class = serializers.RoleSerializerWithParentAccess
|
||||||
metadata_class = RoleMetadata
|
metadata_class = RoleMetadata
|
||||||
@@ -763,10 +769,12 @@ class TeamRolesList(SubListAttachDetachAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class TeamObjectRolesList(SubListAPIView):
|
class TeamObjectRolesList(SubListAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.Role
|
model = models.Role
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
parent_model = models.Team
|
parent_model = models.Team
|
||||||
search_fields = ('role_field', 'content_type__model')
|
search_fields = ('role_field', 'content_type__model')
|
||||||
|
deprecated = True
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
po = self.get_parent_object()
|
po = self.get_parent_object()
|
||||||
@@ -784,8 +792,15 @@ class TeamProjectsList(SubListAPIView):
|
|||||||
self.check_parent_access(team)
|
self.check_parent_access(team)
|
||||||
model_ct = ContentType.objects.get_for_model(self.model)
|
model_ct = ContentType.objects.get_for_model(self.model)
|
||||||
parent_ct = ContentType.objects.get_for_model(self.parent_model)
|
parent_ct = ContentType.objects.get_for_model(self.parent_model)
|
||||||
proj_roles = models.Role.objects.filter(Q(ancestors__content_type=parent_ct) & Q(ancestors__object_id=team.pk), content_type=model_ct)
|
|
||||||
return self.model.accessible_objects(self.request.user, 'read_role').filter(pk__in=[t.content_object.pk for t in proj_roles])
|
rd = get_role_definition(team.member_role)
|
||||||
|
role = ObjectRole.objects.filter(object_id=team.id, content_type=parent_ct, role_definition=rd).first()
|
||||||
|
if role is None:
|
||||||
|
# Team has no permissions, therefore team has no projects
|
||||||
|
return self.model.objects.none()
|
||||||
|
else:
|
||||||
|
project_qs = self.model.accessible_objects(self.request.user, 'read_role')
|
||||||
|
return project_qs.filter(id__in=RoleEvaluation.objects.filter(content_type_id=model_ct.id, role=role).values_list('object_id'))
|
||||||
|
|
||||||
|
|
||||||
class TeamActivityStreamList(SubListAPIView):
|
class TeamActivityStreamList(SubListAPIView):
|
||||||
@@ -800,10 +815,23 @@ class TeamActivityStreamList(SubListAPIView):
|
|||||||
self.check_parent_access(parent)
|
self.check_parent_access(parent)
|
||||||
|
|
||||||
qs = self.request.user.get_queryset(self.model)
|
qs = self.request.user.get_queryset(self.model)
|
||||||
|
|
||||||
return qs.filter(
|
return qs.filter(
|
||||||
Q(team=parent)
|
Q(team=parent)
|
||||||
| Q(project__in=models.Project.accessible_objects(parent.member_role, 'read_role'))
|
| Q(
|
||||||
| Q(credential__in=models.Credential.accessible_objects(parent.member_role, 'read_role'))
|
project__in=RoleEvaluation.objects.filter(
|
||||||
|
role__in=parent.has_roles.all(), content_type_id=ContentType.objects.get_for_model(models.Project).id, codename='view_project'
|
||||||
|
)
|
||||||
|
.values_list('object_id')
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
| Q(
|
||||||
|
credential__in=RoleEvaluation.objects.filter(
|
||||||
|
role__in=parent.has_roles.all(), content_type_id=ContentType.objects.get_for_model(models.Credential).id, codename='view_credential'
|
||||||
|
)
|
||||||
|
.values_list('object_id')
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1055,10 +1083,12 @@ class ProjectAccessList(ResourceAccessList):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectObjectRolesList(SubListAPIView):
|
class ProjectObjectRolesList(SubListAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.Role
|
model = models.Role
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
parent_model = models.Project
|
parent_model = models.Project
|
||||||
search_fields = ('role_field', 'content_type__model')
|
search_fields = ('role_field', 'content_type__model')
|
||||||
|
deprecated = True
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
po = self.get_parent_object()
|
po = self.get_parent_object()
|
||||||
@@ -1216,6 +1246,7 @@ class UserTeamsList(SubListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class UserRolesList(SubListAttachDetachAPIView):
|
class UserRolesList(SubListAttachDetachAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.Role
|
model = models.Role
|
||||||
serializer_class = serializers.RoleSerializerWithParentAccess
|
serializer_class = serializers.RoleSerializerWithParentAccess
|
||||||
metadata_class = RoleMetadata
|
metadata_class = RoleMetadata
|
||||||
@@ -1490,10 +1521,12 @@ class CredentialAccessList(ResourceAccessList):
|
|||||||
|
|
||||||
|
|
||||||
class CredentialObjectRolesList(SubListAPIView):
|
class CredentialObjectRolesList(SubListAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.Role
|
model = models.Role
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
parent_model = models.Credential
|
parent_model = models.Credential
|
||||||
search_fields = ('role_field', 'content_type__model')
|
search_fields = ('role_field', 'content_type__model')
|
||||||
|
deprecated = True
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
po = self.get_parent_object()
|
po = self.get_parent_object()
|
||||||
@@ -2280,13 +2313,6 @@ class JobTemplateList(ListCreateAPIView):
|
|||||||
serializer_class = serializers.JobTemplateSerializer
|
serializer_class = serializers.JobTemplateSerializer
|
||||||
always_allow_superuser = False
|
always_allow_superuser = False
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
ret = super(JobTemplateList, self).post(request, *args, **kwargs)
|
|
||||||
if ret.status_code == 201:
|
|
||||||
job_template = models.JobTemplate.objects.get(id=ret.data['id'])
|
|
||||||
job_template.admin_role.members.add(request.user)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class JobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
class JobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
||||||
model = models.JobTemplate
|
model = models.JobTemplate
|
||||||
@@ -2832,10 +2858,12 @@ class JobTemplateAccessList(ResourceAccessList):
|
|||||||
|
|
||||||
|
|
||||||
class JobTemplateObjectRolesList(SubListAPIView):
|
class JobTemplateObjectRolesList(SubListAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.Role
|
model = models.Role
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
parent_model = models.JobTemplate
|
parent_model = models.JobTemplate
|
||||||
search_fields = ('role_field', 'content_type__model')
|
search_fields = ('role_field', 'content_type__model')
|
||||||
|
deprecated = True
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
po = self.get_parent_object()
|
po = self.get_parent_object()
|
||||||
@@ -3218,10 +3246,12 @@ class WorkflowJobTemplateAccessList(ResourceAccessList):
|
|||||||
|
|
||||||
|
|
||||||
class WorkflowJobTemplateObjectRolesList(SubListAPIView):
|
class WorkflowJobTemplateObjectRolesList(SubListAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.Role
|
model = models.Role
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
parent_model = models.WorkflowJobTemplate
|
parent_model = models.WorkflowJobTemplate
|
||||||
search_fields = ('role_field', 'content_type__model')
|
search_fields = ('role_field', 'content_type__model')
|
||||||
|
deprecated = True
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
po = self.get_parent_object()
|
po = self.get_parent_object()
|
||||||
@@ -4230,6 +4260,7 @@ class ActivityStreamDetail(RetrieveAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class RoleList(ListAPIView):
|
class RoleList(ListAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.Role
|
model = models.Role
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
@@ -4237,11 +4268,13 @@ class RoleList(ListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class RoleDetail(RetrieveAPIView):
|
class RoleDetail(RetrieveAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.Role
|
model = models.Role
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
|
|
||||||
|
|
||||||
class RoleUsersList(SubListAttachDetachAPIView):
|
class RoleUsersList(SubListAttachDetachAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.User
|
model = models.User
|
||||||
serializer_class = serializers.UserSerializer
|
serializer_class = serializers.UserSerializer
|
||||||
parent_model = models.Role
|
parent_model = models.Role
|
||||||
@@ -4276,6 +4309,7 @@ class RoleUsersList(SubListAttachDetachAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class RoleTeamsList(SubListAttachDetachAPIView):
|
class RoleTeamsList(SubListAttachDetachAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.Team
|
model = models.Team
|
||||||
serializer_class = serializers.TeamSerializer
|
serializer_class = serializers.TeamSerializer
|
||||||
parent_model = models.Role
|
parent_model = models.Role
|
||||||
@@ -4320,10 +4354,12 @@ class RoleTeamsList(SubListAttachDetachAPIView):
|
|||||||
team.member_role.children.remove(role)
|
team.member_role.children.remove(role)
|
||||||
else:
|
else:
|
||||||
team.member_role.children.add(role)
|
team.member_role.children.add(role)
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class RoleParentsList(SubListAPIView):
|
class RoleParentsList(SubListAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.Role
|
model = models.Role
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
parent_model = models.Role
|
parent_model = models.Role
|
||||||
@@ -4337,6 +4373,7 @@ class RoleParentsList(SubListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class RoleChildrenList(SubListAPIView):
|
class RoleChildrenList(SubListAPIView):
|
||||||
|
deprecated = True
|
||||||
model = models.Role
|
model = models.Role
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
parent_model = models.Role
|
parent_model = models.Role
|
||||||
|
|||||||
@@ -48,23 +48,23 @@ class AnalyticsRootView(APIView):
|
|||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
data = OrderedDict()
|
data = OrderedDict()
|
||||||
data['authorized'] = reverse('api:analytics_authorized')
|
data['authorized'] = reverse('api:analytics_authorized', request=request)
|
||||||
data['reports'] = reverse('api:analytics_reports_list')
|
data['reports'] = reverse('api:analytics_reports_list', request=request)
|
||||||
data['report_options'] = reverse('api:analytics_report_options_list')
|
data['report_options'] = reverse('api:analytics_report_options_list', request=request)
|
||||||
data['adoption_rate'] = reverse('api:analytics_adoption_rate')
|
data['adoption_rate'] = reverse('api:analytics_adoption_rate', request=request)
|
||||||
data['adoption_rate_options'] = reverse('api:analytics_adoption_rate_options')
|
data['adoption_rate_options'] = reverse('api:analytics_adoption_rate_options', request=request)
|
||||||
data['event_explorer'] = reverse('api:analytics_event_explorer')
|
data['event_explorer'] = reverse('api:analytics_event_explorer', request=request)
|
||||||
data['event_explorer_options'] = reverse('api:analytics_event_explorer_options')
|
data['event_explorer_options'] = reverse('api:analytics_event_explorer_options', request=request)
|
||||||
data['host_explorer'] = reverse('api:analytics_host_explorer')
|
data['host_explorer'] = reverse('api:analytics_host_explorer', request=request)
|
||||||
data['host_explorer_options'] = reverse('api:analytics_host_explorer_options')
|
data['host_explorer_options'] = reverse('api:analytics_host_explorer_options', request=request)
|
||||||
data['job_explorer'] = reverse('api:analytics_job_explorer')
|
data['job_explorer'] = reverse('api:analytics_job_explorer', request=request)
|
||||||
data['job_explorer_options'] = reverse('api:analytics_job_explorer_options')
|
data['job_explorer_options'] = reverse('api:analytics_job_explorer_options', request=request)
|
||||||
data['probe_templates'] = reverse('api:analytics_probe_templates_explorer')
|
data['probe_templates'] = reverse('api:analytics_probe_templates_explorer', request=request)
|
||||||
data['probe_templates_options'] = reverse('api:analytics_probe_templates_options')
|
data['probe_templates_options'] = reverse('api:analytics_probe_templates_options', request=request)
|
||||||
data['probe_template_for_hosts'] = reverse('api:analytics_probe_template_for_hosts_explorer')
|
data['probe_template_for_hosts'] = reverse('api:analytics_probe_template_for_hosts_explorer', request=request)
|
||||||
data['probe_template_for_hosts_options'] = reverse('api:analytics_probe_template_for_hosts_options')
|
data['probe_template_for_hosts_options'] = reverse('api:analytics_probe_template_for_hosts_options', request=request)
|
||||||
data['roi_templates'] = reverse('api:analytics_roi_templates_explorer')
|
data['roi_templates'] = reverse('api:analytics_roi_templates_explorer', request=request)
|
||||||
data['roi_templates_options'] = reverse('api:analytics_roi_templates_options')
|
data['roi_templates_options'] = reverse('api:analytics_roi_templates_options', request=request)
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ class InventoryObjectRolesList(SubListAPIView):
|
|||||||
serializer_class = RoleSerializer
|
serializer_class = RoleSerializer
|
||||||
parent_model = Inventory
|
parent_model = Inventory
|
||||||
search_fields = ('role_field', 'content_type__model')
|
search_fields = ('role_field', 'content_type__model')
|
||||||
|
deprecated = True
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
po = self.get_parent_object()
|
po = self.get_parent_object()
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ class OrganizationObjectRolesList(SubListAPIView):
|
|||||||
serializer_class = RoleSerializer
|
serializer_class = RoleSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
search_fields = ('role_field', 'content_type__model')
|
search_fields = ('role_field', 'content_type__model')
|
||||||
|
deprecated = True
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
po = self.get_parent_object()
|
po = self.get_parent_object()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from django.utils.decorators import method_decorator
|
|||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.urls import reverse as django_reverse
|
||||||
|
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@@ -27,7 +28,7 @@ from awx.main.analytics import all_collectors
|
|||||||
from awx.main.ha import is_ha_environment
|
from awx.main.ha import is_ha_environment
|
||||||
from awx.main.utils import get_awx_version, get_custom_venv_choices
|
from awx.main.utils import get_awx_version, get_custom_venv_choices
|
||||||
from awx.main.utils.licensing import validate_entitlement_manifest
|
from awx.main.utils.licensing import validate_entitlement_manifest
|
||||||
from awx.api.versioning import reverse, drf_reverse
|
from awx.api.versioning import URLPathVersioning, is_optional_api_urlpattern_prefix_request, reverse, drf_reverse
|
||||||
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS
|
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS
|
||||||
from awx.main.models import Project, Organization, Instance, InstanceGroup, JobTemplate
|
from awx.main.models import Project, Organization, Instance, InstanceGroup, JobTemplate
|
||||||
from awx.main.utils import set_environ
|
from awx.main.utils import set_environ
|
||||||
@@ -39,19 +40,19 @@ logger = logging.getLogger('awx.api.views.root')
|
|||||||
class ApiRootView(APIView):
|
class ApiRootView(APIView):
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
name = _('REST API')
|
name = _('REST API')
|
||||||
versioning_class = None
|
versioning_class = URLPathVersioning
|
||||||
swagger_topic = 'Versioning'
|
swagger_topic = 'Versioning'
|
||||||
|
|
||||||
@method_decorator(ensure_csrf_cookie)
|
@method_decorator(ensure_csrf_cookie)
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
'''List supported API versions'''
|
'''List supported API versions'''
|
||||||
|
v2 = reverse('api:api_v2_root_view', request=request, kwargs={'version': 'v2'})
|
||||||
v2 = reverse('api:api_v2_root_view', kwargs={'version': 'v2'})
|
|
||||||
data = OrderedDict()
|
data = OrderedDict()
|
||||||
data['description'] = _('AWX REST API')
|
data['description'] = _('AWX REST API')
|
||||||
data['current_version'] = v2
|
data['current_version'] = v2
|
||||||
data['available_versions'] = dict(v2=v2)
|
data['available_versions'] = dict(v2=v2)
|
||||||
data['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
|
if not is_optional_api_urlpattern_prefix_request(request):
|
||||||
|
data['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
|
||||||
data['custom_logo'] = settings.CUSTOM_LOGO
|
data['custom_logo'] = settings.CUSTOM_LOGO
|
||||||
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
|
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
|
||||||
data['login_redirect_override'] = settings.LOGIN_REDIRECT_OVERRIDE
|
data['login_redirect_override'] = settings.LOGIN_REDIRECT_OVERRIDE
|
||||||
@@ -130,6 +131,10 @@ class ApiVersionRootView(APIView):
|
|||||||
data['mesh_visualizer'] = reverse('api:mesh_visualizer_view', request=request)
|
data['mesh_visualizer'] = reverse('api:mesh_visualizer_view', request=request)
|
||||||
data['bulk'] = reverse('api:bulk', request=request)
|
data['bulk'] = reverse('api:bulk', request=request)
|
||||||
data['analytics'] = reverse('api:analytics_root_view', request=request)
|
data['analytics'] = reverse('api:analytics_root_view', request=request)
|
||||||
|
data['service_index'] = django_reverse('service-index-root')
|
||||||
|
data['role_definitions'] = django_reverse('roledefinition-list')
|
||||||
|
data['role_user_assignments'] = django_reverse('roleuserassignment-list')
|
||||||
|
data['role_team_assignments'] = django_reverse('roleteamassignment-list')
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ from rest_framework.exceptions import ParseError, PermissionDenied
|
|||||||
# Django OAuth Toolkit
|
# Django OAuth Toolkit
|
||||||
from awx.main.models.oauth import OAuth2Application, OAuth2AccessToken
|
from awx.main.models.oauth import OAuth2Application, OAuth2AccessToken
|
||||||
|
|
||||||
|
# django-ansible-base
|
||||||
from ansible_base.lib.utils.validation import to_python_boolean
|
from ansible_base.lib.utils.validation import to_python_boolean
|
||||||
|
from ansible_base.rbac.models import RoleEvaluation
|
||||||
|
from ansible_base.rbac import permission_registry
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.utils import (
|
from awx.main.utils import (
|
||||||
@@ -72,8 +75,6 @@ from awx.main.models import (
|
|||||||
WorkflowJobTemplateNode,
|
WorkflowJobTemplateNode,
|
||||||
WorkflowApproval,
|
WorkflowApproval,
|
||||||
WorkflowApprovalTemplate,
|
WorkflowApprovalTemplate,
|
||||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
|
||||||
ROLE_SINGLETON_SYSTEM_AUDITOR,
|
|
||||||
)
|
)
|
||||||
from awx.main.models.mixins import ResourceMixin
|
from awx.main.models.mixins import ResourceMixin
|
||||||
|
|
||||||
@@ -264,7 +265,11 @@ class BaseAccess(object):
|
|||||||
return self.can_change(obj, data)
|
return self.can_change(obj, data)
|
||||||
|
|
||||||
def can_delete(self, obj):
|
def can_delete(self, obj):
|
||||||
return self.user.is_superuser
|
if self.user.is_superuser:
|
||||||
|
return True
|
||||||
|
if obj._meta.model_name in [cls._meta.model_name for cls in permission_registry.all_registered_models]:
|
||||||
|
return self.user.has_obj_perm(obj, 'delete')
|
||||||
|
return False
|
||||||
|
|
||||||
def can_copy(self, obj):
|
def can_copy(self, obj):
|
||||||
return self.can_add({'reference_obj': obj})
|
return self.can_add({'reference_obj': obj})
|
||||||
@@ -639,7 +644,10 @@ class UserAccess(BaseAccess):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
prefetch_related = ('profile',)
|
prefetch_related = (
|
||||||
|
'profile',
|
||||||
|
'resource',
|
||||||
|
)
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
if settings.ORG_ADMINS_CAN_SEE_ALL_USERS and (self.user.admin_of_organizations.exists() or self.user.auditor_of_organizations.exists()):
|
if settings.ORG_ADMINS_CAN_SEE_ALL_USERS and (self.user.admin_of_organizations.exists() or self.user.auditor_of_organizations.exists()):
|
||||||
@@ -648,9 +656,7 @@ class UserAccess(BaseAccess):
|
|||||||
qs = (
|
qs = (
|
||||||
User.objects.filter(pk__in=Organization.accessible_objects(self.user, 'read_role').values('member_role__members'))
|
User.objects.filter(pk__in=Organization.accessible_objects(self.user, 'read_role').values('member_role__members'))
|
||||||
| User.objects.filter(pk=self.user.id)
|
| User.objects.filter(pk=self.user.id)
|
||||||
| User.objects.filter(
|
| User.objects.filter(is_superuser=True)
|
||||||
pk__in=Role.objects.filter(singleton_name__in=[ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR]).values('members')
|
|
||||||
)
|
|
||||||
).distinct()
|
).distinct()
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@@ -708,6 +714,15 @@ class UserAccess(BaseAccess):
|
|||||||
if not allow_orphans:
|
if not allow_orphans:
|
||||||
# in these cases only superusers can modify orphan users
|
# in these cases only superusers can modify orphan users
|
||||||
return False
|
return False
|
||||||
|
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||||
|
# Permission granted if the user has all permissions that the target user has
|
||||||
|
target_perms = set(
|
||||||
|
RoleEvaluation.objects.filter(role__in=obj.has_roles.all()).values_list('object_id', 'content_type_id', 'codename').distinct()
|
||||||
|
)
|
||||||
|
user_perms = set(
|
||||||
|
RoleEvaluation.objects.filter(role__in=self.user.has_roles.all()).values_list('object_id', 'content_type_id', 'codename').distinct()
|
||||||
|
)
|
||||||
|
return not (target_perms - user_perms)
|
||||||
return not obj.roles.all().exclude(ancestors__in=self.user.roles.all()).exists()
|
return not obj.roles.all().exclude(ancestors__in=self.user.roles.all()).exists()
|
||||||
else:
|
else:
|
||||||
return self.is_all_org_admin(obj)
|
return self.is_all_org_admin(obj)
|
||||||
@@ -835,6 +850,7 @@ class OrganizationAccess(NotificationAttachMixin, BaseAccess):
|
|||||||
prefetch_related = (
|
prefetch_related = (
|
||||||
'created_by',
|
'created_by',
|
||||||
'modified_by',
|
'modified_by',
|
||||||
|
'resource', # dab_resource_registry
|
||||||
)
|
)
|
||||||
# organization admin_role is not a parent of organization auditor_role
|
# organization admin_role is not a parent of organization auditor_role
|
||||||
notification_attach_roles = ['admin_role', 'auditor_role']
|
notification_attach_roles = ['admin_role', 'auditor_role']
|
||||||
@@ -945,9 +961,6 @@ class InventoryAccess(BaseAccess):
|
|||||||
def can_update(self, obj):
|
def can_update(self, obj):
|
||||||
return self.user in obj.update_role
|
return self.user in obj.update_role
|
||||||
|
|
||||||
def can_delete(self, obj):
|
|
||||||
return self.can_admin(obj, None)
|
|
||||||
|
|
||||||
def can_run_ad_hoc_commands(self, obj):
|
def can_run_ad_hoc_commands(self, obj):
|
||||||
return self.user in obj.adhoc_role
|
return self.user in obj.adhoc_role
|
||||||
|
|
||||||
@@ -1303,6 +1316,7 @@ class TeamAccess(BaseAccess):
|
|||||||
'created_by',
|
'created_by',
|
||||||
'modified_by',
|
'modified_by',
|
||||||
'organization',
|
'organization',
|
||||||
|
'resource', # dab_resource_registry
|
||||||
)
|
)
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
@@ -1400,8 +1414,12 @@ class ExecutionEnvironmentAccess(BaseAccess):
|
|||||||
def can_change(self, obj, data):
|
def can_change(self, obj, data):
|
||||||
if obj and obj.organization_id is None:
|
if obj and obj.organization_id is None:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
if self.user not in obj.organization.execution_environment_admin_role:
|
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||||
raise PermissionDenied
|
if not self.user.has_obj_perm(obj, 'change'):
|
||||||
|
raise PermissionDenied
|
||||||
|
else:
|
||||||
|
if self.user not in obj.organization.execution_environment_admin_role:
|
||||||
|
raise PermissionDenied
|
||||||
if data and 'organization' in data:
|
if data and 'organization' in data:
|
||||||
new_org = get_object_from_data('organization', Organization, data, obj=obj)
|
new_org = get_object_from_data('organization', Organization, data, obj=obj)
|
||||||
if not new_org or self.user not in new_org.execution_environment_admin_role:
|
if not new_org or self.user not in new_org.execution_environment_admin_role:
|
||||||
@@ -2587,6 +2605,8 @@ class ScheduleAccess(UnifiedCredentialsMixin, BaseAccess):
|
|||||||
if not JobLaunchConfigAccess(self.user).can_add(data):
|
if not JobLaunchConfigAccess(self.user).can_add(data):
|
||||||
return False
|
return False
|
||||||
if not data:
|
if not data:
|
||||||
|
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||||
|
return self.user.has_roles.filter(permission_partials__codename__in=['execute_jobtemplate', 'update_project', 'update_inventory']).exists()
|
||||||
return Role.objects.filter(role_field__in=['update_role', 'execute_role'], ancestors__in=self.user.roles.all()).exists()
|
return Role.objects.filter(role_field__in=['update_role', 'execute_role'], ancestors__in=self.user.roles.all()).exists()
|
||||||
|
|
||||||
return self.check_related('unified_job_template', UnifiedJobTemplate, data, role_field='execute_role', mandatory=True)
|
return self.check_related('unified_job_template', UnifiedJobTemplate, data, role_field='execute_role', mandatory=True)
|
||||||
@@ -2615,6 +2635,8 @@ class NotificationTemplateAccess(BaseAccess):
|
|||||||
prefetch_related = ('created_by', 'modified_by', 'organization')
|
prefetch_related = ('created_by', 'modified_by', 'organization')
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
|
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||||
|
return self.model.access_qs(self.user, 'view')
|
||||||
return self.model.objects.filter(
|
return self.model.objects.filter(
|
||||||
Q(organization__in=Organization.accessible_objects(self.user, 'notification_admin_role')) | Q(organization__in=self.user.auditor_of_organizations)
|
Q(organization__in=Organization.accessible_objects(self.user, 'notification_admin_role')) | Q(organization__in=self.user.auditor_of_organizations)
|
||||||
).distinct()
|
).distinct()
|
||||||
@@ -2783,7 +2805,7 @@ class ActivityStreamAccess(BaseAccess):
|
|||||||
| Q(notification_template__organization__in=auditing_orgs)
|
| Q(notification_template__organization__in=auditing_orgs)
|
||||||
| Q(notification__notification_template__organization__in=auditing_orgs)
|
| Q(notification__notification_template__organization__in=auditing_orgs)
|
||||||
| Q(label__organization__in=auditing_orgs)
|
| Q(label__organization__in=auditing_orgs)
|
||||||
| Q(role__in=Role.objects.filter(ancestors__in=self.user.roles.all()) if auditing_orgs else [])
|
| Q(role__in=Role.visible_roles(self.user) if auditing_orgs else [])
|
||||||
)
|
)
|
||||||
|
|
||||||
project_set = Project.accessible_pk_qs(self.user, 'read_role')
|
project_set = Project.accessible_pk_qs(self.user, 'read_role')
|
||||||
@@ -2840,13 +2862,10 @@ class RoleAccess(BaseAccess):
|
|||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
result = Role.visible_roles(self.user)
|
result = Role.visible_roles(self.user)
|
||||||
# Sanity check: is the requesting user an orphaned non-admin/auditor?
|
# Make system admin/auditor mandatorily visible.
|
||||||
# if yes, make system admin/auditor mandatorily visible.
|
mandatories = ('system_administrator', 'system_auditor')
|
||||||
if not self.user.is_superuser and not self.user.is_system_auditor and not self.user.organizations.exists():
|
super_qs = Role.objects.filter(singleton_name__in=mandatories)
|
||||||
mandatories = ('system_administrator', 'system_auditor')
|
return result | super_qs
|
||||||
super_qs = Role.objects.filter(singleton_name__in=mandatories)
|
|
||||||
result = result | super_qs
|
|
||||||
return result
|
|
||||||
|
|
||||||
def can_add(self, obj, data):
|
def can_add(self, obj, data):
|
||||||
# Unsupported for now
|
# Unsupported for now
|
||||||
|
|||||||
@@ -1,7 +1,40 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from awx.main.utils.named_url_graph import _customize_graph, generate_graph
|
||||||
|
from awx.conf import register, fields
|
||||||
|
|
||||||
|
|
||||||
class MainConfig(AppConfig):
|
class MainConfig(AppConfig):
|
||||||
name = 'awx.main'
|
name = 'awx.main'
|
||||||
verbose_name = _('Main')
|
verbose_name = _('Main')
|
||||||
|
|
||||||
|
def load_named_url_feature(self):
|
||||||
|
models = [m for m in self.get_models() if hasattr(m, 'get_absolute_url')]
|
||||||
|
generate_graph(models)
|
||||||
|
_customize_graph()
|
||||||
|
register(
|
||||||
|
'NAMED_URL_FORMATS',
|
||||||
|
field_class=fields.DictField,
|
||||||
|
read_only=True,
|
||||||
|
label=_('Formats of all available named urls'),
|
||||||
|
help_text=_('Read-only list of key-value pairs that shows the standard format of all available named URLs.'),
|
||||||
|
category=_('Named URL'),
|
||||||
|
category_slug='named-url',
|
||||||
|
)
|
||||||
|
register(
|
||||||
|
'NAMED_URL_GRAPH_NODES',
|
||||||
|
field_class=fields.DictField,
|
||||||
|
read_only=True,
|
||||||
|
label=_('List of all named url graph nodes.'),
|
||||||
|
help_text=_(
|
||||||
|
'Read-only list of key-value pairs that exposes named URL graph topology.'
|
||||||
|
' Use this list to programmatically generate named URLs for resources'
|
||||||
|
),
|
||||||
|
category=_('Named URL'),
|
||||||
|
category_slug='named-url',
|
||||||
|
)
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
super().ready()
|
||||||
|
|
||||||
|
self.load_named_url_feature()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
|
from django.core.checks import Error
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
# Django REST Framework
|
# Django REST Framework
|
||||||
@@ -954,3 +955,27 @@ def logging_validate(serializer, attrs):
|
|||||||
|
|
||||||
|
|
||||||
register_validate('logging', logging_validate)
|
register_validate('logging', logging_validate)
|
||||||
|
|
||||||
|
|
||||||
|
def csrf_trusted_origins_validate(serializer, attrs):
|
||||||
|
if not serializer.instance or not hasattr(serializer.instance, 'CSRF_TRUSTED_ORIGINS'):
|
||||||
|
return attrs
|
||||||
|
if 'CSRF_TRUSTED_ORIGINS' not in attrs:
|
||||||
|
return attrs
|
||||||
|
errors = []
|
||||||
|
for origin in attrs['CSRF_TRUSTED_ORIGINS']:
|
||||||
|
if "://" not in origin:
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
"As of Django 4.0, the values in the CSRF_TRUSTED_ORIGINS "
|
||||||
|
"setting must start with a scheme (usually http:// or "
|
||||||
|
"https://) but found %s. See the release notes for details." % origin,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if errors:
|
||||||
|
error_messages = [error.msg for error in errors]
|
||||||
|
raise serializers.ValidationError(_('\n'.join(error_messages)))
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
register_validate('system', csrf_trusted_origins_validate)
|
||||||
|
|||||||
@@ -114,3 +114,28 @@ SUBSCRIPTION_USAGE_MODEL_UNIQUE_HOSTS = 'unique_managed_hosts'
|
|||||||
|
|
||||||
# Shared prefetch to use for creating a queryset for the purpose of writing or saving facts
|
# Shared prefetch to use for creating a queryset for the purpose of writing or saving facts
|
||||||
HOST_FACTS_FIELDS = ('name', 'ansible_facts', 'ansible_facts_modified', 'modified', 'inventory_id')
|
HOST_FACTS_FIELDS = ('name', 'ansible_facts', 'ansible_facts_modified', 'modified', 'inventory_id')
|
||||||
|
|
||||||
|
# Data for RBAC compatibility layer
|
||||||
|
role_name_to_perm_mapping = {
|
||||||
|
'adhoc_role': ['adhoc_'],
|
||||||
|
'approval_role': ['approve_'],
|
||||||
|
'auditor_role': ['audit_'],
|
||||||
|
'admin_role': ['change_', 'add_', 'delete_'],
|
||||||
|
'execute_role': ['execute_'],
|
||||||
|
'read_role': ['view_'],
|
||||||
|
'update_role': ['update_'],
|
||||||
|
'member_role': ['member_'],
|
||||||
|
'use_role': ['use_'],
|
||||||
|
}
|
||||||
|
|
||||||
|
org_role_to_permission = {
|
||||||
|
'notification_admin_role': 'add_notificationtemplate',
|
||||||
|
'project_admin_role': 'add_project',
|
||||||
|
'execute_role': 'execute_jobtemplate',
|
||||||
|
'inventory_admin_role': 'add_inventory',
|
||||||
|
'credential_admin_role': 'add_credential',
|
||||||
|
'workflow_admin_role': 'add_workflowjobtemplate',
|
||||||
|
'job_template_admin_role': 'change_jobtemplate', # TODO: this doesnt really work, solution not clear
|
||||||
|
'execution_environment_admin_role': 'add_executionenvironment',
|
||||||
|
'auditor_role': 'view_project', # TODO: also doesnt really work
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import psycopg
|
import psycopg
|
||||||
import select
|
import select
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
@@ -94,8 +95,8 @@ class PubSub(object):
|
|||||||
|
|
||||||
|
|
||||||
def create_listener_connection():
|
def create_listener_connection():
|
||||||
conf = settings.DATABASES['default'].copy()
|
conf = deepcopy(settings.DATABASES['default'])
|
||||||
conf['OPTIONS'] = conf.get('OPTIONS', {}).copy()
|
conf['OPTIONS'] = deepcopy(conf.get('OPTIONS', {}))
|
||||||
# Modify the application name to distinguish from other connections the process might use
|
# Modify the application name to distinguish from other connections the process might use
|
||||||
conf['OPTIONS']['application_name'] = get_application_name(settings.CLUSTER_HOST_ID, function='listener')
|
conf['OPTIONS']['application_name'] = get_application_name(settings.CLUSTER_HOST_ID, function='listener')
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import json
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from awx.conf import settings_registry
|
from awx.conf import settings_registry
|
||||||
|
|
||||||
|
|
||||||
@@ -40,6 +41,15 @@ class Command(BaseCommand):
|
|||||||
"USER_SEARCH": False,
|
"USER_SEARCH": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def is_enabled(self, settings, keys):
|
||||||
|
missing_fields = []
|
||||||
|
for key, required in keys.items():
|
||||||
|
if required and not settings.get(key):
|
||||||
|
missing_fields.append(key)
|
||||||
|
if missing_fields:
|
||||||
|
return False, missing_fields
|
||||||
|
return True, None
|
||||||
|
|
||||||
def get_awx_ldap_settings(self) -> dict[str, dict[str, Any]]:
|
def get_awx_ldap_settings(self) -> dict[str, dict[str, Any]]:
|
||||||
awx_ldap_settings = {}
|
awx_ldap_settings = {}
|
||||||
|
|
||||||
@@ -64,15 +74,17 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
if new_key == "SERVER_URI" and value:
|
if new_key == "SERVER_URI" and value:
|
||||||
value = value.split(", ")
|
value = value.split(", ")
|
||||||
|
grouped_settings[index][new_key] = value
|
||||||
|
|
||||||
|
if type(value).__name__ == "LDAPSearch":
|
||||||
|
data = []
|
||||||
|
data.append(value.base_dn)
|
||||||
|
data.append("SCOPE_SUBTREE")
|
||||||
|
data.append(value.filterstr)
|
||||||
|
grouped_settings[index][new_key] = data
|
||||||
|
|
||||||
return grouped_settings
|
return grouped_settings
|
||||||
|
|
||||||
def is_enabled(self, settings, keys):
|
|
||||||
for key, required in keys.items():
|
|
||||||
if required and not settings.get(key):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_awx_saml_settings(self) -> dict[str, Any]:
|
def get_awx_saml_settings(self) -> dict[str, Any]:
|
||||||
awx_saml_settings = {}
|
awx_saml_settings = {}
|
||||||
for awx_saml_setting in settings_registry.get_registered_settings(category_slug='saml'):
|
for awx_saml_setting in settings_registry.get_registered_settings(category_slug='saml'):
|
||||||
@@ -82,7 +94,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def format_config_data(self, enabled, awx_settings, type, keys, name):
|
def format_config_data(self, enabled, awx_settings, type, keys, name):
|
||||||
config = {
|
config = {
|
||||||
"type": f"awx.authentication.authenticator_plugins.{type}",
|
"type": f"ansible_base.authentication.authenticator_plugins.{type}",
|
||||||
"name": name,
|
"name": name,
|
||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
"create_objects": True,
|
"create_objects": True,
|
||||||
@@ -130,7 +142,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
# dump SAML settings
|
# dump SAML settings
|
||||||
awx_saml_settings = self.get_awx_saml_settings()
|
awx_saml_settings = self.get_awx_saml_settings()
|
||||||
awx_saml_enabled = self.is_enabled(awx_saml_settings, self.DAB_SAML_AUTHENTICATOR_KEYS)
|
awx_saml_enabled, saml_missing_fields = self.is_enabled(awx_saml_settings, self.DAB_SAML_AUTHENTICATOR_KEYS)
|
||||||
if awx_saml_enabled:
|
if awx_saml_enabled:
|
||||||
awx_saml_name = awx_saml_settings["ENABLED_IDPS"]
|
awx_saml_name = awx_saml_settings["ENABLED_IDPS"]
|
||||||
data.append(
|
data.append(
|
||||||
@@ -142,21 +154,25 @@ class Command(BaseCommand):
|
|||||||
awx_saml_name,
|
awx_saml_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
data.append({"SAML_missing_fields": saml_missing_fields})
|
||||||
|
|
||||||
# dump LDAP settings
|
# dump LDAP settings
|
||||||
awx_ldap_group_settings = self.get_awx_ldap_settings()
|
awx_ldap_group_settings = self.get_awx_ldap_settings()
|
||||||
for awx_ldap_name, awx_ldap_settings in enumerate(awx_ldap_group_settings.values()):
|
for awx_ldap_name, awx_ldap_settings in awx_ldap_group_settings.items():
|
||||||
enabled = self.is_enabled(awx_ldap_settings, self.DAB_LDAP_AUTHENTICATOR_KEYS)
|
awx_ldap_enabled, ldap_missing_fields = self.is_enabled(awx_ldap_settings, self.DAB_LDAP_AUTHENTICATOR_KEYS)
|
||||||
if enabled:
|
if awx_ldap_enabled:
|
||||||
data.append(
|
data.append(
|
||||||
self.format_config_data(
|
self.format_config_data(
|
||||||
enabled,
|
awx_ldap_enabled,
|
||||||
awx_ldap_settings,
|
awx_ldap_settings,
|
||||||
"ldap",
|
"ldap",
|
||||||
self.DAB_LDAP_AUTHENTICATOR_KEYS,
|
self.DAB_LDAP_AUTHENTICATOR_KEYS,
|
||||||
str(awx_ldap_name),
|
f"LDAP_{awx_ldap_name}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
data.append({f"LDAP_{awx_ldap_name}_missing_fields": ldap_missing_fields})
|
||||||
|
|
||||||
# write to file if requested
|
# write to file if requested
|
||||||
if options["output_file"]:
|
if options["output_file"]:
|
||||||
|
|||||||
@@ -165,11 +165,10 @@ class Command(BaseCommand):
|
|||||||
return
|
return
|
||||||
|
|
||||||
WebsocketsMetricsServer().start()
|
WebsocketsMetricsServer().start()
|
||||||
websocket_relay_manager = WebSocketRelayManager()
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
asyncio.run(websocket_relay_manager.run())
|
asyncio.run(WebSocketRelayManager().run())
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info('Shutting down Websocket Relayer')
|
logger.info('Shutting down Websocket Relayer')
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Copyright (c) 2015 Ansible, Inc.
|
# Copyright (c) 2015 Ansible, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -9,20 +10,16 @@ from pathlib import Path
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import logout
|
from django.contrib.auth import logout
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.db.migrations.recorder import MigrationRecorder
|
from django.db.migrations.recorder import MigrationRecorder
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.apps import apps
|
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.urls import reverse, resolve
|
from django.urls import reverse, resolve
|
||||||
|
|
||||||
from awx.main import migrations
|
from awx.main import migrations
|
||||||
from awx.main.utils.named_url_graph import generate_graph, GraphNode
|
|
||||||
from awx.conf import fields, register
|
|
||||||
from awx.main.utils.profiling import AWXProfiler
|
from awx.main.utils.profiling import AWXProfiler
|
||||||
from awx.main.utils.common import memoize
|
from awx.main.utils.common import memoize
|
||||||
|
from awx.urls import get_urlpatterns
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('awx.main.middleware')
|
logger = logging.getLogger('awx.main.middleware')
|
||||||
@@ -61,7 +58,7 @@ class TimingMiddleware(threading.local, MiddlewareMixin):
|
|||||||
response['X-API-Profile-File'] = self.prof.stop()
|
response['X-API-Profile-File'] = self.prof.stop()
|
||||||
perf_logger.debug(
|
perf_logger.debug(
|
||||||
f'request: {request}, response_time: {response["X-API-Total-Time"]}',
|
f'request: {request}, response_time: {response["X-API-Total-Time"]}',
|
||||||
extra=dict(python_objects=dict(request=request, response=response, X_API_TOTAL_TIME=response["X-API-Total-Time"])),
|
extra=dict(python_objects=dict(request=request, response=response, X_API_TOTAL_TIME=response["X-API-Total-Time"], x_request_id=request.get('x-request-id', 'not-set'))),
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -100,49 +97,7 @@ class DisableLocalAuthMiddleware(MiddlewareMixin):
|
|||||||
logout(request)
|
logout(request)
|
||||||
|
|
||||||
|
|
||||||
def _customize_graph():
|
|
||||||
from awx.main.models import Instance, Schedule, UnifiedJobTemplate
|
|
||||||
|
|
||||||
for model in [Schedule, UnifiedJobTemplate]:
|
|
||||||
if model in settings.NAMED_URL_GRAPH:
|
|
||||||
settings.NAMED_URL_GRAPH[model].remove_bindings()
|
|
||||||
settings.NAMED_URL_GRAPH.pop(model)
|
|
||||||
if User not in settings.NAMED_URL_GRAPH:
|
|
||||||
settings.NAMED_URL_GRAPH[User] = GraphNode(User, ['username'], [])
|
|
||||||
settings.NAMED_URL_GRAPH[User].add_bindings()
|
|
||||||
if Instance not in settings.NAMED_URL_GRAPH:
|
|
||||||
settings.NAMED_URL_GRAPH[Instance] = GraphNode(Instance, ['hostname'], [])
|
|
||||||
settings.NAMED_URL_GRAPH[Instance].add_bindings()
|
|
||||||
|
|
||||||
|
|
||||||
class URLModificationMiddleware(MiddlewareMixin):
|
class URLModificationMiddleware(MiddlewareMixin):
|
||||||
def __init__(self, get_response):
|
|
||||||
models = [m for m in apps.get_app_config('main').get_models() if hasattr(m, 'get_absolute_url')]
|
|
||||||
generate_graph(models)
|
|
||||||
_customize_graph()
|
|
||||||
register(
|
|
||||||
'NAMED_URL_FORMATS',
|
|
||||||
field_class=fields.DictField,
|
|
||||||
read_only=True,
|
|
||||||
label=_('Formats of all available named urls'),
|
|
||||||
help_text=_('Read-only list of key-value pairs that shows the standard format of all available named URLs.'),
|
|
||||||
category=_('Named URL'),
|
|
||||||
category_slug='named-url',
|
|
||||||
)
|
|
||||||
register(
|
|
||||||
'NAMED_URL_GRAPH_NODES',
|
|
||||||
field_class=fields.DictField,
|
|
||||||
read_only=True,
|
|
||||||
label=_('List of all named url graph nodes.'),
|
|
||||||
help_text=_(
|
|
||||||
'Read-only list of key-value pairs that exposes named URL graph topology.'
|
|
||||||
' Use this list to programmatically generate named URLs for resources'
|
|
||||||
),
|
|
||||||
category=_('Named URL'),
|
|
||||||
category_slug='named-url',
|
|
||||||
)
|
|
||||||
super().__init__(get_response)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _hijack_for_old_jt_name(node, kwargs, named_url):
|
def _hijack_for_old_jt_name(node, kwargs, named_url):
|
||||||
try:
|
try:
|
||||||
@@ -220,3 +175,27 @@ class MigrationRanCheckMiddleware(MiddlewareMixin):
|
|||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
if is_migrating() and getattr(resolve(request.path), 'url_name', '') != 'migrations_notran':
|
if is_migrating() and getattr(resolve(request.path), 'url_name', '') != 'migrations_notran':
|
||||||
return redirect(reverse("ui:migrations_notran"))
|
return redirect(reverse("ui:migrations_notran"))
|
||||||
|
|
||||||
|
|
||||||
|
class OptionalURLPrefixPath(MiddlewareMixin):
|
||||||
|
@functools.lru_cache
|
||||||
|
def _url_optional(self, prefix):
|
||||||
|
# Relavant Django code path https://github.com/django/django/blob/stable/4.2.x/django/core/handlers/base.py#L300
|
||||||
|
#
|
||||||
|
# resolve_request(request)
|
||||||
|
# get_resolver(request.urlconf)
|
||||||
|
# _get_cached_resolver(request.urlconf) <-- cached via @functools.cache
|
||||||
|
#
|
||||||
|
# Django will attempt to cache the value(s) of request.urlconf
|
||||||
|
# Being hashable is a prerequisit for being cachable.
|
||||||
|
# tuple() is hashable list() is not.
|
||||||
|
# Hence the tuple(list()) wrap.
|
||||||
|
return tuple(get_urlpatterns(prefix=prefix))
|
||||||
|
|
||||||
|
def process_request(self, request):
|
||||||
|
prefix = settings.OPTIONAL_API_URLPATTERN_PREFIX
|
||||||
|
|
||||||
|
if request.path.startswith(f"/api/{prefix}"):
|
||||||
|
request.urlconf = self._url_optional(prefix)
|
||||||
|
else:
|
||||||
|
request.urlconf = 'awx.urls'
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('main', '0189_inbound_hop_nodes'),
|
('main', '0189_inbound_hop_nodes'),
|
||||||
]
|
]
|
||||||
|
|||||||
85
awx/main/migrations/0191_add_django_permissions.py
Normal file
85
awx/main/migrations/0191_add_django_permissions.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-13 20:10
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('main', '0190_alter_inventorysource_source_and_more'),
|
||||||
|
('dab_rbac', '__first__'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Add custom permissions for all special actions, like update, use, adhoc, and so on
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='credential',
|
||||||
|
options={'ordering': ('name',), 'permissions': [('use_credential', 'Can use credential in a job or related resource')]},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='instancegroup',
|
||||||
|
options={'permissions': [('use_instancegroup', 'Can use instance group in a preference list of a resource')]},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='inventory',
|
||||||
|
options={
|
||||||
|
'ordering': ('name',),
|
||||||
|
'permissions': [
|
||||||
|
('use_inventory', 'Can use inventory in a job template'),
|
||||||
|
('adhoc_inventory', 'Can run ad hoc commands'),
|
||||||
|
('update_inventory', 'Can update inventory sources in inventory'),
|
||||||
|
],
|
||||||
|
'verbose_name_plural': 'inventories',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='jobtemplate',
|
||||||
|
options={'ordering': ('name',), 'permissions': [('execute_jobtemplate', 'Can run this job template')]},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='project',
|
||||||
|
options={
|
||||||
|
'ordering': ('id',),
|
||||||
|
'permissions': [('update_project', 'Can run a project update'), ('use_project', 'Can use project in a job template')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='workflowjobtemplate',
|
||||||
|
options={
|
||||||
|
'permissions': [
|
||||||
|
('execute_workflowjobtemplate', 'Can run this workflow job template'),
|
||||||
|
('approve_workflowjobtemplate', 'Can approve steps in this workflow job template'),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='organization',
|
||||||
|
options={
|
||||||
|
'default_permissions': ('change', 'delete', 'view'),
|
||||||
|
'ordering': ('name',),
|
||||||
|
'permissions': [
|
||||||
|
('member_organization', 'Basic participation permissions for organization'),
|
||||||
|
('audit_organization', 'Audit everything inside the organization'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='team',
|
||||||
|
options={'ordering': ('organization__name', 'name'), 'permissions': [('member_team', 'Inherit all roles assigned to this team')]},
|
||||||
|
),
|
||||||
|
# Remove add default permission for a few models
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='jobtemplate',
|
||||||
|
options={
|
||||||
|
'default_permissions': ('change', 'delete', 'view'),
|
||||||
|
'ordering': ('name',),
|
||||||
|
'permissions': [('execute_jobtemplate', 'Can run this job template')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='instancegroup',
|
||||||
|
options={
|
||||||
|
'default_permissions': ('change', 'delete', 'view'),
|
||||||
|
'permissions': [('use_instancegroup', 'Can use instance group in a preference list of a resource')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
20
awx/main/migrations/0192_custom_roles.py
Normal file
20
awx/main/migrations/0192_custom_roles.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-21 02:06
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from awx.main.migrations._dab_rbac import migrate_to_new_rbac, create_permissions_as_operation, setup_managed_role_definitions
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('main', '0191_add_django_permissions'),
|
||||||
|
('dab_rbac', '__first__'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# make sure permissions and content types have been created by now
|
||||||
|
# these normally run in a post_migrate signal but we need them for our logic
|
||||||
|
migrations.RunPython(create_permissions_as_operation, migrations.RunPython.noop),
|
||||||
|
migrations.RunPython(setup_managed_role_definitions, migrations.RunPython.noop),
|
||||||
|
migrations.RunPython(migrate_to_new_rbac, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
354
awx/main/migrations/_dab_rbac.py
Normal file
354
awx/main/migrations/_dab_rbac.py
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.apps import apps as global_apps
|
||||||
|
from django.db.models import ForeignKey
|
||||||
|
from django.conf import settings
|
||||||
|
from ansible_base.rbac.migrations._utils import give_permissions
|
||||||
|
from ansible_base.rbac.management import create_dab_permissions
|
||||||
|
|
||||||
|
from awx.main.fields import ImplicitRoleField
|
||||||
|
from awx.main.constants import role_name_to_perm_mapping
|
||||||
|
|
||||||
|
from ansible_base.rbac.permission_registry import permission_registry
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('awx.main.migrations._dab_rbac')
|
||||||
|
|
||||||
|
|
||||||
|
def create_permissions_as_operation(apps, schema_editor):
|
||||||
|
create_dab_permissions(global_apps.get_app_config("main"), apps=apps)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Data structures and methods for the migration of old Role model to ObjectRole
|
||||||
|
"""
|
||||||
|
|
||||||
|
system_admin = ImplicitRoleField(name='system_administrator')
|
||||||
|
system_auditor = ImplicitRoleField(name='system_auditor')
|
||||||
|
system_admin.model = None
|
||||||
|
system_auditor.model = None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_parent_role(f, role_path):
|
||||||
|
"""
|
||||||
|
Given a field and a path declared in parent_role from the field definition, like
|
||||||
|
execute_role = ImplicitRoleField(parent_role='admin_role')
|
||||||
|
This expects to be passed in (execute_role object, "admin_role")
|
||||||
|
It hould return the admin_role from that object
|
||||||
|
"""
|
||||||
|
if role_path == 'singleton:system_administrator':
|
||||||
|
return system_admin
|
||||||
|
elif role_path == 'singleton:system_auditor':
|
||||||
|
return system_auditor
|
||||||
|
else:
|
||||||
|
related_field = f
|
||||||
|
current_model = f.model
|
||||||
|
for related_field_name in role_path.split('.'):
|
||||||
|
related_field = current_model._meta.get_field(related_field_name)
|
||||||
|
if isinstance(related_field, ForeignKey) and not isinstance(related_field, ImplicitRoleField):
|
||||||
|
current_model = related_field.related_model
|
||||||
|
return related_field
|
||||||
|
|
||||||
|
|
||||||
|
def build_role_map(apps):
|
||||||
|
"""
|
||||||
|
For the old Role model, this builds and returns dictionaries (children, parents)
|
||||||
|
which give a global mapping of the ImplicitRoleField instances according to the graph
|
||||||
|
"""
|
||||||
|
models = set(apps.get_app_config('main').get_models())
|
||||||
|
|
||||||
|
all_fields = set()
|
||||||
|
parents = {}
|
||||||
|
children = {}
|
||||||
|
|
||||||
|
all_fields.add(system_admin)
|
||||||
|
all_fields.add(system_auditor)
|
||||||
|
|
||||||
|
for cls in models:
|
||||||
|
for f in cls._meta.get_fields():
|
||||||
|
if isinstance(f, ImplicitRoleField):
|
||||||
|
all_fields.add(f)
|
||||||
|
|
||||||
|
for f in all_fields:
|
||||||
|
if f.parent_role is not None:
|
||||||
|
if isinstance(f.parent_role, str):
|
||||||
|
parent_roles = [f.parent_role]
|
||||||
|
else:
|
||||||
|
parent_roles = f.parent_role
|
||||||
|
|
||||||
|
# SPECIAL CASE: organization auditor_role is not a child of admin_role
|
||||||
|
# this makes no practical sense and conflicts with expected managed role
|
||||||
|
# so we put it in as a hack here
|
||||||
|
if f.name == 'auditor_role' and f.model._meta.model_name == 'organization':
|
||||||
|
parent_roles.append('admin_role')
|
||||||
|
|
||||||
|
parent_list = []
|
||||||
|
for rel_name in parent_roles:
|
||||||
|
parent_list.append(resolve_parent_role(f, rel_name))
|
||||||
|
|
||||||
|
parents[f] = parent_list
|
||||||
|
|
||||||
|
# build children lookup from parents lookup
|
||||||
|
for child_field, parent_list in parents.items():
|
||||||
|
for parent_field in parent_list:
|
||||||
|
children.setdefault(parent_field, [])
|
||||||
|
children[parent_field].append(child_field)
|
||||||
|
|
||||||
|
return (parents, children)
|
||||||
|
|
||||||
|
|
||||||
|
def get_descendents(f, children_map):
|
||||||
|
"""
|
||||||
|
Given ImplicitRoleField F and the children mapping, returns all descendents
|
||||||
|
of that field, as a set of other fields, including itself
|
||||||
|
"""
|
||||||
|
ret = {f}
|
||||||
|
if f in children_map:
|
||||||
|
for child_field in children_map[f]:
|
||||||
|
ret.update(get_descendents(child_field, children_map))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_permissions_for_role(role_field, children_map, apps):
|
||||||
|
Permission = apps.get_model('dab_rbac', 'DABPermission')
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
|
||||||
|
perm_list = []
|
||||||
|
for child_field in get_descendents(role_field, children_map):
|
||||||
|
if child_field.name in role_name_to_perm_mapping:
|
||||||
|
for perm_name in role_name_to_perm_mapping[child_field.name]:
|
||||||
|
if perm_name == 'add_' and role_field.model._meta.model_name != 'organization':
|
||||||
|
continue # only organizations can contain add permissions
|
||||||
|
perm = Permission.objects.filter(content_type=ContentType.objects.get_for_model(child_field.model), codename__startswith=perm_name).first()
|
||||||
|
if perm is not None and perm not in perm_list:
|
||||||
|
perm_list.append(perm)
|
||||||
|
|
||||||
|
# special case for two models that have object roles but no organization roles in old system
|
||||||
|
if role_field.name == 'notification_admin_role' or (role_field.name == 'admin_role' and role_field.model._meta.model_name == 'organization'):
|
||||||
|
ct = ContentType.objects.get_for_model(apps.get_model('main', 'NotificationTemplate'))
|
||||||
|
perm_list.extend(list(Permission.objects.filter(content_type=ct)))
|
||||||
|
if role_field.name == 'execution_environment_admin_role' or (role_field.name == 'admin_role' and role_field.model._meta.model_name == 'organization'):
|
||||||
|
ct = ContentType.objects.get_for_model(apps.get_model('main', 'ExecutionEnvironment'))
|
||||||
|
perm_list.extend(list(Permission.objects.filter(content_type=ct)))
|
||||||
|
|
||||||
|
# more special cases for those same above special org-level roles
|
||||||
|
if role_field.name == 'auditor_role':
|
||||||
|
for codename in ('view_notificationtemplate', 'view_executionenvironment'):
|
||||||
|
perm_list.append(Permission.objects.get(codename=codename))
|
||||||
|
|
||||||
|
return perm_list
|
||||||
|
|
||||||
|
|
||||||
|
def model_class(ct, apps):
|
||||||
|
"""
|
||||||
|
You can not use model methods in migrations, so this duplicates
|
||||||
|
what ContentType.model_class does, using current apps
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return apps.get_model(ct.app_label, ct.model)
|
||||||
|
except LookupError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_to_new_rbac(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
This method moves the assigned permissions from the old rbac.py models
|
||||||
|
to the new RoleDefinition and ObjectRole models
|
||||||
|
"""
|
||||||
|
Role = apps.get_model('main', 'Role')
|
||||||
|
RoleDefinition = apps.get_model('dab_rbac', 'RoleDefinition')
|
||||||
|
RoleUserAssignment = apps.get_model('dab_rbac', 'RoleUserAssignment')
|
||||||
|
Permission = apps.get_model('dab_rbac', 'DABPermission')
|
||||||
|
|
||||||
|
# remove add premissions that are not valid for migrations from old versions
|
||||||
|
for perm_str in ('add_organization', 'add_jobtemplate'):
|
||||||
|
perm = Permission.objects.filter(codename=perm_str).first()
|
||||||
|
if perm:
|
||||||
|
perm.delete()
|
||||||
|
|
||||||
|
managed_definitions = dict()
|
||||||
|
for role_definition in RoleDefinition.objects.filter(managed=True):
|
||||||
|
permissions = frozenset(role_definition.permissions.values_list('id', flat=True))
|
||||||
|
managed_definitions[permissions] = role_definition
|
||||||
|
|
||||||
|
# Build map of old role model
|
||||||
|
parents, children = build_role_map(apps)
|
||||||
|
|
||||||
|
# NOTE: this import is expected to break at some point, and then just move the data here
|
||||||
|
from awx.main.models.rbac import role_descriptions
|
||||||
|
|
||||||
|
for role in Role.objects.prefetch_related('members', 'parents').iterator():
|
||||||
|
if role.singleton_name:
|
||||||
|
continue # only bothering to migrate object roles
|
||||||
|
|
||||||
|
team_roles = []
|
||||||
|
for parent in role.parents.all():
|
||||||
|
if parent.id not in json.loads(role.implicit_parents):
|
||||||
|
team_roles.append(parent)
|
||||||
|
|
||||||
|
# we will not create any roles that do not have any users or teams
|
||||||
|
if not (role.members.all() or team_roles):
|
||||||
|
logger.debug(f'Skipping role {role.role_field} for {role.content_type.model}-{role.object_id} due to no members')
|
||||||
|
continue
|
||||||
|
|
||||||
|
# get a list of permissions that the old role would grant
|
||||||
|
object_cls = apps.get_model(f'main.{role.content_type.model}')
|
||||||
|
object = object_cls.objects.get(pk=role.object_id) # WORKAROUND, role.content_object does not work in migrations
|
||||||
|
f = object._meta.get_field(role.role_field) # should be ImplicitRoleField
|
||||||
|
perm_list = get_permissions_for_role(f, children, apps)
|
||||||
|
|
||||||
|
permissions = frozenset(perm.id for perm in perm_list)
|
||||||
|
|
||||||
|
# With the needed permissions established, obtain the RoleDefinition this will need, priorities:
|
||||||
|
# 1. If it exists as a managed RoleDefinition then obviously use that
|
||||||
|
# 2. If we already created this for a prior role, use that
|
||||||
|
# 3. Create a new RoleDefinition that lists those permissions
|
||||||
|
if permissions in managed_definitions:
|
||||||
|
role_definition = managed_definitions[permissions]
|
||||||
|
else:
|
||||||
|
action = role.role_field.rsplit('_', 1)[0] # remove the _field ending of the name
|
||||||
|
role_definition_name = f'{model_class(role.content_type, apps).__name__} {action.title()}'
|
||||||
|
|
||||||
|
description = role_descriptions[role.role_field]
|
||||||
|
if type(description) == dict:
|
||||||
|
if role.content_type.model in description:
|
||||||
|
description = description.get(role.content_type.model)
|
||||||
|
else:
|
||||||
|
description = description.get('default')
|
||||||
|
if '%s' in description:
|
||||||
|
description = description % role.content_type.model
|
||||||
|
|
||||||
|
role_definition, created = RoleDefinition.objects.get_or_create(
|
||||||
|
name=role_definition_name,
|
||||||
|
defaults={'description': description, 'content_type_id': role.content_type_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
logger.info(f'Created custom Role Definition {role_definition_name}, pk={role_definition.pk}')
|
||||||
|
role_definition.permissions.set(perm_list)
|
||||||
|
|
||||||
|
# Create the object role and add users to it
|
||||||
|
give_permissions(
|
||||||
|
apps,
|
||||||
|
role_definition,
|
||||||
|
users=role.members.all(),
|
||||||
|
teams=[tr.object_id for tr in team_roles],
|
||||||
|
object_id=role.object_id,
|
||||||
|
content_type_id=role.content_type_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create new replacement system auditor role
|
||||||
|
new_system_auditor, created = RoleDefinition.objects.get_or_create(
|
||||||
|
name='System Auditor',
|
||||||
|
defaults={'description': 'Migrated singleton role giving read permission to everything', 'managed': True},
|
||||||
|
)
|
||||||
|
new_system_auditor.permissions.add(*list(Permission.objects.filter(codename__startswith='view')))
|
||||||
|
|
||||||
|
# migrate is_system_auditor flag, because it is no longer handled by a system role
|
||||||
|
old_system_auditor = Role.objects.filter(singleton_name='system_auditor').first()
|
||||||
|
if old_system_auditor:
|
||||||
|
# if the system auditor role is not present, this is a new install and no users should exist
|
||||||
|
ct = 0
|
||||||
|
for user in role.members.all():
|
||||||
|
RoleUserAssignment.objects.create(user=user, role_definition=new_system_auditor)
|
||||||
|
ct += 1
|
||||||
|
if ct:
|
||||||
|
logger.info(f'Migrated {ct} users to new system auditor flag')
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_managed(name, description, ct, permissions, RoleDefinition):
|
||||||
|
role_definition, created = RoleDefinition.objects.get_or_create(name=name, defaults={'managed': True, 'description': description, 'content_type': ct})
|
||||||
|
role_definition.permissions.set(list(permissions))
|
||||||
|
|
||||||
|
if not role_definition.managed:
|
||||||
|
role_definition.managed = True
|
||||||
|
role_definition.save(update_fields=['managed'])
|
||||||
|
|
||||||
|
if created:
|
||||||
|
logger.info(f'Created RoleDefinition {role_definition.name} pk={role_definition} with {len(permissions)} permissions')
|
||||||
|
|
||||||
|
return role_definition
|
||||||
|
|
||||||
|
|
||||||
|
def setup_managed_role_definitions(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Idepotent method to create or sync the managed role definitions
|
||||||
|
"""
|
||||||
|
to_create = settings.ANSIBLE_BASE_ROLE_PRECREATE
|
||||||
|
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
Permission = apps.get_model('dab_rbac', 'DABPermission')
|
||||||
|
RoleDefinition = apps.get_model('dab_rbac', 'RoleDefinition')
|
||||||
|
Organization = apps.get_model(settings.ANSIBLE_BASE_ORGANIZATION_MODEL)
|
||||||
|
org_ct = ContentType.objects.get_for_model(Organization)
|
||||||
|
managed_role_definitions = []
|
||||||
|
|
||||||
|
org_perms = set()
|
||||||
|
for cls in permission_registry._registry:
|
||||||
|
ct = ContentType.objects.get_for_model(cls)
|
||||||
|
object_perms = set(Permission.objects.filter(content_type=ct))
|
||||||
|
# Special case for InstanceGroup which has an organiation field, but is not an organization child object
|
||||||
|
if cls._meta.model_name != 'instancegroup':
|
||||||
|
org_perms.update(object_perms)
|
||||||
|
|
||||||
|
if 'object_admin' in to_create and cls != Organization:
|
||||||
|
indiv_perms = object_perms.copy()
|
||||||
|
add_perms = [perm for perm in indiv_perms if perm.codename.startswith('add_')]
|
||||||
|
if add_perms:
|
||||||
|
for perm in add_perms:
|
||||||
|
indiv_perms.remove(perm)
|
||||||
|
|
||||||
|
managed_role_definitions.append(
|
||||||
|
get_or_create_managed(
|
||||||
|
to_create['object_admin'].format(cls=cls), f'Has all permissions to a single {cls._meta.verbose_name}', ct, indiv_perms, RoleDefinition
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'org_children' in to_create and cls != Organization:
|
||||||
|
org_child_perms = object_perms.copy()
|
||||||
|
org_child_perms.add(Permission.objects.get(codename='view_organization'))
|
||||||
|
|
||||||
|
managed_role_definitions.append(
|
||||||
|
get_or_create_managed(
|
||||||
|
to_create['org_children'].format(cls=cls),
|
||||||
|
f'Has all permissions to {cls._meta.verbose_name_plural} within an organization',
|
||||||
|
org_ct,
|
||||||
|
org_child_perms,
|
||||||
|
RoleDefinition,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'special' in to_create:
|
||||||
|
special_perms = []
|
||||||
|
for perm in object_perms:
|
||||||
|
if perm.codename.split('_')[0] not in ('add', 'change', 'update', 'delete', 'view'):
|
||||||
|
special_perms.append(perm)
|
||||||
|
for perm in special_perms:
|
||||||
|
action = perm.codename.split('_')[0]
|
||||||
|
view_perm = Permission.objects.get(content_type=ct, codename__startswith='view_')
|
||||||
|
managed_role_definitions.append(
|
||||||
|
get_or_create_managed(
|
||||||
|
to_create['special'].format(cls=cls, action=action.title()),
|
||||||
|
f'Has {action} permissions to a single {cls._meta.verbose_name}',
|
||||||
|
ct,
|
||||||
|
[perm, view_perm],
|
||||||
|
RoleDefinition,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'org_admin' in to_create:
|
||||||
|
managed_role_definitions.append(
|
||||||
|
get_or_create_managed(
|
||||||
|
to_create['org_admin'].format(cls=Organization),
|
||||||
|
'Has all permissions to a single organization and all objects inside of it',
|
||||||
|
org_ct,
|
||||||
|
org_perms,
|
||||||
|
RoleDefinition,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
unexpected_role_definitions = RoleDefinition.objects.filter(managed=True).exclude(pk__in=[rd.pk for rd in managed_role_definitions])
|
||||||
|
for role_definition in unexpected_role_definitions:
|
||||||
|
logger.info(f'Deleting old managed role definition {role_definition.name}, pk={role_definition.pk}')
|
||||||
|
role_definition.delete()
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
# Copyright (c) 2015 Ansible, Inc.
|
# Copyright (c) 2015 Ansible, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.conf import settings # noqa
|
from django.conf import settings # noqa
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db.models.signals import pre_delete # noqa
|
from django.db.models.signals import pre_delete # noqa
|
||||||
|
|
||||||
|
# django-ansible-base
|
||||||
|
from ansible_base.resource_registry.fields import AnsibleResourceField
|
||||||
|
from ansible_base.rbac import permission_registry
|
||||||
|
from ansible_base.rbac.models import RoleDefinition, RoleUserAssignment
|
||||||
from ansible_base.lib.utils.models import prevent_search
|
from ansible_base.lib.utils.models import prevent_search
|
||||||
|
from ansible_base.lib.utils.models import user_summary_fields
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.models.base import BaseModel, PrimordialModel, accepts_json, CLOUD_INVENTORY_SOURCES, VERBOSITY_CHOICES # noqa
|
from awx.main.models.base import BaseModel, PrimordialModel, accepts_json, CLOUD_INVENTORY_SOURCES, VERBOSITY_CHOICES # noqa
|
||||||
@@ -99,6 +106,8 @@ from awx.main.access import get_user_queryset, check_user_access, check_user_acc
|
|||||||
User.add_to_class('get_queryset', get_user_queryset)
|
User.add_to_class('get_queryset', get_user_queryset)
|
||||||
User.add_to_class('can_access', check_user_access)
|
User.add_to_class('can_access', check_user_access)
|
||||||
User.add_to_class('can_access_with_errors', check_user_access_with_errors)
|
User.add_to_class('can_access_with_errors', check_user_access_with_errors)
|
||||||
|
User.add_to_class('resource', AnsibleResourceField(primary_key_field="id"))
|
||||||
|
User.add_to_class('summary_fields', user_summary_fields)
|
||||||
|
|
||||||
|
|
||||||
def convert_jsonfields():
|
def convert_jsonfields():
|
||||||
@@ -191,11 +200,21 @@ User.add_to_class('auditor_of_organizations', user_get_auditor_of_organizations)
|
|||||||
User.add_to_class('created', created)
|
User.add_to_class('created', created)
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_auditor_role():
|
||||||
|
rd, created = RoleDefinition.objects.get_or_create(
|
||||||
|
name='System Auditor', defaults={'description': 'Migrated singleton role giving read permission to everything'}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
rd.permissions.add(*list(permission_registry.permission_qs.filter(codename__startswith='view')))
|
||||||
|
return rd
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user_is_system_auditor(user):
|
def user_is_system_auditor(user):
|
||||||
if not hasattr(user, '_is_system_auditor'):
|
if not hasattr(user, '_is_system_auditor'):
|
||||||
if user.pk:
|
if user.pk:
|
||||||
user._is_system_auditor = user.roles.filter(singleton_name='system_auditor', role_field='system_auditor').exists()
|
rd = get_system_auditor_role()
|
||||||
|
user._is_system_auditor = RoleUserAssignment.objects.filter(user=user, role_definition=rd).exists()
|
||||||
else:
|
else:
|
||||||
# Odd case where user is unsaved, this should never be relied on
|
# Odd case where user is unsaved, this should never be relied on
|
||||||
return False
|
return False
|
||||||
@@ -209,17 +228,17 @@ def user_is_system_auditor(user, tf):
|
|||||||
# time they've logged in, and we've just created the new User in this
|
# time they've logged in, and we've just created the new User in this
|
||||||
# request), we need one to set up the system auditor role
|
# request), we need one to set up the system auditor role
|
||||||
user.save()
|
user.save()
|
||||||
if tf:
|
rd = get_system_auditor_role()
|
||||||
role = Role.singleton('system_auditor')
|
assignment = RoleUserAssignment.objects.filter(user=user, role_definition=rd).first()
|
||||||
# must check if member to not duplicate activity stream
|
prior_value = bool(assignment)
|
||||||
if user not in role.members.all():
|
if prior_value != bool(tf):
|
||||||
role.members.add(user)
|
if assignment:
|
||||||
user._is_system_auditor = True
|
assignment.delete()
|
||||||
else:
|
else:
|
||||||
role = Role.singleton('system_auditor')
|
rd.give_global_permission(user)
|
||||||
if user in role.members.all():
|
user._is_system_auditor = bool(tf)
|
||||||
role.members.remove(user)
|
entry = ActivityStream.objects.create(changes=json.dumps({"is_system_auditor": [prior_value, bool(tf)]}), object1='user', operation='update')
|
||||||
user._is_system_auditor = False
|
entry.user.add(user)
|
||||||
|
|
||||||
|
|
||||||
User.add_to_class('is_system_auditor', user_is_system_auditor)
|
User.add_to_class('is_system_auditor', user_is_system_auditor)
|
||||||
@@ -287,6 +306,10 @@ activity_stream_registrar.connect(WorkflowApprovalTemplate)
|
|||||||
activity_stream_registrar.connect(OAuth2Application)
|
activity_stream_registrar.connect(OAuth2Application)
|
||||||
activity_stream_registrar.connect(OAuth2AccessToken)
|
activity_stream_registrar.connect(OAuth2AccessToken)
|
||||||
|
|
||||||
|
# Register models
|
||||||
|
permission_registry.register(Project, Team, WorkflowJobTemplate, JobTemplate, Inventory, Organization, Credential, NotificationTemplate, ExecutionEnvironment)
|
||||||
|
permission_registry.register(InstanceGroup, parent_field_name=None) # Not part of an organization
|
||||||
|
|
||||||
# prevent API filtering on certain Django-supplied sensitive fields
|
# prevent API filtering on certain Django-supplied sensitive fields
|
||||||
prevent_search(User._meta.get_field('password'))
|
prevent_search(User._meta.get_field('password'))
|
||||||
prevent_search(OAuth2AccessToken._meta.get_field('token'))
|
prevent_search(OAuth2AccessToken._meta.get_field('token'))
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
# django-ansible-base
|
||||||
|
from ansible_base.lib.utils.models import get_type_for_model
|
||||||
|
|
||||||
# Django-CRUM
|
# Django-CRUM
|
||||||
from crum import get_current_user
|
from crum import get_current_user
|
||||||
|
|
||||||
@@ -139,6 +142,23 @@ class BaseModel(models.Model):
|
|||||||
self.save(update_fields=update_fields)
|
self.save(update_fields=update_fields)
|
||||||
return update_fields
|
return update_fields
|
||||||
|
|
||||||
|
def summary_fields(self):
|
||||||
|
"""
|
||||||
|
This exists for use by django-ansible-base,
|
||||||
|
which has standard patterns that differ from AWX, but we enable views from DAB
|
||||||
|
for those views to list summary_fields for AWX models, those models need to provide this
|
||||||
|
"""
|
||||||
|
from awx.api.serializers import SUMMARIZABLE_FK_FIELDS
|
||||||
|
|
||||||
|
model_name = get_type_for_model(self)
|
||||||
|
related_fields = SUMMARIZABLE_FK_FIELDS.get(model_name, {})
|
||||||
|
summary_data = {}
|
||||||
|
for field_name in related_fields:
|
||||||
|
fval = getattr(self, field_name, None)
|
||||||
|
if fval is not None:
|
||||||
|
summary_data[field_name] = fval
|
||||||
|
return summary_data
|
||||||
|
|
||||||
|
|
||||||
class CreatedModifiedModel(BaseModel):
|
class CreatedModifiedModel(BaseModel):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
|||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
unique_together = ('organization', 'name', 'credential_type')
|
unique_together = ('organization', 'name', 'credential_type')
|
||||||
|
permissions = [('use_credential', 'Can use credential in a job or related resource')]
|
||||||
|
|
||||||
PASSWORD_FIELDS = ['inputs']
|
PASSWORD_FIELDS = ['inputs']
|
||||||
FIELDS_TO_PRESERVE_AT_COPY = ['input_sources']
|
FIELDS_TO_PRESERVE_AT_COPY = ['input_sources']
|
||||||
@@ -1231,6 +1232,14 @@ ManagedCredentialType(
|
|||||||
'multiline': True,
|
'multiline': True,
|
||||||
'help_text': gettext_noop('Terraform backend config as Hashicorp configuration language.'),
|
'help_text': gettext_noop('Terraform backend config as Hashicorp configuration language.'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'id': 'gce_credentials',
|
||||||
|
'label': gettext_noop('Google Cloud Platform account credentials'),
|
||||||
|
'type': 'string',
|
||||||
|
'secret': True,
|
||||||
|
'multiline': True,
|
||||||
|
'help_text': gettext_noop('Google Cloud Platform account credentials in JSON format.'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
'required': ['configuration'],
|
'required': ['configuration'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -130,3 +130,10 @@ def terraform(cred, env, private_data_dir):
|
|||||||
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||||
f.write(cred.get_input('configuration'))
|
f.write(cred.get_input('configuration'))
|
||||||
env['TF_BACKEND_CONFIG_FILE'] = to_container_path(path, private_data_dir)
|
env['TF_BACKEND_CONFIG_FILE'] = to_container_path(path, private_data_dir)
|
||||||
|
# Handle env variables for GCP account credentials
|
||||||
|
if 'gce_credentials' in cred.inputs:
|
||||||
|
handle, path = tempfile.mkstemp(dir=os.path.join(private_data_dir, 'env'))
|
||||||
|
with os.fdopen(handle, 'w') as f:
|
||||||
|
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||||
|
f.write(cred.get_input('gce_credentials'))
|
||||||
|
env['GOOGLE_BACKEND_CREDENTIALS'] = to_container_path(path, private_data_dir)
|
||||||
|
|||||||
@@ -485,6 +485,9 @@ class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin, ResourceMi
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
|
permissions = [('use_instancegroup', 'Can use instance group in a preference list of a resource')]
|
||||||
|
# Since this has no direct organization field only superuser can add, so remove add permission
|
||||||
|
default_permissions = ('change', 'delete', 'view')
|
||||||
|
|
||||||
def set_default_policy_fields(self):
|
def set_default_policy_fields(self):
|
||||||
self.policy_instance_list = []
|
self.policy_instance_list = []
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import os.path
|
|||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
import tempfile
|
||||||
|
import stat
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -89,6 +91,11 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin):
|
|||||||
verbose_name_plural = _('inventories')
|
verbose_name_plural = _('inventories')
|
||||||
unique_together = [('name', 'organization')]
|
unique_together = [('name', 'organization')]
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
|
permissions = [
|
||||||
|
('use_inventory', 'Can use inventory in a job template'),
|
||||||
|
('adhoc_inventory', 'Can run ad hoc commands'),
|
||||||
|
('update_inventory', 'Can update inventory sources in inventory'),
|
||||||
|
]
|
||||||
|
|
||||||
organization = models.ForeignKey(
|
organization = models.ForeignKey(
|
||||||
'Organization',
|
'Organization',
|
||||||
@@ -1400,7 +1407,7 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin,
|
|||||||
return selected_groups
|
return selected_groups
|
||||||
|
|
||||||
|
|
||||||
class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin):
|
class CustomInventoryScript(CommonModelNameNotUnique):
|
||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
@@ -1633,17 +1640,39 @@ class satellite6(PluginFileInjector):
|
|||||||
|
|
||||||
class terraform(PluginFileInjector):
|
class terraform(PluginFileInjector):
|
||||||
plugin_name = 'terraform_state'
|
plugin_name = 'terraform_state'
|
||||||
base_injector = 'managed'
|
|
||||||
namespace = 'cloud'
|
namespace = 'cloud'
|
||||||
collection = 'terraform'
|
collection = 'terraform'
|
||||||
use_fqcn = True
|
use_fqcn = True
|
||||||
|
|
||||||
def inventory_as_dict(self, inventory_update, private_data_dir):
|
def inventory_as_dict(self, inventory_update, private_data_dir):
|
||||||
env = super(terraform, self).get_plugin_env(inventory_update, private_data_dir, None)
|
|
||||||
ret = super().inventory_as_dict(inventory_update, private_data_dir)
|
ret = super().inventory_as_dict(inventory_update, private_data_dir)
|
||||||
ret['backend_config_files'] = env["TF_BACKEND_CONFIG_FILE"]
|
credential = inventory_update.get_cloud_credential()
|
||||||
|
config_cred = credential.get_input('configuration')
|
||||||
|
if config_cred:
|
||||||
|
handle, path = tempfile.mkstemp(dir=os.path.join(private_data_dir, 'env'))
|
||||||
|
with os.fdopen(handle, 'w') as f:
|
||||||
|
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||||
|
f.write(config_cred)
|
||||||
|
ret['backend_config_files'] = to_container_path(path, private_data_dir)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def build_plugin_private_data(self, inventory_update, private_data_dir):
|
||||||
|
credential = inventory_update.get_cloud_credential()
|
||||||
|
|
||||||
|
private_data = {'credentials': {}}
|
||||||
|
gce_cred = credential.get_input('gce_credentials')
|
||||||
|
if gce_cred:
|
||||||
|
private_data['credentials'][credential] = gce_cred
|
||||||
|
return private_data
|
||||||
|
|
||||||
|
def get_plugin_env(self, inventory_update, private_data_dir, private_data_files):
|
||||||
|
env = super(terraform, self).get_plugin_env(inventory_update, private_data_dir, private_data_files)
|
||||||
|
credential = inventory_update.get_cloud_credential()
|
||||||
|
cred_data = private_data_files['credentials']
|
||||||
|
if cred_data[credential]:
|
||||||
|
env['GOOGLE_BACKEND_CREDENTIALS'] = to_container_path(cred_data[credential], private_data_dir)
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
class controller(PluginFileInjector):
|
class controller(PluginFileInjector):
|
||||||
plugin_name = 'tower' # TODO: relying on routing for now, update after EEs pick up revised collection
|
plugin_name = 'tower' # TODO: relying on routing for now, update after EEs pick up revised collection
|
||||||
|
|||||||
@@ -205,6 +205,9 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
|||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
|
permissions = [('execute_jobtemplate', 'Can run this job template')]
|
||||||
|
# Remove add permission, ability to add comes from use permission for inventory, project, credentials
|
||||||
|
default_permissions = ('change', 'delete', 'view')
|
||||||
|
|
||||||
job_type = models.CharField(
|
job_type = models.CharField(
|
||||||
max_length=64,
|
max_length=64,
|
||||||
|
|||||||
@@ -19,13 +19,14 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from ansible_base.lib.utils.models import prevent_search
|
from ansible_base.lib.utils.models import prevent_search
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.models.rbac import Role, RoleAncestorEntry
|
|
||||||
|
from awx.main.models.rbac import Role, RoleAncestorEntry, to_permissions
|
||||||
from awx.main.utils import parse_yaml_or_json, get_custom_venv_choices, get_licenser, polymorphic
|
from awx.main.utils import parse_yaml_or_json, get_custom_venv_choices, get_licenser, polymorphic
|
||||||
from awx.main.utils.execution_environments import get_default_execution_environment
|
from awx.main.utils.execution_environments import get_default_execution_environment
|
||||||
from awx.main.utils.encryption import decrypt_value, get_encryption_key, is_encrypted
|
from awx.main.utils.encryption import decrypt_value, get_encryption_key, is_encrypted
|
||||||
from awx.main.utils.polymorphic import build_polymorphic_ctypes_map
|
from awx.main.utils.polymorphic import build_polymorphic_ctypes_map
|
||||||
from awx.main.fields import AskForField
|
from awx.main.fields import AskForField
|
||||||
from awx.main.constants import ACTIVE_STATES
|
from awx.main.constants import ACTIVE_STATES, org_role_to_permission
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('awx.main.models.mixins')
|
logger = logging.getLogger('awx.main.models.mixins')
|
||||||
@@ -64,6 +65,18 @@ class ResourceMixin(models.Model):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _accessible_pk_qs(cls, accessor, role_field, content_types=None):
|
def _accessible_pk_qs(cls, accessor, role_field, content_types=None):
|
||||||
|
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||||
|
if cls._meta.model_name == 'organization' and role_field in org_role_to_permission:
|
||||||
|
# Organization roles can not use the DAB RBAC shortcuts
|
||||||
|
# like Organization.access_qs(user, 'change_jobtemplate') is needed
|
||||||
|
# not just Organization.access_qs(user, 'change') is needed
|
||||||
|
if accessor.is_superuser:
|
||||||
|
return cls.objects.values_list('id')
|
||||||
|
|
||||||
|
codename = org_role_to_permission[role_field]
|
||||||
|
|
||||||
|
return cls.access_ids_qs(accessor, codename, content_types=content_types)
|
||||||
|
return cls.access_ids_qs(accessor, to_permissions[role_field], content_types=content_types)
|
||||||
if accessor._meta.model_name == 'user':
|
if accessor._meta.model_name == 'user':
|
||||||
ancestor_roles = accessor.roles.all()
|
ancestor_roles = accessor.roles.all()
|
||||||
elif type(accessor) == Role:
|
elif type(accessor) == Role:
|
||||||
|
|||||||
@@ -498,7 +498,7 @@ class JobNotificationMixin(object):
|
|||||||
# Body should have at least 2 CRLF, some clients will interpret
|
# Body should have at least 2 CRLF, some clients will interpret
|
||||||
# the email incorrectly with blank body. So we will check that
|
# the email incorrectly with blank body. So we will check that
|
||||||
|
|
||||||
if len(body.strip().splitlines()) <= 2:
|
if len(body.strip().splitlines()) < 1:
|
||||||
# blank body
|
# blank body
|
||||||
body = '\r\n'.join(
|
body = '\r\n'.join(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from django.contrib.sessions.models import Session
|
|||||||
from django.utils.timezone import now as tz_now
|
from django.utils.timezone import now as tz_now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
# django-ansible-base
|
||||||
|
from ansible_base.resource_registry.fields import AnsibleResourceField
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
@@ -33,6 +35,12 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi
|
|||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
|
permissions = [
|
||||||
|
('member_organization', 'Basic participation permissions for organization'),
|
||||||
|
('audit_organization', 'Audit everything inside the organization'),
|
||||||
|
]
|
||||||
|
# Remove add permission, only superuser can add
|
||||||
|
default_permissions = ('change', 'delete', 'view')
|
||||||
|
|
||||||
instance_groups = OrderedManyToManyField('InstanceGroup', blank=True, through='OrganizationInstanceGroupMembership')
|
instance_groups = OrderedManyToManyField('InstanceGroup', blank=True, through='OrganizationInstanceGroupMembership')
|
||||||
galaxy_credentials = OrderedManyToManyField(
|
galaxy_credentials = OrderedManyToManyField(
|
||||||
@@ -103,6 +111,7 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi
|
|||||||
approval_role = ImplicitRoleField(
|
approval_role = ImplicitRoleField(
|
||||||
parent_role='admin_role',
|
parent_role='admin_role',
|
||||||
)
|
)
|
||||||
|
resource = AnsibleResourceField(primary_key_field="id")
|
||||||
|
|
||||||
def get_absolute_url(self, request=None):
|
def get_absolute_url(self, request=None):
|
||||||
return reverse('api:organization_detail', kwargs={'pk': self.pk}, request=request)
|
return reverse('api:organization_detail', kwargs={'pk': self.pk}, request=request)
|
||||||
@@ -134,6 +143,7 @@ class Team(CommonModelNameNotUnique, ResourceMixin):
|
|||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
unique_together = [('organization', 'name')]
|
unique_together = [('organization', 'name')]
|
||||||
ordering = ('organization__name', 'name')
|
ordering = ('organization__name', 'name')
|
||||||
|
permissions = [('member_team', 'Inherit all roles assigned to this team')]
|
||||||
|
|
||||||
organization = models.ForeignKey(
|
organization = models.ForeignKey(
|
||||||
'Organization',
|
'Organization',
|
||||||
@@ -151,6 +161,7 @@ class Team(CommonModelNameNotUnique, ResourceMixin):
|
|||||||
read_role = ImplicitRoleField(
|
read_role = ImplicitRoleField(
|
||||||
parent_role=['organization.auditor_role', 'member_role'],
|
parent_role=['organization.auditor_role', 'member_role'],
|
||||||
)
|
)
|
||||||
|
resource = AnsibleResourceField(primary_key_field="id")
|
||||||
|
|
||||||
def get_absolute_url(self, request=None):
|
def get_absolute_url(self, request=None):
|
||||||
return reverse('api:team_detail', kwargs={'pk': self.pk}, request=request)
|
return reverse('api:team_detail', kwargs={'pk': self.pk}, request=request)
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
|
|||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
|
permissions = [('update_project', 'Can run a project update'), ('use_project', 'Can use project in a job template')]
|
||||||
|
|
||||||
default_environment = models.ForeignKey(
|
default_environment = models.ForeignKey(
|
||||||
'ExecutionEnvironment',
|
'ExecutionEnvironment',
|
||||||
|
|||||||
@@ -7,14 +7,27 @@ import threading
|
|||||||
import contextlib
|
import contextlib
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
# django-rest-framework
|
||||||
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.db import models, transaction, connection
|
from django.db import models, transaction, connection
|
||||||
|
from django.db.models.signals import m2m_changed
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.apps import apps
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
# Ansible_base app
|
||||||
|
from ansible_base.rbac.models import RoleDefinition
|
||||||
|
from ansible_base.lib.utils.models import get_type_for_model
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
|
from awx.main.migrations._dab_rbac import build_role_map, get_permissions_for_role
|
||||||
|
from awx.main.constants import role_name_to_perm_mapping, org_role_to_permission
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Role',
|
'Role',
|
||||||
@@ -75,6 +88,11 @@ role_descriptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
to_permissions = {}
|
||||||
|
for k, v in role_name_to_perm_mapping.items():
|
||||||
|
to_permissions[k] = v[0].strip('_')
|
||||||
|
|
||||||
|
|
||||||
tls = threading.local() # thread local storage
|
tls = threading.local() # thread local storage
|
||||||
|
|
||||||
|
|
||||||
@@ -86,10 +104,8 @@ def check_singleton(func):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
sys_admin = Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR)
|
|
||||||
sys_audit = Role.singleton(ROLE_SINGLETON_SYSTEM_AUDITOR)
|
|
||||||
user = args[0]
|
user = args[0]
|
||||||
if user in sys_admin or user in sys_audit:
|
if user.is_superuser or user.is_system_auditor:
|
||||||
if len(args) == 2:
|
if len(args) == 2:
|
||||||
return args[1]
|
return args[1]
|
||||||
return Role.objects.all()
|
return Role.objects.all()
|
||||||
@@ -169,6 +185,24 @@ class Role(models.Model):
|
|||||||
|
|
||||||
def __contains__(self, accessor):
|
def __contains__(self, accessor):
|
||||||
if accessor._meta.model_name == 'user':
|
if accessor._meta.model_name == 'user':
|
||||||
|
if accessor.is_superuser:
|
||||||
|
return True
|
||||||
|
if self.role_field == 'system_administrator':
|
||||||
|
return accessor.is_superuser
|
||||||
|
elif self.role_field == 'system_auditor':
|
||||||
|
return accessor.is_system_auditor
|
||||||
|
elif self.role_field in ('read_role', 'auditor_role') and accessor.is_system_auditor:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||||
|
if self.content_object and self.content_object._meta.model_name == 'organization' and self.role_field in org_role_to_permission:
|
||||||
|
codename = org_role_to_permission[self.role_field]
|
||||||
|
|
||||||
|
return accessor.has_obj_perm(self.content_object, codename)
|
||||||
|
|
||||||
|
if self.role_field not in to_permissions:
|
||||||
|
raise Exception(f'{self.role_field} evaluated but not a translatable permission')
|
||||||
|
return accessor.has_obj_perm(self.content_object, to_permissions[self.role_field])
|
||||||
return self.ancestors.filter(members=accessor).exists()
|
return self.ancestors.filter(members=accessor).exists()
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f'Role evaluations only valid for users, received {accessor}')
|
raise RuntimeError(f'Role evaluations only valid for users, received {accessor}')
|
||||||
@@ -280,6 +314,9 @@ class Role(models.Model):
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
|
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||||
|
return
|
||||||
|
|
||||||
if len(additions) == 0 and len(removals) == 0:
|
if len(additions) == 0 and len(removals) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -412,6 +449,12 @@ class Role(models.Model):
|
|||||||
in their organization, but some of those roles descend from
|
in their organization, but some of those roles descend from
|
||||||
organization admin_role, but not auditor_role.
|
organization admin_role, but not auditor_role.
|
||||||
"""
|
"""
|
||||||
|
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||||
|
from ansible_base.rbac.models import RoleEvaluation
|
||||||
|
|
||||||
|
q = RoleEvaluation.objects.filter(role__in=user.has_roles.all()).values_list('object_id', 'content_type_id').query
|
||||||
|
return roles_qs.extra(where=[f'(object_id,content_type_id) in ({q})'])
|
||||||
|
|
||||||
return roles_qs.filter(
|
return roles_qs.filter(
|
||||||
id__in=RoleAncestorEntry.objects.filter(
|
id__in=RoleAncestorEntry.objects.filter(
|
||||||
descendent__in=RoleAncestorEntry.objects.filter(ancestor_id__in=list(user.roles.values_list('id', flat=True))).values_list(
|
descendent__in=RoleAncestorEntry.objects.filter(ancestor_id__in=list(user.roles.values_list('id', flat=True))).values_list(
|
||||||
@@ -434,6 +477,13 @@ class Role(models.Model):
|
|||||||
return self.singleton_name in [ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR]
|
return self.singleton_name in [ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR]
|
||||||
|
|
||||||
|
|
||||||
|
class AncestorManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||||
|
raise RuntimeError('The old RBAC system has been disabled, this should never be called')
|
||||||
|
return super(AncestorManager, self).get_queryset()
|
||||||
|
|
||||||
|
|
||||||
class RoleAncestorEntry(models.Model):
|
class RoleAncestorEntry(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
@@ -451,6 +501,8 @@ class RoleAncestorEntry(models.Model):
|
|||||||
content_type_id = models.PositiveIntegerField(null=False)
|
content_type_id = models.PositiveIntegerField(null=False)
|
||||||
object_id = models.PositiveIntegerField(null=False)
|
object_id = models.PositiveIntegerField(null=False)
|
||||||
|
|
||||||
|
objects = AncestorManager()
|
||||||
|
|
||||||
|
|
||||||
def role_summary_fields_generator(content_object, role_field):
|
def role_summary_fields_generator(content_object, role_field):
|
||||||
global role_descriptions
|
global role_descriptions
|
||||||
@@ -479,3 +531,168 @@ def role_summary_fields_generator(content_object, role_field):
|
|||||||
summary['name'] = role_names[role_field]
|
summary['name'] = role_names[role_field]
|
||||||
summary['id'] = getattr(content_object, '{}_id'.format(role_field))
|
summary['id'] = getattr(content_object, '{}_id'.format(role_field))
|
||||||
return summary
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------- Custom Role Compatibility -------------------------
|
||||||
|
# The following are methods to connect this (old) RBAC system to the new
|
||||||
|
# system which allows custom roles
|
||||||
|
# this follows the ORM interface layer documented in docs/rbac.md
|
||||||
|
def get_role_codenames(role):
|
||||||
|
obj = role.content_object
|
||||||
|
if obj is None:
|
||||||
|
return
|
||||||
|
f = obj._meta.get_field(role.role_field)
|
||||||
|
parents, children = build_role_map(apps)
|
||||||
|
return [perm.codename for perm in get_permissions_for_role(f, children, apps)]
|
||||||
|
|
||||||
|
|
||||||
|
def get_role_definition(role):
|
||||||
|
"""Given a old-style role, this gives a role definition in the new RBAC system for it"""
|
||||||
|
obj = role.content_object
|
||||||
|
if obj is None:
|
||||||
|
return
|
||||||
|
f = obj._meta.get_field(role.role_field)
|
||||||
|
action_name = f.name.rsplit("_", 1)[0]
|
||||||
|
rd_name = f'{type(obj).__name__} {action_name.title()} Compat'
|
||||||
|
perm_list = get_role_codenames(role)
|
||||||
|
defaults = {'content_type_id': role.content_type_id}
|
||||||
|
try:
|
||||||
|
rd, created = RoleDefinition.objects.get_or_create(name=rd_name, permissions=perm_list, defaults=defaults)
|
||||||
|
except ValidationError:
|
||||||
|
# This is a tricky case - practically speaking, users should not be allowed to create team roles
|
||||||
|
# or roles that include the team member permission.
|
||||||
|
# If we need to create this for compatibility purposes then we will create it as a managed non-editable role
|
||||||
|
defaults['managed'] = True
|
||||||
|
rd, created = RoleDefinition.objects.get_or_create(name=rd_name, permissions=perm_list, defaults=defaults)
|
||||||
|
return rd
|
||||||
|
|
||||||
|
|
||||||
|
def get_role_from_object_role(object_role):
|
||||||
|
"""
|
||||||
|
Given an object role from the new system, return the corresponding role from the old system
|
||||||
|
reverses naming from get_role_definition, and the ANSIBLE_BASE_ROLE_PRECREATE setting.
|
||||||
|
"""
|
||||||
|
rd = object_role.role_definition
|
||||||
|
if rd.name.endswith(' Compat'):
|
||||||
|
model_name, role_name, _ = rd.name.split()
|
||||||
|
role_name = role_name.lower()
|
||||||
|
role_name += '_role'
|
||||||
|
elif rd.name.endswith(' Admin') and rd.name.count(' ') == 2:
|
||||||
|
# cases like "Organization Project Admin"
|
||||||
|
model_name, target_model_name, role_name = rd.name.split()
|
||||||
|
role_name = role_name.lower()
|
||||||
|
model_cls = apps.get_model('main', target_model_name)
|
||||||
|
target_model_name = get_type_for_model(model_cls)
|
||||||
|
if target_model_name == 'notification_template':
|
||||||
|
target_model_name = 'notification' # total exception
|
||||||
|
role_name = f'{target_model_name}_admin_role'
|
||||||
|
elif rd.name.endswith(' Admin'):
|
||||||
|
# cases like "project-admin"
|
||||||
|
role_name = 'admin_role'
|
||||||
|
else:
|
||||||
|
print(rd.name)
|
||||||
|
model_name, role_name = rd.name.split()
|
||||||
|
role_name = role_name.lower()
|
||||||
|
role_name += '_role'
|
||||||
|
return getattr(object_role.content_object, role_name)
|
||||||
|
|
||||||
|
|
||||||
|
def give_or_remove_permission(role, actor, giving=True):
|
||||||
|
obj = role.content_object
|
||||||
|
if obj is None:
|
||||||
|
return
|
||||||
|
rd = get_role_definition(role)
|
||||||
|
rd.give_or_remove_permission(actor, obj, giving=giving)
|
||||||
|
|
||||||
|
|
||||||
|
class SyncEnabled(threading.local):
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = True
|
||||||
|
|
||||||
|
|
||||||
|
rbac_sync_enabled = SyncEnabled()
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def disable_rbac_sync():
|
||||||
|
try:
|
||||||
|
previous_value = rbac_sync_enabled.enabled
|
||||||
|
rbac_sync_enabled.enabled = False
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
rbac_sync_enabled.enabled = previous_value
|
||||||
|
|
||||||
|
|
||||||
|
def give_creator_permissions(user, obj):
|
||||||
|
assignment = RoleDefinition.objects.give_creator_permissions(user, obj)
|
||||||
|
if assignment:
|
||||||
|
with disable_rbac_sync():
|
||||||
|
old_role = get_role_from_object_role(assignment.object_role)
|
||||||
|
old_role.members.add(user)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_members_to_new_rbac(instance, action, model, pk_set, reverse, **kwargs):
|
||||||
|
if action.startswith('pre_'):
|
||||||
|
return
|
||||||
|
if not rbac_sync_enabled.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
if action == 'post_add':
|
||||||
|
is_giving = True
|
||||||
|
elif action == 'post_remove':
|
||||||
|
is_giving = False
|
||||||
|
elif action == 'post_clear':
|
||||||
|
raise RuntimeError('Clearing of role members not supported')
|
||||||
|
|
||||||
|
if reverse:
|
||||||
|
user = instance
|
||||||
|
else:
|
||||||
|
role = instance
|
||||||
|
|
||||||
|
for user_or_role_id in pk_set:
|
||||||
|
if reverse:
|
||||||
|
role = Role.objects.get(pk=user_or_role_id)
|
||||||
|
else:
|
||||||
|
user = get_user_model().objects.get(pk=user_or_role_id)
|
||||||
|
give_or_remove_permission(role, user, giving=is_giving)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_parents_to_new_rbac(instance, action, model, pk_set, reverse, **kwargs):
|
||||||
|
if action.startswith('pre_'):
|
||||||
|
return
|
||||||
|
|
||||||
|
if action == 'post_add':
|
||||||
|
is_giving = True
|
||||||
|
elif action == 'post_remove':
|
||||||
|
is_giving = False
|
||||||
|
elif action == 'post_clear':
|
||||||
|
raise RuntimeError('Clearing of role members not supported')
|
||||||
|
|
||||||
|
if reverse:
|
||||||
|
parent_role = instance
|
||||||
|
else:
|
||||||
|
child_role = instance
|
||||||
|
|
||||||
|
for role_id in pk_set:
|
||||||
|
if reverse:
|
||||||
|
child_role = Role.objects.get(id=role_id)
|
||||||
|
else:
|
||||||
|
parent_role = Role.objects.get(id=role_id)
|
||||||
|
|
||||||
|
# To a fault, we want to avoid running this if triggered from implicit_parents management
|
||||||
|
# we only want to do anything if we know for sure this is a non-implicit team role
|
||||||
|
if parent_role.role_field == 'member_role' and parent_role.content_type.model == 'team':
|
||||||
|
# Team internal parents are member_role->read_role and admin_role->member_role
|
||||||
|
# for the same object, this parenting will also be implicit_parents management
|
||||||
|
# do nothing for internal parents, but OTHER teams may still be assigned permissions to a team
|
||||||
|
if (child_role.content_type_id == parent_role.content_type_id) and (child_role.object_id == parent_role.object_id):
|
||||||
|
return
|
||||||
|
|
||||||
|
from awx.main.models.organization import Team
|
||||||
|
|
||||||
|
team = Team.objects.get(pk=parent_role.object_id)
|
||||||
|
give_or_remove_permission(child_role, team, giving=is_giving)
|
||||||
|
|
||||||
|
|
||||||
|
m2m_changed.connect(sync_members_to_new_rbac, Role.members.through)
|
||||||
|
m2m_changed.connect(sync_parents_to_new_rbac, Role.parents.through)
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel,
|
|||||||
from awx.main.dispatch import get_task_queuename
|
from awx.main.dispatch import get_task_queuename
|
||||||
from awx.main.dispatch.control import Control as ControlDispatcher
|
from awx.main.dispatch.control import Control as ControlDispatcher
|
||||||
from awx.main.registrar import activity_stream_registrar
|
from awx.main.registrar import activity_stream_registrar
|
||||||
from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin
|
from awx.main.models.mixins import TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin
|
||||||
|
from awx.main.models.rbac import to_permissions
|
||||||
from awx.main.utils.common import (
|
from awx.main.utils.common import (
|
||||||
camelcase_to_underscore,
|
camelcase_to_underscore,
|
||||||
get_model_for_type,
|
get_model_for_type,
|
||||||
@@ -210,7 +211,15 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn
|
|||||||
# do not use this if in a subclass
|
# do not use this if in a subclass
|
||||||
if cls != UnifiedJobTemplate:
|
if cls != UnifiedJobTemplate:
|
||||||
return super(UnifiedJobTemplate, cls).accessible_pk_qs(accessor, role_field)
|
return super(UnifiedJobTemplate, cls).accessible_pk_qs(accessor, role_field)
|
||||||
return ResourceMixin._accessible_pk_qs(cls, accessor, role_field, content_types=cls._submodels_with_roles())
|
from ansible_base.rbac.models import RoleEvaluation
|
||||||
|
|
||||||
|
action = to_permissions[role_field]
|
||||||
|
|
||||||
|
return (
|
||||||
|
RoleEvaluation.objects.filter(role__in=accessor.has_roles.all(), codename__startswith=action, content_type_id__in=cls._submodels_with_roles())
|
||||||
|
.values_list('object_id')
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
def _perform_unique_checks(self, unique_checks):
|
def _perform_unique_checks(self, unique_checks):
|
||||||
# Handle the list of unique fields returned above. Replace with an
|
# Handle the list of unique fields returned above. Replace with an
|
||||||
@@ -1599,7 +1608,8 @@ class UnifiedJob(
|
|||||||
extra["controller_node"] = self.controller_node or "NOT_SET"
|
extra["controller_node"] = self.controller_node or "NOT_SET"
|
||||||
elif state == "execution_node_chosen":
|
elif state == "execution_node_chosen":
|
||||||
extra["execution_node"] = self.execution_node or "NOT_SET"
|
extra["execution_node"] = self.execution_node or "NOT_SET"
|
||||||
logger_job_lifecycle.info(msg, extra=extra)
|
|
||||||
|
logger_job_lifecycle.info(f"{msg} {json.dumps(extra)}")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def launched_by(self):
|
def launched_by(self):
|
||||||
|
|||||||
@@ -467,6 +467,10 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
|
permissions = [
|
||||||
|
('execute_workflowjobtemplate', 'Can run this workflow job template'),
|
||||||
|
('approve_workflowjobtemplate', 'Can approve steps in this workflow job template'),
|
||||||
|
]
|
||||||
|
|
||||||
notification_templates_approvals = models.ManyToManyField(
|
notification_templates_approvals = models.ManyToManyField(
|
||||||
"NotificationTemplate",
|
"NotificationTemplate",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ def rebuild_role_ancestor_list(reverse, model, instance, pk_set, action, **kwarg
|
|||||||
|
|
||||||
def sync_superuser_status_to_rbac(instance, **kwargs):
|
def sync_superuser_status_to_rbac(instance, **kwargs):
|
||||||
'When the is_superuser flag is changed on a user, reflect that in the membership of the System Admnistrator role'
|
'When the is_superuser flag is changed on a user, reflect that in the membership of the System Admnistrator role'
|
||||||
|
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||||
|
return
|
||||||
update_fields = kwargs.get('update_fields', None)
|
update_fields = kwargs.get('update_fields', None)
|
||||||
if update_fields and 'is_superuser' not in update_fields:
|
if update_fields and 'is_superuser' not in update_fields:
|
||||||
return
|
return
|
||||||
@@ -137,6 +139,8 @@ def sync_superuser_status_to_rbac(instance, **kwargs):
|
|||||||
|
|
||||||
def sync_rbac_to_superuser_status(instance, sender, **kwargs):
|
def sync_rbac_to_superuser_status(instance, sender, **kwargs):
|
||||||
'When the is_superuser flag is false but a user has the System Admin role, update the database to reflect that'
|
'When the is_superuser flag is false but a user has the System Admin role, update the database to reflect that'
|
||||||
|
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||||
|
return
|
||||||
if kwargs['action'] in ['post_add', 'post_remove', 'post_clear']:
|
if kwargs['action'] in ['post_add', 'post_remove', 'post_clear']:
|
||||||
new_status_value = bool(kwargs['action'] == 'post_add')
|
new_status_value = bool(kwargs['action'] == 'post_add')
|
||||||
if hasattr(instance, 'singleton_name'): # duck typing, role.members.add() vs user.roles.add()
|
if hasattr(instance, 'singleton_name'): # duck typing, role.members.add() vs user.roles.add()
|
||||||
|
|||||||
@@ -49,6 +49,70 @@ class ReceptorConnectionType(Enum):
|
|||||||
STREAMTLS = 2
|
STREAMTLS = 2
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Translate receptorctl messages that come in over stdout into
|
||||||
|
structured messages. Currently, these are error messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ReceptorErrorBase:
|
||||||
|
_MESSAGE = 'Receptor Error'
|
||||||
|
|
||||||
|
def __init__(self, node: str = 'N/A', state_name: str = 'N/A'):
|
||||||
|
self.node = node
|
||||||
|
self.state_name = state_name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.__class__.__name__} '{self._MESSAGE}' on node '{self.node}' with state '{self.state_name}'"
|
||||||
|
|
||||||
|
|
||||||
|
class WorkUnitError(ReceptorErrorBase):
|
||||||
|
_MESSAGE = 'unknown work unit '
|
||||||
|
|
||||||
|
def __init__(self, work_unit_id: str, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.work_unit_id = work_unit_id
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{super().__str__()} work unit id '{self.work_unit_id}'"
|
||||||
|
|
||||||
|
|
||||||
|
class WorkUnitCancelError(WorkUnitError):
|
||||||
|
_MESSAGE = 'error cancelling remote unit: unknown work unit '
|
||||||
|
|
||||||
|
|
||||||
|
class WorkUnitResultsError(WorkUnitError):
|
||||||
|
_MESSAGE = 'Failed to get results: unknown work unit '
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownError(ReceptorErrorBase):
|
||||||
|
_MESSAGE = 'Unknown receptor ctl error'
|
||||||
|
|
||||||
|
def __init__(self, msg, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._MESSAGE = msg
|
||||||
|
|
||||||
|
|
||||||
|
class FuzzyError:
|
||||||
|
def __new__(self, e: RuntimeError, node: str, state_name: str):
|
||||||
|
"""
|
||||||
|
At the time of writing this comment all of the sub-classes detection
|
||||||
|
is centralized in this parent class. It's like a Router().
|
||||||
|
Someone may find it better to push down the error detection logic into
|
||||||
|
each sub-class.
|
||||||
|
"""
|
||||||
|
msg = e.args[0]
|
||||||
|
|
||||||
|
common_startswith = (WorkUnitCancelError, WorkUnitResultsError, WorkUnitError)
|
||||||
|
|
||||||
|
for klass in common_startswith:
|
||||||
|
if msg.startswith(klass._MESSAGE):
|
||||||
|
work_unit_id = msg[len(klass._MESSAGE) :]
|
||||||
|
return klass(work_unit_id, node=node, state_name=state_name)
|
||||||
|
|
||||||
|
return UnknownError(msg, node=node, state_name=state_name)
|
||||||
|
|
||||||
|
|
||||||
def read_receptor_config():
|
def read_receptor_config():
|
||||||
# for K8S deployments, getting a lock is necessary as another process
|
# for K8S deployments, getting a lock is necessary as another process
|
||||||
# may be re-writing the config at this time
|
# may be re-writing the config at this time
|
||||||
@@ -185,6 +249,7 @@ def run_until_complete(node, timing_data=None, **kwargs):
|
|||||||
timing_data['transmit_timing'] = run_start - transmit_start
|
timing_data['transmit_timing'] = run_start - transmit_start
|
||||||
run_timing = 0.0
|
run_timing = 0.0
|
||||||
stdout = ''
|
stdout = ''
|
||||||
|
state_name = 'local var never set'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resultfile = receptor_ctl.get_work_results(unit_id)
|
resultfile = receptor_ctl.get_work_results(unit_id)
|
||||||
@@ -205,13 +270,33 @@ def run_until_complete(node, timing_data=None, **kwargs):
|
|||||||
stdout = resultfile.read()
|
stdout = resultfile.read()
|
||||||
stdout = str(stdout, encoding='utf-8')
|
stdout = str(stdout, encoding='utf-8')
|
||||||
|
|
||||||
|
except RuntimeError as e:
|
||||||
|
receptor_e = FuzzyError(e, node, state_name)
|
||||||
|
if type(receptor_e) in (
|
||||||
|
WorkUnitError,
|
||||||
|
WorkUnitResultsError,
|
||||||
|
):
|
||||||
|
logger.warning(f'While consuming job results: {receptor_e}')
|
||||||
|
else:
|
||||||
|
raise
|
||||||
finally:
|
finally:
|
||||||
if settings.RECEPTOR_RELEASE_WORK:
|
if settings.RECEPTOR_RELEASE_WORK:
|
||||||
res = receptor_ctl.simple_command(f"work release {unit_id}")
|
try:
|
||||||
if res != {'released': unit_id}:
|
res = receptor_ctl.simple_command(f"work release {unit_id}")
|
||||||
logger.warning(f'Could not confirm release of receptor work unit id {unit_id} from {node}, data: {res}')
|
|
||||||
|
|
||||||
receptor_ctl.close()
|
if res != {'released': unit_id}:
|
||||||
|
logger.warning(f'Could not confirm release of receptor work unit id {unit_id} from {node}, data: {res}')
|
||||||
|
|
||||||
|
receptor_ctl.close()
|
||||||
|
except RuntimeError as e:
|
||||||
|
receptor_e = FuzzyError(e, node, state_name)
|
||||||
|
if type(receptor_e) in (
|
||||||
|
WorkUnitError,
|
||||||
|
WorkUnitCancelError,
|
||||||
|
):
|
||||||
|
logger.warning(f"While releasing work: {receptor_e}")
|
||||||
|
else:
|
||||||
|
logger.error(f"While releasing work: {receptor_e}")
|
||||||
|
|
||||||
if state_name.lower() == 'failed':
|
if state_name.lower() == 'failed':
|
||||||
work_detail = status.get('Detail', '')
|
work_detail = status.get('Detail', '')
|
||||||
@@ -275,7 +360,7 @@ def _convert_args_to_cli(vargs):
|
|||||||
args = ['cleanup']
|
args = ['cleanup']
|
||||||
for option in ('exclude_strings', 'remove_images'):
|
for option in ('exclude_strings', 'remove_images'):
|
||||||
if vargs.get(option):
|
if vargs.get(option):
|
||||||
args.append('--{}={}'.format(option.replace('_', '-'), ' '.join(vargs.get(option))))
|
args.append('--{}="{}"'.format(option.replace('_', '-'), ' '.join(vargs.get(option))))
|
||||||
for option in ('file_pattern', 'image_prune', 'process_isolation_executable', 'grace_period'):
|
for option in ('file_pattern', 'image_prune', 'process_isolation_executable', 'grace_period'):
|
||||||
if vargs.get(option) is True:
|
if vargs.get(option) is True:
|
||||||
args.append('--{}'.format(option.replace('_', '-')))
|
args.append('--{}'.format(option.replace('_', '-')))
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
tasks:
|
tasks:
|
||||||
- name: Hello Message
|
- name: Hello Message
|
||||||
debug:
|
ansible.builtin.debug:
|
||||||
msg: "Hello World!"
|
msg: "Hello World!"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"TF_BACKEND_CONFIG_FILE": "{{ file_reference }}"
|
"GOOGLE_BACKEND_CREDENTIALS": "{{ file_reference }}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class TestSwaggerGeneration:
|
|||||||
# The number of API endpoints changes over time, but let's just check
|
# The number of API endpoints changes over time, but let's just check
|
||||||
# for a reasonable number here; if this test starts failing, raise/lower the bounds
|
# for a reasonable number here; if this test starts failing, raise/lower the bounds
|
||||||
paths = JSON['paths']
|
paths = JSON['paths']
|
||||||
assert 250 < len(paths) < 375
|
assert 250 < len(paths) < 400
|
||||||
assert set(list(paths['/api/'].keys())) == set(['get', 'parameters'])
|
assert set(list(paths['/api/'].keys())) == set(['get', 'parameters'])
|
||||||
assert set(list(paths['/api/v2/'].keys())) == set(['get', 'parameters'])
|
assert set(list(paths['/api/v2/'].keys())) == set(['get', 'parameters'])
|
||||||
assert set(list(sorted(paths['/api/v2/credentials/'].keys()))) == set(['get', 'post', 'parameters'])
|
assert set(list(sorted(paths['/api/v2/credentials/'].keys()))) == set(['get', 'post', 'parameters'])
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from prometheus_client.parser import text_string_to_metric_families
|
|||||||
from awx.main import models
|
from awx.main import models
|
||||||
from awx.main.analytics.metrics import metrics
|
from awx.main.analytics.metrics import metrics
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
from awx.main.models.rbac import Role
|
|
||||||
|
|
||||||
EXPECTED_VALUES = {
|
EXPECTED_VALUES = {
|
||||||
'awx_system_info': 1.0,
|
'awx_system_info': 1.0,
|
||||||
@@ -66,7 +65,6 @@ def test_metrics_permissions(get, admin, org_admin, alice, bob, organization):
|
|||||||
organization.auditor_role.members.add(bob)
|
organization.auditor_role.members.add(bob)
|
||||||
assert get(get_metrics_view_db_only(), user=bob).status_code == 403
|
assert get(get_metrics_view_db_only(), user=bob).status_code == 403
|
||||||
|
|
||||||
Role.singleton('system_auditor').members.add(bob)
|
|
||||||
bob.is_system_auditor = True
|
bob.is_system_auditor = True
|
||||||
assert get(get_metrics_view_db_only(), user=bob).status_code == 200
|
assert get(get_metrics_view_db_only(), user=bob).status_code == 200
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from django.test import Client
|
|||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
|
|
||||||
from awx.api.generics import LoggedLoginView
|
from awx.api.generics import LoggedLoginView
|
||||||
from awx.api.versioning import drf_reverse
|
from rest_framework.reverse import reverse as drf_reverse
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -385,10 +385,9 @@ def test_list_created_org_credentials(post, get, organization, org_admin, org_me
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin, credentialtype_ssh, order_by):
|
def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin, credentialtype_ssh, order_by):
|
||||||
for i, password in enumerate(('abc', 'def', 'xyz')):
|
for i, password in enumerate(('abc', 'def', 'xyz')):
|
||||||
response = post(reverse('api:credential_list'), {'organization': organization.id, 'name': 'C%d' % i, 'password': password}, org_admin)
|
post(reverse('api:credential_list'), {'organization': organization.id, 'name': 'C%d' % i, 'password': password}, org_admin, expect=400)
|
||||||
|
|
||||||
response = get(reverse('api:credential_list'), org_admin, QUERY_STRING='order_by=%s' % order_by, status=400)
|
get(reverse('api:credential_list'), org_admin, QUERY_STRING='order_by=%s' % order_by, expect=400)
|
||||||
assert response.status_code == 400
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -399,8 +398,7 @@ def test_inputs_cannot_contain_extra_fields(get, post, organization, admin, cred
|
|||||||
'credential_type': credentialtype_ssh.pk,
|
'credential_type': credentialtype_ssh.pk,
|
||||||
'inputs': {'invalid_field': 'foo'},
|
'inputs': {'invalid_field': 'foo'},
|
||||||
}
|
}
|
||||||
response = post(reverse('api:credential_list'), params, admin)
|
response = post(reverse('api:credential_list'), params, admin, expect=400)
|
||||||
assert response.status_code == 400
|
|
||||||
assert "'invalid_field' was unexpected" in response.data['inputs'][0]
|
assert "'invalid_field' was unexpected" in response.data['inputs'][0]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ from django.db import connection
|
|||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from django.utils.encoding import smart_str, smart_bytes
|
from django.utils.encoding import smart_str, smart_bytes
|
||||||
|
|
||||||
|
from rest_framework.reverse import reverse as drf_reverse
|
||||||
|
|
||||||
from awx.main.utils.encryption import decrypt_value, get_encryption_key
|
from awx.main.utils.encryption import decrypt_value, get_encryption_key
|
||||||
from awx.api.versioning import reverse, drf_reverse
|
from awx.api.versioning import reverse
|
||||||
from awx.main.models.oauth import OAuth2Application as Application, OAuth2AccessToken as AccessToken
|
from awx.main.models.oauth import OAuth2Application as Application, OAuth2AccessToken as AccessToken
|
||||||
from awx.main.tests.functional import immediate_on_commit
|
from awx.main.tests.functional import immediate_on_commit
|
||||||
from awx.sso.models import UserEnterpriseAuth
|
from awx.sso.models import UserEnterpriseAuth
|
||||||
|
|||||||
@@ -3,17 +3,6 @@ import pytest
|
|||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_admin_visible_to_orphaned_users(get, alice):
|
|
||||||
names = set()
|
|
||||||
|
|
||||||
response = get(reverse('api:role_list'), user=alice)
|
|
||||||
for item in response.data['results']:
|
|
||||||
names.add(item['name'])
|
|
||||||
assert 'System Auditor' in names
|
|
||||||
assert 'System Administrator' in names
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('role,code', [('member_role', 400), ('admin_role', 400), ('inventory_admin_role', 204)])
|
@pytest.mark.parametrize('role,code', [('member_role', 400), ('admin_role', 400), ('inventory_admin_role', 204)])
|
||||||
@pytest.mark.parametrize('reversed', [True, False])
|
@pytest.mark.parametrize('reversed', [True, False])
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ from awx.main.models.organization import (
|
|||||||
Organization,
|
Organization,
|
||||||
Team,
|
Team,
|
||||||
)
|
)
|
||||||
from awx.main.models.rbac import Role
|
|
||||||
from awx.main.models.notifications import NotificationTemplate, Notification
|
from awx.main.models.notifications import NotificationTemplate, Notification
|
||||||
from awx.main.models.events import (
|
from awx.main.models.events import (
|
||||||
JobEvent,
|
JobEvent,
|
||||||
@@ -434,7 +433,7 @@ def admin(user):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def system_auditor(user):
|
def system_auditor(user):
|
||||||
u = user('an-auditor', False)
|
u = user('an-auditor', False)
|
||||||
Role.singleton('system_auditor').members.add(u)
|
u.is_system_auditor = True
|
||||||
return u
|
return u
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
10
awx/main/tests/functional/dab_rbac/conftest.py
Normal file
10
awx/main/tests/functional/dab_rbac/conftest.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import pytest
|
||||||
|
from django.apps import apps
|
||||||
|
|
||||||
|
from awx.main.migrations._dab_rbac import setup_managed_role_definitions
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def managed_roles():
|
||||||
|
"Run the migration script to pre-create managed role definitions"
|
||||||
|
setup_managed_role_definitions(apps, None)
|
||||||
111
awx/main/tests/functional/dab_rbac/test_access_list.py
Normal file
111
awx/main/tests/functional/dab_rbac/test_access_list.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from awx.main.models import User
|
||||||
|
from awx.api.versioning import reverse
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_access_list_superuser(get, admin_user, inventory):
|
||||||
|
url = reverse('api:inventory_access_list', kwargs={'pk': inventory.id})
|
||||||
|
|
||||||
|
response = get(url, user=admin_user, expect=200)
|
||||||
|
by_username = {}
|
||||||
|
for entry in response.data['results']:
|
||||||
|
by_username[entry['username']] = entry
|
||||||
|
assert 'admin' in by_username
|
||||||
|
|
||||||
|
assert len(by_username['admin']['summary_fields']['indirect_access']) == 1
|
||||||
|
assert len(by_username['admin']['summary_fields']['direct_access']) == 0
|
||||||
|
access_entry = by_username['admin']['summary_fields']['indirect_access'][0]
|
||||||
|
assert sorted(access_entry['descendant_roles']) == sorted(['adhoc_role', 'use_role', 'update_role', 'read_role', 'admin_role'])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_access_list_system_auditor(get, admin_user, inventory):
|
||||||
|
sys_auditor = User.objects.create(username='sys-aud')
|
||||||
|
sys_auditor.is_system_auditor = True
|
||||||
|
assert sys_auditor.is_system_auditor
|
||||||
|
url = reverse('api:inventory_access_list', kwargs={'pk': inventory.id})
|
||||||
|
|
||||||
|
response = get(url, user=admin_user, expect=200)
|
||||||
|
by_username = {}
|
||||||
|
for entry in response.data['results']:
|
||||||
|
by_username[entry['username']] = entry
|
||||||
|
assert 'sys-aud' in by_username
|
||||||
|
|
||||||
|
assert len(by_username['sys-aud']['summary_fields']['indirect_access']) == 1
|
||||||
|
assert len(by_username['sys-aud']['summary_fields']['direct_access']) == 0
|
||||||
|
access_entry = by_username['sys-aud']['summary_fields']['indirect_access'][0]
|
||||||
|
assert access_entry['descendant_roles'] == ['read_role']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_access_list_direct_access(get, admin_user, inventory):
|
||||||
|
u1 = User.objects.create(username='u1')
|
||||||
|
|
||||||
|
inventory.admin_role.members.add(u1)
|
||||||
|
|
||||||
|
url = reverse('api:inventory_access_list', kwargs={'pk': inventory.id})
|
||||||
|
response = get(url, user=admin_user, expect=200)
|
||||||
|
by_username = {}
|
||||||
|
for entry in response.data['results']:
|
||||||
|
by_username[entry['username']] = entry
|
||||||
|
assert 'u1' in by_username
|
||||||
|
|
||||||
|
assert len(by_username['u1']['summary_fields']['direct_access']) == 1
|
||||||
|
assert len(by_username['u1']['summary_fields']['indirect_access']) == 0
|
||||||
|
access_entry = by_username['u1']['summary_fields']['direct_access'][0]
|
||||||
|
assert sorted(access_entry['descendant_roles']) == sorted(['adhoc_role', 'use_role', 'update_role', 'read_role', 'admin_role'])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_access_list_organization_access(get, admin_user, inventory):
|
||||||
|
u2 = User.objects.create(username='u2')
|
||||||
|
|
||||||
|
inventory.organization.inventory_admin_role.members.add(u2)
|
||||||
|
|
||||||
|
# User has indirect access to the inventory
|
||||||
|
url = reverse('api:inventory_access_list', kwargs={'pk': inventory.id})
|
||||||
|
response = get(url, user=admin_user, expect=200)
|
||||||
|
by_username = {}
|
||||||
|
for entry in response.data['results']:
|
||||||
|
by_username[entry['username']] = entry
|
||||||
|
assert 'u2' in by_username
|
||||||
|
|
||||||
|
assert len(by_username['u2']['summary_fields']['indirect_access']) == 1
|
||||||
|
assert len(by_username['u2']['summary_fields']['direct_access']) == 0
|
||||||
|
access_entry = by_username['u2']['summary_fields']['indirect_access'][0]
|
||||||
|
assert sorted(access_entry['descendant_roles']) == sorted(['adhoc_role', 'use_role', 'update_role', 'read_role', 'admin_role'])
|
||||||
|
|
||||||
|
# Test that user shows up in the organization access list with direct access of expected roles
|
||||||
|
url = reverse('api:organization_access_list', kwargs={'pk': inventory.organization_id})
|
||||||
|
response = get(url, user=admin_user, expect=200)
|
||||||
|
by_username = {}
|
||||||
|
for entry in response.data['results']:
|
||||||
|
by_username[entry['username']] = entry
|
||||||
|
assert 'u2' in by_username
|
||||||
|
|
||||||
|
assert len(by_username['u2']['summary_fields']['direct_access']) == 1
|
||||||
|
assert len(by_username['u2']['summary_fields']['indirect_access']) == 0
|
||||||
|
access_entry = by_username['u2']['summary_fields']['direct_access'][0]
|
||||||
|
assert sorted(access_entry['descendant_roles']) == sorted(['inventory_admin_role', 'read_role'])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_team_indirect_access(get, team, admin_user, inventory):
|
||||||
|
u1 = User.objects.create(username='u1')
|
||||||
|
team.member_role.members.add(u1)
|
||||||
|
|
||||||
|
inventory.organization.inventory_admin_role.parents.add(team.member_role)
|
||||||
|
|
||||||
|
url = reverse('api:inventory_access_list', kwargs={'pk': inventory.id})
|
||||||
|
response = get(url, user=admin_user, expect=200)
|
||||||
|
by_username = {}
|
||||||
|
for entry in response.data['results']:
|
||||||
|
by_username[entry['username']] = entry
|
||||||
|
assert 'u1' in by_username
|
||||||
|
|
||||||
|
assert len(by_username['u1']['summary_fields']['direct_access']) == 1
|
||||||
|
assert len(by_username['u1']['summary_fields']['indirect_access']) == 0
|
||||||
|
access_entry = by_username['u1']['summary_fields']['direct_access'][0]
|
||||||
|
assert sorted(access_entry['descendant_roles']) == sorted(['adhoc_role', 'use_role', 'update_role', 'read_role', 'admin_role'])
|
||||||
45
awx/main/tests/functional/dab_rbac/test_dab_migration.py
Normal file
45
awx/main/tests/functional/dab_rbac/test_dab_migration.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import pytest
|
||||||
|
from django.apps import apps
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
|
from awx.main.migrations._dab_rbac import setup_managed_role_definitions
|
||||||
|
|
||||||
|
from ansible_base.rbac.models import RoleDefinition
|
||||||
|
|
||||||
|
INVENTORY_OBJ_PERMISSIONS = ['view_inventory', 'adhoc_inventory', 'use_inventory', 'change_inventory', 'delete_inventory', 'update_inventory']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_managed_definitions_precreate():
|
||||||
|
with override_settings(
|
||||||
|
ANSIBLE_BASE_ROLE_PRECREATE={
|
||||||
|
'object_admin': '{cls._meta.model_name}-admin',
|
||||||
|
'org_admin': 'organization-admin',
|
||||||
|
'org_children': 'organization-{cls._meta.model_name}-admin',
|
||||||
|
'special': '{cls._meta.model_name}-{action}',
|
||||||
|
}
|
||||||
|
):
|
||||||
|
setup_managed_role_definitions(apps, None)
|
||||||
|
rd = RoleDefinition.objects.get(name='inventory-admin')
|
||||||
|
assert rd.managed is True
|
||||||
|
# add permissions do not go in the object-level admin
|
||||||
|
assert set(rd.permissions.values_list('codename', flat=True)) == set(INVENTORY_OBJ_PERMISSIONS)
|
||||||
|
|
||||||
|
# test org-level object admin permissions
|
||||||
|
rd = RoleDefinition.objects.get(name='organization-inventory-admin')
|
||||||
|
assert rd.managed is True
|
||||||
|
assert set(rd.permissions.values_list('codename', flat=True)) == set(['add_inventory', 'view_organization'] + INVENTORY_OBJ_PERMISSIONS)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_managed_definitions_custom_obj_admin_name():
|
||||||
|
with override_settings(
|
||||||
|
ANSIBLE_BASE_ROLE_PRECREATE={
|
||||||
|
'object_admin': 'foo-{cls._meta.model_name}-foo',
|
||||||
|
}
|
||||||
|
):
|
||||||
|
setup_managed_role_definitions(apps, None)
|
||||||
|
rd = RoleDefinition.objects.get(name='foo-inventory-foo')
|
||||||
|
assert rd.managed is True
|
||||||
|
# add permissions do not go in the object-level admin
|
||||||
|
assert set(rd.permissions.values_list('codename', flat=True)) == set(INVENTORY_OBJ_PERMISSIONS)
|
||||||
90
awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py
Normal file
90
awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.urls import reverse as django_reverse
|
||||||
|
|
||||||
|
from awx.api.versioning import reverse
|
||||||
|
from awx.main.models import JobTemplate, Inventory, Organization
|
||||||
|
|
||||||
|
from ansible_base.rbac.models import RoleDefinition
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_managed_roles_created(managed_roles):
|
||||||
|
"Managed RoleDefinitions are created in post_migration signal, we expect to see them here"
|
||||||
|
for cls in (JobTemplate, Inventory):
|
||||||
|
ct = ContentType.objects.get_for_model(cls)
|
||||||
|
rds = list(RoleDefinition.objects.filter(content_type=ct))
|
||||||
|
assert len(rds) > 1
|
||||||
|
assert f'{cls.__name__} Admin' in [rd.name for rd in rds]
|
||||||
|
for rd in rds:
|
||||||
|
assert rd.managed is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_custom_read_role(admin_user, post, managed_roles):
|
||||||
|
rd_url = django_reverse('roledefinition-list')
|
||||||
|
resp = post(
|
||||||
|
url=rd_url, data={"name": "read role made for test", "content_type": "awx.inventory", "permissions": ['view_inventory']}, user=admin_user, expect=201
|
||||||
|
)
|
||||||
|
rd_id = resp.data['id']
|
||||||
|
rd = RoleDefinition.objects.get(id=rd_id)
|
||||||
|
assert rd.content_type == ContentType.objects.get_for_model(Inventory)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_custom_system_roles_prohibited(admin_user, post):
|
||||||
|
rd_url = django_reverse('roledefinition-list')
|
||||||
|
resp = post(url=rd_url, data={"name": "read role made for test", "content_type": None, "permissions": ['view_inventory']}, user=admin_user, expect=400)
|
||||||
|
assert 'System-wide roles are not enabled' in str(resp.data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_assignment_to_invisible_user(admin_user, alice, rando, inventory, post, managed_roles):
|
||||||
|
"Alice can not see rando, and so can not give them a role assignment"
|
||||||
|
rd = RoleDefinition.objects.get(name='Inventory Admin')
|
||||||
|
rd.give_permission(alice, inventory)
|
||||||
|
url = django_reverse('roleuserassignment-list')
|
||||||
|
r = post(url=url, data={"user": rando.id, "role_definition": rd.id, "object_id": inventory.id}, user=alice, expect=400)
|
||||||
|
assert 'does not exist' in str(r.data)
|
||||||
|
assert not rando.has_obj_perm(inventory, 'change')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_assign_managed_role(admin_user, alice, rando, inventory, post, managed_roles, organization):
|
||||||
|
rd = RoleDefinition.objects.get(name='Inventory Admin')
|
||||||
|
rd.give_permission(alice, inventory)
|
||||||
|
# When alice and rando are members of the same org, they can see each other
|
||||||
|
member_rd = RoleDefinition.objects.get(name='Organization Member')
|
||||||
|
for u in (alice, rando):
|
||||||
|
member_rd.give_permission(u, organization)
|
||||||
|
# Now that alice has full permissions to the inventory, and can see rando, she will give rando permission
|
||||||
|
url = django_reverse('roleuserassignment-list')
|
||||||
|
post(url=url, data={"user": rando.id, "role_definition": rd.id, "object_id": inventory.id}, user=alice, expect=201)
|
||||||
|
assert rando.has_obj_perm(inventory, 'change') is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_assign_custom_delete_role(admin_user, rando, inventory, delete, patch):
|
||||||
|
rd, _ = RoleDefinition.objects.get_or_create(
|
||||||
|
name='inventory-delete', permissions=['delete_inventory', 'view_inventory'], content_type=ContentType.objects.get_for_model(Inventory)
|
||||||
|
)
|
||||||
|
rd.give_permission(rando, inventory)
|
||||||
|
inv_id = inventory.pk
|
||||||
|
inv_url = reverse('api:inventory_detail', kwargs={'pk': inv_id})
|
||||||
|
patch(url=inv_url, data={"description": "new"}, user=rando, expect=403)
|
||||||
|
delete(url=inv_url, user=rando, expect=202)
|
||||||
|
assert Inventory.objects.get(id=inv_id).pending_deletion
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_assign_custom_add_role(admin_user, rando, organization, post, managed_roles):
|
||||||
|
rd, _ = RoleDefinition.objects.get_or_create(
|
||||||
|
name='inventory-add', permissions=['add_inventory', 'view_organization'], content_type=ContentType.objects.get_for_model(Organization)
|
||||||
|
)
|
||||||
|
rd.give_permission(rando, organization)
|
||||||
|
url = reverse('api:inventory_list')
|
||||||
|
r = post(url=url, data={'name': 'abc', 'organization': organization.id}, user=rando, expect=201)
|
||||||
|
inv_id = r.data['id']
|
||||||
|
inventory = Inventory.objects.get(id=inv_id)
|
||||||
|
assert rando.has_obj_perm(inventory, 'change')
|
||||||
107
awx/main/tests/functional/dab_rbac/test_translation_layer.py
Normal file
107
awx/main/tests/functional/dab_rbac/test_translation_layer.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from awx.main.models.rbac import get_role_from_object_role, give_creator_permissions
|
||||||
|
from awx.main.models import User, Organization, WorkflowJobTemplate, WorkflowJobTemplateNode, Team
|
||||||
|
from awx.api.versioning import reverse
|
||||||
|
|
||||||
|
from ansible_base.rbac.models import RoleUserAssignment
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'role_name',
|
||||||
|
['execution_environment_admin_role', 'project_admin_role', 'admin_role', 'auditor_role', 'read_role', 'execute_role', 'notification_admin_role'],
|
||||||
|
)
|
||||||
|
def test_round_trip_roles(organization, rando, role_name, managed_roles):
|
||||||
|
"""
|
||||||
|
Make an assignment with the old-style role,
|
||||||
|
get the equivelent new role
|
||||||
|
get the old role again
|
||||||
|
"""
|
||||||
|
getattr(organization, role_name).members.add(rando)
|
||||||
|
assignment = RoleUserAssignment.objects.get(user=rando)
|
||||||
|
print(assignment.role_definition.name)
|
||||||
|
old_role = get_role_from_object_role(assignment.object_role)
|
||||||
|
assert old_role.id == getattr(organization, role_name).id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_organization_level_permissions(organization, inventory, managed_roles):
|
||||||
|
u1 = User.objects.create(username='alice')
|
||||||
|
u2 = User.objects.create(username='bob')
|
||||||
|
|
||||||
|
organization.inventory_admin_role.members.add(u1)
|
||||||
|
organization.workflow_admin_role.members.add(u2)
|
||||||
|
|
||||||
|
assert u1 in inventory.admin_role
|
||||||
|
assert u1 in organization.inventory_admin_role
|
||||||
|
assert u2 in organization.workflow_admin_role
|
||||||
|
|
||||||
|
assert u2 not in organization.inventory_admin_role
|
||||||
|
assert u1 not in organization.workflow_admin_role
|
||||||
|
assert not (set(u1.has_roles.all()) & set(u2.has_roles.all())) # user have no roles in common
|
||||||
|
|
||||||
|
# Old style
|
||||||
|
assert set(Organization.accessible_objects(u1, 'inventory_admin_role')) == set([organization])
|
||||||
|
assert set(Organization.accessible_objects(u2, 'inventory_admin_role')) == set()
|
||||||
|
assert set(Organization.accessible_objects(u1, 'workflow_admin_role')) == set()
|
||||||
|
assert set(Organization.accessible_objects(u2, 'workflow_admin_role')) == set([organization])
|
||||||
|
|
||||||
|
# New style
|
||||||
|
assert set(Organization.access_qs(u1, 'add_inventory')) == set([organization])
|
||||||
|
assert set(Organization.access_qs(u1, 'change_inventory')) == set([organization])
|
||||||
|
assert set(Organization.access_qs(u2, 'add_inventory')) == set()
|
||||||
|
assert set(Organization.access_qs(u1, 'add_workflowjobtemplate')) == set()
|
||||||
|
assert set(Organization.access_qs(u2, 'add_workflowjobtemplate')) == set([organization])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_organization_execute_role(organization, rando, managed_roles):
|
||||||
|
organization.execute_role.members.add(rando)
|
||||||
|
assert rando in organization.execute_role
|
||||||
|
assert set(Organization.accessible_objects(rando, 'execute_role')) == set([organization])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_workflow_approval_list(get, post, admin_user, managed_roles):
|
||||||
|
workflow_job_template = WorkflowJobTemplate.objects.create()
|
||||||
|
approval_node = WorkflowJobTemplateNode.objects.create(workflow_job_template=workflow_job_template)
|
||||||
|
url = reverse('api:workflow_job_template_node_create_approval', kwargs={'pk': approval_node.pk, 'version': 'v2'})
|
||||||
|
post(url, {'name': 'URL Test', 'description': 'An approval', 'timeout': 0}, user=admin_user)
|
||||||
|
approval_node.refresh_from_db()
|
||||||
|
approval_jt = approval_node.unified_job_template
|
||||||
|
approval_jt.create_unified_job()
|
||||||
|
|
||||||
|
r = get(url=reverse('api:workflow_approval_list'), user=admin_user, expect=200)
|
||||||
|
assert r.data['count'] >= 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_creator_permission(rando, admin_user, inventory, managed_roles):
|
||||||
|
give_creator_permissions(rando, inventory)
|
||||||
|
assert rando in inventory.admin_role
|
||||||
|
assert rando in inventory.admin_role.members.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_team_team_read_role(rando, team, admin_user, post, managed_roles):
|
||||||
|
orgs = [Organization.objects.create(name=f'foo-{i}') for i in range(2)]
|
||||||
|
teams = [Team.objects.create(name=f'foo-{i}', organization=orgs[i]) for i in range(2)]
|
||||||
|
teams[1].member_role.members.add(rando)
|
||||||
|
|
||||||
|
# give second team read permission to first team through the API for regression testing
|
||||||
|
url = reverse('api:role_teams_list', kwargs={'pk': teams[0].read_role.pk, 'version': 'v2'})
|
||||||
|
post(url, {'id': teams[1].id}, user=admin_user)
|
||||||
|
|
||||||
|
# user should be able to view the first team
|
||||||
|
assert rando in teams[0].read_role
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_implicit_parents_no_assignments(organization):
|
||||||
|
"""Through the normal course of creating models, we should not be changing DAB RBAC permissions"""
|
||||||
|
with mock.patch('awx.main.models.rbac.give_or_remove_permission') as mck:
|
||||||
|
Team.objects.create(name='random team', organization=organization)
|
||||||
|
mck.assert_not_called()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from ansible_base.resource_registry.models import Resource
|
||||||
|
|
||||||
|
from awx.api.versioning import reverse
|
||||||
|
|
||||||
|
|
||||||
|
def assert_has_resource(list_response, obj=None):
|
||||||
|
data = list_response.data
|
||||||
|
assert 'resource' in data['results'][0]['summary_fields']
|
||||||
|
resource_data = data['results'][0]['summary_fields']['resource']
|
||||||
|
assert resource_data['ansible_id']
|
||||||
|
resource = Resource.objects.filter(ansible_id=resource_data['ansible_id']).first()
|
||||||
|
assert resource
|
||||||
|
assert resource.content_object
|
||||||
|
if obj:
|
||||||
|
objects = [Resource.objects.get(ansible_id=entry['summary_fields']['resource']['ansible_id']).content_object for entry in data['results']]
|
||||||
|
assert obj in objects
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_organization_ansible_id(organization, admin_user, get):
|
||||||
|
url = reverse('api:organization_list')
|
||||||
|
response = get(url=url, user=admin_user, expect=200)
|
||||||
|
assert_has_resource(response, obj=organization)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_team_ansible_id(team, admin_user, get):
|
||||||
|
url = reverse('api:team_list')
|
||||||
|
response = get(url=url, user=admin_user, expect=200)
|
||||||
|
assert_has_resource(response, obj=team)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_ansible_id(rando, admin_user, get):
|
||||||
|
url = reverse('api:user_list')
|
||||||
|
response = get(url=url, user=admin_user, expect=200)
|
||||||
|
assert_has_resource(response, obj=rando)
|
||||||
@@ -104,11 +104,13 @@ class TestRolesAssociationEntries:
|
|||||||
else:
|
else:
|
||||||
assert len(entry_qs) == 1
|
assert len(entry_qs) == 1
|
||||||
# unfortunate, the original creation does _not_ set a real is_auditor field
|
# unfortunate, the original creation does _not_ set a real is_auditor field
|
||||||
assert 'is_system_auditor' not in json.loads(entry_qs[0].changes)
|
assert 'is_system_auditor' not in json.loads(entry_qs[0].changes) # NOTE: if this fails, see special note
|
||||||
|
# special note - if system auditor flag is moved to user model then we expect this assertion to be changed
|
||||||
|
# make sure that an extra entry is not created, expectation for count would change to 1
|
||||||
if value:
|
if value:
|
||||||
auditor_changes = json.loads(entry_qs[1].changes)
|
entry = entry_qs[1]
|
||||||
assert auditor_changes['object2'] == 'user'
|
assert json.loads(entry.changes) == {'is_system_auditor': [False, True]}
|
||||||
assert auditor_changes['object2_pk'] == u.pk
|
assert entry.object1 == 'user'
|
||||||
|
|
||||||
def test_user_no_op_api(self, system_auditor):
|
def test_user_no_op_api(self, system_auditor):
|
||||||
as_ct = ActivityStream.objects.count()
|
as_ct = ActivityStream.objects.count()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
# AWX context managers for testing
|
# AWX context managers for testing
|
||||||
from awx.main.models.rbac import batch_role_ancestor_rebuilding
|
|
||||||
from awx.main.signals import disable_activity_stream, disable_computed_fields, update_inventory_computed_fields
|
from awx.main.signals import disable_activity_stream, disable_computed_fields, update_inventory_computed_fields
|
||||||
|
|
||||||
# AWX models
|
# AWX models
|
||||||
@@ -10,15 +9,6 @@ from awx.main.models import ActivityStream, Job
|
|||||||
from awx.main.tests.functional import immediate_on_commit
|
from awx.main.tests.functional import immediate_on_commit
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_rbac_batch_rebuilding(rando, organization):
|
|
||||||
with batch_role_ancestor_rebuilding():
|
|
||||||
organization.admin_role.members.add(rando)
|
|
||||||
inventory = organization.inventories.create(name='test-inventory')
|
|
||||||
assert rando not in inventory.admin_role
|
|
||||||
assert rando in inventory.admin_role
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_disable_activity_stream():
|
def test_disable_activity_stream():
|
||||||
with disable_activity_stream():
|
with disable_activity_stream():
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ def test_org_factory_roles(organization_factory):
|
|||||||
teams=['team1', 'team2'],
|
teams=['team1', 'team2'],
|
||||||
users=['team1:foo', 'bar'],
|
users=['team1:foo', 'bar'],
|
||||||
projects=['baz', 'bang'],
|
projects=['baz', 'bang'],
|
||||||
roles=['team2.member_role:foo', 'team1.admin_role:bar', 'team1.admin_role:team2.admin_role', 'baz.admin_role:foo'],
|
roles=['team2.member_role:foo', 'team1.admin_role:bar', 'team1.member_role:team2.admin_role', 'baz.admin_role:foo'],
|
||||||
)
|
)
|
||||||
|
|
||||||
assert objects.users.bar in objects.teams.team2.admin_role
|
assert objects.users.bar in objects.teams.team2.admin_role
|
||||||
assert objects.users.foo in objects.projects.baz.admin_role
|
assert objects.users.foo in objects.projects.baz.admin_role
|
||||||
assert objects.users.foo in objects.teams.team1.member_role
|
assert objects.users.foo in objects.teams.team1.member_role
|
||||||
assert objects.teams.team2.admin_role in objects.teams.team1.admin_role.children.all()
|
assert objects.teams.team2.admin_role in objects.teams.team1.member_role.children.all()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from django_test_migrations.plan import all_migrations, nodes_to_tuples
|
from django_test_migrations.plan import all_migrations, nodes_to_tuples
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Most tests that live in here can probably be deleted at some point. They are mainly
|
Most tests that live in here can probably be deleted at some point. They are mainly
|
||||||
@@ -68,3 +69,19 @@ class TestMigrationSmoke:
|
|||||||
bar_peers = bar.peers.all()
|
bar_peers = bar.peers.all()
|
||||||
assert len(bar_peers) == 1
|
assert len(bar_peers) == 1
|
||||||
assert fooaddr in bar_peers
|
assert fooaddr in bar_peers
|
||||||
|
|
||||||
|
def test_migrate_DAB_RBAC(self, migrator):
|
||||||
|
old_state = migrator.apply_initial_migration(('main', '0190_alter_inventorysource_source_and_more'))
|
||||||
|
Organization = old_state.apps.get_model('main', 'Organization')
|
||||||
|
User = old_state.apps.get_model('auth', 'User')
|
||||||
|
|
||||||
|
org = Organization.objects.create(name='arbitrary-org', created=now(), modified=now())
|
||||||
|
user = User.objects.create(username='random-user')
|
||||||
|
org.read_role.members.add(user)
|
||||||
|
|
||||||
|
new_state = migrator.apply_tested_migration(
|
||||||
|
('main', '0192_custom_roles'),
|
||||||
|
)
|
||||||
|
|
||||||
|
RoleUserAssignment = new_state.apps.get_model('dab_rbac', 'RoleUserAssignment')
|
||||||
|
assert RoleUserAssignment.objects.filter(user=user.id, object_id=org.id).exists()
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
@@ -23,25 +20,6 @@ from awx.main.models import ( # noqa
|
|||||||
User,
|
User,
|
||||||
WorkflowJobTemplate,
|
WorkflowJobTemplate,
|
||||||
)
|
)
|
||||||
from awx.conf import settings_registry
|
|
||||||
|
|
||||||
|
|
||||||
def setup_module(module):
|
|
||||||
# In real-world scenario, named url graph structure is populated by __init__
|
|
||||||
# of URLModificationMiddleware. The way Django bootstraps ensures the initialization
|
|
||||||
# will happen *once and only once*, while the number of initialization is uncontrollable
|
|
||||||
# in unit test environment. So it is wrapped by try-except block to mute any
|
|
||||||
# unwanted exceptions.
|
|
||||||
try:
|
|
||||||
URLModificationMiddleware(mock.Mock())
|
|
||||||
except ImproperlyConfigured:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def teardown_module(module):
|
|
||||||
# settings_registry will be persistent states unless we explicitly clean them up.
|
|
||||||
settings_registry.unregister('NAMED_URL_FORMATS')
|
|
||||||
settings_registry.unregister('NAMED_URL_GRAPH_NODES')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import pytest
|
|||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
from awx.main.models.rbac import Role, ROLE_SINGLETON_SYSTEM_ADMINISTRATOR
|
from awx.main.models.rbac import Role
|
||||||
|
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -31,8 +33,6 @@ def test_get_roles_list_user(organization, inventory, team, get, user):
|
|||||||
'Users can see all roles they have access to, but not all roles'
|
'Users can see all roles they have access to, but not all roles'
|
||||||
this_user = user('user-test_get_roles_list_user')
|
this_user = user('user-test_get_roles_list_user')
|
||||||
organization.member_role.members.add(this_user)
|
organization.member_role.members.add(this_user)
|
||||||
custom_role = Role.objects.create(role_field='custom_role-test_get_roles_list_user')
|
|
||||||
organization.member_role.children.add(custom_role)
|
|
||||||
|
|
||||||
url = reverse('api:role_list')
|
url = reverse('api:role_list')
|
||||||
response = get(url, this_user)
|
response = get(url, this_user)
|
||||||
@@ -46,10 +46,8 @@ def test_get_roles_list_user(organization, inventory, team, get, user):
|
|||||||
for r in roles['results']:
|
for r in roles['results']:
|
||||||
role_hash[r['id']] = r
|
role_hash[r['id']] = r
|
||||||
|
|
||||||
assert Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).id in role_hash
|
|
||||||
assert organization.admin_role.id in role_hash
|
assert organization.admin_role.id in role_hash
|
||||||
assert organization.member_role.id in role_hash
|
assert organization.member_role.id in role_hash
|
||||||
assert custom_role.id in role_hash
|
|
||||||
|
|
||||||
assert inventory.admin_role.id not in role_hash
|
assert inventory.admin_role.id not in role_hash
|
||||||
assert team.member_role.id not in role_hash
|
assert team.member_role.id not in role_hash
|
||||||
@@ -57,7 +55,8 @@ def test_get_roles_list_user(organization, inventory, team, get, user):
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_roles_visibility(get, organization, project, admin, alice, bob):
|
def test_roles_visibility(get, organization, project, admin, alice, bob):
|
||||||
Role.singleton('system_auditor').members.add(alice)
|
alice.is_system_auditor = True
|
||||||
|
alice.save()
|
||||||
assert get(reverse('api:role_list') + '?id=%d' % project.update_role.id, user=admin).data['count'] == 1
|
assert get(reverse('api:role_list') + '?id=%d' % project.update_role.id, user=admin).data['count'] == 1
|
||||||
assert get(reverse('api:role_list') + '?id=%d' % project.update_role.id, user=alice).data['count'] == 1
|
assert get(reverse('api:role_list') + '?id=%d' % project.update_role.id, user=alice).data['count'] == 1
|
||||||
assert get(reverse('api:role_list') + '?id=%d' % project.update_role.id, user=bob).data['count'] == 0
|
assert get(reverse('api:role_list') + '?id=%d' % project.update_role.id, user=bob).data['count'] == 0
|
||||||
@@ -67,7 +66,8 @@ def test_roles_visibility(get, organization, project, admin, alice, bob):
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_roles_filter_visibility(get, organization, project, admin, alice, bob):
|
def test_roles_filter_visibility(get, organization, project, admin, alice, bob):
|
||||||
Role.singleton('system_auditor').members.add(alice)
|
alice.is_system_auditor = True
|
||||||
|
alice.save()
|
||||||
project.update_role.members.add(admin)
|
project.update_role.members.add(admin)
|
||||||
|
|
||||||
assert get(reverse('api:user_roles_list', kwargs={'pk': admin.id}) + '?id=%d' % project.update_role.id, user=admin).data['count'] == 1
|
assert get(reverse('api:user_roles_list', kwargs={'pk': admin.id}) + '?id=%d' % project.update_role.id, user=admin).data['count'] == 1
|
||||||
@@ -105,15 +105,6 @@ def test_cant_delete_role(delete, admin, inventory):
|
|||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_get_user_roles_list(get, admin):
|
|
||||||
url = reverse('api:user_roles_list', kwargs={'pk': admin.id})
|
|
||||||
response = get(url, admin)
|
|
||||||
assert response.status_code == 200
|
|
||||||
roles = response.data
|
|
||||||
assert roles['count'] > 0 # 'system_administrator' role if nothing else
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_user_view_other_user_roles(organization, inventory, team, get, alice, bob):
|
def test_user_view_other_user_roles(organization, inventory, team, get, alice, bob):
|
||||||
'Users can see roles for other users, but only the roles that that user has access to see as well'
|
'Users can see roles for other users, but only the roles that that user has access to see as well'
|
||||||
@@ -141,7 +132,6 @@ def test_user_view_other_user_roles(organization, inventory, team, get, alice, b
|
|||||||
|
|
||||||
assert organization.admin_role.id in role_hash
|
assert organization.admin_role.id in role_hash
|
||||||
assert custom_role.id not in role_hash # doesn't show up in the user roles list, not an explicit grant
|
assert custom_role.id not in role_hash # doesn't show up in the user roles list, not an explicit grant
|
||||||
assert Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).id not in role_hash
|
|
||||||
assert inventory.admin_role.id not in role_hash
|
assert inventory.admin_role.id not in role_hash
|
||||||
assert team.member_role.id not in role_hash # alice can't see this
|
assert team.member_role.id not in role_hash # alice can't see this
|
||||||
|
|
||||||
@@ -197,6 +187,7 @@ def test_remove_role_from_user(role, post, admin):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@override_settings(ANSIBLE_BASE_ALLOW_TEAM_ORG_ADMIN=True)
|
||||||
def test_get_teams_roles_list(get, team, organization, admin):
|
def test_get_teams_roles_list(get, team, organization, admin):
|
||||||
team.member_role.children.add(organization.admin_role)
|
team.member_role.children.add(organization.admin_role)
|
||||||
url = reverse('api:team_roles_list', kwargs={'pk': team.id})
|
url = reverse('api:team_roles_list', kwargs={'pk': team.id})
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from awx.main.models import (
|
|
||||||
Role,
|
|
||||||
Organization,
|
|
||||||
Project,
|
|
||||||
)
|
|
||||||
from awx.main.fields import update_role_parentage_for_instance
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_auto_inheritance_by_children(organization, alice):
|
|
||||||
A = Role.objects.create()
|
|
||||||
B = Role.objects.create()
|
|
||||||
A.members.add(alice)
|
|
||||||
|
|
||||||
assert alice not in organization.admin_role
|
|
||||||
assert Organization.accessible_objects(alice, 'admin_role').count() == 0
|
|
||||||
A.children.add(B)
|
|
||||||
assert alice not in organization.admin_role
|
|
||||||
assert Organization.accessible_objects(alice, 'admin_role').count() == 0
|
|
||||||
A.children.add(organization.admin_role)
|
|
||||||
assert alice in organization.admin_role
|
|
||||||
assert Organization.accessible_objects(alice, 'admin_role').count() == 1
|
|
||||||
A.children.remove(organization.admin_role)
|
|
||||||
assert alice not in organization.admin_role
|
|
||||||
B.children.add(organization.admin_role)
|
|
||||||
assert alice in organization.admin_role
|
|
||||||
B.children.remove(organization.admin_role)
|
|
||||||
assert alice not in organization.admin_role
|
|
||||||
assert Organization.accessible_objects(alice, 'admin_role').count() == 0
|
|
||||||
|
|
||||||
# We've had the case where our pre/post save init handlers in our field descriptors
|
|
||||||
# end up creating a ton of role objects because of various not-so-obvious issues
|
|
||||||
assert Role.objects.count() < 50
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_auto_inheritance_by_parents(organization, alice):
|
|
||||||
A = Role.objects.create()
|
|
||||||
B = Role.objects.create()
|
|
||||||
A.members.add(alice)
|
|
||||||
|
|
||||||
assert alice not in organization.admin_role
|
|
||||||
B.parents.add(A)
|
|
||||||
assert alice not in organization.admin_role
|
|
||||||
organization.admin_role.parents.add(A)
|
|
||||||
assert alice in organization.admin_role
|
|
||||||
organization.admin_role.parents.remove(A)
|
|
||||||
assert alice not in organization.admin_role
|
|
||||||
organization.admin_role.parents.add(B)
|
|
||||||
assert alice in organization.admin_role
|
|
||||||
organization.admin_role.parents.remove(B)
|
|
||||||
assert alice not in organization.admin_role
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_accessible_objects(organization, alice, bob):
|
|
||||||
A = Role.objects.create()
|
|
||||||
A.members.add(alice)
|
|
||||||
B = Role.objects.create()
|
|
||||||
B.members.add(alice)
|
|
||||||
B.members.add(bob)
|
|
||||||
|
|
||||||
assert Organization.accessible_objects(alice, 'admin_role').count() == 0
|
|
||||||
assert Organization.accessible_objects(bob, 'admin_role').count() == 0
|
|
||||||
A.children.add(organization.admin_role)
|
|
||||||
assert Organization.accessible_objects(alice, 'admin_role').count() == 1
|
|
||||||
assert Organization.accessible_objects(bob, 'admin_role').count() == 0
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_team_symantics(organization, team, alice):
|
|
||||||
assert alice not in organization.auditor_role
|
|
||||||
team.member_role.children.add(organization.auditor_role)
|
|
||||||
assert alice not in organization.auditor_role
|
|
||||||
team.member_role.members.add(alice)
|
|
||||||
assert alice in organization.auditor_role
|
|
||||||
team.member_role.members.remove(alice)
|
|
||||||
assert alice not in organization.auditor_role
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_auto_field_adjustments(organization, inventory, team, alice):
|
|
||||||
'Ensures the auto role reparenting is working correctly through non m2m fields'
|
|
||||||
org2 = Organization.objects.create(name='Org 2', description='org 2')
|
|
||||||
org2.admin_role.members.add(alice)
|
|
||||||
assert alice not in inventory.admin_role
|
|
||||||
inventory.organization = org2
|
|
||||||
inventory.save()
|
|
||||||
assert alice in inventory.admin_role
|
|
||||||
inventory.organization = organization
|
|
||||||
inventory.save()
|
|
||||||
assert alice not in inventory.admin_role
|
|
||||||
# assert False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_implicit_deletes(alice):
|
|
||||||
'Ensures implicit resources and roles delete themselves'
|
|
||||||
delorg = Organization.objects.create(name='test-org')
|
|
||||||
child = Role.objects.create()
|
|
||||||
child.parents.add(delorg.admin_role)
|
|
||||||
delorg.admin_role.members.add(alice)
|
|
||||||
|
|
||||||
admin_role_id = delorg.admin_role.id
|
|
||||||
auditor_role_id = delorg.auditor_role.id
|
|
||||||
|
|
||||||
assert child.ancestors.count() > 1
|
|
||||||
assert Role.objects.filter(id=admin_role_id).count() == 1
|
|
||||||
assert Role.objects.filter(id=auditor_role_id).count() == 1
|
|
||||||
n_alice_roles = alice.roles.count()
|
|
||||||
n_system_admin_children = Role.singleton('system_administrator').children.count()
|
|
||||||
|
|
||||||
delorg.delete()
|
|
||||||
|
|
||||||
assert Role.objects.filter(id=admin_role_id).count() == 0
|
|
||||||
assert Role.objects.filter(id=auditor_role_id).count() == 0
|
|
||||||
assert alice.roles.count() == (n_alice_roles - 1)
|
|
||||||
assert Role.singleton('system_administrator').children.count() == (n_system_admin_children - 1)
|
|
||||||
assert child.ancestors.count() == 1
|
|
||||||
assert child.ancestors.all()[0] == child
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_content_object(user):
|
|
||||||
'Ensure our content_object stuf seems to be working'
|
|
||||||
|
|
||||||
org = Organization.objects.create(name='test-org')
|
|
||||||
assert org.admin_role.content_object.id == org.id
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_hierarchy_rebuilding_multi_path():
|
|
||||||
'Tests a subdtle cases around role hierarchy rebuilding when you have multiple paths to the same role of different length'
|
|
||||||
|
|
||||||
X = Role.objects.create()
|
|
||||||
A = Role.objects.create()
|
|
||||||
B = Role.objects.create()
|
|
||||||
C = Role.objects.create()
|
|
||||||
D = Role.objects.create()
|
|
||||||
|
|
||||||
A.children.add(B)
|
|
||||||
A.children.add(D)
|
|
||||||
B.children.add(C)
|
|
||||||
C.children.add(D)
|
|
||||||
|
|
||||||
assert A.is_ancestor_of(D)
|
|
||||||
assert X.is_ancestor_of(D) is False
|
|
||||||
|
|
||||||
X.children.add(A)
|
|
||||||
|
|
||||||
assert X.is_ancestor_of(D) is True
|
|
||||||
|
|
||||||
X.children.remove(A)
|
|
||||||
|
|
||||||
# This can be the stickler, the rebuilder needs to ensure that D's role
|
|
||||||
# hierarchy is built after both A and C are updated.
|
|
||||||
assert X.is_ancestor_of(D) is False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_auto_parenting():
|
|
||||||
org1 = Organization.objects.create(name='org1')
|
|
||||||
org2 = Organization.objects.create(name='org2')
|
|
||||||
|
|
||||||
prj1 = Project.objects.create(name='prj1')
|
|
||||||
prj2 = Project.objects.create(name='prj2')
|
|
||||||
|
|
||||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role) is False
|
|
||||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role) is False
|
|
||||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role) is False
|
|
||||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role) is False
|
|
||||||
|
|
||||||
prj1.organization = org1
|
|
||||||
prj1.save()
|
|
||||||
|
|
||||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role)
|
|
||||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role) is False
|
|
||||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role) is False
|
|
||||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role) is False
|
|
||||||
|
|
||||||
prj2.organization = org1
|
|
||||||
prj2.save()
|
|
||||||
|
|
||||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role)
|
|
||||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role)
|
|
||||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role) is False
|
|
||||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role) is False
|
|
||||||
|
|
||||||
prj1.organization = org2
|
|
||||||
prj1.save()
|
|
||||||
|
|
||||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role) is False
|
|
||||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role)
|
|
||||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role)
|
|
||||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role) is False
|
|
||||||
|
|
||||||
prj2.organization = org2
|
|
||||||
prj2.save()
|
|
||||||
|
|
||||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role) is False
|
|
||||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role) is False
|
|
||||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role)
|
|
||||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_update_parents_keeps_teams(team, project):
|
|
||||||
project.update_role.parents.add(team.member_role)
|
|
||||||
assert list(Project.accessible_objects(team.member_role, 'update_role')) == [project] # test prep sanity check
|
|
||||||
update_role_parentage_for_instance(project)
|
|
||||||
assert list(Project.accessible_objects(team.member_role, 'update_role')) == [project] # actual assertion
|
|
||||||
@@ -4,7 +4,7 @@ import pytest
|
|||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
from awx.main.access import BaseAccess, JobTemplateAccess, ScheduleAccess
|
from awx.main.access import BaseAccess, JobTemplateAccess, ScheduleAccess
|
||||||
from awx.main.models.jobs import JobTemplate
|
from awx.main.models.jobs import JobTemplate
|
||||||
from awx.main.models import Project, Organization, Inventory, Schedule, User
|
from awx.main.models import Project, Organization, Schedule
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(BaseAccess, 'check_license', return_value=None)
|
@mock.patch.object(BaseAccess, 'check_license', return_value=None)
|
||||||
@@ -177,7 +177,7 @@ def test_job_template_creator_access(project, organization, rando, post):
|
|||||||
jt_pk = response.data['id']
|
jt_pk = response.data['id']
|
||||||
jt_obj = JobTemplate.objects.get(pk=jt_pk)
|
jt_obj = JobTemplate.objects.get(pk=jt_pk)
|
||||||
# Creating a JT should place the creator in the admin role
|
# Creating a JT should place the creator in the admin role
|
||||||
assert rando in jt_obj.admin_role.members.all()
|
assert rando in jt_obj.admin_role
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -283,48 +283,3 @@ class TestProjectOrganization:
|
|||||||
assert org_admin not in jt.admin_role
|
assert org_admin not in jt.admin_role
|
||||||
patch(url=jt.get_absolute_url(), data={'project': project.id}, user=admin_user, expect=200)
|
patch(url=jt.get_absolute_url(), data={'project': project.id}, user=admin_user, expect=200)
|
||||||
assert org_admin in jt.admin_role
|
assert org_admin in jt.admin_role
|
||||||
|
|
||||||
def test_inventory_read_transfer_direct(self, patch):
|
|
||||||
orgs = []
|
|
||||||
invs = []
|
|
||||||
admins = []
|
|
||||||
for i in range(2):
|
|
||||||
org = Organization.objects.create(name='org{}'.format(i))
|
|
||||||
org_admin = User.objects.create(username='user{}'.format(i))
|
|
||||||
inv = Inventory.objects.create(organization=org, name='inv{}'.format(i))
|
|
||||||
org.auditor_role.members.add(org_admin)
|
|
||||||
|
|
||||||
orgs.append(org)
|
|
||||||
admins.append(org_admin)
|
|
||||||
invs.append(inv)
|
|
||||||
|
|
||||||
jt = JobTemplate.objects.create(name='foo', inventory=invs[0])
|
|
||||||
assert admins[0] in jt.read_role
|
|
||||||
assert admins[1] not in jt.read_role
|
|
||||||
|
|
||||||
jt.inventory = invs[1]
|
|
||||||
jt.save(update_fields=['inventory'])
|
|
||||||
assert admins[0] not in jt.read_role
|
|
||||||
assert admins[1] in jt.read_role
|
|
||||||
|
|
||||||
def test_inventory_read_transfer_indirect(self, patch):
|
|
||||||
orgs = []
|
|
||||||
admins = []
|
|
||||||
for i in range(2):
|
|
||||||
org = Organization.objects.create(name='org{}'.format(i))
|
|
||||||
org_admin = User.objects.create(username='user{}'.format(i))
|
|
||||||
org.auditor_role.members.add(org_admin)
|
|
||||||
|
|
||||||
orgs.append(org)
|
|
||||||
admins.append(org_admin)
|
|
||||||
|
|
||||||
inv = Inventory.objects.create(organization=orgs[0], name='inv{}'.format(i))
|
|
||||||
|
|
||||||
jt = JobTemplate.objects.create(name='foo', inventory=inv)
|
|
||||||
assert admins[0] in jt.read_role
|
|
||||||
assert admins[1] not in jt.read_role
|
|
||||||
|
|
||||||
inv.organization = orgs[1]
|
|
||||||
inv.save(update_fields=['organization'])
|
|
||||||
assert admins[0] not in jt.read_role
|
|
||||||
assert admins[1] in jt.read_role
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
|
|
||||||
from awx.main.migrations import _rbac as rbac
|
from awx.main.migrations import _rbac as rbac
|
||||||
from awx.main.models import UnifiedJobTemplate, InventorySource, Inventory, JobTemplate, Project, Organization, User
|
from awx.main.models import UnifiedJobTemplate, InventorySource, Inventory, JobTemplate, Project, Organization
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -49,27 +47,3 @@ def test_implied_organization_subquery_job_template():
|
|||||||
assert jt.test_field is None
|
assert jt.test_field is None
|
||||||
else:
|
else:
|
||||||
assert jt.test_field == jt.project.organization_id
|
assert jt.test_field == jt.project.organization_id
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_give_explicit_inventory_permission():
|
|
||||||
dual_admin = User.objects.create(username='alice')
|
|
||||||
inv_admin = User.objects.create(username='bob')
|
|
||||||
inv_org = Organization.objects.create(name='inv-org')
|
|
||||||
proj_org = Organization.objects.create(name='proj-org')
|
|
||||||
|
|
||||||
inv_org.admin_role.members.add(inv_admin, dual_admin)
|
|
||||||
proj_org.admin_role.members.add(dual_admin)
|
|
||||||
|
|
||||||
proj = Project.objects.create(name="test-proj", organization=proj_org)
|
|
||||||
inv = Inventory.objects.create(name='test-inv', organization=inv_org)
|
|
||||||
|
|
||||||
jt = JobTemplate.objects.create(name='foo', project=proj, inventory=inv)
|
|
||||||
|
|
||||||
assert dual_admin in jt.admin_role
|
|
||||||
|
|
||||||
rbac.restore_inventory_admins(apps, None)
|
|
||||||
|
|
||||||
assert inv_admin in jt.admin_role.members.all()
|
|
||||||
assert dual_admin not in jt.admin_role.members.all()
|
|
||||||
assert dual_admin in jt.admin_role
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ def test_team_accessible_by(team, user, project):
|
|||||||
u = user('team_member', False)
|
u = user('team_member', False)
|
||||||
|
|
||||||
team.member_role.children.add(project.use_role)
|
team.member_role.children.add(project.use_role)
|
||||||
assert list(Project.accessible_objects(team.member_role, 'read_role')) == [project]
|
assert list(Project.accessible_objects(team, 'read_role')) == [project]
|
||||||
assert u not in project.read_role
|
assert u not in project.read_role
|
||||||
|
|
||||||
team.member_role.members.add(u)
|
team.member_role.members.add(u)
|
||||||
@@ -104,7 +104,7 @@ def test_team_accessible_objects(team, user, project):
|
|||||||
u = user('team_member', False)
|
u = user('team_member', False)
|
||||||
|
|
||||||
team.member_role.children.add(project.use_role)
|
team.member_role.children.add(project.use_role)
|
||||||
assert len(Project.accessible_objects(team.member_role, 'read_role')) == 1
|
assert len(Project.accessible_objects(team, 'read_role')) == 1
|
||||||
assert not Project.accessible_objects(u, 'read_role')
|
assert not Project.accessible_objects(u, 'read_role')
|
||||||
|
|
||||||
team.member_role.members.add(u)
|
team.member_role.members.add(u)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from unittest import mock
|
|||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
from awx.main.access import UserAccess, RoleAccess, TeamAccess
|
from awx.main.access import UserAccess, RoleAccess, TeamAccess
|
||||||
from awx.main.models import User, Organization, Inventory, Role
|
from awx.main.models import User, Organization, Inventory, get_system_auditor_role
|
||||||
|
|
||||||
|
|
||||||
class TestSysAuditorTransactional(TransactionTestCase):
|
class TestSysAuditorTransactional(TransactionTestCase):
|
||||||
@@ -18,7 +18,8 @@ class TestSysAuditorTransactional(TransactionTestCase):
|
|||||||
|
|
||||||
def test_auditor_caching(self):
|
def test_auditor_caching(self):
|
||||||
rando = self.rando()
|
rando = self.rando()
|
||||||
with self.assertNumQueries(1):
|
get_system_auditor_role() # pre-create role, normally done by migrations
|
||||||
|
with self.assertNumQueries(2):
|
||||||
v = rando.is_system_auditor
|
v = rando.is_system_auditor
|
||||||
assert not v
|
assert not v
|
||||||
with self.assertNumQueries(0):
|
with self.assertNumQueries(0):
|
||||||
@@ -153,34 +154,3 @@ def test_org_admin_cannot_delete_member_attached_to_other_group(org_admin, org_m
|
|||||||
access = UserAccess(org_admin)
|
access = UserAccess(org_admin)
|
||||||
other_org.member_role.members.add(org_member)
|
other_org.member_role.members.add(org_member)
|
||||||
assert not access.can_delete(org_member)
|
assert not access.can_delete(org_member)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('reverse', (True, False))
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_consistency_of_is_superuser_flag(reverse):
|
|
||||||
users = [User.objects.create(username='rando_{}'.format(i)) for i in range(2)]
|
|
||||||
for u in users:
|
|
||||||
assert u.is_superuser is False
|
|
||||||
|
|
||||||
system_admin = Role.singleton('system_administrator')
|
|
||||||
if reverse:
|
|
||||||
for u in users:
|
|
||||||
u.roles.add(system_admin)
|
|
||||||
else:
|
|
||||||
system_admin.members.add(*[u.id for u in users]) # like .add(42, 54)
|
|
||||||
|
|
||||||
for u in users:
|
|
||||||
u.refresh_from_db()
|
|
||||||
assert u.is_superuser is True
|
|
||||||
|
|
||||||
users[0].roles.clear()
|
|
||||||
for u in users:
|
|
||||||
u.refresh_from_db()
|
|
||||||
assert users[0].is_superuser is False
|
|
||||||
assert users[1].is_superuser is True
|
|
||||||
|
|
||||||
system_admin.members.clear()
|
|
||||||
|
|
||||||
for u in users:
|
|
||||||
u.refresh_from_db()
|
|
||||||
assert u.is_superuser is False
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db()
|
|
||||||
def test_admin_not_member(team):
|
|
||||||
"""Test to ensure we don't add admin_role as a parent to team.member_role, as
|
|
||||||
this creates a cycle with organization administration, which we've decided
|
|
||||||
to remove support for
|
|
||||||
|
|
||||||
(2016-06-16) I think this might have been resolved. I'm asserting
|
|
||||||
this to be true in the mean time.
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert team.admin_role.is_ancestor_of(team.member_role) is True
|
|
||||||
@@ -52,7 +52,7 @@ class TestDumpAuthConfigCommand(TestCase):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
self.expected_config = [
|
self.expected_config = [
|
||||||
{
|
{
|
||||||
"type": "awx.authentication.authenticator_plugins.saml",
|
"type": "ansible_base.authentication.authenticator_plugins.saml",
|
||||||
"name": "Keycloak",
|
"name": "Keycloak",
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"create_objects": True,
|
"create_objects": True,
|
||||||
@@ -94,14 +94,14 @@ class TestDumpAuthConfigCommand(TestCase):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "awx.authentication.authenticator_plugins.ldap",
|
"type": "ansible_base.authentication.authenticator_plugins.ldap",
|
||||||
"name": "1",
|
"name": "LDAP_1",
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"create_objects": True,
|
"create_objects": True,
|
||||||
"users_unique": False,
|
"users_unique": False,
|
||||||
"remove_users": True,
|
"remove_users": True,
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"SERVER_URI": "SERVER_URI",
|
"SERVER_URI": ["SERVER_URI"],
|
||||||
"BIND_DN": "BIND_DN",
|
"BIND_DN": "BIND_DN",
|
||||||
"BIND_PASSWORD": "BIND_PASSWORD",
|
"BIND_PASSWORD": "BIND_PASSWORD",
|
||||||
"CONNECTION_OPTIONS": {},
|
"CONNECTION_OPTIONS": {},
|
||||||
@@ -119,4 +119,14 @@ class TestDumpAuthConfigCommand(TestCase):
|
|||||||
def test_json_returned_from_cmd(self):
|
def test_json_returned_from_cmd(self):
|
||||||
output = StringIO()
|
output = StringIO()
|
||||||
call_command("dump_auth_config", stdout=output)
|
call_command("dump_auth_config", stdout=output)
|
||||||
assert json.loads(output.getvalue()) == self.expected_config
|
cmmd_output = json.loads(output.getvalue())
|
||||||
|
|
||||||
|
# check configured SAML return
|
||||||
|
assert cmmd_output[0] == self.expected_config[0]
|
||||||
|
|
||||||
|
# check configured LDAP return
|
||||||
|
assert cmmd_output[2] == self.expected_config[1]
|
||||||
|
|
||||||
|
# check unconfigured LDAP return
|
||||||
|
assert "LDAP_0_missing_fields" in cmmd_output[1]
|
||||||
|
assert cmmd_output[1]["LDAP_0_missing_fields"] == ['SERVER_URI', 'GROUP_TYPE', 'GROUP_TYPE_PARAMS', 'USER_DN_TEMPLATE', 'USER_ATTR_MAP']
|
||||||
|
|||||||
@@ -1106,6 +1106,44 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
config = open(local_path, 'r').read()
|
config = open(local_path, 'r').read()
|
||||||
assert config == hcl_config
|
assert config == hcl_config
|
||||||
|
|
||||||
|
def test_terraform_gcs_backend_credentials(self, job, private_data_dir, mock_me):
|
||||||
|
terraform = CredentialType.defaults['terraform']()
|
||||||
|
hcl_config = '''
|
||||||
|
backend "gcs" {
|
||||||
|
bucket = "gce_storage"
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
gce_backend_credentials = '''
|
||||||
|
{
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "sample",
|
||||||
|
"private_key_id": "eeeeeeeeeeeeeeeeeeeeeeeeeee",
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n-----END PRIVATE KEY-----\n",
|
||||||
|
"client_email": "sample@sample.iam.gserviceaccount.com",
|
||||||
|
"client_id": "0123456789",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/cloud-content-robot%40sample.iam.gserviceaccount.com",
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
credential = Credential(pk=1, credential_type=terraform, inputs={'configuration': hcl_config, 'gce_credentials': gce_backend_credentials})
|
||||||
|
credential.inputs['configuration'] = encrypt_field(credential, 'configuration')
|
||||||
|
credential.inputs['gce_credentials'] = encrypt_field(credential, 'gce_credentials')
|
||||||
|
job.credentials.add(credential)
|
||||||
|
|
||||||
|
env = {}
|
||||||
|
safe_env = {}
|
||||||
|
credential.credential_type.inject_credential(credential, env, safe_env, [], private_data_dir)
|
||||||
|
|
||||||
|
local_path = to_host_path(env['TF_BACKEND_CONFIG_FILE'], private_data_dir)
|
||||||
|
config = open(local_path, 'r').read()
|
||||||
|
assert config == hcl_config
|
||||||
|
|
||||||
|
credentials_path = to_host_path(env['GOOGLE_BACKEND_CREDENTIALS'], private_data_dir)
|
||||||
|
credentials = open(credentials_path, 'r').read()
|
||||||
|
assert credentials == gce_backend_credentials
|
||||||
|
|
||||||
def test_custom_environment_injectors_with_jinja_syntax_error(self, private_data_dir, mock_me):
|
def test_custom_environment_injectors_with_jinja_syntax_error(self, private_data_dir, mock_me):
|
||||||
some_cloud = CredentialType(
|
some_cloud = CredentialType(
|
||||||
kind='cloud',
|
kind='cloud',
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class mockHost:
|
|||||||
@mock.patch('awx.main.utils.filters.get_model', return_value=mockHost())
|
@mock.patch('awx.main.utils.filters.get_model', return_value=mockHost())
|
||||||
class TestSmartFilterQueryFromString:
|
class TestSmartFilterQueryFromString:
|
||||||
@mock.patch(
|
@mock.patch(
|
||||||
'ansible_base.rest_filters.rest_framework.field_lookup_backend.get_fields_from_path', lambda model, path: ([model], path)
|
'ansible_base.rest_filters.rest_framework.field_lookup_backend.get_fields_from_path', lambda model, path, **kwargs: ([model], path)
|
||||||
) # disable field filtering, because a__b isn't a real Host field
|
) # disable field filtering, because a__b isn't a real Host field
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"filter_string,q_expected",
|
"filter_string,q_expected",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from awx.main.tasks.receptor import _convert_args_to_cli
|
|||||||
|
|
||||||
def test_file_cleanup_scenario():
|
def test_file_cleanup_scenario():
|
||||||
args = _convert_args_to_cli({'exclude_strings': ['awx_423_', 'awx_582_'], 'file_pattern': '/tmp/awx_*_*'})
|
args = _convert_args_to_cli({'exclude_strings': ['awx_423_', 'awx_582_'], 'file_pattern': '/tmp/awx_*_*'})
|
||||||
assert ' '.join(args) == 'cleanup --exclude-strings=awx_423_ awx_582_ --file-pattern=/tmp/awx_*_*'
|
assert ' '.join(args) == 'cleanup --exclude-strings="awx_423_ awx_582_" --file-pattern=/tmp/awx_*_*'
|
||||||
|
|
||||||
|
|
||||||
def test_image_cleanup_scenario():
|
def test_image_cleanup_scenario():
|
||||||
@@ -17,5 +17,5 @@ def test_image_cleanup_scenario():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
' '.join(args) == 'cleanup --remove-images=quay.invalid/foo/bar:latest quay.invalid/foo/bar:devel --image-prune --process-isolation-executable=podman'
|
' '.join(args) == 'cleanup --remove-images="quay.invalid/foo/bar:latest quay.invalid/foo/bar:devel" --image-prune --process-isolation-executable=podman'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
from copy import copy
|
from copy import copy
|
||||||
import json
|
import json
|
||||||
import json_log_formatter
|
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import socket
|
import socket
|
||||||
@@ -15,15 +14,6 @@ from django.core.serializers.json import DjangoJSONEncoder
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
class JobLifeCycleFormatter(json_log_formatter.JSONFormatter):
|
|
||||||
def json_record(self, message: str, extra: dict, record: logging.LogRecord):
|
|
||||||
if 'time' not in extra:
|
|
||||||
extra['time'] = now()
|
|
||||||
if record.exc_info:
|
|
||||||
extra['exc_info'] = self.formatException(record.exc_info)
|
|
||||||
return extra
|
|
||||||
|
|
||||||
|
|
||||||
class TimeFormatter(logging.Formatter):
|
class TimeFormatter(logging.Formatter):
|
||||||
"""
|
"""
|
||||||
Custom log formatter used for inventory imports
|
Custom log formatter used for inventory imports
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from collections import deque
|
|||||||
# Django
|
# Django
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
|
|
||||||
|
|
||||||
NAMED_URL_RES_DILIMITER = "++"
|
NAMED_URL_RES_DILIMITER = "++"
|
||||||
@@ -245,6 +244,8 @@ def _generate_configurations(nodes):
|
|||||||
|
|
||||||
|
|
||||||
def _dfs(configuration, model, graph, dead_ends, new_deadends, parents):
|
def _dfs(configuration, model, graph, dead_ends, new_deadends, parents):
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
parents.add(model)
|
parents.add(model)
|
||||||
fields, fk_names = configuration[model][0][:], configuration[model][1][:]
|
fields, fk_names = configuration[model][0][:], configuration[model][1][:]
|
||||||
adj_list = []
|
adj_list = []
|
||||||
@@ -306,3 +307,19 @@ def generate_graph(models):
|
|||||||
def reset_counters():
|
def reset_counters():
|
||||||
for node in settings.NAMED_URL_GRAPH.values():
|
for node in settings.NAMED_URL_GRAPH.values():
|
||||||
node.counter = 0
|
node.counter = 0
|
||||||
|
|
||||||
|
|
||||||
|
def _customize_graph():
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from awx.main.models import Instance, Schedule, UnifiedJobTemplate
|
||||||
|
|
||||||
|
for model in [Schedule, UnifiedJobTemplate]:
|
||||||
|
if model in settings.NAMED_URL_GRAPH:
|
||||||
|
settings.NAMED_URL_GRAPH[model].remove_bindings()
|
||||||
|
settings.NAMED_URL_GRAPH.pop(model)
|
||||||
|
if User not in settings.NAMED_URL_GRAPH:
|
||||||
|
settings.NAMED_URL_GRAPH[User] = GraphNode(User, ['username'], [])
|
||||||
|
settings.NAMED_URL_GRAPH[User].add_bindings()
|
||||||
|
if Instance not in settings.NAMED_URL_GRAPH:
|
||||||
|
settings.NAMED_URL_GRAPH[Instance] = GraphNode(Instance, ['hostname'], [])
|
||||||
|
settings.NAMED_URL_GRAPH[Instance].add_bindings()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
|
||||||
@@ -241,7 +242,7 @@ class WebSocketRelayManager(object):
|
|||||||
# In this case, we'll be sharing a redis, no need to relay.
|
# In this case, we'll be sharing a redis, no need to relay.
|
||||||
if payload.get("hostname") == self.local_hostname:
|
if payload.get("hostname") == self.local_hostname:
|
||||||
hostname = payload.get("hostname")
|
hostname = payload.get("hostname")
|
||||||
logger.debug("Received a heartbeat request for {hostname}. Skipping as we use redis for local host.")
|
logger.debug(f"Received a heartbeat request for {hostname}. Skipping as we use redis for local host.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
action = payload.get("action")
|
action = payload.get("action")
|
||||||
@@ -284,6 +285,8 @@ class WebSocketRelayManager(object):
|
|||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
# Handle the case where the task was already cancelled by the time we got here.
|
# Handle the case where the task was already cancelled by the time we got here.
|
||||||
pass
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to cancel relay connection for {hostname}: {e}")
|
||||||
|
|
||||||
del self.relay_connections[hostname]
|
del self.relay_connections[hostname]
|
||||||
|
|
||||||
@@ -294,6 +297,8 @@ class WebSocketRelayManager(object):
|
|||||||
self.stats_mgr.delete_remote_host_stats(hostname)
|
self.stats_mgr.delete_remote_host_stats(hostname)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to delete stats for {hostname}: {e}")
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
event_loop = asyncio.get_running_loop()
|
event_loop = asyncio.get_running_loop()
|
||||||
@@ -302,67 +307,90 @@ class WebSocketRelayManager(object):
|
|||||||
self.stats_mgr.start()
|
self.stats_mgr.start()
|
||||||
|
|
||||||
# Set up a pg_notify consumer for allowing web nodes to "provision" and "deprovision" themselves gracefully.
|
# Set up a pg_notify consumer for allowing web nodes to "provision" and "deprovision" themselves gracefully.
|
||||||
database_conf = settings.DATABASES['default'].copy()
|
database_conf = deepcopy(settings.DATABASES['default'])
|
||||||
database_conf['OPTIONS'] = database_conf.get('OPTIONS', {}).copy()
|
database_conf['OPTIONS'] = deepcopy(database_conf.get('OPTIONS', {}))
|
||||||
|
|
||||||
for k, v in settings.LISTENER_DATABASES.get('default', {}).items():
|
for k, v in settings.LISTENER_DATABASES.get('default', {}).items():
|
||||||
database_conf[k] = v
|
database_conf[k] = v
|
||||||
for k, v in settings.LISTENER_DATABASES.get('default', {}).get('OPTIONS', {}).items():
|
for k, v in settings.LISTENER_DATABASES.get('default', {}).get('OPTIONS', {}).items():
|
||||||
database_conf['OPTIONS'][k] = v
|
database_conf['OPTIONS'][k] = v
|
||||||
|
|
||||||
|
if 'PASSWORD' in database_conf:
|
||||||
|
database_conf['OPTIONS']['password'] = database_conf.pop('PASSWORD')
|
||||||
|
|
||||||
task = None
|
task = None
|
||||||
|
|
||||||
|
# Managing the async_conn here so that we can close it if we need to restart the connection
|
||||||
|
async_conn = None
|
||||||
|
|
||||||
# Establishes a websocket connection to /websocket/relay on all API servers
|
# Establishes a websocket connection to /websocket/relay on all API servers
|
||||||
while True:
|
try:
|
||||||
if not task or task.done():
|
while True:
|
||||||
|
if not task or task.done():
|
||||||
|
try:
|
||||||
|
# Try to close the connection if it's open
|
||||||
|
if async_conn:
|
||||||
|
try:
|
||||||
|
await async_conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to close connection to database for pg_notify: {e}")
|
||||||
|
|
||||||
|
# and re-establish the connection
|
||||||
|
async_conn = await psycopg.AsyncConnection.connect(
|
||||||
|
dbname=database_conf['NAME'],
|
||||||
|
host=database_conf['HOST'],
|
||||||
|
user=database_conf['USER'],
|
||||||
|
port=database_conf['PORT'],
|
||||||
|
**database_conf.get("OPTIONS", {}),
|
||||||
|
)
|
||||||
|
await async_conn.set_autocommit(True)
|
||||||
|
|
||||||
|
# before creating the task that uses the connection
|
||||||
|
task = event_loop.create_task(self.on_ws_heartbeat(async_conn), name="on_ws_heartbeat")
|
||||||
|
logger.info("Creating `on_ws_heartbeat` task in event loop.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to connect to database for pg_notify: {e}")
|
||||||
|
|
||||||
|
future_remote_hosts = self.known_hosts.keys()
|
||||||
|
current_remote_hosts = self.relay_connections.keys()
|
||||||
|
deleted_remote_hosts = set(current_remote_hosts) - set(future_remote_hosts)
|
||||||
|
new_remote_hosts = set(future_remote_hosts) - set(current_remote_hosts)
|
||||||
|
|
||||||
|
# This loop handles if we get an advertisement from a host we already know about but
|
||||||
|
# the advertisement has a different IP than we are currently connected to.
|
||||||
|
for hostname, address in self.known_hosts.items():
|
||||||
|
if hostname not in self.relay_connections:
|
||||||
|
# We've picked up a new hostname that we don't know about yet.
|
||||||
|
continue
|
||||||
|
|
||||||
|
if address != self.relay_connections[hostname].remote_host:
|
||||||
|
deleted_remote_hosts.add(hostname)
|
||||||
|
new_remote_hosts.add(hostname)
|
||||||
|
|
||||||
|
# Delete any hosts with closed connections
|
||||||
|
for hostname, relay_conn in self.relay_connections.items():
|
||||||
|
if not relay_conn.connected:
|
||||||
|
deleted_remote_hosts.add(hostname)
|
||||||
|
|
||||||
|
if deleted_remote_hosts:
|
||||||
|
logger.info(f"Removing {deleted_remote_hosts} from websocket broadcast list")
|
||||||
|
await asyncio.gather(*[self.cleanup_offline_host(h) for h in deleted_remote_hosts])
|
||||||
|
|
||||||
|
if new_remote_hosts:
|
||||||
|
logger.info(f"Adding {new_remote_hosts} to websocket broadcast list")
|
||||||
|
|
||||||
|
for h in new_remote_hosts:
|
||||||
|
stats = self.stats_mgr.new_remote_host_stats(h)
|
||||||
|
relay_connection = WebsocketRelayConnection(name=self.local_hostname, stats=stats, remote_host=self.known_hosts[h])
|
||||||
|
relay_connection.start()
|
||||||
|
self.relay_connections[h] = relay_connection
|
||||||
|
|
||||||
|
await asyncio.sleep(settings.BROADCAST_WEBSOCKET_NEW_INSTANCE_POLL_RATE_SECONDS)
|
||||||
|
finally:
|
||||||
|
if async_conn:
|
||||||
|
logger.info("Shutting down db connection for wsrelay.")
|
||||||
try:
|
try:
|
||||||
async_conn = await psycopg.AsyncConnection.connect(
|
await async_conn.close()
|
||||||
dbname=database_conf['NAME'],
|
|
||||||
host=database_conf['HOST'],
|
|
||||||
user=database_conf['USER'],
|
|
||||||
password=database_conf['PASSWORD'],
|
|
||||||
port=database_conf['PORT'],
|
|
||||||
**database_conf.get("OPTIONS", {}),
|
|
||||||
)
|
|
||||||
|
|
||||||
task = event_loop.create_task(self.on_ws_heartbeat(async_conn), name="on_ws_heartbeat")
|
|
||||||
logger.info("Creating `on_ws_heartbeat` task in event loop.")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to connect to database for pg_notify: {e}")
|
logger.info(f"Failed to close connection to database for pg_notify: {e}")
|
||||||
|
|
||||||
future_remote_hosts = self.known_hosts.keys()
|
|
||||||
current_remote_hosts = self.relay_connections.keys()
|
|
||||||
deleted_remote_hosts = set(current_remote_hosts) - set(future_remote_hosts)
|
|
||||||
new_remote_hosts = set(future_remote_hosts) - set(current_remote_hosts)
|
|
||||||
|
|
||||||
# This loop handles if we get an advertisement from a host we already know about but
|
|
||||||
# the advertisement has a different IP than we are currently connected to.
|
|
||||||
for hostname, address in self.known_hosts.items():
|
|
||||||
if hostname not in self.relay_connections:
|
|
||||||
# We've picked up a new hostname that we don't know about yet.
|
|
||||||
continue
|
|
||||||
|
|
||||||
if address != self.relay_connections[hostname].remote_host:
|
|
||||||
deleted_remote_hosts.add(hostname)
|
|
||||||
new_remote_hosts.add(hostname)
|
|
||||||
|
|
||||||
# Delete any hosts with closed connections
|
|
||||||
for hostname, relay_conn in self.relay_connections.items():
|
|
||||||
if not relay_conn.connected:
|
|
||||||
deleted_remote_hosts.add(hostname)
|
|
||||||
|
|
||||||
if deleted_remote_hosts:
|
|
||||||
logger.info(f"Removing {deleted_remote_hosts} from websocket broadcast list")
|
|
||||||
await asyncio.gather(*[self.cleanup_offline_host(h) for h in deleted_remote_hosts])
|
|
||||||
|
|
||||||
if new_remote_hosts:
|
|
||||||
logger.info(f"Adding {new_remote_hosts} to websocket broadcast list")
|
|
||||||
|
|
||||||
for h in new_remote_hosts:
|
|
||||||
stats = self.stats_mgr.new_remote_host_stats(h)
|
|
||||||
relay_connection = WebsocketRelayConnection(name=self.local_hostname, stats=stats, remote_host=self.known_hosts[h])
|
|
||||||
relay_connection.start()
|
|
||||||
self.relay_connections[h] = relay_connection
|
|
||||||
|
|
||||||
await asyncio.sleep(settings.BROADCAST_WEBSOCKET_NEW_INSTANCE_POLL_RATE_SECONDS)
|
|
||||||
|
|||||||
@@ -277,6 +277,9 @@ SESSION_COOKIE_SECURE = True
|
|||||||
# Note: This setting may be overridden by database settings.
|
# Note: This setting may be overridden by database settings.
|
||||||
SESSION_COOKIE_AGE = 1800
|
SESSION_COOKIE_AGE = 1800
|
||||||
|
|
||||||
|
# Option to change userLoggedIn cookie SameSite policy.
|
||||||
|
USER_COOKIE_SAMESITE = 'Lax'
|
||||||
|
|
||||||
# Name of the cookie that contains the session information.
|
# Name of the cookie that contains the session information.
|
||||||
# Note: Changing this value may require changes to any clients.
|
# Note: Changing this value may require changes to any clients.
|
||||||
SESSION_COOKIE_NAME = 'awx_sessionid'
|
SESSION_COOKIE_NAME = 'awx_sessionid'
|
||||||
@@ -355,6 +358,7 @@ INSTALLED_APPS = [
|
|||||||
'ansible_base.rest_filters',
|
'ansible_base.rest_filters',
|
||||||
'ansible_base.jwt_consumer',
|
'ansible_base.jwt_consumer',
|
||||||
'ansible_base.resource_registry',
|
'ansible_base.resource_registry',
|
||||||
|
'ansible_base.rbac',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -497,6 +501,12 @@ CACHES = {'default': {'BACKEND': 'awx.main.cache.AWXRedisCache', 'LOCATION': 'un
|
|||||||
SOCIAL_AUTH_STRATEGY = 'social_django.strategy.DjangoStrategy'
|
SOCIAL_AUTH_STRATEGY = 'social_django.strategy.DjangoStrategy'
|
||||||
SOCIAL_AUTH_STORAGE = 'social_django.models.DjangoStorage'
|
SOCIAL_AUTH_STORAGE = 'social_django.models.DjangoStorage'
|
||||||
SOCIAL_AUTH_USER_MODEL = 'auth.User'
|
SOCIAL_AUTH_USER_MODEL = 'auth.User'
|
||||||
|
ROLE_SINGLETON_USER_RELATIONSHIP = ''
|
||||||
|
ROLE_SINGLETON_TEAM_RELATIONSHIP = ''
|
||||||
|
|
||||||
|
# We want to short-circuit RBAC methods to get permission to system admins and auditors
|
||||||
|
ROLE_BYPASS_SUPERUSER_FLAGS = ['is_superuser']
|
||||||
|
ROLE_BYPASS_ACTION_FLAGS = {'view': 'is_system_auditor'}
|
||||||
|
|
||||||
_SOCIAL_AUTH_PIPELINE_BASE = (
|
_SOCIAL_AUTH_PIPELINE_BASE = (
|
||||||
'social_core.pipeline.social_auth.social_details',
|
'social_core.pipeline.social_auth.social_details',
|
||||||
@@ -849,7 +859,6 @@ LOGGING = {
|
|||||||
'json': {'()': 'awx.main.utils.formatters.LogstashFormatter'},
|
'json': {'()': 'awx.main.utils.formatters.LogstashFormatter'},
|
||||||
'timed_import': {'()': 'awx.main.utils.formatters.TimeFormatter', 'format': '%(relativeSeconds)9.3f %(levelname)-8s %(message)s'},
|
'timed_import': {'()': 'awx.main.utils.formatters.TimeFormatter', 'format': '%(relativeSeconds)9.3f %(levelname)-8s %(message)s'},
|
||||||
'dispatcher': {'format': '%(asctime)s %(levelname)-8s [%(guid)s] %(name)s PID:%(process)d %(message)s'},
|
'dispatcher': {'format': '%(asctime)s %(levelname)-8s [%(guid)s] %(name)s PID:%(process)d %(message)s'},
|
||||||
'job_lifecycle': {'()': 'awx.main.utils.formatters.JobLifeCycleFormatter'},
|
|
||||||
},
|
},
|
||||||
# Extended below based on install scenario. You probably don't want to add something directly here.
|
# Extended below based on install scenario. You probably don't want to add something directly here.
|
||||||
# See 'handler_config' below.
|
# See 'handler_config' below.
|
||||||
@@ -874,6 +883,7 @@ LOGGING = {
|
|||||||
'loggers': {
|
'loggers': {
|
||||||
'django': {'handlers': ['console']},
|
'django': {'handlers': ['console']},
|
||||||
'django.request': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'WARNING'},
|
'django.request': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'WARNING'},
|
||||||
|
'ansible_base': {'handlers': ['console', 'file', 'tower_warnings']},
|
||||||
'daphne': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'INFO'},
|
'daphne': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'INFO'},
|
||||||
'rest_framework.request': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'WARNING', 'propagate': False},
|
'rest_framework.request': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'WARNING', 'propagate': False},
|
||||||
'py.warnings': {'handlers': ['console']},
|
'py.warnings': {'handlers': ['console']},
|
||||||
@@ -917,7 +927,7 @@ handler_config = {
|
|||||||
'wsrelay': {'filename': 'wsrelay.log'},
|
'wsrelay': {'filename': 'wsrelay.log'},
|
||||||
'task_system': {'filename': 'task_system.log'},
|
'task_system': {'filename': 'task_system.log'},
|
||||||
'rbac_migrations': {'filename': 'tower_rbac_migrations.log'},
|
'rbac_migrations': {'filename': 'tower_rbac_migrations.log'},
|
||||||
'job_lifecycle': {'filename': 'job_lifecycle.log', 'formatter': 'job_lifecycle'},
|
'job_lifecycle': {'filename': 'job_lifecycle.log'},
|
||||||
'rsyslog_configurer': {'filename': 'rsyslog_configurer.log'},
|
'rsyslog_configurer': {'filename': 'rsyslog_configurer.log'},
|
||||||
'cache_clear': {'filename': 'cache_clear.log'},
|
'cache_clear': {'filename': 'cache_clear.log'},
|
||||||
'ws_heartbeat': {'filename': 'ws_heartbeat.log'},
|
'ws_heartbeat': {'filename': 'ws_heartbeat.log'},
|
||||||
@@ -1003,6 +1013,7 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'awx.main.middleware.DisableLocalAuthMiddleware',
|
'awx.main.middleware.DisableLocalAuthMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'awx.main.middleware.OptionalURLPrefixPath',
|
||||||
'awx.sso.middleware.SocialAuthMiddleware',
|
'awx.sso.middleware.SocialAuthMiddleware',
|
||||||
'crum.CurrentRequestUserMiddleware',
|
'crum.CurrentRequestUserMiddleware',
|
||||||
'awx.main.middleware.URLModificationMiddleware',
|
'awx.main.middleware.URLModificationMiddleware',
|
||||||
@@ -1121,13 +1132,55 @@ METRICS_SUBSYSTEM_CONFIG = {
|
|||||||
ANSIBLE_BASE_TEAM_MODEL = 'main.Team'
|
ANSIBLE_BASE_TEAM_MODEL = 'main.Team'
|
||||||
ANSIBLE_BASE_ORGANIZATION_MODEL = 'main.Organization'
|
ANSIBLE_BASE_ORGANIZATION_MODEL = 'main.Organization'
|
||||||
ANSIBLE_BASE_RESOURCE_CONFIG_MODULE = 'awx.resource_api'
|
ANSIBLE_BASE_RESOURCE_CONFIG_MODULE = 'awx.resource_api'
|
||||||
|
ANSIBLE_BASE_PERMISSION_MODEL = 'main.Permission'
|
||||||
|
|
||||||
from ansible_base.lib import dynamic_config # noqa: E402
|
from ansible_base.lib import dynamic_config # noqa: E402
|
||||||
|
|
||||||
settings_file = os.path.join(os.path.dirname(dynamic_config.__file__), 'dynamic_settings.py')
|
include(os.path.join(os.path.dirname(dynamic_config.__file__), 'dynamic_settings.py'))
|
||||||
include(settings_file)
|
|
||||||
|
|
||||||
# Add a postfix to the API URL patterns
|
# Add a postfix to the API URL patterns
|
||||||
# example if set to '' API pattern will be /api
|
# example if set to '' API pattern will be /api
|
||||||
# example if set to 'controller' API pattern will be /api AND /api/controller
|
# example if set to 'controller' API pattern will be /api AND /api/controller
|
||||||
OPTIONAL_API_URLPATTERN_PREFIX = ''
|
OPTIONAL_API_URLPATTERN_PREFIX = ''
|
||||||
|
|
||||||
|
# Use AWX base view, to give 401 on unauthenticated requests
|
||||||
|
ANSIBLE_BASE_CUSTOM_VIEW_PARENT = 'awx.api.generics.APIView'
|
||||||
|
|
||||||
|
# Settings for the ansible_base RBAC system
|
||||||
|
|
||||||
|
# Only used internally, names of the managed RoleDefinitions to create
|
||||||
|
ANSIBLE_BASE_ROLE_PRECREATE = {
|
||||||
|
'object_admin': '{cls.__name__} Admin',
|
||||||
|
'org_admin': 'Organization Admin',
|
||||||
|
'org_children': 'Organization {cls.__name__} Admin',
|
||||||
|
'special': '{cls.__name__} {action}',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Name for auto-created roles that give users permissions to what they create
|
||||||
|
ANSIBLE_BASE_ROLE_CREATOR_NAME = '{cls.__name__} Creator'
|
||||||
|
|
||||||
|
# Use the new Gateway RBAC system for evaluations? You should. We will remove the old system soon.
|
||||||
|
ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED = True
|
||||||
|
|
||||||
|
# Permissions a user will get when creating a new item
|
||||||
|
ANSIBLE_BASE_CREATOR_DEFAULTS = ['change', 'delete', 'execute', 'use', 'adhoc', 'approve', 'update', 'view']
|
||||||
|
|
||||||
|
# This is a stopgap, will delete after resource registry integration
|
||||||
|
ANSIBLE_BASE_SERVICE_PREFIX = "awx"
|
||||||
|
|
||||||
|
# Temporary, for old roles API compatibility, save child permissions at organization level
|
||||||
|
ANSIBLE_BASE_CACHE_PARENT_PERMISSIONS = True
|
||||||
|
|
||||||
|
# Currently features are enabled to keep compatibility with old system, except custom roles
|
||||||
|
ANSIBLE_BASE_ALLOW_TEAM_ORG_ADMIN = False
|
||||||
|
# ANSIBLE_BASE_ALLOW_CUSTOM_ROLES = True
|
||||||
|
ANSIBLE_BASE_ALLOW_CUSTOM_TEAM_ROLES = False
|
||||||
|
ANSIBLE_BASE_ALLOW_SINGLETON_USER_ROLES = True
|
||||||
|
ANSIBLE_BASE_ALLOW_SINGLETON_TEAM_ROLES = False # System auditor has always been restricted to users
|
||||||
|
ANSIBLE_BASE_ALLOW_SINGLETON_ROLES_API = False # Do not allow creating user-defined system-wide roles
|
||||||
|
|
||||||
|
# system username for django-ansible-base
|
||||||
|
SYSTEM_USERNAME = None
|
||||||
|
|
||||||
|
# Use AWX base view, to give 401 on unauthenticated requests
|
||||||
|
ANSIBLE_BASE_CUSTOM_VIEW_PARENT = 'awx.api.generics.APIView'
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ class CompleteView(BaseRedirectView):
|
|||||||
response = super(CompleteView, self).dispatch(request, *args, **kwargs)
|
response = super(CompleteView, self).dispatch(request, *args, **kwargs)
|
||||||
if self.request.user and self.request.user.is_authenticated:
|
if self.request.user and self.request.user.is_authenticated:
|
||||||
logger.info(smart_str(u"User {} logged in".format(self.request.user.username)))
|
logger.info(smart_str(u"User {} logged in".format(self.request.user.username)))
|
||||||
response.set_cookie('userLoggedIn', 'true', secure=getattr(settings, 'SESSION_COOKIE_SECURE', False))
|
response.set_cookie(
|
||||||
|
'userLoggedIn', 'true', secure=getattr(settings, 'SESSION_COOKIE_SECURE', False), samesite=getattr(settings, 'USER_COOKIE_SAMESITE', 'Lax')
|
||||||
|
)
|
||||||
response.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid'))
|
response.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid'))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="{% url 'api:login' %}?next={{ request.get_full_path }}" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="Log in"><span class="glyphicon glyphicon-log-in"></span>Log in</a></li>
|
<li><a href="{% url 'api:login' %}?next={{ request.get_full_path }}" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="Log in"><span class="glyphicon glyphicon-log-in"></span>Log in</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="//docs.ansible.com/ansible-tower/{{short_tower_version}}/html/towerapi/index.html" target="_blank" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="{% trans 'API Guide' %}"><span class="glyphicon glyphicon-question-sign"></span><span class="visible-xs-inline">{% trans 'API Guide' %}</span></a></li>
|
<li><a href="//ansible.readthedocs.io/projects/awx/en/latest/rest_api/index.html" target="_blank" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="{% trans 'API Guide' %}"><span class="glyphicon glyphicon-question-sign"></span><span class="visible-xs-inline">{% trans 'API Guide' %}</span></a></li>
|
||||||
<li><a href="/" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="{% trans 'Back to application' %}"><span class="glyphicon glyphicon-circle-arrow-left"></span><span class="visible-xs-inline">{% trans 'Back to application' %}</span></a></li>
|
<li><a href="/" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="{% trans 'Back to application' %}"><span class="glyphicon glyphicon-circle-arrow-left"></span><span class="visible-xs-inline">{% trans 'Back to application' %}</span></a></li>
|
||||||
<li class="hidden-xs"><a href="#" class="resize" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="{% trans 'Resize' %}"><span class="glyphicon glyphicon-resize-full"></span></a></li>
|
<li class="hidden-xs"><a href="#" class="resize" data-toggle="tooltip" data-placement="bottom" data-delay="1000" title="{% trans 'Resize' %}"><span class="glyphicon glyphicon-resize-full"></span></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import AssociateModal from 'components/AssociateModal';
|
|||||||
import ErrorDetail from 'components/ErrorDetail';
|
import ErrorDetail from 'components/ErrorDetail';
|
||||||
import AlertModal from 'components/AlertModal';
|
import AlertModal from 'components/AlertModal';
|
||||||
import useToast, { AlertVariant } from 'hooks/useToast';
|
import useToast, { AlertVariant } from 'hooks/useToast';
|
||||||
import { getQSConfig, parseQueryString, mergeParams } from 'util/qs';
|
import { getQSConfig, parseQueryString } from 'util/qs';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
||||||
import DataListToolbar from 'components/DataListToolbar';
|
import DataListToolbar from 'components/DataListToolbar';
|
||||||
@@ -106,62 +106,38 @@ function InstancePeerList({ setBreadcrumb }) {
|
|||||||
const { selected, isAllSelected, handleSelect, clearSelected, selectAll } =
|
const { selected, isAllSelected, handleSelect, clearSelected, selectAll } =
|
||||||
useSelected(peers);
|
useSelected(peers);
|
||||||
|
|
||||||
const fetchInstancesToAssociate = useCallback(
|
const fetchPeersToAssociate = useCallback(
|
||||||
async (params) => {
|
async (params) => {
|
||||||
const address_list = [];
|
const address_list = [];
|
||||||
|
|
||||||
const instances = await InstancesAPI.read(
|
// do not show this instance or instances that are already peered
|
||||||
mergeParams(params, {
|
// to this instance (reverse_peers)
|
||||||
...{ not__node_type: ['control', 'hybrid'] },
|
const not_instances = instance.reverse_peers;
|
||||||
})
|
not_instances.push(instance.id);
|
||||||
);
|
|
||||||
const receptors = (await ReceptorAPI.read()).data.results;
|
|
||||||
|
|
||||||
// get instance ids of the current peered receptor ids
|
params.not__instance = not_instances;
|
||||||
const already_peered_instance_ids = [];
|
params.is_internal = false;
|
||||||
for (let h = 0; h < instance.peers.length; h++) {
|
// do not show the current peers
|
||||||
const matched = receptors.filter((obj) => obj.id === instance.peers[h]);
|
if (instance.peers.length > 0) {
|
||||||
matched.forEach((element) => {
|
params.not__id__in = instance.peers.join(',');
|
||||||
already_peered_instance_ids.push(element.instance);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let q = 0; q < receptors.length; q++) {
|
const receptoraddresses = await ReceptorAPI.read(params);
|
||||||
const receptor = receptors[q];
|
|
||||||
|
|
||||||
if (already_peered_instance_ids.includes(receptor.instance)) {
|
// retrieve the instances that are associated with those receptor addresses
|
||||||
// ignore reverse peers
|
const instance_ids = receptoraddresses.data.results.map(
|
||||||
continue;
|
(obj) => obj.instance
|
||||||
}
|
);
|
||||||
|
const instance_ids_str = instance_ids.join(',');
|
||||||
|
const instances = await InstancesAPI.read({ id__in: instance_ids_str });
|
||||||
|
|
||||||
if (instance.peers.includes(receptor.id)) {
|
for (let q = 0; q < receptoraddresses.data.results.length; q++) {
|
||||||
// no links to existing links
|
const receptor = receptoraddresses.data.results[q];
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instance.id === receptor.instance) {
|
|
||||||
// no links to thy self
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instance.managed) {
|
|
||||||
// no managed nodes
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const host = instances.data.results.filter(
|
const host = instances.data.results.filter(
|
||||||
(obj) => obj.id === receptor.instance
|
(obj) => obj.id === receptor.instance
|
||||||
)[0];
|
)[0];
|
||||||
|
|
||||||
if (host === undefined) {
|
|
||||||
// no hosts
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receptor.is_internal) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const copy = receptor;
|
const copy = receptor;
|
||||||
copy.hostname = host.hostname;
|
copy.hostname = host.hostname;
|
||||||
copy.node_type = host.node_type;
|
copy.node_type = host.node_type;
|
||||||
@@ -169,9 +145,9 @@ function InstancePeerList({ setBreadcrumb }) {
|
|||||||
address_list.push(copy);
|
address_list.push(copy);
|
||||||
}
|
}
|
||||||
|
|
||||||
instances.data.results = address_list;
|
receptoraddresses.data.results = address_list;
|
||||||
|
|
||||||
return instances;
|
return receptoraddresses;
|
||||||
},
|
},
|
||||||
[instance]
|
[instance]
|
||||||
);
|
);
|
||||||
@@ -191,7 +167,7 @@ function InstancePeerList({ setBreadcrumb }) {
|
|||||||
fetchPeers();
|
fetchPeers();
|
||||||
addToast({
|
addToast({
|
||||||
id: instancesPeerToAssociate,
|
id: instancesPeerToAssociate,
|
||||||
title: t`Please be sure to run the install bundle for the selected instance(s) again in order to see changes take effect.`,
|
title: t`Please be sure to run the install bundle for ${instance.hostname} again in order to see changes take effect.`,
|
||||||
variant: AlertVariant.success,
|
variant: AlertVariant.success,
|
||||||
hasTimeout: true,
|
hasTimeout: true,
|
||||||
});
|
});
|
||||||
@@ -315,13 +291,13 @@ function InstancePeerList({ setBreadcrumb }) {
|
|||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<AssociateModal
|
<AssociateModal
|
||||||
header={t`Instances`}
|
header={t`Instances`}
|
||||||
fetchRequest={fetchInstancesToAssociate}
|
fetchRequest={fetchPeersToAssociate}
|
||||||
isModalOpen={isModalOpen}
|
isModalOpen={isModalOpen}
|
||||||
onAssociate={handlePeerAssociate}
|
onAssociate={handlePeerAssociate}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsModalOpen(false)}
|
||||||
title={t`Select Peer Addresses`}
|
title={t`Select Peer Addresses`}
|
||||||
optionsRequest={readInstancesOptions}
|
optionsRequest={readInstancesOptions}
|
||||||
displayKey="hostname"
|
displayKey="address"
|
||||||
columns={[
|
columns={[
|
||||||
{ key: 'hostname', name: t`Name` },
|
{ key: 'hostname', name: t`Name` },
|
||||||
{ key: 'address', name: t`Address` },
|
{ key: 'address', name: t`Address` },
|
||||||
|
|||||||
@@ -78,12 +78,14 @@ function MiscAuthenticationEdit() {
|
|||||||
default: OAUTH2_PROVIDER_OPTIONS.default.ACCESS_TOKEN_EXPIRE_SECONDS,
|
default: OAUTH2_PROVIDER_OPTIONS.default.ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||||
type: OAUTH2_PROVIDER_OPTIONS.child.type,
|
type: OAUTH2_PROVIDER_OPTIONS.child.type,
|
||||||
label: t`Access Token Expiration`,
|
label: t`Access Token Expiration`,
|
||||||
|
help_text: t`Access Token Expiration in seconds`,
|
||||||
},
|
},
|
||||||
REFRESH_TOKEN_EXPIRE_SECONDS: {
|
REFRESH_TOKEN_EXPIRE_SECONDS: {
|
||||||
...OAUTH2_PROVIDER_OPTIONS,
|
...OAUTH2_PROVIDER_OPTIONS,
|
||||||
default: OAUTH2_PROVIDER_OPTIONS.default.REFRESH_TOKEN_EXPIRE_SECONDS,
|
default: OAUTH2_PROVIDER_OPTIONS.default.REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||||
type: OAUTH2_PROVIDER_OPTIONS.child.type,
|
type: OAUTH2_PROVIDER_OPTIONS.child.type,
|
||||||
label: t`Refresh Token Expiration`,
|
label: t`Refresh Token Expiration`,
|
||||||
|
help_text: t`Refresh Token Expiration in seconds`,
|
||||||
},
|
},
|
||||||
AUTHORIZATION_CODE_EXPIRE_SECONDS: {
|
AUTHORIZATION_CODE_EXPIRE_SECONDS: {
|
||||||
...OAUTH2_PROVIDER_OPTIONS,
|
...OAUTH2_PROVIDER_OPTIONS,
|
||||||
@@ -91,6 +93,7 @@ function MiscAuthenticationEdit() {
|
|||||||
OAUTH2_PROVIDER_OPTIONS.default.AUTHORIZATION_CODE_EXPIRE_SECONDS,
|
OAUTH2_PROVIDER_OPTIONS.default.AUTHORIZATION_CODE_EXPIRE_SECONDS,
|
||||||
type: OAUTH2_PROVIDER_OPTIONS.child.type,
|
type: OAUTH2_PROVIDER_OPTIONS.child.type,
|
||||||
label: t`Authorization Code Expiration`,
|
label: t`Authorization Code Expiration`,
|
||||||
|
help_text: t`Authorization Code Expiration in seconds`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
63
awx/urls.py
63
awx/urls.py
@@ -2,43 +2,54 @@
|
|||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import path, re_path, include
|
from django.urls import re_path, include, path
|
||||||
|
|
||||||
|
from ansible_base.lib.dynamic_config.dynamic_urls import api_urls, api_version_urls, root_urls
|
||||||
|
|
||||||
from ansible_base.resource_registry.urls import urlpatterns as resource_api_urls
|
from ansible_base.resource_registry.urls import urlpatterns as resource_api_urls
|
||||||
|
|
||||||
from awx.main.views import handle_400, handle_403, handle_404, handle_500, handle_csp_violation, handle_login_redirect
|
from awx.main.views import handle_400, handle_403, handle_404, handle_500, handle_csp_violation, handle_login_redirect
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
def get_urlpatterns(prefix=None):
|
||||||
re_path(r'', include('awx.ui.urls', namespace='ui')),
|
if not prefix:
|
||||||
re_path(r'^ui_next/.*', include('awx.ui_next.urls', namespace='ui_next')),
|
prefix = '/'
|
||||||
path('api/', include('awx.api.urls', namespace='api')),
|
else:
|
||||||
]
|
prefix = f'/{prefix}/'
|
||||||
|
|
||||||
if settings.OPTIONAL_API_URLPATTERN_PREFIX:
|
urlpatterns = [
|
||||||
urlpatterns += [
|
re_path(r'', include('awx.ui.urls', namespace='ui')),
|
||||||
path(f'api/{settings.OPTIONAL_API_URLPATTERN_PREFIX}/', include('awx.api.urls')),
|
re_path(r'^ui_next/.*', include('awx.ui_next.urls', namespace='ui_next')),
|
||||||
|
path(f'api{prefix}', include('awx.api.urls', namespace='api')),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
re_path(r'^api/v2/', include(resource_api_urls)),
|
path(f'api{prefix}v2/', include(resource_api_urls)),
|
||||||
re_path(r'^sso/', include('awx.sso.urls', namespace='sso')),
|
path(f'api{prefix}v2/', include(api_version_urls)),
|
||||||
re_path(r'^sso/', include('social_django.urls', namespace='social')),
|
path(f'api{prefix}', include(api_urls)),
|
||||||
re_path(r'^(?:api/)?400.html$', handle_400),
|
path('', include(root_urls)),
|
||||||
re_path(r'^(?:api/)?403.html$', handle_403),
|
re_path(r'^sso/', include('awx.sso.urls', namespace='sso')),
|
||||||
re_path(r'^(?:api/)?404.html$', handle_404),
|
re_path(r'^sso/', include('social_django.urls', namespace='social')),
|
||||||
re_path(r'^(?:api/)?500.html$', handle_500),
|
re_path(r'^(?:api/)?400.html$', handle_400),
|
||||||
re_path(r'^csp-violation/', handle_csp_violation),
|
re_path(r'^(?:api/)?403.html$', handle_403),
|
||||||
re_path(r'^login/', handle_login_redirect),
|
re_path(r'^(?:api/)?404.html$', handle_404),
|
||||||
]
|
re_path(r'^(?:api/)?500.html$', handle_500),
|
||||||
|
re_path(r'^csp-violation/', handle_csp_violation),
|
||||||
|
re_path(r'^login/', handle_login_redirect),
|
||||||
|
]
|
||||||
|
|
||||||
if settings.SETTINGS_MODULE == 'awx.settings.development':
|
if settings.SETTINGS_MODULE == 'awx.settings.development':
|
||||||
try:
|
try:
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
|
|
||||||
urlpatterns += [re_path(r'^__debug__/', include(debug_toolbar.urls))]
|
urlpatterns += [re_path(r'^__debug__/', include(debug_toolbar.urls))]
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
return urlpatterns
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = get_urlpatterns()
|
||||||
|
|
||||||
handler400 = 'awx.main.views.handle_400'
|
handler400 = 'awx.main.views.handle_400'
|
||||||
handler403 = 'awx.main.views.handle_403'
|
handler403 = 'awx.main.views.handle_403'
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ Notable releases of the `awx.awx` collection:
|
|||||||
- 7.0.0 is intended to be identical to the content prior to the migration, aside from changes necessary to function as a collection.
|
- 7.0.0 is intended to be identical to the content prior to the migration, aside from changes necessary to function as a collection.
|
||||||
- 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/).
|
- 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/).
|
||||||
- 19.2.1 large renaming purged "tower" names (like options and module names), adding redirects for old names
|
- 19.2.1 large renaming purged "tower" names (like options and module names), adding redirects for old names
|
||||||
|
- 21.11.0 "tower" modules deprecated and symlinks removed.
|
||||||
- X.X.X added support of named URLs to all modules. Anywhere that previously accepted name or id can also support named URLs
|
- X.X.X added support of named URLs to all modules. Anywhere that previously accepted name or id can also support named URLs
|
||||||
- 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable.
|
- 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable.
|
||||||
|
|
||||||
@@ -112,7 +113,7 @@ Ansible source, set up a dedicated virtual environment:
|
|||||||
|
|
||||||
```
|
```
|
||||||
mkvirtualenv my_new_venv
|
mkvirtualenv my_new_venv
|
||||||
# may need to replace psycopg2 with psycopg2-binary in requirements/requirements.txt
|
# may need to replace psycopg3 with psycopg3-binary in requirements/requirements.txt
|
||||||
pip install -r requirements/requirements.txt -r requirements/requirements_dev.txt -r requirements/requirements_git.txt
|
pip install -r requirements/requirements.txt -r requirements/requirements_dev.txt -r requirements/requirements_git.txt
|
||||||
make clean-api
|
make clean-api
|
||||||
pip install -e <path to your Ansible>
|
pip install -e <path to your Ansible>
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ action_groups:
|
|||||||
- project
|
- project
|
||||||
- project_update
|
- project_update
|
||||||
- role
|
- role
|
||||||
|
- role_definition
|
||||||
|
- role_team_assignment
|
||||||
|
- role_user_assignment
|
||||||
- schedule
|
- schedule
|
||||||
- settings
|
- settings
|
||||||
- subscriptions
|
- subscriptions
|
||||||
|
|||||||
@@ -652,7 +652,7 @@ class ControllerAPIModule(ControllerModule):
|
|||||||
# If we have neither of these, then we can try un-authenticated access
|
# If we have neither of these, then we can try un-authenticated access
|
||||||
self.authenticated = True
|
self.authenticated = True
|
||||||
|
|
||||||
def delete_if_needed(self, existing_item, on_delete=None, auto_exit=True):
|
def delete_if_needed(self, existing_item, item_type=None, on_delete=None, auto_exit=True):
|
||||||
# This will exit from the module on its own.
|
# This will exit from the module on its own.
|
||||||
# If the method successfully deletes an item and on_delete param is defined,
|
# If the method successfully deletes an item and on_delete param is defined,
|
||||||
# the on_delete parameter will be called as a method pasing in this object and the json from the response
|
# the on_delete parameter will be called as a method pasing in this object and the json from the response
|
||||||
@@ -664,8 +664,9 @@ class ControllerAPIModule(ControllerModule):
|
|||||||
# If we have an item, we can try to delete it
|
# If we have an item, we can try to delete it
|
||||||
try:
|
try:
|
||||||
item_url = existing_item['url']
|
item_url = existing_item['url']
|
||||||
item_type = existing_item['type']
|
|
||||||
item_id = existing_item['id']
|
item_id = existing_item['id']
|
||||||
|
if not item_type:
|
||||||
|
item_type = existing_item['type']
|
||||||
item_name = self.get_item_name(existing_item, allow_unknown=True)
|
item_name = self.get_item_name(existing_item, allow_unknown=True)
|
||||||
except KeyError as ke:
|
except KeyError as ke:
|
||||||
self.fail_json(msg="Unable to process delete of item due to missing data {0}".format(ke))
|
self.fail_json(msg="Unable to process delete of item due to missing data {0}".format(ke))
|
||||||
@@ -907,7 +908,7 @@ class ControllerAPIModule(ControllerModule):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update_if_needed(self, existing_item, new_item, on_update=None, auto_exit=True, associations=None):
|
def update_if_needed(self, existing_item, new_item, item_type=None, on_update=None, auto_exit=True, associations=None):
|
||||||
# This will exit from the module on its own
|
# This will exit from the module on its own
|
||||||
# If the method successfully updates an item and on_update param is defined,
|
# If the method successfully updates an item and on_update param is defined,
|
||||||
# the on_update parameter will be called as a method pasing in this object and the json from the response
|
# the on_update parameter will be called as a method pasing in this object and the json from the response
|
||||||
@@ -921,7 +922,8 @@ class ControllerAPIModule(ControllerModule):
|
|||||||
# If we have an item, we can see if it needs an update
|
# If we have an item, we can see if it needs an update
|
||||||
try:
|
try:
|
||||||
item_url = existing_item['url']
|
item_url = existing_item['url']
|
||||||
item_type = existing_item['type']
|
if not item_type:
|
||||||
|
item_type = existing_item['type']
|
||||||
if item_type == 'user':
|
if item_type == 'user':
|
||||||
item_name = existing_item['username']
|
item_name = existing_item['username']
|
||||||
elif item_type == 'workflow_job_template_node':
|
elif item_type == 'workflow_job_template_node':
|
||||||
@@ -990,7 +992,7 @@ class ControllerAPIModule(ControllerModule):
|
|||||||
new_item.pop(key)
|
new_item.pop(key)
|
||||||
|
|
||||||
if existing_item:
|
if existing_item:
|
||||||
return self.update_if_needed(existing_item, new_item, on_update=on_update, auto_exit=auto_exit, associations=associations)
|
return self.update_if_needed(existing_item, new_item, item_type=item_type, on_update=on_update, auto_exit=auto_exit, associations=associations)
|
||||||
else:
|
else:
|
||||||
return self.create_if_needed(
|
return self.create_if_needed(
|
||||||
existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, auto_exit=auto_exit, associations=associations
|
existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, auto_exit=auto_exit, associations=associations
|
||||||
|
|||||||
@@ -147,8 +147,12 @@ def main():
|
|||||||
if redirect_uris is not None:
|
if redirect_uris is not None:
|
||||||
application_fields['redirect_uris'] = ' '.join(redirect_uris)
|
application_fields['redirect_uris'] = ' '.join(redirect_uris)
|
||||||
|
|
||||||
# If the state was present and we can let the module build or update the existing application, this will return on its own
|
response = module.create_or_update_if_needed(application, application_fields, endpoint='applications', item_type='application', auto_exit=False)
|
||||||
module.create_or_update_if_needed(application, application_fields, endpoint='applications', item_type='application')
|
if 'client_id' in response:
|
||||||
|
module.json_output['client_id'] = response['client_id']
|
||||||
|
if 'client_secret' in response:
|
||||||
|
module.json_output['client_secret'] = response['client_secret']
|
||||||
|
module.exit_json(**module.json_output)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
114
awx_collection/plugins/modules/role_definition.py
Normal file
114
awx_collection/plugins/modules/role_definition.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# coding: utf-8 -*-
|
||||||
|
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'}
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: role_definition
|
||||||
|
author: "Seth Foster (@fosterseth)"
|
||||||
|
short_description: Add role definition to Automation Platform Controller
|
||||||
|
description:
|
||||||
|
- Contains a list of permissions and a resource type that can then be assigned to users or teams.
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of this role definition.
|
||||||
|
required: True
|
||||||
|
type: str
|
||||||
|
permissions:
|
||||||
|
description:
|
||||||
|
- List of permissions to include in the role definition.
|
||||||
|
required: True
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
content_type:
|
||||||
|
description:
|
||||||
|
- The type of resource this applies to.
|
||||||
|
required: True
|
||||||
|
type: str
|
||||||
|
description:
|
||||||
|
description:
|
||||||
|
- Optional description of this role definition.
|
||||||
|
type: str
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- The desired state of the role definition.
|
||||||
|
default: present
|
||||||
|
choices:
|
||||||
|
- present
|
||||||
|
- absent
|
||||||
|
type: str
|
||||||
|
extends_documentation_fragment: awx.awx.auth
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Create Role Definition
|
||||||
|
role_definition:
|
||||||
|
name: test_view_jt
|
||||||
|
permissions:
|
||||||
|
- awx.view_jobtemplate
|
||||||
|
- awx.execute_jobtemplate
|
||||||
|
content_type: awx.jobtemplate
|
||||||
|
description: role definition to view and execute jt
|
||||||
|
state: present
|
||||||
|
'''
|
||||||
|
|
||||||
|
from ..module_utils.controller_api import ControllerAPIModule
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Any additional arguments that are not fields of the item can be added here
|
||||||
|
argument_spec = dict(
|
||||||
|
name=dict(required=True, type='str'),
|
||||||
|
permissions=dict(required=True, type='list', elements='str'),
|
||||||
|
content_type=dict(required=True, type='str'),
|
||||||
|
description=dict(required=False, type='str'),
|
||||||
|
state=dict(default='present', choices=['present', 'absent']),
|
||||||
|
)
|
||||||
|
|
||||||
|
module = ControllerAPIModule(argument_spec=argument_spec)
|
||||||
|
|
||||||
|
name = module.params.get('name')
|
||||||
|
permissions = module.params.get('permissions')
|
||||||
|
content_type = module.params.get('content_type')
|
||||||
|
description = module.params.get('description')
|
||||||
|
state = module.params.get('state')
|
||||||
|
if description is None:
|
||||||
|
description = ''
|
||||||
|
|
||||||
|
role_definition = module.get_one('role_definitions', name_or_id=name)
|
||||||
|
|
||||||
|
if state == 'absent':
|
||||||
|
module.delete_if_needed(
|
||||||
|
role_definition,
|
||||||
|
item_type='role_definition',
|
||||||
|
)
|
||||||
|
|
||||||
|
post_kwargs = {
|
||||||
|
'name': name,
|
||||||
|
'permissions': permissions,
|
||||||
|
'content_type': content_type,
|
||||||
|
'description': description
|
||||||
|
}
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
module.create_or_update_if_needed(
|
||||||
|
role_definition,
|
||||||
|
post_kwargs,
|
||||||
|
endpoint='role_definitions',
|
||||||
|
item_type='role_definition',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
123
awx_collection/plugins/modules/role_team_assignment.py
Normal file
123
awx_collection/plugins/modules/role_team_assignment.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# coding: utf-8 -*-
|
||||||
|
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'}
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: role_team_assignment
|
||||||
|
author: "Seth Foster (@fosterseth)"
|
||||||
|
short_description: Gives a team permission to a resource or an organization.
|
||||||
|
description:
|
||||||
|
- Use this endpoint to give a team permission to a resource or an organization.
|
||||||
|
- After creation, the assignment cannot be edited, but can be deleted to remove those permissions.
|
||||||
|
options:
|
||||||
|
role_definition:
|
||||||
|
description:
|
||||||
|
- The name or id of the role definition to assign to the team.
|
||||||
|
required: True
|
||||||
|
type: str
|
||||||
|
object_id:
|
||||||
|
description:
|
||||||
|
- Primary key of the object this assignment applies to.
|
||||||
|
required: True
|
||||||
|
type: int
|
||||||
|
team:
|
||||||
|
description:
|
||||||
|
- The name or id of the team to assign to the object.
|
||||||
|
required: False
|
||||||
|
type: str
|
||||||
|
object_ansible_id:
|
||||||
|
description:
|
||||||
|
- Resource id of the object this role applies to. Alternative to the object_id field.
|
||||||
|
required: False
|
||||||
|
type: int
|
||||||
|
team_ansible_id:
|
||||||
|
description:
|
||||||
|
- Resource id of the team who will receive permissions from this assignment. Alternative to team field.
|
||||||
|
required: False
|
||||||
|
type: int
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- The desired state of the role definition.
|
||||||
|
default: present
|
||||||
|
choices:
|
||||||
|
- present
|
||||||
|
- absent
|
||||||
|
type: str
|
||||||
|
extends_documentation_fragment: awx.awx.auth
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Give Team A JT permissions
|
||||||
|
role_team_assignment:
|
||||||
|
role_definition: launch JT
|
||||||
|
object_id: 1
|
||||||
|
team: Team A
|
||||||
|
state: present
|
||||||
|
'''
|
||||||
|
|
||||||
|
from ..module_utils.controller_api import ControllerAPIModule
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Any additional arguments that are not fields of the item can be added here
|
||||||
|
argument_spec = dict(
|
||||||
|
team=dict(required=False, type='str'),
|
||||||
|
object_id=dict(required=True, type='int'),
|
||||||
|
role_definition=dict(required=True, type='str'),
|
||||||
|
object_ansible_id=dict(required=False, type='int'),
|
||||||
|
team_ansible_id=dict(required=False, type='int'),
|
||||||
|
state=dict(default='present', choices=['present', 'absent']),
|
||||||
|
)
|
||||||
|
|
||||||
|
module = ControllerAPIModule(argument_spec=argument_spec)
|
||||||
|
|
||||||
|
team = module.params.get('team')
|
||||||
|
object_id = module.params.get('object_id')
|
||||||
|
role_definition_str = module.params.get('role_definition')
|
||||||
|
object_ansible_id = module.params.get('object_ansible_id')
|
||||||
|
team_ansible_id = module.params.get('team_ansible_id')
|
||||||
|
state = module.params.get('state')
|
||||||
|
|
||||||
|
role_definition = module.get_one('role_definitions', allow_none=False, name_or_id=role_definition_str)
|
||||||
|
team = module.get_one('teams', allow_none=False, name_or_id=team)
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
'role_definition': role_definition['id'],
|
||||||
|
'object_id': object_id,
|
||||||
|
'team': team['id'],
|
||||||
|
'object_ansible_id': object_ansible_id,
|
||||||
|
'team_ansible_id': team_ansible_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
# get rid of None type values
|
||||||
|
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
||||||
|
role_team_assignment = module.get_one('role_team_assignments', **{'data': kwargs})
|
||||||
|
|
||||||
|
if state == 'absent':
|
||||||
|
module.delete_if_needed(
|
||||||
|
role_team_assignment,
|
||||||
|
item_type='role_team_assignment',
|
||||||
|
)
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
module.create_if_needed(
|
||||||
|
role_team_assignment,
|
||||||
|
kwargs,
|
||||||
|
endpoint='role_team_assignments',
|
||||||
|
item_type='role_team_assignment',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
124
awx_collection/plugins/modules/role_user_assignment.py
Normal file
124
awx_collection/plugins/modules/role_user_assignment.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# coding: utf-8 -*-
|
||||||
|
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'}
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: role_user_assignment
|
||||||
|
author: "Seth Foster (@fosterseth)"
|
||||||
|
short_description: Gives a user permission to a resource or an organization.
|
||||||
|
description:
|
||||||
|
- Use this endpoint to give a user permission to a resource or an organization.
|
||||||
|
- After creation, the assignment cannot be edited, but can be deleted to remove those permissions.
|
||||||
|
options:
|
||||||
|
role_definition:
|
||||||
|
description:
|
||||||
|
- The name or id of the role definition to assign to the user.
|
||||||
|
required: True
|
||||||
|
type: str
|
||||||
|
object_id:
|
||||||
|
description:
|
||||||
|
- Primary key of the object this assignment applies to.
|
||||||
|
required: True
|
||||||
|
type: int
|
||||||
|
user:
|
||||||
|
description:
|
||||||
|
- The name or id of the user to assign to the object.
|
||||||
|
required: False
|
||||||
|
type: str
|
||||||
|
object_ansible_id:
|
||||||
|
description:
|
||||||
|
- Resource id of the object this role applies to. Alternative to the object_id field.
|
||||||
|
required: False
|
||||||
|
type: int
|
||||||
|
user_ansible_id:
|
||||||
|
description:
|
||||||
|
- Resource id of the user who will receive permissions from this assignment. Alternative to user field.
|
||||||
|
required: False
|
||||||
|
type: int
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- The desired state of the role definition.
|
||||||
|
default: present
|
||||||
|
choices:
|
||||||
|
- present
|
||||||
|
- absent
|
||||||
|
type: str
|
||||||
|
extends_documentation_fragment: awx.awx.auth
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Give Bob JT permissions
|
||||||
|
role_user_assignment:
|
||||||
|
role_definition: launch JT
|
||||||
|
object_id: 1
|
||||||
|
user: bob
|
||||||
|
state: present
|
||||||
|
'''
|
||||||
|
|
||||||
|
from ..module_utils.controller_api import ControllerAPIModule
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Any additional arguments that are not fields of the item can be added here
|
||||||
|
argument_spec = dict(
|
||||||
|
user=dict(required=False, type='str'),
|
||||||
|
object_id=dict(required=True, type='int'),
|
||||||
|
role_definition=dict(required=True, type='str'),
|
||||||
|
object_ansible_id=dict(required=False, type='int'),
|
||||||
|
user_ansible_id=dict(required=False, type='int'),
|
||||||
|
state=dict(default='present', choices=['present', 'absent']),
|
||||||
|
)
|
||||||
|
|
||||||
|
module = ControllerAPIModule(argument_spec=argument_spec)
|
||||||
|
|
||||||
|
user = module.params.get('user')
|
||||||
|
object_id = module.params.get('object_id')
|
||||||
|
role_definition_str = module.params.get('role_definition')
|
||||||
|
object_ansible_id = module.params.get('object_ansible_id')
|
||||||
|
user_ansible_id = module.params.get('user_ansible_id')
|
||||||
|
state = module.params.get('state')
|
||||||
|
|
||||||
|
role_definition = module.get_one('role_definitions', allow_none=False, name_or_id=role_definition_str)
|
||||||
|
user = module.get_one('users', allow_none=False, name_or_id=user)
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
'role_definition': role_definition['id'],
|
||||||
|
'object_id': object_id,
|
||||||
|
'user': user['id'],
|
||||||
|
'object_ansible_id': object_ansible_id,
|
||||||
|
'user_ansible_id': user_ansible_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
# get rid of None type values
|
||||||
|
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
||||||
|
|
||||||
|
role_user_assignment = module.get_one('role_user_assignments', **{'data': kwargs})
|
||||||
|
|
||||||
|
if state == 'absent':
|
||||||
|
module.delete_if_needed(
|
||||||
|
role_user_assignment,
|
||||||
|
item_type='role_user_assignment',
|
||||||
|
)
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
module.create_if_needed(
|
||||||
|
role_user_assignment,
|
||||||
|
kwargs,
|
||||||
|
endpoint='role_user_assignments',
|
||||||
|
item_type='role_user_assignment',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -39,6 +39,16 @@ options:
|
|||||||
description:
|
description:
|
||||||
- Limit to use for the I(job_template).
|
- Limit to use for the I(job_template).
|
||||||
type: str
|
type: str
|
||||||
|
tags:
|
||||||
|
description:
|
||||||
|
- Specific tags to apply from the I(job_template).
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
skip_tags:
|
||||||
|
description:
|
||||||
|
- Specific tags to skip from the I(job_template).
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
scm_branch:
|
scm_branch:
|
||||||
description:
|
description:
|
||||||
- A specific branch of the SCM project to run the template on.
|
- A specific branch of the SCM project to run the template on.
|
||||||
@@ -100,6 +110,8 @@ def main():
|
|||||||
organization=dict(),
|
organization=dict(),
|
||||||
inventory=dict(),
|
inventory=dict(),
|
||||||
limit=dict(),
|
limit=dict(),
|
||||||
|
tags=dict(type='list', elements='str'),
|
||||||
|
skip_tags=dict(type='list', elements='str'),
|
||||||
scm_branch=dict(),
|
scm_branch=dict(),
|
||||||
extra_vars=dict(type='dict'),
|
extra_vars=dict(type='dict'),
|
||||||
wait=dict(required=False, default=True, type='bool'),
|
wait=dict(required=False, default=True, type='bool'),
|
||||||
@@ -128,6 +140,14 @@ def main():
|
|||||||
if field_val is not None:
|
if field_val is not None:
|
||||||
optional_args[field_name] = field_val
|
optional_args[field_name] = field_val
|
||||||
|
|
||||||
|
# Special treatment of tags parameters
|
||||||
|
job_tags = module.params.get('tags')
|
||||||
|
if job_tags is not None:
|
||||||
|
optional_args['job_tags'] = ",".join(job_tags)
|
||||||
|
skip_tags = module.params.get('skip_tags')
|
||||||
|
if skip_tags is not None:
|
||||||
|
optional_args['skip_tags'] = ",".join(skip_tags)
|
||||||
|
|
||||||
# Create a datastructure to pass into our job launch
|
# Create a datastructure to pass into our job launch
|
||||||
post_data = {}
|
post_data = {}
|
||||||
for arg_name, arg_value in optional_args.items():
|
for arg_name, arg_value in optional_args.items():
|
||||||
@@ -152,6 +172,8 @@ def main():
|
|||||||
check_vars_to_prompts = {
|
check_vars_to_prompts = {
|
||||||
'inventory': 'ask_inventory_on_launch',
|
'inventory': 'ask_inventory_on_launch',
|
||||||
'limit': 'ask_limit_on_launch',
|
'limit': 'ask_limit_on_launch',
|
||||||
|
'job_tags': 'ask_tags_on_launch',
|
||||||
|
'skip_tags': 'ask_skip_tags_on_launch',
|
||||||
'scm_branch': 'ask_scm_branch_on_launch',
|
'scm_branch': 'ask_scm_branch_on_launch',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import pytest
|
|||||||
|
|
||||||
from ansible.module_utils.six import raise_from
|
from ansible.module_utils.six import raise_from
|
||||||
|
|
||||||
|
from ansible_base.rbac.models import RoleDefinition, DABPermission
|
||||||
from awx.main.tests.functional.conftest import _request
|
from awx.main.tests.functional.conftest import _request
|
||||||
from awx.main.tests.functional.conftest import credentialtype_scm, credentialtype_ssh # noqa: F401; pylint: disable=unused-variable
|
from awx.main.tests.functional.conftest import credentialtype_scm, credentialtype_ssh # noqa: F401; pylint: disable=unused-variable
|
||||||
from awx.main.models import (
|
from awx.main.models import (
|
||||||
@@ -31,9 +32,11 @@ from awx.main.models import (
|
|||||||
WorkflowJobTemplate,
|
WorkflowJobTemplate,
|
||||||
NotificationTemplate,
|
NotificationTemplate,
|
||||||
Schedule,
|
Schedule,
|
||||||
|
Team,
|
||||||
)
|
)
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
|
||||||
HAS_TOWER_CLI = False
|
HAS_TOWER_CLI = False
|
||||||
@@ -258,6 +261,11 @@ def job_template(project, inventory):
|
|||||||
return JobTemplate.objects.create(name='test-jt', project=project, inventory=inventory, playbook='helloworld.yml')
|
return JobTemplate.objects.create(name='test-jt', project=project, inventory=inventory, playbook='helloworld.yml')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def team(organization):
|
||||||
|
return Team.objects.create(name='test-team', organization=organization)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def machine_credential(credentialtype_ssh, organization): # noqa: F811
|
def machine_credential(credentialtype_ssh, organization): # noqa: F811
|
||||||
return Credential.objects.create(credential_type=credentialtype_ssh, name='machine-cred', inputs={'username': 'test_user', 'password': 'pas4word'})
|
return Credential.objects.create(credential_type=credentialtype_ssh, name='machine-cred', inputs={'username': 'test_user', 'password': 'pas4word'})
|
||||||
@@ -331,6 +339,15 @@ def notification_template(organization):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def job_template_role_definition():
|
||||||
|
rd = RoleDefinition.objects.create(name='test_view_jt', content_type=ContentType.objects.get_for_model(JobTemplate))
|
||||||
|
permission_codenames = ['view_jobtemplate', 'execute_jobtemplate']
|
||||||
|
permissions = DABPermission.objects.filter(codename__in=permission_codenames)
|
||||||
|
rd.permissions.add(*permissions)
|
||||||
|
return rd
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def scm_credential(credentialtype_scm, organization): # noqa: F811
|
def scm_credential(credentialtype_scm, organization): # noqa: F811
|
||||||
return Credential.objects.create(
|
return Credential.objects.create(
|
||||||
|
|||||||
@@ -155,4 +155,4 @@ def test_build_notification_message_undefined(run_module, admin_user, organizati
|
|||||||
nt = NotificationTemplate.objects.get(id=result['id'])
|
nt = NotificationTemplate.objects.get(id=result['id'])
|
||||||
|
|
||||||
body = job.build_notification_message(nt, 'running')
|
body = job.build_notification_message(nt, 'running')
|
||||||
assert 'The template rendering return a blank body' in body[1]
|
assert '{"started_by": "My Placeholder"}' in body[1]
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ def test_grant_organization_permission(run_module, admin_user, organization, sta
|
|||||||
assert not result.get('failed', False), result.get('msg', result)
|
assert not result.get('failed', False), result.get('msg', result)
|
||||||
|
|
||||||
if state == 'present':
|
if state == 'present':
|
||||||
assert rando in organization.execute_role
|
assert rando in organization.admin_role
|
||||||
else:
|
else:
|
||||||
assert rando not in organization.execute_role
|
assert rando not in organization.admin_role
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
122
awx_collection/test/awx/test_role_definition.py
Normal file
122
awx_collection/test/awx/test_role_definition.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ansible_base.rbac.models import RoleDefinition
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_create_new(run_module, admin_user):
|
||||||
|
result = run_module(
|
||||||
|
'role_definition',
|
||||||
|
{
|
||||||
|
'name': 'test_view_jt',
|
||||||
|
'permissions': ['awx.view_jobtemplate', 'awx.execute_jobtemplate'],
|
||||||
|
'content_type': 'awx.jobtemplate',
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
assert result['changed']
|
||||||
|
|
||||||
|
role_definition = RoleDefinition.objects.get(name='test_view_jt')
|
||||||
|
assert role_definition
|
||||||
|
permission_codenames = [p.codename for p in role_definition.permissions.all()]
|
||||||
|
assert set(permission_codenames) == set(['view_jobtemplate', 'execute_jobtemplate'])
|
||||||
|
assert role_definition.content_type.model == 'jobtemplate'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_update_existing(run_module, admin_user):
|
||||||
|
result = run_module(
|
||||||
|
'role_definition',
|
||||||
|
{
|
||||||
|
'name': 'test_view_jt',
|
||||||
|
'permissions': ['awx.view_jobtemplate'],
|
||||||
|
'content_type': 'awx.jobtemplate',
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
|
||||||
|
assert result['changed']
|
||||||
|
|
||||||
|
role_definition = RoleDefinition.objects.get(name='test_view_jt')
|
||||||
|
permission_codenames = [p.codename for p in role_definition.permissions.all()]
|
||||||
|
assert set(permission_codenames) == set(['view_jobtemplate'])
|
||||||
|
assert role_definition.content_type.model == 'jobtemplate'
|
||||||
|
|
||||||
|
result = run_module(
|
||||||
|
'role_definition',
|
||||||
|
{
|
||||||
|
'name': 'test_view_jt',
|
||||||
|
'permissions': ['awx.view_jobtemplate', 'awx.execute_jobtemplate'],
|
||||||
|
'content_type': 'awx.jobtemplate',
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
|
||||||
|
assert result['changed']
|
||||||
|
|
||||||
|
role_definition.refresh_from_db()
|
||||||
|
permission_codenames = [p.codename for p in role_definition.permissions.all()]
|
||||||
|
assert set(permission_codenames) == set(['view_jobtemplate', 'execute_jobtemplate'])
|
||||||
|
assert role_definition.content_type.model == 'jobtemplate'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_delete_existing(run_module, admin_user):
|
||||||
|
result = run_module(
|
||||||
|
'role_definition',
|
||||||
|
{
|
||||||
|
'name': 'test_view_jt',
|
||||||
|
'permissions': ['awx.view_jobtemplate', 'awx.execute_jobtemplate'],
|
||||||
|
'content_type': 'awx.jobtemplate',
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
|
||||||
|
assert result['changed']
|
||||||
|
|
||||||
|
role_definition = RoleDefinition.objects.get(name='test_view_jt')
|
||||||
|
assert role_definition
|
||||||
|
|
||||||
|
result = run_module(
|
||||||
|
'role_definition',
|
||||||
|
{
|
||||||
|
'name': 'test_view_jt',
|
||||||
|
'permissions': ['awx.view_jobtemplate', 'awx.execute_jobtemplate'],
|
||||||
|
'content_type': 'awx.jobtemplate',
|
||||||
|
'state': 'absent',
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
|
||||||
|
assert result['changed']
|
||||||
|
|
||||||
|
with pytest.raises(RoleDefinition.DoesNotExist):
|
||||||
|
role_definition.refresh_from_db()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_idempotence(run_module, admin_user):
|
||||||
|
result = run_module(
|
||||||
|
'role_definition',
|
||||||
|
{
|
||||||
|
'name': 'test_view_jt',
|
||||||
|
'permissions': ['awx.view_jobtemplate', 'awx.execute_jobtemplate'],
|
||||||
|
'content_type': 'awx.jobtemplate',
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
|
||||||
|
assert result['changed']
|
||||||
|
|
||||||
|
result = run_module(
|
||||||
|
'role_definition',
|
||||||
|
{
|
||||||
|
'name': 'test_view_jt',
|
||||||
|
'permissions': ['awx.view_jobtemplate', 'awx.execute_jobtemplate'],
|
||||||
|
'content_type': 'awx.jobtemplate',
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
|
||||||
|
assert not result['changed']
|
||||||
|
|
||||||
|
role_definition = RoleDefinition.objects.get(name='test_view_jt')
|
||||||
|
permission_codenames = [p.codename for p in role_definition.permissions.all()]
|
||||||
|
assert set(permission_codenames) == set(['view_jobtemplate', 'execute_jobtemplate'])
|
||||||
70
awx_collection/test/awx/test_role_team_assignment.py
Normal file
70
awx_collection/test/awx/test_role_team_assignment.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ansible_base.rbac.models import RoleTeamAssignment
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_create_new(run_module, admin_user, team, job_template, job_template_role_definition):
|
||||||
|
result = run_module(
|
||||||
|
'role_team_assignment',
|
||||||
|
{
|
||||||
|
'team': team.name,
|
||||||
|
'object_id': job_template.id,
|
||||||
|
'role_definition': job_template_role_definition.name,
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
assert result['changed']
|
||||||
|
assert RoleTeamAssignment.objects.filter(team=team, object_id=job_template.id, role_definition=job_template_role_definition).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_idempotence(run_module, admin_user, team, job_template, job_template_role_definition):
|
||||||
|
result = run_module(
|
||||||
|
'role_team_assignment',
|
||||||
|
{
|
||||||
|
'team': team.name,
|
||||||
|
'object_id': job_template.id,
|
||||||
|
'role_definition': job_template_role_definition.name,
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
assert result['changed']
|
||||||
|
|
||||||
|
result = run_module(
|
||||||
|
'role_team_assignment',
|
||||||
|
{
|
||||||
|
'team': team.name,
|
||||||
|
'object_id': job_template.id,
|
||||||
|
'role_definition': job_template_role_definition.name,
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
assert not result['changed']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_delete_existing(run_module, admin_user, team, job_template, job_template_role_definition):
|
||||||
|
result = run_module(
|
||||||
|
'role_team_assignment',
|
||||||
|
{
|
||||||
|
'team': team.name,
|
||||||
|
'object_id': job_template.id,
|
||||||
|
'role_definition': job_template_role_definition.name,
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
assert result['changed']
|
||||||
|
assert RoleTeamAssignment.objects.filter(team=team, object_id=job_template.id, role_definition=job_template_role_definition).exists()
|
||||||
|
|
||||||
|
result = run_module(
|
||||||
|
'role_team_assignment',
|
||||||
|
{
|
||||||
|
'team': team.name,
|
||||||
|
'object_id': job_template.id,
|
||||||
|
'role_definition': job_template_role_definition.name,
|
||||||
|
'state': 'absent'
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
assert result['changed']
|
||||||
|
assert not RoleTeamAssignment.objects.filter(team=team, object_id=job_template.id, role_definition=job_template_role_definition).exists()
|
||||||
70
awx_collection/test/awx/test_role_user_assignment.py
Normal file
70
awx_collection/test/awx/test_role_user_assignment.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ansible_base.rbac.models import RoleUserAssignment
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_create_new(run_module, admin_user, job_template, job_template_role_definition):
|
||||||
|
result = run_module(
|
||||||
|
'role_user_assignment',
|
||||||
|
{
|
||||||
|
'user': admin_user.username,
|
||||||
|
'object_id': job_template.id,
|
||||||
|
'role_definition': job_template_role_definition.name,
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
assert result['changed']
|
||||||
|
assert RoleUserAssignment.objects.filter(user=admin_user, object_id=job_template.id, role_definition=job_template_role_definition).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_idempotence(run_module, admin_user, job_template, job_template_role_definition):
|
||||||
|
result = run_module(
|
||||||
|
'role_user_assignment',
|
||||||
|
{
|
||||||
|
'user': admin_user.username,
|
||||||
|
'object_id': job_template.id,
|
||||||
|
'role_definition': job_template_role_definition.name,
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
assert result['changed']
|
||||||
|
|
||||||
|
result = run_module(
|
||||||
|
'role_user_assignment',
|
||||||
|
{
|
||||||
|
'user': admin_user.username,
|
||||||
|
'object_id': job_template.id,
|
||||||
|
'role_definition': job_template_role_definition.name,
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
assert not result['changed']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_delete_existing(run_module, admin_user, job_template, job_template_role_definition):
|
||||||
|
result = run_module(
|
||||||
|
'role_user_assignment',
|
||||||
|
{
|
||||||
|
'user': admin_user.username,
|
||||||
|
'object_id': job_template.id,
|
||||||
|
'role_definition': job_template_role_definition.name,
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
assert result['changed']
|
||||||
|
assert RoleUserAssignment.objects.filter(user=admin_user, object_id=job_template.id, role_definition=job_template_role_definition).exists()
|
||||||
|
|
||||||
|
result = run_module(
|
||||||
|
'role_user_assignment',
|
||||||
|
{
|
||||||
|
'user': admin_user.username,
|
||||||
|
'object_id': job_template.id,
|
||||||
|
'role_definition': job_template_role_definition.name,
|
||||||
|
'state': 'absent'
|
||||||
|
},
|
||||||
|
admin_user)
|
||||||
|
assert result['changed']
|
||||||
|
assert not RoleUserAssignment.objects.filter(user=admin_user, object_id=job_template.id, role_definition=job_template_role_definition).exists()
|
||||||
@@ -4,7 +4,7 @@ __metaclass__ = type
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from awx.main.models import WorkflowJobTemplate, NotificationTemplate
|
from awx.main.models import WorkflowJobTemplate, WorkflowJob, NotificationTemplate
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -135,6 +135,37 @@ def test_associate_only_on_success(run_module, admin_user, organization, project
|
|||||||
assert list(wfjt.notification_templates_error.values_list('id', flat=True)) == [nt1.id]
|
assert list(wfjt.notification_templates_error.values_list('id', flat=True)) == [nt1.id]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_workflow_launch_with_prompting(run_module, admin_user, organization, inventory):
|
||||||
|
WorkflowJobTemplate.objects.create(
|
||||||
|
name='foo-workflow-launch-test',
|
||||||
|
organization=organization,
|
||||||
|
ask_variables_on_launch=True,
|
||||||
|
ask_inventory_on_launch=True,
|
||||||
|
ask_tags_on_launch=True,
|
||||||
|
ask_skip_tags_on_launch=True,
|
||||||
|
)
|
||||||
|
result = run_module(
|
||||||
|
'workflow_launch',
|
||||||
|
dict(
|
||||||
|
name='foo-workflow-launch-test',
|
||||||
|
inventory=inventory.name,
|
||||||
|
wait=False,
|
||||||
|
extra_vars={"var1": "My First Variable", "var2": "My Second Variable", "var3": "My Third Variable"},
|
||||||
|
tags=["my_tag"],
|
||||||
|
skip_tags=["your_tag", "their_tag"],
|
||||||
|
),
|
||||||
|
admin_user,
|
||||||
|
)
|
||||||
|
assert result.get('changed', True), result
|
||||||
|
|
||||||
|
job = WorkflowJob.objects.get(id=result['id'])
|
||||||
|
assert job.extra_vars == '{"var1": "My First Variable", "var2": "My Second Variable", "var3": "My Third Variable"}'
|
||||||
|
assert job.inventory == inventory
|
||||||
|
assert job.job_tags == "my_tag"
|
||||||
|
assert job.skip_tags == "your_tag,their_tag"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_delete_with_spec(run_module, admin_user, organization, survey_spec):
|
def test_delete_with_spec(run_module, admin_user, organization, survey_spec):
|
||||||
WorkflowJobTemplate.objects.create(organization=organization, name='foo-workflow', survey_enabled=True, survey_spec=survey_spec)
|
WorkflowJobTemplate.objects.create(organization=organization, name='foo-workflow', survey_enabled=True, survey_spec=survey_spec)
|
||||||
|
|||||||
@@ -1,63 +1,63 @@
|
|||||||
---
|
---
|
||||||
- name: Generate a random string for test
|
- name: Generate a random string for test
|
||||||
set_fact:
|
ansible.builtin.set_fact:
|
||||||
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||||
when: test_id is not defined
|
when: test_id is not defined
|
||||||
|
|
||||||
- name: Generate names
|
- name: Generate names
|
||||||
set_fact:
|
ansible.builtin.set_fact:
|
||||||
inv_name: "AWX-Collection-tests-ad_hoc_command_cancel-inventory-{{ test_id }}"
|
inv_name: "AWX-Collection-tests-ad_hoc_command_cancel-inventory-{{ test_id }}"
|
||||||
ssh_cred_name: "AWX-Collection-tests-ad_hoc_command_cancel-ssh-cred-{{ test_id }}"
|
ssh_cred_name: "AWX-Collection-tests-ad_hoc_command_cancel-ssh-cred-{{ test_id }}"
|
||||||
org_name: "AWX-Collection-tests-ad_hoc_command_cancel-org-{{ test_id }}"
|
org_name: "AWX-Collection-tests-ad_hoc_command_cancel-org-{{ test_id }}"
|
||||||
|
|
||||||
- name: Create a New Organization
|
- name: Create a New Organization
|
||||||
organization:
|
awx.awx.organization:
|
||||||
name: "{{ org_name }}"
|
name: "{{ org_name }}"
|
||||||
|
|
||||||
- name: Create an Inventory
|
- name: Create an Inventory
|
||||||
inventory:
|
awx.awx.inventory:
|
||||||
name: "{{ inv_name }}"
|
name: "{{ inv_name }}"
|
||||||
organization: "{{ org_name }}"
|
organization: "{{ org_name }}"
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
- name: Add localhost to the Inventory
|
- name: Add localhost to the Inventory
|
||||||
host:
|
awx.awx.host:
|
||||||
name: localhost
|
name: localhost
|
||||||
inventory: "{{ inv_name }}"
|
inventory: "{{ inv_name }}"
|
||||||
variables:
|
variables:
|
||||||
ansible_connection: local
|
ansible_connection: local
|
||||||
|
|
||||||
- name: Create a Credential
|
- name: Create a Credential
|
||||||
credential:
|
awx.awx.credential:
|
||||||
name: "{{ ssh_cred_name }}"
|
name: "{{ ssh_cred_name }}"
|
||||||
organization: "{{ org_name }}"
|
organization: "{{ org_name }}"
|
||||||
credential_type: 'Machine'
|
credential_type: 'Machine'
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
- name: Launch an Ad Hoc Command
|
- name: Launch an Ad Hoc Command
|
||||||
ad_hoc_command:
|
awx.awx.ad_hoc_command:
|
||||||
inventory: "{{ inv_name }}"
|
inventory: "{{ inv_name }}"
|
||||||
credential: "{{ ssh_cred_name }}"
|
credential: "{{ ssh_cred_name }}"
|
||||||
module_name: "command"
|
module_name: "command"
|
||||||
module_args: "sleep 100"
|
module_args: "sleep 100"
|
||||||
register: command
|
register: command
|
||||||
|
|
||||||
- assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- "command is changed"
|
- "command is changed"
|
||||||
|
|
||||||
- name: Cancel the command
|
- name: Cancel the command
|
||||||
ad_hoc_command_cancel:
|
awx.awx.ad_hoc_command_cancel:
|
||||||
command_id: "{{ command.id }}"
|
command_id: "{{ command.id }}"
|
||||||
request_timeout: 60
|
request_timeout: 60
|
||||||
register: results
|
register: results
|
||||||
|
|
||||||
- assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- results is changed
|
- results is changed
|
||||||
|
|
||||||
- name: "Wait for up to a minute until the job enters the can_cancel: False state"
|
- name: "Wait for up to a minute until the job enters the can_cancel: False state"
|
||||||
debug:
|
ansible.builtin.debug:
|
||||||
msg: "The job can_cancel status has transitioned into False, we can proceed with testing"
|
msg: "The job can_cancel status has transitioned into False, we can proceed with testing"
|
||||||
until: not job_status
|
until: not job_status
|
||||||
retries: 6
|
retries: 6
|
||||||
@@ -66,51 +66,51 @@
|
|||||||
job_status: "{{ lookup('awx.awx.controller_api', 'ad_hoc_commands/'+ command.id | string +'/cancel')['can_cancel'] }}"
|
job_status: "{{ lookup('awx.awx.controller_api', 'ad_hoc_commands/'+ command.id | string +'/cancel')['can_cancel'] }}"
|
||||||
|
|
||||||
- name: Cancel the command with hard error if it's not running
|
- name: Cancel the command with hard error if it's not running
|
||||||
ad_hoc_command_cancel:
|
awx.awx.ad_hoc_command_cancel:
|
||||||
command_id: "{{ command.id }}"
|
command_id: "{{ command.id }}"
|
||||||
fail_if_not_running: true
|
fail_if_not_running: true
|
||||||
register: results
|
register: results
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
|
|
||||||
- assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- results is failed
|
- results is failed
|
||||||
|
|
||||||
- name: Cancel an already canceled command (assert failure)
|
- name: Cancel an already canceled command (assert failure)
|
||||||
ad_hoc_command_cancel:
|
awx.awx.ad_hoc_command_cancel:
|
||||||
command_id: "{{ command.id }}"
|
command_id: "{{ command.id }}"
|
||||||
fail_if_not_running: true
|
fail_if_not_running: true
|
||||||
register: results
|
register: results
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
|
|
||||||
- assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- results is failed
|
- results is failed
|
||||||
|
|
||||||
- name: Check module fails with correct msg
|
- name: Check module fails with correct msg
|
||||||
ad_hoc_command_cancel:
|
awx.awx.ad_hoc_command_cancel:
|
||||||
command_id: 9999999999
|
command_id: 9999999999
|
||||||
register: result
|
register: result
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
|
|
||||||
- assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- "result.msg == 'Unable to find command with id 9999999999'"
|
- "result.msg == 'Unable to find command with id 9999999999'"
|
||||||
|
|
||||||
- name: Delete the Credential
|
- name: Delete the Credential
|
||||||
credential:
|
awx.awx.credential:
|
||||||
name: "{{ ssh_cred_name }}"
|
name: "{{ ssh_cred_name }}"
|
||||||
organization: "{{ org_name }}"
|
organization: "{{ org_name }}"
|
||||||
credential_type: 'Machine'
|
credential_type: 'Machine'
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
- name: Delete the Inventory
|
- name: Delete the Inventory
|
||||||
inventory:
|
awx.awx.inventory:
|
||||||
name: "{{ inv_name }}"
|
name: "{{ inv_name }}"
|
||||||
organization: "{{ org_name }}"
|
organization: "{{ org_name }}"
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
- name: Remove the Organization
|
- name: Remove the Organization
|
||||||
organization:
|
awx.awx.organization:
|
||||||
name: "{{ org_name }}"
|
name: "{{ org_name }}"
|
||||||
state: absent
|
state: absent
|
||||||
|
|||||||
@@ -103,6 +103,7 @@
|
|||||||
- assert:
|
- assert:
|
||||||
that:
|
that:
|
||||||
- "result is changed"
|
- "result is changed"
|
||||||
|
- "'client_secret' in result"
|
||||||
|
|
||||||
- name: Rename an inventory
|
- name: Rename an inventory
|
||||||
application:
|
application:
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
---
|
---
|
||||||
- name: Generate a test ID
|
- name: Generate a test ID
|
||||||
set_fact:
|
ansible.builtin.set_fact:
|
||||||
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||||
when: test_id is not defined
|
when: test_id is not defined
|
||||||
|
|
||||||
- name: Generate hostnames
|
- name: Generate hostnames
|
||||||
set_fact:
|
ansible.builtin.set_fact:
|
||||||
hostname1: "AWX-Collection-tests-instance1.{{ test_id }}.example.com"
|
hostname1: "AWX-Collection-tests-instance1.{{ test_id }}.example.com"
|
||||||
hostname2: "AWX-Collection-tests-instance2.{{ test_id }}.example.com"
|
hostname2: "AWX-Collection-tests-instance2.{{ test_id }}.example.com"
|
||||||
hostname3: "AWX-Collection-tests-instance3.{{ test_id }}.example.com"
|
hostname3: "AWX-Collection-tests-instance3.{{ test_id }}.example.com"
|
||||||
register: facts
|
register: facts
|
||||||
|
|
||||||
- name: Get the k8s setting
|
- name: Get the k8s setting
|
||||||
set_fact:
|
ansible.builtin.set_fact:
|
||||||
IS_K8S: "{{ controller_settings['IS_K8S'] | default(False) }}"
|
IS_K8S: "{{ controller_settings['IS_K8S'] | default(False) }}"
|
||||||
vars:
|
vars:
|
||||||
controller_settings: "{{ lookup('awx.awx.controller_api', 'settings/all') }}"
|
controller_settings: "{{ lookup('awx.awx.controller_api', 'settings/all') }}"
|
||||||
|
|
||||||
- debug:
|
- ansible.builtin.debug:
|
||||||
msg: "Skipping instance test since this is instance is not running on a K8s platform"
|
msg: "Skipping instance test since this is instance is not running on a K8s platform"
|
||||||
when: not IS_K8S
|
when: not IS_K8S
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
- "{{ hostname2 }}"
|
- "{{ hostname2 }}"
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
- assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- result is changed
|
- result is changed
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
capacity_adjustment: 0.4
|
capacity_adjustment: 0.4
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
- assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- result is changed
|
- result is changed
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
capacity_adjustment: 0.7
|
capacity_adjustment: 0.7
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
- assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- result is changed
|
- result is changed
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
node_state: installed
|
node_state: installed
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
- assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- result is changed
|
- result is changed
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
node_state: installed
|
node_state: installed
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
- assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- result is changed
|
- result is changed
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
- "{{ hostname2 }}"
|
- "{{ hostname2 }}"
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
- assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- result is changed
|
- result is changed
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
peers: []
|
peers: []
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
- assert:
|
- ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- result is changed
|
- result is changed
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user