diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000000..d6a5be7470 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,53 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 90 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 30 + +# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) +onlyLabels: [] + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - security + - planned + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: false + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: false + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: false + +# Label to use when marking as stale +staleLabel: stale + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 30 +# Limit to only `issues` or `pulls` +only: pulls + +pulls: + markComment: |- + This pull request has been marked 'stale' due to lack of recent activity. If there is no further activity, the PR will be closed in another 30 days. Thank you for your contribution! + + unmarkComment: >- + This pull request is no longer marked for closure. + + closeComment: >- + This pull request has been closed due to inactivity. If you feel this is in error, please reopen the pull request or file a new PR with the relevant details. + +issues: + markComment: |- + This issue has been marked 'stale' due to lack of recent activity. If there is no further activity, the issue will be closed in another 30 days. Thank you for your contribution! + + unmarkComment: >- + This issue is no longer marked for closure. + + closeComment: >- + This issue has been closed due to inactivity. If you feel this is in error, please reopen the issue or file a new issue with the relevant details. diff --git a/.github/template_gitref b/.github/template_gitref index ac18707d39..aa506a230a 100644 --- a/.github/template_gitref +++ b/.github/template_gitref @@ -1 +1 @@ -2021.08.26-132-g4a6cd66 +2021.08.26-139-g0d40f35 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4f2edb28e..7c364a7321 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ --- name: Galaxy CI -on: {pull_request: {branches: ['*']}, push: {branches: ['*']}} +on: {pull_request: {branches: ['**']}, push: {branches: ['**']}} jobs: check_commit: diff --git a/.github/workflows/ci_insights.yml b/.github/workflows/ci_insights.yml index f41cb91ad0..3c8b34b556 100644 --- a/.github/workflows/ci_insights.yml +++ b/.github/workflows/ci_insights.yml @@ -3,14 +3,14 @@ name: Insights on: pull_request: branches: - - '*' + - '**' paths-ignore: - 'docs/**' - 'mkdocs.yml' - 'CHANGES/**' push: branches: - - '*' + - '**' workflow_dispatch: jobs: diff --git a/.github/workflows/ci_standalone-community.yml b/.github/workflows/ci_standalone-community.yml index 58baa8bfaf..e40320a843 100644 --- a/.github/workflows/ci_standalone-community.yml +++ b/.github/workflows/ci_standalone-community.yml @@ -3,14 +3,14 @@ name: Standalone Community on: pull_request: branches: - - '*' + - '**' paths-ignore: - 'docs/**' - 'mkdocs.yml' - 'CHANGES/**' push: branches: - - '*' + - '**' workflow_dispatch: jobs: diff --git a/.github/workflows/ci_standalone-ldap.yml b/.github/workflows/ci_standalone-ldap.yml index ef3cfe1669..0122882780 100644 --- a/.github/workflows/ci_standalone-ldap.yml +++ b/.github/workflows/ci_standalone-ldap.yml @@ -3,14 +3,14 @@ name: Standalone LDAP on: pull_request: branches: - - '*' + - '**' paths-ignore: - 'docs/**' - 'mkdocs.yml' - 'CHANGES/**' push: branches: - - '*' + - '**' workflow_dispatch: jobs: diff --git a/.github/workflows/ci_standalone-rbac-roles.yml b/.github/workflows/ci_standalone-rbac-roles.yml new file mode 100644 index 0000000000..547ac6a7b8 --- /dev/null +++ b/.github/workflows/ci_standalone-rbac-roles.yml @@ -0,0 +1,76 @@ +--- +name: Standalone RBAC Roles +on: + pull_request: + branches: + - '**' + paths-ignore: + - 'docs/**' + - 'mkdocs.yml' + - 'CHANGES/**' + push: + branches: + - '**' + workflow_dispatch: + +jobs: + + integration: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-python@v2 + with: + python-version: "3.8" + + - name: Update apt + run: sudo apt -y update + + - name: Install LDAP requirements + run: sudo apt-get install -y libsasl2-dev python-dev libldap2-dev libssl-dev build-essential + + - name: Install docker-compose + run: pip3 install --upgrade docker-compose + + - name: collect system info + run: whoami; id; pwd; ls -al; uname -a ; df -h .; mount ; cat /etc/issue; docker --version ; ps aux | fgrep -i docker; ls -al /var/run/containerd/containerd.sock + + - name: clone the hub ui repo + run: git clone https://github.com/ansible/ansible-hub-ui /tmp/ansible-hub-ui + + - name: create the .compose.env file + run: rm -f .compose.env; cp .compose.env.example .compose.env + + - name: workaround github worker permissions issues + run: sed -i.bak 's/PIP_EDITABLE_INSTALL=1/PIP_EDITABLE_INSTALL=0/' .compose.env + + - name: workaround github worker permissions issues + run: sed -i.bak 's/WITH_DEV_INSTALL=1/WITH_DEV_INSTALL=0/' .compose.env + + - name: set the hub ui path in .compose.env + run: echo "ANSIBLE_HUB_UI_PATH='/tmp/ansible-hub-ui'" >> .compose.env + + - name: disable approval setting override + run: sed -i.bak 's/PULP_GALAXY_REQUIRE_CONTENT_APPROVAL/#PULP_GALAXY_REQUIRE_CONTENT_APPROVAL/' dev/standalone/galaxy_ng.env + + - name: build stack + run: make docker/build + + - name: run migrations + run: make docker/migrate + + - name: load translations + run: make docker/translations + + - name: start the compose stack + run: ./compose up -d + + - name: give stack some time to spin up + run: sleep 120 + + - name: set keyring on staging repo for signature upload + run: ./compose exec -T api ./entrypoint.sh manage set-repo-keyring --repository staging --keyring /etc/pulp/certs/galaxy.kbx -y + + - name: run the integration tests + run: HUB_LOCAL=1 ./dev/standalone-rbac-roles/RUN_INTEGRATION.sh diff --git a/.github/workflows/ci_standalone.yml b/.github/workflows/ci_standalone.yml index 5251f611d3..37e8c25e2e 100644 --- a/.github/workflows/ci_standalone.yml +++ b/.github/workflows/ci_standalone.yml @@ -3,14 +3,14 @@ name: Standalone on: pull_request: branches: - - '*' + - '**' paths-ignore: - 'docs/**' - 'mkdocs.yml' - 'CHANGES/**' push: branches: - - '*' + - '**' workflow_dispatch: jobs: diff --git a/.github/workflows/scripts/before_install.sh b/.github/workflows/scripts/before_install.sh index d46fa6a24b..26b1a7a231 100755 --- a/.github/workflows/scripts/before_install.sh +++ b/.github/workflows/scripts/before_install.sh @@ -111,7 +111,7 @@ fi -git clone --depth=1 https://github.com/pulp/pulpcore.git --branch 3.18.1 +git clone --depth=1 https://github.com/pulp/pulpcore.git --branch 3.20.0 cd pulpcore @@ -122,7 +122,7 @@ fi cd .. -git clone --depth=1 https://github.com/pulp/pulp_ansible.git --branch 0.13.0 +git clone --depth=1 https://github.com/pulp/pulp_ansible.git --branch 0.14.0 cd pulp_ansible if [ -n "$PULP_ANSIBLE_PR_NUMBER" ]; then @@ -132,7 +132,7 @@ fi cd .. -git clone --depth=1 https://github.com/pulp/pulp_container.git --branch 2.10.2 +git clone --depth=1 https://github.com/pulp/pulp_container.git --branch 2.13.1 cd pulp_container if [ -n "$PULP_CONTAINER_PR_NUMBER" ]; then @@ -152,8 +152,6 @@ fi cd .. - - # Intall requirements for ansible playbooks pip install docker netaddr boto3 ansible diff --git a/.github/workflows/scripts/install.sh b/.github/workflows/scripts/install.sh index 764865df02..488b23e227 100755 --- a/.github/workflows/scripts/install.sh +++ b/.github/workflows/scripts/install.sh @@ -28,24 +28,22 @@ pip install -r functest_requirements.txt cd .ci/ansible/ TAG=ci_build - if [ -e $REPO_ROOT/../pulp_ansible ]; then PULP_ANSIBLE=./pulp_ansible else - PULP_ANSIBLE=git+https://github.com/pulp/pulp_ansible.git@0.13.0 + PULP_ANSIBLE=git+https://github.com/pulp/pulp_ansible.git@0.14.0 fi - if [ -e $REPO_ROOT/../pulp_container ]; then PULP_CONTAINER=./pulp_container else - PULP_CONTAINER=git+https://github.com/pulp/pulp_container.git@2.10.2 + PULP_CONTAINER=git+https://github.com/pulp/pulp_container.git@2.13.1 fi - if [ -e $REPO_ROOT/../galaxy-importer ]; then GALAXY_IMPORTER=./galaxy-importer else GALAXY_IMPORTER=git+https://github.com/ansible/galaxy-importer.git@v0.4.5 fi +PULPCORE=./pulpcore if [[ "$TEST" == "plugin-from-pypi" ]]; then PLUGIN_NAME=galaxy_ng elif [[ "${RELEASE_WORKFLOW:-false}" == "true" ]]; then @@ -87,7 +85,7 @@ plugins: - name: galaxy-importer source: $GALAXY_IMPORTER - name: pulpcore - source: ./pulpcore + source: "${PULPCORE}" VARSYAML fi diff --git a/.github/workflows/scripts/script.sh b/.github/workflows/scripts/script.sh index 4bea4da748..c9ed2681ea 100755 --- a/.github/workflows/scripts/script.sh +++ b/.github/workflows/scripts/script.sh @@ -121,8 +121,6 @@ export PYTHONPATH=$REPO_ROOT/../pulp_container${PYTHONPATH:+:${PYTHONPATH}} export PYTHONPATH=$REPO_ROOT/../galaxy-importer${PYTHONPATH:+:${PYTHONPATH}} export PYTHONPATH=$REPO_ROOT${PYTHONPATH:+:${PYTHONPATH}} - - if [[ "$TEST" == "performance" ]]; then if [[ -z ${PERFORMANCE_TEST+x} ]]; then pytest -vv -r sx --color=yes --pyargs --capture=no --durations=0 galaxy_ng.tests.performance @@ -137,13 +135,13 @@ if [ -f $FUNC_TEST_SCRIPT ]; then else if [[ "$GITHUB_WORKFLOW" == "Galaxy Nightly CI/CD" ]]; then - pytest -v -r sx --color=yes --suppress-no-test-exit-code --pyargs galaxy_ng.tests.functional -m parallel -n 8 - pytest -v -r sx --color=yes --pyargs galaxy_ng.tests.functional -m "not parallel" + pytest -v -r sx --color=yes --suppress-no-test-exit-code --pyargs galaxy_ng.tests.functional -m parallel -n 8 --nightly + pytest -v -r sx --color=yes --pyargs galaxy_ng.tests.functional -m "not parallel" --nightly else - pytest -v -r sx --color=yes --suppress-no-test-exit-code --pyargs galaxy_ng.tests.functional -m "parallel and not nightly" -n 8 - pytest -v -r sx --color=yes --pyargs galaxy_ng.tests.functional -m "not parallel and not nightly" + pytest -v -r sx --color=yes --suppress-no-test-exit-code --pyargs galaxy_ng.tests.functional -m parallel -n 8 + pytest -v -r sx --color=yes --pyargs galaxy_ng.tests.functional -m "not parallel" fi diff --git a/CHANGES/1092.misc b/CHANGES/1092.misc new file mode 100644 index 0000000000..857e6a6d12 --- /dev/null +++ b/CHANGES/1092.misc @@ -0,0 +1 @@ +Create galaxy_ng specific Roles \ No newline at end of file diff --git a/CHANGES/1093.misc b/CHANGES/1093.misc new file mode 100644 index 0000000000..b90a363290 --- /dev/null +++ b/CHANGES/1093.misc @@ -0,0 +1 @@ +Removing django-guardian and migrating to RBAC Roles \ No newline at end of file diff --git a/CHANGES/1128.misc b/CHANGES/1128.misc new file mode 100644 index 0000000000..fc6325376a --- /dev/null +++ b/CHANGES/1128.misc @@ -0,0 +1 @@ +Migration to move Permissions from Groups to custom Roles, filtering against Galaxy locked Roles where applicable \ No newline at end of file diff --git a/CHANGES/1526.misc b/CHANGES/1526.misc new file mode 100644 index 0000000000..468409563e --- /dev/null +++ b/CHANGES/1526.misc @@ -0,0 +1 @@ +Update pulp_container to 2.12, update pulpcore to 3.19 \ No newline at end of file diff --git a/CHANGES/1595.bugfix b/CHANGES/1595.bugfix new file mode 100644 index 0000000000..006db6abb5 --- /dev/null +++ b/CHANGES/1595.bugfix @@ -0,0 +1 @@ +Fix 500 error when listing Group Roles \ No newline at end of file diff --git a/CHANGES/1609.misc b/CHANGES/1609.misc new file mode 100644 index 0000000000..995dec1aab --- /dev/null +++ b/CHANGES/1609.misc @@ -0,0 +1 @@ +Create tests for RBAC roles. \ No newline at end of file diff --git a/CHANGES/1643.misc b/CHANGES/1643.misc new file mode 100644 index 0000000000..78b1cbb0da --- /dev/null +++ b/CHANGES/1643.misc @@ -0,0 +1 @@ +Upgrade to pulpcore 3.20. \ No newline at end of file diff --git a/CHANGES/1757.misc b/CHANGES/1757.misc new file mode 100644 index 0000000000..92c8dd1c54 --- /dev/null +++ b/CHANGES/1757.misc @@ -0,0 +1 @@ +Fix bug preventing users with object permissions from adding groups to container namespaces. \ No newline at end of file diff --git a/CHANGES/1765.bugfix b/CHANGES/1765.bugfix new file mode 100644 index 0000000000..50fcffa889 --- /dev/null +++ b/CHANGES/1765.bugfix @@ -0,0 +1 @@ +Remove guardian foreign key contraints in rbac migration diff --git a/CHANGES/1766.bugfix b/CHANGES/1766.bugfix new file mode 100644 index 0000000000..3ca0d571d6 --- /dev/null +++ b/CHANGES/1766.bugfix @@ -0,0 +1 @@ +Allow roles assignment to group with `change_group` permission diff --git a/CHANGES/1805.bugfix b/CHANGES/1805.bugfix new file mode 100644 index 0000000000..2691986e67 --- /dev/null +++ b/CHANGES/1805.bugfix @@ -0,0 +1 @@ +Remove conditional `view_task`. \ No newline at end of file diff --git a/CHANGES/1828.misc b/CHANGES/1828.misc new file mode 100644 index 0000000000..bd7a2dbdcf --- /dev/null +++ b/CHANGES/1828.misc @@ -0,0 +1 @@ +Remove deprecated media type from pulp_container. \ No newline at end of file diff --git a/CHANGES/1850.misc b/CHANGES/1850.misc new file mode 100644 index 0000000000..7687de45e5 --- /dev/null +++ b/CHANGES/1850.misc @@ -0,0 +1 @@ +Add object integration tests for RBAC roles \ No newline at end of file diff --git a/dev/common/RUN_INTEGRATION.sh b/dev/common/RUN_INTEGRATION.sh index e8668c0df5..477637dc8b 100755 --- a/dev/common/RUN_INTEGRATION.sh +++ b/dev/common/RUN_INTEGRATION.sh @@ -41,10 +41,10 @@ docker exec -i galaxy_ng_api_1 /entrypoint.sh manage shell < dev/common/setup_te # export HUB_LOCAL=1 # dev/common/RUN_INTEGRATION.sh --pdb -sv --log-cli-level=DEBUG "-m standalone_only" -k mytest if [[ -z $HUB_LOCAL ]]; then - pytest --capture=no -m "not standalone_only and not community_only" $@ -v galaxy_ng/tests/integration + pytest --capture=no -m "not standalone_only and not community_only and not rbac_roles" $@ -v galaxy_ng/tests/integration RC=$? else - pytest --capture=no -m "not cloud_only and not community_only" -v $@ galaxy_ng/tests/integration + pytest --capture=no -m "not cloud_only and not community_only and not rbac_roles" -v $@ galaxy_ng/tests/integration RC=$? if [[ $RC != 0 ]]; then diff --git a/dev/common/setup_test_data.py b/dev/common/setup_test_data.py index 85ddf2c46b..83ef33ed81 100644 --- a/dev/common/setup_test_data.py +++ b/dev/common/setup_test_data.py @@ -1,4 +1,6 @@ -from guardian.shortcuts import assign_perm +from django.contrib.contenttypes.models import ContentType +from pulpcore.plugin.models.role import GroupRole, Role +from pulpcore.plugin.util import assign_role from rest_framework.authtoken.models import Token from galaxy_ng.app.models import Namespace @@ -11,33 +13,18 @@ print("Add a group that has namespace permissions") test_group, _ = Group.objects.get_or_create(name="ns_group_for_tests") - -print("Add partner-engineers group") -# TODO: remove when we run mgmt command maintain-pe-group on every crc deploy after roles rbac +print("Ensure partner-engineers group created and has roles assigned") PE_GROUP_NAME = "system:partner-engineers" pe_group, _ = Group.objects.get_or_create(name=PE_GROUP_NAME) -pe_perms = [ - # groups - "galaxy.view_group", - "galaxy.delete_group", - "galaxy.add_group", - "galaxy.change_group", - # users - "galaxy.view_user", - "galaxy.delete_user", - "galaxy.add_user", - "galaxy.change_user", - # collections - "ansible.modify_ansible_repo_content", - "ansible.delete_collection", - # namespaces - "galaxy.add_namespace", - "galaxy.change_namespace", - "galaxy.upload_to_namespace", - "galaxy.delete_namespace", +pe_roles = [ + "galaxy.group_admin", + "galaxy.user_admin", + "galaxy.collection_admin", ] -for perm in pe_perms: - assign_perm(perm, pe_group) +roles_in_group = [group_role.role.name for group_role in pe_group.object_roles.all()] +for role in pe_roles: + if role not in roles_in_group: + assign_role(rolename=role, entity=pe_group) print("Get or create test users to match keycloak passwords") @@ -85,12 +72,21 @@ print("Get or create namespaces + add object permissions to group") -# TODO: after roles rbac, add object permissions to new role, then add role to group for nsname in ["autohubtest2", "autohubtest3"]: ns, _ = Namespace.objects.get_or_create(name=nsname) - assign_perm("change_namespace", test_group, ns) - assign_perm("upload_to_namespace", test_group, ns) + # connect group to role for this namespace object + GroupRole.objects.get_or_create( + role=Role.objects.get(name="galaxy.collection_namespace_owner"), + group=test_group, + content_type=ContentType.objects.get(model="namespace"), + object_id=ns.id, + ) signing_ns, _ = Namespace.objects.get_or_create(name="signing") -assign_perm("change_namespace", pe_group, signing_ns) -assign_perm("upload_to_namespace", pe_group, signing_ns) +# connect group to role for this namespace object +GroupRole.objects.get_or_create( + role=Role.objects.get(name="galaxy.collection_namespace_owner"), + group=pe_group, + content_type=ContentType.objects.get(model="namespace"), + object_id=signing_ns.id, +) diff --git a/dev/insights/RUN_INTEGRATION.sh b/dev/insights/RUN_INTEGRATION.sh index f2eacab6fe..016bdc4c96 100755 --- a/dev/insights/RUN_INTEGRATION.sh +++ b/dev/insights/RUN_INTEGRATION.sh @@ -31,7 +31,7 @@ pip show epdb || pip install epdb # export HUB_LOCAL=1 # dev/common/RUN_INTEGRATION.sh --pdb -sv --log-cli-level=DEBUG "-m standalone_only" -k mytest -pytest --capture=no --tb=short -m "not standalone_only and not community_only" $@ -v galaxy_ng/tests/integration +pytest --capture=no --tb=short -m "not standalone_only and not community_only and not rbac_roles" $@ -v galaxy_ng/tests/integration RC=$? exit $RC diff --git a/dev/standalone-rbac-roles/Dockerfile b/dev/standalone-rbac-roles/Dockerfile new file mode 100644 index 0000000000..96c41ed7e6 --- /dev/null +++ b/dev/standalone-rbac-roles/Dockerfile @@ -0,0 +1,16 @@ +ARG DEV_IMAGE_SUFFIX + +FROM localhost/galaxy_ng/galaxy_ng:base${DEV_IMAGE_SUFFIX:-} + +COPY requirements/requirements.standalone.txt /tmp/requirements.standalone.txt + +RUN set -ex; \ + if [[ "${LOCK_REQUIREMENTS}" -eq "1" ]]; then \ + pip install --no-cache-dir --requirement /tmp/requirements.standalone.txt; \ + fi + +USER root + +RUN dnf install -y gettext; + +USER galaxy diff --git a/dev/standalone-rbac-roles/RUN_INTEGRATION.sh b/dev/standalone-rbac-roles/RUN_INTEGRATION.sh new file mode 100755 index 0000000000..c882b98104 --- /dev/null +++ b/dev/standalone-rbac-roles/RUN_INTEGRATION.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Separate workflow created for rbac_roles tests due to runtime being 10+ minutes. +# Standalone integration workflow run time is also 10+ minutes. + +# Expected to be called by: +# - GitHub Actions ci_standalone.yml for DeploymentMode.STANDALONE +# - Developer Env makefile commands for DeploymentMode.STANDALONE +# - TODO: Ephemeral Env pr_check.sh (merge smoke_test.sh into this) for DeploymentMode.INSIGHTS + +set -e + +which virtualenv || pip install --user virtualenv + +VENVPATH=/tmp/gng_testing +PIP=${VENVPATH}/bin/pip + +if [[ ! -d $VENVPATH ]]; then + virtualenv $VENVPATH + $PIP install --retries=0 --verbose --upgrade pip wheel +fi +source $VENVPATH/bin/activate +echo "PYTHON: $(which python)" + +pip install -r integration_requirements.txt +pip show epdb || pip install epdb + +echo "Setting up test data" +docker exec -i galaxy_ng_api_1 /entrypoint.sh manage shell < dev/common/setup_test_data.py + + +#export HUB_API_ROOT='http://localhost:5001/api/' +pytest --capture=no --tb=short -m "rbac_roles" $@ -v galaxy_ng/tests/integration +RC=$? + +exit $RC diff --git a/dev/standalone-rbac-roles/docker-compose-ui.yaml b/dev/standalone-rbac-roles/docker-compose-ui.yaml new file mode 100644 index 0000000000..c2373c8414 --- /dev/null +++ b/dev/standalone-rbac-roles/docker-compose-ui.yaml @@ -0,0 +1,8 @@ +version: "3.7" + +services: + ui: + environment: + - "API_PROXY_HOST=api" + - "API_PROXY_PORT=8000" + - "DEPLOYMENT_MODE=${COMPOSE_PROFILE}" diff --git a/dev/standalone-rbac-roles/docker-compose.yml b/dev/standalone-rbac-roles/docker-compose.yml new file mode 100644 index 0000000000..c0fbd8c027 --- /dev/null +++ b/dev/standalone-rbac-roles/docker-compose.yml @@ -0,0 +1,16 @@ +--- +version: "3.7" + +services: + api: + env_file: + - './standalone/galaxy_ng.env' + - '../.compose.env' + worker: + env_file: + - './standalone/galaxy_ng.env' + - '../.compose.env' + content-app: + env_file: + - './standalone/galaxy_ng.env' + - '../.compose.env' diff --git a/dev/standalone-rbac-roles/galaxy_ng.env b/dev/standalone-rbac-roles/galaxy_ng.env new file mode 100644 index 0000000000..8c5e6049b3 --- /dev/null +++ b/dev/standalone-rbac-roles/galaxy_ng.env @@ -0,0 +1,30 @@ +PULP_CONTENT_PATH_PREFIX=/api/automation-hub/v3/artifacts/collections/ + +PULP_GALAXY_API_PATH_PREFIX=/api/automation-hub/ +PULP_GALAXY_AUTHENTICATION_CLASSES=['rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.BasicAuthentication'] +PULP_GALAXY_DEPLOYMENT_MODE=standalone + +# Commenting this out so local cypress can pass. +# If needed can be specified in the `.compose.env` file. +# PULP_GALAXY_REQUIRE_CONTENT_APPROVAL=false +# PULP_GALAXY_AUTO_SIGN_COLLECTIONS=true + +PULP_GALAXY_COLLECTION_SIGNING_SERVICE=ansible-default +PULP_RH_ENTITLEMENT_REQUIRED=insights + +PULP_ANSIBLE_API_HOSTNAME=http://localhost:5001 +PULP_ANSIBLE_CONTENT_HOSTNAME=http://localhost:24816/api/automation-hub/v3/artifacts/collections + +PULP_TOKEN_AUTH_DISABLED=true +PULP_CONTENT_ORIGIN="http://localhost:24816" + + +# Pulp container requires this to be set in order to provide docker registry +# compatible token authentication. +# https://docs.pulpproject.org/pulp_container/workflows/authentication.html + +PULP_TOKEN_AUTH_DISABLED=false +PULP_TOKEN_SERVER=http://localhost:5001/token/ +PULP_TOKEN_SIGNATURE_ALGORITHM=ES256 +PULP_PUBLIC_KEY_PATH=/src/galaxy_ng/dev/common/container_auth_public_key.pem +PULP_PRIVATE_KEY_PATH=/src/galaxy_ng/dev/common/container_auth_private_key.pem diff --git a/dev/standalone/integration-test-dockerfile b/dev/standalone/integration-test-dockerfile index 839c429c6d..619138d15a 100644 --- a/dev/standalone/integration-test-dockerfile +++ b/dev/standalone/integration-test-dockerfile @@ -4,6 +4,7 @@ WORKDIR /app/ # Install test requirements first to make running the tests faster SHELL ["/bin/bash", "-c"] +RUN apt-get -y update && apt-get -y install podman COPY integration_requirements.txt /app/integration_requirements.txt RUN pip install virtualenv && \ virtualenv /tmp/gng_testing && \ diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index f5eb96d937..6808432bdf 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -90,10 +90,9 @@ run_service() { process_init_files /entrypoints.d/* - # TODO: re-enable after RBAC work is completed - # if [[ "$PULP_GALAXY_DEPLOYMENT_MODE" = "insights" ]]; then - # django-admin maintain-pe-group - # fi + if [[ "$PULP_GALAXY_DEPLOYMENT_MODE" = "insights" ]]; then + django-admin maintain-pe-group + fi if [[ "$ENABLE_SIGNING" -eq "1" ]]; then setup_signing_keyring diff --git a/functest_requirements.txt b/functest_requirements.txt index b682132d07..5d9ad3336a 100644 --- a/functest_requirements.txt +++ b/functest_requirements.txt @@ -1,4 +1,3 @@ git+https://github.com/pulp/pulp-smash.git#egg=pulp-smash -pulp-container>=2.10.0,<2.11.0 pytest orionutils diff --git a/galaxy_ng/app/__init__.py b/galaxy_ng/app/__init__.py index df04c6bdec..1482d35b3e 100644 --- a/galaxy_ng/app/__init__.py +++ b/galaxy_ng/app/__init__.py @@ -7,6 +7,7 @@ class PulpGalaxyPluginAppConfig(PulpPluginAppConfig): name = "galaxy_ng.app" label = "galaxy" version = "4.6.0dev" + python_package_name = "galaxy-ng" def ready(self): super().ready() diff --git a/galaxy_ng/app/access_control/access_policy.py b/galaxy_ng/app/access_control/access_policy.py index 424a25f75b..7c48d7bcc9 100644 --- a/galaxy_ng/app/access_control/access_policy.py +++ b/galaxy_ng/app/access_control/access_policy.py @@ -7,7 +7,6 @@ from pulpcore.plugin.access_policy import AccessPolicyFromDB -from pulp_container.app.access_policy import NamespacedAccessPolicyMixin from pulp_container.app import models as container_models from galaxy_ng.app import models @@ -34,6 +33,10 @@ def get_view_urlpattern(view): return view.urlpattern() +def has_model_or_object_permissions(user, permission, obj): + return user.has_perm(permission) or user.has_perm(permission, obj) + + class AccessPolicyBase(AccessPolicyFromDB): """ This class is capable of loading access policy statements from galaxy_ng's hardcoded list of @@ -128,7 +131,11 @@ def can_update_collection(self, request, view, permission): return False collection = view.get_object() namespace = models.Namespace.objects.get(name=collection.namespace) - return request.user.has_perm("galaxy.upload_to_namespace", namespace) + return has_model_or_object_permissions( + request.user, + "galaxy.upload_to_namespace", + namespace + ) def can_create_collection(self, request, view, permission): data = view._get_data(request) @@ -136,7 +143,11 @@ def can_create_collection(self, request, view, permission): namespace = models.Namespace.objects.get(name=data["filename"].namespace) except models.Namespace.DoesNotExist: raise NotFound(_("Namespace in filename not found.")) - return request.user.has_perm("galaxy.upload_to_namespace", namespace) + return has_model_or_object_permissions( + request.user, + "galaxy.upload_to_namespace", + namespace + ) def can_sign_collections(self, request, view, permission): # Repository is required on the CollectionSign payload @@ -151,8 +162,10 @@ def can_sign_collections(self, request, view, permission): namespace = models.Namespace.objects.get(name=namespace) except models.Namespace.DoesNotExist: raise NotFound(_('Namespace not found.')) - return can_modify_repo and request.user.has_perm( - 'galaxy.upload_to_namespace', namespace + return can_modify_repo and has_model_or_object_permissions( + request.user, + "galaxy.upload_to_namespace", + namespace ) # the other filtering options are content_units and name/version @@ -165,6 +178,20 @@ def unauthenticated_collection_download_enabled(self, request, view, permission) def unauthenticated_collection_access_enabled(self, request, view, action): return settings.GALAXY_ENABLE_UNAUTHENTICATED_COLLECTION_ACCESS + def has_concrete_perms(self, request, view, action, permission): + # Function the same as has_model_or_object_perms, but uses the concrete model + # instead of the proxy model + if request.user.has_perm(permission): + return True + + # if the object is a proxy object, get the concrete object and use that for the + # permission comparison + obj = view.get_object() + if obj._meta.proxy: + obj = obj._meta.concrete_model.objects.get(pk=obj.pk) + + return request.user.has_perm(permission, obj) + class AppRootAccessPolicy(AccessPolicyBase): NAME = "AppRootViewSet" @@ -267,9 +294,7 @@ class ContainerReadmeAccessPolicy(AccessPolicyBase): def has_container_namespace_perms(self, request, view, action, permission): readme = view.get_object() - return request.user.has_perm(permission) or request.user.has_perm( - permission, readme.container.namespace - ) + return has_model_or_object_permissions(request.user, permission, readme.container.namespace) class ContainerNamespaceAccessPolicy(AccessPolicyBase): @@ -280,9 +305,44 @@ class ContainerRegistryRemoteAccessPolicy(AccessPolicyBase): NAME = "ContainerRegistryRemoteViewSet" -class ContainerRemoteAccessPolicy(AccessPolicyBase, NamespacedAccessPolicyMixin): +class ContainerRemoteAccessPolicy(AccessPolicyBase): NAME = "ContainerRemoteViewSet" + # Copied from pulp_container/app/global_access_conditions.py + def has_namespace_or_obj_perms(self, request, view, action, permission): + """ + Check if a user has a namespace-level perms or object-level permission + """ + ns_perm = "container.namespace_{}".format(permission.split(".", 1)[1]) + if self.has_namespace_obj_perms(request, view, action, ns_perm): + return True + else: + return request.user.has_perm(permission) or request.user.has_perm( + permission, view.get_object() + ) + + # Copied from pulp_container/app/global_access_conditions.py + def has_namespace_obj_perms(self, request, view, action, permission): + """ + Check if a user has object-level perms on the namespace associated with the distribution + or repository. + """ + if request.user.has_perm(permission): + return True + obj = view.get_object() + if type(obj) == container_models.ContainerDistribution: + namespace = obj.namespace + return request.user.has_perm(permission, namespace) + elif type(obj) == container_models.ContainerPushRepository: + for dist in obj.distributions.all(): + if request.user.has_perm(permission, dist.cast().namespace): + return True + elif type(obj) == container_models.ContainerPushRepositoryVersion: + for dist in obj.repository.distributions.all(): + if request.user.has_perm(permission, dist.cast().namespace): + return True + return False + def has_distro_permission(self, request, view, action, permission): class FakeView: def __init__(self, obj): diff --git a/galaxy_ng/app/access_control/fields.py b/galaxy_ng/app/access_control/fields.py index b72b17ed8b..d898dccf9c 100644 --- a/galaxy_ng/app/access_control/fields.py +++ b/galaxy_ng/app/access_control/fields.py @@ -1,44 +1,38 @@ -from django.contrib.auth.models import Permission -from django.db.models import Q from django.utils.translation import gettext_lazy as _ -from guardian.shortcuts import get_perms_for_model - from rest_framework import serializers from rest_framework.exceptions import ValidationError +from pulpcore.plugin.models.role import Role + +from pulpcore.plugin.util import get_perms_for_model + from galaxy_ng.app.models import auth as auth_models class GroupPermissionField(serializers.Field): def _validate_group(self, group_data): - if 'object_permissions' not in group_data: + if 'object_roles' not in group_data: raise ValidationError(detail={ - 'groups': _('object_permissions field is required')}) + 'groups': _('object_roles field is required')}) if 'id' not in group_data and 'name' not in group_data: raise ValidationError(detail={ 'groups': _('id or name field is required')}) - perms = group_data['object_permissions'] + roles = group_data['object_roles'] - if not isinstance(perms, list): + if not isinstance(roles, list): raise ValidationError(detail={ - 'groups': _('object_permissions must be a list of strings')}) + 'groups': _('object_roles must be a list of strings')}) # validate that the permissions exist - for perm in perms: - if '.' in perm: - app_label, codename = perm.split('.', maxsplit=1) - filter_q = Q(content_type__app_label=app_label) & Q(codename=codename) - else: - filter_q = Q(codename=perm) - + for role in roles: # TODO(newswangerd): Figure out how to make this one SQL query instead of # performing N queries for each permission - if not Permission.objects.filter(filter_q).exists(): + if not Role.objects.filter(name=role).exists(): raise ValidationError(detail={ - 'groups': _('Permission {} does not exist').format(perm)}) + 'groups': _('Role {} does not exist').format(role)}) def to_representation(self, value): rep = [] @@ -46,7 +40,7 @@ def to_representation(self, value): rep.append({ 'id': group.id, 'name': group.name, - 'object_permissions': value[group] + 'object_roles': value[group] }) return rep @@ -65,7 +59,10 @@ def to_internal_value(self, data): group_filter[field] = group_data[field] try: group = auth_models.Group.objects.get(**group_filter) - internal[group] = group_data['object_permissions'] + if 'object_permissions' in group_data: + internal[group] = group_data['object_permissions'] + if 'object_roles' in group_data: + internal[group] = group_data['object_roles'] except auth_models.Group.DoesNotExist: raise ValidationError(detail={ 'groups': _("Group name=%s, id=%s does not exist") % ( @@ -78,14 +75,17 @@ def to_internal_value(self, data): class MyPermissionsField(serializers.Serializer): - def to_representation(self, obj): + def to_representation(self, original_obj): request = self.context.get('request', None) if request is None: return [] user = request.user - # guardian's get_perms(user, obj) method only returns user permissions, - # not all permissions a user has. + if original_obj._meta.proxy: + obj = original_obj._meta.concrete_model.objects.get(pk=original_obj.pk) + else: + obj = original_obj + my_perms = [] for perm in get_perms_for_model(type(obj)).all(): codename = "{}.{}".format(perm.content_type.app_label, perm.codename) diff --git a/galaxy_ng/app/access_control/mixins.py b/galaxy_ng/app/access_control/mixins.py index d0ac4c2896..52e37a01e8 100644 --- a/galaxy_ng/app/access_control/mixins.py +++ b/galaxy_ng/app/access_control/mixins.py @@ -1,5 +1,15 @@ from django.db import transaction -from guardian.shortcuts import get_groups_with_perms, assign_perm, remove_perm +from django.core.exceptions import BadRequest +from django.utils.translation import gettext_lazy as _ + +from rest_framework.exceptions import ValidationError + +from pulpcore.plugin.util import ( + assign_role, + remove_role, + get_groups_with_perms_attached_roles, +) + from django_lifecycle import hook @@ -8,7 +18,8 @@ class GroupModelPermissionsMixin: @property def groups(self): - return get_groups_with_perms(self, attach_perms=True) + return get_groups_with_perms_attached_roles( + self, include_model_permissions=False, for_concrete_model=True) @groups.setter def groups(self, groups): @@ -16,20 +27,35 @@ def groups(self, groups): @transaction.atomic def _set_groups(self, groups): - # guardian doesn't allow adding permissions to objects that haven't been + # Can't add permissions to objects that haven't been # saved. When creating new objects, save group data to _groups where it # can be picked up by the post save hook. if self._state.adding: self._groups = groups else: - current_groups = get_groups_with_perms(self, attach_perms=True) + obj = self + + # If the model is a proxy model, get the original model since pulp + # doesn't allow us to assign permissions to proxied models. + if self._meta.proxy: + obj = self._meta.concrete_model.objects.get(pk=self.pk) + + current_groups = get_groups_with_perms_attached_roles( + obj, include_model_permissions=False) for group in current_groups: for perm in current_groups[group]: - remove_perm(perm, group, self) + remove_role(perm, group, obj) for group in groups: - for perm in groups[group]: - assign_perm(perm, group, self) + for role in groups[group]: + try: + assign_role(role, group, obj) + except BadRequest: + raise ValidationError( + detail={'groups': _('Role {role} does not exist or does not ' + 'have any permissions related to this object.' + ).format(role=role)} + ) @hook('after_save') def set_object_groups(self): diff --git a/galaxy_ng/app/access_control/statements/__init__.py b/galaxy_ng/app/access_control/statements/__init__.py index 84103d5bad..0870a5527b 100644 --- a/galaxy_ng/app/access_control/statements/__init__.py +++ b/galaxy_ng/app/access_control/statements/__init__.py @@ -1,9 +1,11 @@ from .standalone import STANDALONE_STATEMENTS from .insights import INSIGHTS_STATEMENTS from .pulp_container import PULP_CONTAINER_VIEWSETS +from .roles import LOCKED_ROLES __all__ = ( STANDALONE_STATEMENTS, INSIGHTS_STATEMENTS, - PULP_CONTAINER_VIEWSETS + PULP_CONTAINER_VIEWSETS, + LOCKED_ROLES ) diff --git a/galaxy_ng/app/access_control/statements/insights.py b/galaxy_ng/app/access_control/statements/insights.py index 4701679316..d252f51b93 100644 --- a/galaxy_ng/app/access_control/statements/insights.py +++ b/galaxy_ng/app/access_control/statements/insights.py @@ -263,4 +263,41 @@ "condition": "has_rh_entitlements", }, ], + + 'groups/roles': [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": "create", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_perms:galaxy.change_group" + }, + { + "action": "destroy", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_perms:galaxy.change_group" + }, + { + "action": "*", + "principal": "admin", + "effect": "allow" + } + ], + 'roles': [ + { + "action": ["list"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": "*", + "principal": "admin", + "effect": "allow" + } + ] } diff --git a/galaxy_ng/app/access_control/statements/pulp_container.py b/galaxy_ng/app/access_control/statements/pulp_container.py index 2a35aa7edd..c6847d15ab 100644 --- a/galaxy_ng/app/access_control/statements/pulp_container.py +++ b/galaxy_ng/app/access_control/statements/pulp_container.py @@ -84,8 +84,6 @@ ], }, ], - # Removed permission assignement. Filtering out the container groups - # proved to be too much of a challenge. "creation_hooks": [] }, @@ -130,28 +128,15 @@ "condition": "has_model_or_obj_perms:container.namespace_view_containerdistribution", # noqa: E501 }, ], - # Removed group creation for owner. Filtering out the container groups proved to be too - # much of a challenge. "creation_hooks": [ { - "function": "add_for_object_creator", - "parameters": None, - "permissions": [ - "container.view_containernamespace", - "container.delete_containernamespace", - # Add `container.change_containernamespace` permissions so the namespace - # owner can add additional groups to their namespace. - "container.change_containernamespace", - "container.namespace_add_containerdistribution", - "container.namespace_delete_containerdistribution", - "container.namespace_view_containerdistribution", - "container.namespace_pull_containerdistribution", - "container.namespace_push_containerdistribution", - "container.namespace_change_containerdistribution", - "container.namespace_view_containerpushrepository", - "container.namespace_modify_content_containerpushrepository", - ], - }, + "function": "add_roles_for_object_creator", + "parameters": { + "roles": [ + "galaxy.execution_environment_namespace_owner", + ], + }, + } ], }, diff --git a/galaxy_ng/app/access_control/statements/roles.py b/galaxy_ng/app/access_control/statements/roles.py new file mode 100644 index 0000000000..a56b43a11d --- /dev/null +++ b/galaxy_ng/app/access_control/statements/roles.py @@ -0,0 +1,153 @@ +LOCKED_ROLES = { + "galaxy.content_admin": { + "permissions": [ + "galaxy.add_namespace", + "galaxy.change_namespace", + "galaxy.delete_namespace", + "galaxy.upload_to_namespace", + "ansible.delete_collection", + "ansible.change_collectionremote", + "ansible.view_collectionremote", + "ansible.modify_ansible_repo_content", + "container.delete_containerrepository", + "container.namespace_change_containerdistribution", + "container.namespace_modify_content_containerpushrepository", + "container.namespace_push_containerdistribution", + "container.add_containernamespace", + "container.change_containernamespace", + "container.namespace_add_containerdistribution", + "galaxy.add_containerregistryremote", + "galaxy.change_containerregistryremote", + "galaxy.delete_containerregistryremote", + ], + "description": "Manage all content types." + }, + + # COLLECTIONS + "galaxy.collection_admin": { + "permissions": [ + "galaxy.add_namespace", + "galaxy.change_namespace", + "galaxy.delete_namespace", + "galaxy.upload_to_namespace", + "ansible.delete_collection", + "ansible.change_collectionremote", + "ansible.view_collectionremote", + "ansible.modify_ansible_repo_content", + ], + "description": ( + "Create, delete and change collection namespaces. " + "Upload and delete collections. Sync collections from remotes. " + "Approve and reject collections.") + }, + "galaxy.collection_publisher": { + "permissions": [ + "galaxy.add_namespace", + "galaxy.change_namespace", + "galaxy.upload_to_namespace", + ], + "description": "Upload and modify collections." + }, + "galaxy.collection_curator": { + "permissions": [ + "ansible.change_collectionremote", + "ansible.view_collectionremote", + "ansible.modify_ansible_repo_content", + ], + "description": "Approve, reject and sync collections from remotes.", + }, + "galaxy.collection_namespace_owner": { + "permissions": [ + "galaxy.change_namespace", + "galaxy.upload_to_namespace", + ], + "description": "Change and upload collections to namespaces." + }, + + # EXECUTION ENVIRONMENTS + "galaxy.execution_environment_admin": { + "permissions": [ + "container.delete_containerrepository", + "container.namespace_change_containerdistribution", + "container.namespace_modify_content_containerpushrepository", + "container.namespace_push_containerdistribution", + "container.add_containernamespace", + "container.change_containernamespace", + "container.namespace_add_containerdistribution", + "galaxy.add_containerregistryremote", + "galaxy.change_containerregistryremote", + "galaxy.delete_containerregistryremote", + ], + "description": ( + "Push, delete, and change execution environments. " + "Create, delete and change remote registries.") + }, + "galaxy.execution_environment_publisher": { + "permissions": [ + "container.namespace_change_containerdistribution", + "container.namespace_modify_content_containerpushrepository", + "container.namespace_push_containerdistribution", + "container.add_containernamespace", + "container.change_containernamespace", + "container.namespace_add_containerdistribution", + ], + "description": "Push, and change execution environments." + }, + "galaxy.execution_environment_namespace_owner": { + "permissions": [ + "container.change_containernamespace", + "container.namespace_push_containerdistribution", + "container.namespace_change_containerdistribution", + "container.namespace_modify_content_containerpushrepository", + "container.namespace_add_containerdistribution", + ], + "description": ( + "Create and update execution environments under existing " + "container namespaces.") + }, + "galaxy.execution_environment_collaborator": { + "permissions": [ + "container.namespace_push_containerdistribution", + "container.namespace_change_containerdistribution", + "container.namespace_modify_content_containerpushrepository", + ], + "description": "Change existing execution environments." + }, + + # ADMIN STUFF + "galaxy.group_admin": { + "permissions": [ + "galaxy.view_group", + "galaxy.delete_group", + "galaxy.add_group", + "galaxy.change_group", + ], + "description": "View, add, remove and change groups." + }, + "galaxy.user_admin": { + "permissions": [ + "galaxy.view_user", + "galaxy.delete_user", + "galaxy.add_user", + "galaxy.change_user", + ], + "description": "View, add, remove and change users." + }, + "galaxy.synclist_owner": { + "permissions": [ + "galaxy.add_synclist", + "galaxy.change_synclist", + "galaxy.delete_synclist", + "galaxy.view_synclist", + ], + "description": "View, add, remove and change synclists." + }, + "galaxy.task_admin": { + "permissions": [ + "core.change_task", + "core.delete_task", + "core.view_task" + ], + "description": "View, and cancel any task." + }, +} diff --git a/galaxy_ng/app/access_control/statements/standalone.py b/galaxy_ng/app/access_control/statements/standalone.py index 54af3150eb..ddfe996f42 100644 --- a/galaxy_ng/app/access_control/statements/standalone.py +++ b/galaxy_ng/app/access_control/statements/standalone.py @@ -349,7 +349,7 @@ "action": ["update"], "principal": "authenticated", "effect": "allow", - "condition": "has_model_or_obj_perms:container.change_containernamespace" + "condition": "has_concrete_perms:container.change_containernamespace" }, ], @@ -425,4 +425,41 @@ "condition": "has_distro_permission:container.change_containerdistribution" }, ], + + 'groups/roles': [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": "create", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_perms:galaxy.change_group" + }, + { + "action": "destroy", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_perms:galaxy.change_group" + }, + { + "action": "*", + "principal": "admin", + "effect": "allow" + } + ], + 'roles': [ + { + "action": ["list"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": "*", + "principal": "admin", + "effect": "allow" + } + ] } diff --git a/galaxy_ng/app/api/ui/serializers/execution_environment.py b/galaxy_ng/app/api/ui/serializers/execution_environment.py index 2376ef0559..bd2a6e2d99 100644 --- a/galaxy_ng/app/api/ui/serializers/execution_environment.py +++ b/galaxy_ng/app/api/ui/serializers/execution_environment.py @@ -8,7 +8,7 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field -from guardian.shortcuts import get_users_with_perms +from pulpcore.plugin.util import get_users_with_perms from pulp_container.app import models as container_models from pulp_container.app import serializers as container_serializers @@ -38,9 +38,9 @@ class Meta: @extend_schema_field(serializers.ListField) def get_owners(self, namespace): - return get_users_with_perms(namespace, with_group_users=False).values_list( - "username", flat=True - ) + return get_users_with_perms( + namespace, with_group_users=False, for_concrete_model=True + ).values_list("username", flat=True) class ContainerNamespaceDetailSerializer(ContainerNamespaceSerializer): @@ -182,7 +182,7 @@ def get_layers(self, obj): def get_config_blob(self, obj): if not obj.config_blob: return {} - return {"digest": obj.config_blob.digest, "media_type": obj.config_blob.media_type} + return {"digest": obj.config_blob.digest} @extend_schema_field(serializers.ListField) def get_tags(self, obj): @@ -228,7 +228,6 @@ def get_config_blob(self, obj): return { "digest": obj.config_blob.digest, - "media_type": obj.config_blob.media_type, "data": config_json, } diff --git a/galaxy_ng/app/api/ui/urls.py b/galaxy_ng/app/api/ui/urls.py index aa4d0d32c3..bf9ca91e74 100644 --- a/galaxy_ng/app/api/ui/urls.py +++ b/galaxy_ng/app/api/ui/urls.py @@ -125,16 +125,6 @@ "/", viewsets.GroupViewSet.as_view({'get': 'retrieve', 'delete': 'destroy'}), name='group-detail'), - path( - "/model-permissions/", - viewsets.GroupModelPermissionViewSet.as_view({ - 'get': 'list', 'post': 'create'}), - name='group-model-permissions'), - path( - "/model-permissions//", - viewsets.GroupModelPermissionViewSet.as_view({ - 'get': 'retrieve', 'delete': 'destroy'}), - name='group-model-permissions-detail'), path( "/users/", viewsets.GroupUserViewSet.as_view({ diff --git a/galaxy_ng/app/api/ui/viewsets/__init__.py b/galaxy_ng/app/api/ui/viewsets/__init__.py index 2b666c994f..fe1a764b7c 100644 --- a/galaxy_ng/app/api/ui/viewsets/__init__.py +++ b/galaxy_ng/app/api/ui/viewsets/__init__.py @@ -14,7 +14,7 @@ from .user import UserViewSet, CurrentUserViewSet from .synclist import SyncListViewSet from .root import APIRootView -from .group import GroupViewSet, GroupModelPermissionViewSet, GroupUserViewSet +from .group import GroupViewSet, GroupUserViewSet from .distribution import DistributionViewSet, MyDistributionViewSet from .execution_environment import ( ContainerRepositoryViewSet, @@ -41,7 +41,6 @@ 'SyncListViewSet', 'APIRootView', 'GroupViewSet', - 'GroupModelPermissionViewSet', 'GroupUserViewSet', 'DistributionViewSet', 'MyDistributionViewSet', diff --git a/galaxy_ng/app/api/ui/viewsets/distribution.py b/galaxy_ng/app/api/ui/viewsets/distribution.py index 69f6d86878..05a926a262 100644 --- a/galaxy_ng/app/api/ui/viewsets/distribution.py +++ b/galaxy_ng/app/api/ui/viewsets/distribution.py @@ -1,6 +1,6 @@ from rest_framework import mixins from pulp_ansible.app import models as pulp_models -from guardian.shortcuts import get_objects_for_user +from pulpcore.plugin.util import get_objects_for_user from galaxy_ng.app.access_control import access_policy from galaxy_ng.app.api.ui import serializers, versioning @@ -31,7 +31,7 @@ def get_queryset(self): 'galaxy.change_synclist', any_perm=True, accept_global_perms=False, - klass=models.SyncList + qs=models.SyncList.objects.all() ) # TODO: find a better way query this data diff --git a/galaxy_ng/app/api/ui/viewsets/execution_environment.py b/galaxy_ng/app/api/ui/viewsets/execution_environment.py index 05fa502125..1bd4ed57e6 100644 --- a/galaxy_ng/app/api/ui/viewsets/execution_environment.py +++ b/galaxy_ng/app/api/ui/viewsets/execution_environment.py @@ -6,7 +6,7 @@ from django_filters import filters from django_filters.rest_framework import DjangoFilterBackend, filterset from drf_spectacular.utils import extend_schema -from guardian.shortcuts import get_objects_for_user +from pulpcore.plugin.util import get_objects_for_user from pulp_container.app import models as container_models from pulpcore.plugin import models as core_models from pulpcore.plugin.serializers import AsyncOperationResponseSerializer @@ -47,7 +47,7 @@ class Meta: def has_permissions(self, queryset, name, value): perms = self.request.query_params.getlist(name) namespaces = get_objects_for_user( - self.request.user, perms, klass=container_models.ContainerNamespace) + self.request.user, perms, qs=container_models.ContainerNamespace.objects.all()) return self.queryset.filter(namespace__in=namespaces) diff --git a/galaxy_ng/app/api/ui/viewsets/group.py b/galaxy_ng/app/api/ui/viewsets/group.py index 4b708022a1..1a82b355d9 100644 --- a/galaxy_ng/app/api/ui/viewsets/group.py +++ b/galaxy_ng/app/api/ui/viewsets/group.py @@ -47,9 +47,5 @@ def create(self, request, *args, **kwargs): return super().create(request, *args, **kwargs) -class GroupModelPermissionViewSet(LocalSettingsMixin, viewsets.GroupModelPermissionViewSet): - permission_classes = [access_policy.GroupAccessPolicy] - - class GroupUserViewSet(LocalSettingsMixin, viewsets.GroupUserViewSet): permission_classes = [access_policy.GroupAccessPolicy] diff --git a/galaxy_ng/app/api/ui/viewsets/my_namespace.py b/galaxy_ng/app/api/ui/viewsets/my_namespace.py index e9bd3b6706..8e96d4973c 100644 --- a/galaxy_ng/app/api/ui/viewsets/my_namespace.py +++ b/galaxy_ng/app/api/ui/viewsets/my_namespace.py @@ -1,5 +1,5 @@ from galaxy_ng.app import models -from guardian.shortcuts import get_objects_for_user +from pulpcore.plugin.util import get_objects_for_user from .namespace import NamespaceViewSet @@ -10,5 +10,5 @@ def get_queryset(self): self.request.user, ('galaxy.change_namespace', 'galaxy.upload_to_namespace'), any_perm=True, - klass=models.Namespace + qs=models.Namespace.objects.all() ) diff --git a/galaxy_ng/app/api/ui/viewsets/my_synclist.py b/galaxy_ng/app/api/ui/viewsets/my_synclist.py index b3528394af..10d1a312c5 100644 --- a/galaxy_ng/app/api/ui/viewsets/my_synclist.py +++ b/galaxy_ng/app/api/ui/viewsets/my_synclist.py @@ -2,7 +2,8 @@ from django.http import HttpResponse from django.shortcuts import get_object_or_404 -from guardian.shortcuts import get_objects_for_user +from pulpcore.plugin.util import get_objects_for_user + from rest_framework.decorators import action from galaxy_ng.app import models @@ -25,9 +26,8 @@ def get_queryset(self): return get_objects_for_user( self.request.user, "galaxy.change_synclist", - any_perm=True, - accept_global_perms=False, - klass=models.SyncList, + # any_perm=True, + qs=models.SyncList.objects.all(), ) # TODO: on UI click of synclist toggle the UI makes 2 calls diff --git a/galaxy_ng/app/api/urls.py b/galaxy_ng/app/api/urls.py index 41b892d8c4..6d04cbd221 100644 --- a/galaxy_ng/app/api/urls.py +++ b/galaxy_ng/app/api/urls.py @@ -41,14 +41,14 @@ path("content//", views.ApiRootView.as_view(), - name="root"), + name="content-root"), # This can be removed when ansible-galaxy stops appending '/api' to the # urls. path("content//api/", views.ApiRedirectView.as_view(), name="api-redirect", - kwargs={"reverse_url_name": "galaxy:api:content:root"}), + kwargs={"reverse_url_name": "galaxy:api:v3:content-root"}), ] v3_combined = [ diff --git a/galaxy_ng/app/api/v3/serializers/namespace.py b/galaxy_ng/app/api/v3/serializers/namespace.py index e31826bc7d..493e60c8db 100644 --- a/galaxy_ng/app/api/v3/serializers/namespace.py +++ b/galaxy_ng/app/api/v3/serializers/namespace.py @@ -8,6 +8,8 @@ from rest_framework.exceptions import ValidationError from rest_framework import serializers +from pulpcore.plugin.serializers import IdentityField + from galaxy_ng.app import models from galaxy_ng.app.access_control.fields import GroupPermissionField, MyPermissionsField from galaxy_ng.app.api.base import RelatedFieldsBaseSerializer @@ -72,9 +74,13 @@ class NamespaceSerializer(serializers.ModelSerializer): groups = GroupPermissionField() related_fields = NamespaceRelatedFieldSerializer(source="*") + # Add a pulp href to namespaces so that it can be referenced in the roles API. + pulp_href = IdentityField(view_name="pulp_ansible/namespaces-detail", lookup_field="pk") + class Meta: model = models.Namespace fields = ( + 'pulp_href', 'id', 'name', 'company', @@ -139,6 +145,7 @@ class NamespaceSummarySerializer(NamespaceSerializer): class Meta: model = models.Namespace fields = ( + 'pulp_href', 'id', 'name', 'company', diff --git a/galaxy_ng/app/auth/auth.py b/galaxy_ng/app/auth/auth.py index f25d31dc96..7dfde6464f 100644 --- a/galaxy_ng/app/auth/auth.py +++ b/galaxy_ng/app/auth/auth.py @@ -4,7 +4,9 @@ from django.conf import settings from django.db import transaction -from guardian import shortcuts + +from pulpcore.plugin.util import get_objects_for_group + from pulp_ansible.app.models import AnsibleDistribution, AnsibleRepository from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed @@ -79,11 +81,9 @@ def _ensure_group(self, account_scope, account): def _ensure_synclists(self, group): with transaction.atomic(): # check for existing synclists - perms = ['galaxy.view_synclist'] synclists_owned_by_group = \ - shortcuts.get_objects_for_group(group, perms, klass=SyncList, - any_perm=False, accept_global_perms=True) + get_objects_for_group(group, 'galaxy.view_synclist', SyncList.objects.all()) if synclists_owned_by_group: return synclists_owned_by_group @@ -105,8 +105,7 @@ def _ensure_synclists(self, group): }, ) - default_synclist.groups = {group: ['galaxy.view_synclist', 'galaxy.add_synclist', - 'galaxy.delete_synclist', 'galaxy.change_synclist']} + default_synclist.groups = {group: ['galaxy.synclist_owner']} default_synclist.save() return default_synclist diff --git a/galaxy_ng/app/management/commands/maintain-pe-group.py b/galaxy_ng/app/management/commands/maintain-pe-group.py index afc6d95bf8..b324563065 100644 --- a/galaxy_ng/app/management/commands/maintain-pe-group.py +++ b/galaxy_ng/app/management/commands/maintain-pe-group.py @@ -1,5 +1,6 @@ from django.core.management import BaseCommand -from guardian.shortcuts import assign_perm + +from pulpcore.plugin.util import assign_role from galaxy_ng.app.models.auth import Group @@ -9,8 +10,8 @@ class Command(BaseCommand): """ This command creates or updates a partner engineering group - with a standard set of permissions. Intended to be used for - settings.GALAXY_DEPLOYMENT_MODE==insights. + with a standard set of permissions via Galaxy locked roles. + Intended to be used for settings.GALAXY_DEPLOYMENT_MODE==insights. $ django-admin maintain-pe-group """ @@ -18,34 +19,23 @@ class Command(BaseCommand): help = "Creates/updates partner engineering group with permissions" def handle(self, *args, **options): - pe_group, created = Group.objects.get_or_create(name=PE_GROUP_NAME) - if created: + pe_group, group_created = Group.objects.get_or_create(name=PE_GROUP_NAME) + if group_created: self.stdout.write(f"Created group '{PE_GROUP_NAME}'") else: self.stdout.write(f"Group '{PE_GROUP_NAME}' already exists") - pe_perms = [ - # groups - "galaxy.view_group", - "galaxy.delete_group", - "galaxy.add_group", - "galaxy.change_group", - # users - "galaxy.view_user", - "galaxy.delete_user", - "galaxy.add_user", - "galaxy.change_user", - # collections - "ansible.modify_ansible_repo_content", - "ansible.delete_collection", - # namespaces - "galaxy.add_namespace", - "galaxy.change_namespace", - "galaxy.upload_to_namespace", - "galaxy.delete_namespace", + pe_roles = [ + 'galaxy.group_admin', + 'galaxy.user_admin', + 'galaxy.collection_admin', ] - for perm in pe_perms: - assign_perm(perm, pe_group) + roles_in_group = [group_role.role.name for group_role in pe_group.object_roles.all()] + for role in pe_roles: + if role not in roles_in_group: + assign_role(rolename=role, entity=pe_group) - self.stdout.write(f"Permissions assigned to '{PE_GROUP_NAME}'") + self.stdout.write( + f"Roles assigned to '{PE_GROUP_NAME}'" + ) diff --git a/galaxy_ng/app/migrations/0004_rbac.py b/galaxy_ng/app/migrations/0004_rbac.py index dfc8fbca15..6a303a89ee 100644 --- a/galaxy_ng/app/migrations/0004_rbac.py +++ b/galaxy_ng/app/migrations/0004_rbac.py @@ -4,46 +4,6 @@ from django.contrib.auth.management import create_permissions -# note: using assign_perm from guardian.shortcuts doesn't work in migrations -# https://github.com/django-guardian/django-guardian/issues/281#issuecomment-156264129 -# The following function is adapted from https://gist.github.com/xuhcc/67871719116bdc0fee6c -def assign_perm(apps, perm, owner, obj=None): - Permission = apps.get_model('auth', 'Permission') - UserObjectPermission = apps.get_model('guardian', 'UserObjectPermission') - GroupObjectPermission = apps.get_model('guardian', 'GroupObjectPermission') - app_label, codename = perm.split('.', 1) - - perm = Permission.objects.get( - content_type__app_label=app_label, - codename=codename) - kwargs = { - 'permission': perm, - 'content_type': perm.content_type, - 'object_pk': obj.pk, - } - kwargs['group'] = owner - return GroupObjectPermission.objects.get_or_create(**kwargs) - - - -def convert_namespace_groups_to_permissions(apps, schema_editor): - NamespaceModel = apps.get_model('galaxy', 'Namespace') - - for namespace in NamespaceModel.objects.all(): - for group in namespace.groups.all(): - assign_perm(apps, 'galaxy.change_namespace', group, namespace) - assign_perm(apps, 'galaxy.upload_to_namespace', group, namespace) - - -def convert_synclist_groups_to_permissions(apps, schema_editor): - SyncListModel = apps.get_model('galaxy', 'SyncList') - - for synclist in SyncListModel.objects.all(): - for group in synclist.groups.all(): - assign_perm(apps, 'galaxy.change_synclist', group, synclist) - assign_perm(apps, 'galaxy.view_synclist', group, synclist) - - # Ensures that the new permissions are added to the database so that they can # be assigned during migrations # based off of https://stackoverflow.com/a/40092780 @@ -58,9 +18,13 @@ class Migration(migrations.Migration): dependencies = [ ('galaxy', '0003_inbound_repo_per_namespace'), - ('guardian', '0002_generic_permissions_index'), ] + # The commented out operations were used to map the group fields that were part of the legacy permissioning + # system when galaxy_ng was only deployed on console.redhat.com to django guardian permissions. With the + # removal of django guardian, they can no longer be run. Since the first release of galaxy_ng (4.2) + # included all the migrations up to 0013, theses data migrations are not required by any deployments and + # can safely be removed. operations = [ migrations.AlterModelOptions( name='namespace', @@ -69,16 +33,16 @@ class Migration(migrations.Migration): migrations.RunPython( code=initialize_permissions, ), - migrations.RunPython( - code=convert_namespace_groups_to_permissions, - ), + # migrations.RunPython( + # code=convert_namespace_groups_to_permissions, + # ), migrations.RemoveField( model_name='namespace', name='groups', ), - migrations.RunPython( - code=convert_synclist_groups_to_permissions, - ), + # migrations.RunPython( + # code=convert_synclist_groups_to_permissions, + # ), migrations.RemoveField( model_name='synclist', name='groups', diff --git a/galaxy_ng/app/migrations/0013_partner_engineer_group_migrations.py b/galaxy_ng/app/migrations/0013_partner_engineer_group_migrations.py index d2aafb08bc..a1e5eebd7c 100644 --- a/galaxy_ng/app/migrations/0013_partner_engineer_group_migrations.py +++ b/galaxy_ng/app/migrations/0013_partner_engineer_group_migrations.py @@ -1,53 +1,9 @@ from django.db import migrations -from django.core import exceptions - - -# note: using assign_perm from guardian.shortcuts doesn't work in migrations -# https://github.com/django-guardian/django-guardian/issues/281#issuecomment-156264129 -# The following function is adapted from https://gist.github.com/xuhcc/67871719116bdc0fee6c -def assign_perm(apps, perm, owner): - PermissionModel = apps.get_model('auth', 'Permission') - app_label, codename = perm.split('.', 1) - - perm = PermissionModel.objects.get( - content_type__app_label=app_label, - codename=codename) - - owner.permissions.add(perm) - - -def convert_namespace_groups_to_permissions(apps, schema_editor): - GroupModel = apps.get_model('galaxy', 'Group') - pe_perms = [ - # groups - 'galaxy.view_group', - 'galaxy.delete_group', - 'galaxy.add_group', - 'galaxy.change_group', - - # users - 'galaxy.view_user', - 'galaxy.delete_user', - 'galaxy.add_user', - 'galaxy.change_user', - - # collections - 'ansible.modify_ansible_repo_content', - - # namespaces - 'galaxy.add_namespace', - 'galaxy.change_namespace', - 'galaxy.upload_to_namespace', - ] - - try: - pe_group = GroupModel.objects.get(name="system:partner-engineers") - for perm in pe_perms: - assign_perm(apps, perm, pe_group) - except exceptions.ObjectDoesNotExist: - pass +# This migration used to assign a bunch of permissions to the system:partner-engineer group +# back when galaxy_ng was only deployed on console.redhat.com. Since this was a data migration +# that was only used for console.redhat.com, it can be safely removed. class Migration(migrations.Migration): dependencies = [ @@ -56,6 +12,6 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython( - code=convert_namespace_groups_to_permissions, + code=migrations.RunPython.noop, ), ] diff --git a/galaxy_ng/app/migrations/0029_move_perms_to_roles.py b/galaxy_ng/app/migrations/0029_move_perms_to_roles.py new file mode 100644 index 0000000000..b1508c088a --- /dev/null +++ b/galaxy_ng/app/migrations/0029_move_perms_to_roles.py @@ -0,0 +1,454 @@ +from django.db import migrations, connection +from django.conf import settings + + +OBJECT_PERMISSION_TRANSLATOR = [ + (( + ("container", "change_containernamespace"), + ("container", "namespace_push_containerdistribution"), + ("container", "namespace_change_containerdistribution"), + ("container", "namespace_modify_content_containerpushrepository"), + ("container", "namespace_add_containerdistribution"), + ), "galaxy.execution_environment_namespace_owner"), + (( + ("container", "namespace_push_containerdistribution"), + ("container", "namespace_change_containerdistribution"), + ("container", "namespace_modify_content_containerpushrepository"), + ), "galaxy.execution_environment_collaborator"), + (( + ("galaxy", "change_namespace"), + ("galaxy", "upload_to_namespace"), + ), "galaxy.collection_namespace_owner"), + (( + ("galaxy", "add_synclist"), + ("galaxy", "change_synclist"), + ("galaxy", "delete_synclist"), + ("galaxy", "view_synclist"), + ), "galaxy.synclist_owner"), +] + +GLOBAL_PERMISSION_TRANSLATOR = [ + (( + ("galaxy", "add_namespace"), + ("galaxy", "change_namespace"), + ("galaxy", "delete_namespace"), + ("galaxy", "upload_to_namespace"), + ("ansible", "delete_collection"), + ("ansible", "change_collectionremote"), + ("ansible", "view_collectionremote"), + ("ansible", "modify_ansible_repo_content"), + ("container", "delete_containerrepository"), + ("container", "namespace_change_containerdistribution"), + ("container", "namespace_modify_content_containerpushrepository"), + ("container", "namespace_push_containerdistribution"), + ("container", "add_containernamespace"), + ("container", "change_containernamespace"), + # ("container", "namespace_add_containerdistribution"), + ("galaxy", "add_containerregistryremote"), + ("galaxy", "change_containerregistryremote"), + ("galaxy", "delete_containerregistryremote"), + ), "galaxy.content_admin"), + + # COLLECTIONS + (( + ("galaxy", "add_namespace"), + ("galaxy", "change_namespace"), + ("galaxy", "delete_namespace"), + ("galaxy", "upload_to_namespace"), + ("ansible", "delete_collection"), + ("ansible", "change_collectionremote"), + ("ansible", "view_collectionremote"), + ("ansible", "modify_ansible_repo_content"), + ), "galaxy.collection_admin"), + (( + ("galaxy", "add_namespace"), + ("galaxy", "change_namespace"), + ("galaxy", "upload_to_namespace"), + ), "galaxy.collection_publisher"), + (( + ("ansible", "change_collectionremote"), + ("ansible", "view_collectionremote"), + ("ansible", "modify_ansible_repo_content"), + ), "galaxy.collection_curator"), + (( + ("galaxy", "change_namespace"), + ("galaxy", "upload_to_namespace"), + ), "galaxy.collection_namespace_owner"), + + # EXECUTION ENVIRONMENTS + (( + ("container", "delete_containerrepository"), + ("container", "namespace_change_containerdistribution"), + ("container", "namespace_modify_content_containerpushrepository"), + ("container", "namespace_push_containerdistribution"), + ("container", "add_containernamespace"), + ("container", "change_containernamespace"), + # Excluding this because it's only used for object assignment, not model + # ("container", "namespace_add_containerdistribution"), + ("galaxy", "add_containerregistryremote"), + ("galaxy", "change_containerregistryremote"), + ("galaxy", "delete_containerregistryremote"), + ), "galaxy.execution_environment_admin"), + (( + ("container", "namespace_change_containerdistribution"), + ("container", "namespace_modify_content_containerpushrepository"), + ("container", "namespace_push_containerdistribution"), + ("container", "add_containernamespace"), + ("container", "change_containernamespace"), + # ("container", "namespace_add_containerdistribution"), + ), "galaxy.execution_environment_publisher"), + (( + ("container", "change_containernamespace"), + ("container", "namespace_push_containerdistribution"), + ("container", "namespace_change_containerdistribution"), + ("container", "namespace_modify_content_containerpushrepository"), + # ("container", "namespace_add_containerdistribution"), + ), "galaxy.execution_environment_namespace_owner"), + (( + ("container", "namespace_push_containerdistribution"), + ("container", "namespace_change_containerdistribution"), + ("container", "namespace_modify_content_containerpushrepository"), + ), "galaxy.execution_environment_collaborator"), + + # ADMIN STUFF + (( + ("galaxy", "view_group"), + ("galaxy", "delete_group"), + ("galaxy", "add_group"), + ("galaxy", "change_group"), + ), "galaxy.group_admin"), + (( + ("galaxy", "view_user"), + ("galaxy", "delete_user"), + ("galaxy", "add_user"), + ("galaxy", "change_user"), + ), "galaxy.user_admin"), + (( + ("galaxy", "add_synclist"), + ("galaxy", "change_synclist"), + ("galaxy", "delete_synclist"), + ("galaxy", "view_synclist"), + ), "galaxy.synclist_owner"), + (( + ("core", "change_task"), + ("core", "delete_task"), + ("core", "view_task"), + ), "galaxy.task_admin"), +] + + +def batch_create(model, objects, flush=False): + """ + Save the objects to the database in batches of 1000. + """ + if len(objects) > 1000 or flush: + model.objects.bulk_create(objects) + objects.clear() + + +def get_roles_from_permissions(permission_iterable, translator, Role, Permission, super_permissions=None): + """ + Translates the given set of permissions into roles based on the translator that is passed in. + """ + roles_to_add = [] + if super_permissions is None: + super_permissions = {} + + # Use set comparisons instead of querysets to avoid unnecesary trips to the DB + permissions = set(((p.content_type.app_label, p.codename) for p in permission_iterable)) + + # Iterate through each locked role, apply any roles that match the group's permission + # set and remove any permissions that are applied via roles + for locked_perm_names, locked_rolename in translator: + role_perms = set(locked_perm_names) + + super_perm_for_role = super_permissions.get(locked_rolename, None) + + # Some objects have permissions that allow users to change permissions on the object. + # An example of this is galaxy.change_namespace allows the user to change their own + # permissions on the namespace effectively giving them all the permissions. If the + # user has one of these permissions, just give them the full role for the object. + if role_perms.issubset(permissions) or super_perm_for_role in permissions: + # don't bother setting the permissions on the locked roles. They'll get applied in + # the post migration hook. + role, _ = Role.objects.get_or_create(name=locked_rolename, locked=True) + roles_to_add.append(role) + permissions = permissions - role_perms + + for label, perm in permissions: + # prefix permission roles with _permission: instead of galaxy. so that they are hidden + # by default in the roles UI. + role, created = Role.objects.get_or_create( + name=f"_permission:{label}.{perm}", + description=f"Auto generated role for permission {label}.{perm}." + ) + + if created: + role.permissions.set([Permission.objects.get(codename=perm, content_type__app_label=label)]) + + roles_to_add.append(role) + + return roles_to_add + + +def get_global_group_permissions(group, Role, GroupRole, Permission): + """ + Takes in a group object and returns a list of GroupRole objects to be created for + the given group. + """ + + group_roles = [] + perms = group.permissions.all() + + # If there are no permissions, then our job here is done + if len(perms) == 0: + return group_roles + + roles = get_roles_from_permissions(perms, GLOBAL_PERMISSION_TRANSLATOR, Role, Permission) + + # Add locked roles that match the group's permission set + for role in roles: + group_roles.append(GroupRole(group=group, role=role)) + + return group_roles + + +def get_object_group_permissions(group, Role, GroupRole, ContentType, Permission): + """ + Takes in a group object and returns a list of GroupRole objects to be created for + each object that the group has permissions on. + """ + group_roles = [] + objects_with_perms = {} + + # Use raw sql because guardian won't be available + with connection.cursor() as cursor: + cursor.execute( + "SELECT object_pk, content_type_id, permission_id FROM" + f" guardian_groupobjectpermission WHERE group_id={group.pk};" + ) + + # group the object permissions for this group by object instances to make them easier to process + for object_pk, content_id, permission_id in cursor.fetchall(): + key = (content_id, object_pk) + + if key in objects_with_perms: + objects_with_perms[key].append(permission_id) + else: + objects_with_perms[key] = [permission_id,] + + # for each object permission that this group has, map it to a role. + for k in objects_with_perms: + perm_list = objects_with_perms[k] + content_type_id = k[0] + object_id = k[1] + + content_type = ContentType.objects.get(pk=content_type_id) + + permissions = Permission.objects.filter(pk__in=perm_list) + + # Add any locked roles that match the given group/objects permission set + roles = get_roles_from_permissions( + permissions, + OBJECT_PERMISSION_TRANSLATOR, + Role, + Permission, + super_permissions={ + "galaxy.execution_environment_namespace_owner": + ("container", "change_containernamespace"), + "galaxy.collection_namespace_owner": ("galaxy", "change_namespace"), + } + ) + + # Queue up the locked roles for creation + for role in roles: + group_roles.append(GroupRole( + role=role, + group=group, + content_type=content_type, + object_id=object_id + )) + + return group_roles + + +def add_object_role_for_users_with_permission(role, permission, UserRole, ContentType, User): + user_roles = [] + + # Use raw sql because guardian won't be available + with connection.cursor() as cursor: + cursor.execute( + "SELECT object_pk, content_type_id, user_id FROM" + f" guardian_userobjectpermission WHERE permission_id={permission.pk};" + ) + + for object_pk, content_type_id, user_id in cursor.fetchall(): + user_roles.append(UserRole( + role=role, + user=User.objects.get(pk=user_id), + content_type=ContentType.objects.get(pk=content_type_id), + object_id=object_pk + )), + batch_create(UserRole, user_roles) + + # Create any remaining roles + batch_create(UserRole, user_roles, flush=True) + + +def does_table_exist(table_name): + return table_name in connection.introspection.table_names() + + +def migrate_group_permissions_to_roles(apps, schema_editor): + """ + Migration strategy: + - Apply locked roles to the group that match the group's permission set. + - If any permissions are left over after a role is applied add a role with a single + permission to it to make up for each missing permission. + + Example: + If a group has permissions: + - galaxy.change_namespace + - galaxy.upload_to_namespace + - galaxy.delete_collection + - galaxy.view_group + - galaxy.view_user + + The following roles would get applied: + - galaxy.collection_namespace_owner + - _permission:galaxy.view_group + - _permission:galaxy.view_user + + galaxy.collection_namespace_owner is applied because the user has all the permissions that match it. + After applying galaxy.collection_namespace_owner, the view_group and view_group permissions are left + over so _permission:galaxy.view_group and _permission:galaxy.view_user are created for each + missing permission and added to the group. _permision: roles will only have the + a single permission in them for . + + Users with the ability to change the ownership of objects are given admin roles. For example + if my group has galaxy.change_namespace permissions on namespace foo, but nothing else, give + them the galaxy.collection_namespace_owner role because they can already escalate their permissions. + """ + + is_guardian_table_available = does_table_exist("guardian_groupobjectpermission") + + Group = apps.get_model("galaxy", "Group") + GroupRole = apps.get_model("core", "GroupRole") + Role = apps.get_model("core", "Role") + Permission = apps.get_model("auth", "Permission") + ContentType = apps.get_model("contenttypes", "ContentType") + + group_roles = [] + + # Group Permissions + for group in Group.objects.filter(name__ne="system:partner-engineers"): + group_roles.extend(get_global_group_permissions(group, Role, GroupRole, Permission)) + + # Skip migrating object permissions if guardian is not installed. + if is_guardian_table_available: + group_roles.extend( + get_object_group_permissions(group, Role, GroupRole, ContentType, Permission)) + + batch_create(GroupRole, group_roles) + + # Create any remaining roles + batch_create(GroupRole, group_roles, flush=True) + + +def migrate_user_permissions_to_roles(apps, schema_editor): + """ + Migration Strategy: + + We only care about user permissions for container namespaces and tasks. Global permissions + for users are not used, and they should be ignored if they exist. The only user permissions + that the system uses are ones that get automatically added for users that create tasks and + container namespaces, so these are the only permissions that will get migrated to roles here. + """ + + # Skip the migration if the guardian permission tables don't exist. + if not does_table_exist("guardian_userobjectpermission"): + return + + Permission = apps.get_model("auth", "Permission") + Role = apps.get_model("core", "Role") + UserRole = apps.get_model("core", "UserRole") + ContentType = apps.get_model("contenttypes", "ContentType") + User = apps.get_model(settings.AUTH_USER_MODEL) + + # Get all users with change_containernamespace permissions. Change container namespace allows + # users to set permissions on container namespaces, so it allows us to use it as a proxy for + # users that have administrative rights on container namespace and we can just give them the + # execution environment admin role. + change_container_namespace = Permission.objects.get( + codename="change_containernamespace", content_type__app_label="container") + container_namespace_admin, _ = Role.objects.get_or_create( + name="galaxy.execution_environment_namespace_owner", locked=True) + add_object_role_for_users_with_permission( + container_namespace_admin, change_container_namespace, UserRole, ContentType, User) + + # When tasks are created pulp adds delete task and a few other permissions to the user that + # initiates the task. Delete task is a good proxy for this role. + delete_task = Permission.objects.get( + codename="view_task", content_type__app_label="core") + task_owner, _ = Role.objects.get_or_create(name="galaxy.task_admin", locked=True) + add_object_role_for_users_with_permission( + task_owner, delete_task, UserRole, ContentType, User) + + +def edit_guardian_tables(apps, schema_editor): + """ + Remove foreign key constraints in the guardian tables + guardian_groupobjectpermission and guardian_userobjectpermission. + + This allows for objects in other tables to be deleted without + violating these foreign key constraints. + + This also allows for the these tables to remain in the database for reference purposes. + """ + + tables_to_edit = ["guardian_groupobjectpermission", "guardian_userobjectpermission"] + for table in tables_to_edit: + if not does_table_exist(table): + continue + + with connection.cursor() as cursor: + constraints = connection.introspection.get_constraints(cursor, table) + fk_constraints = [k for (k,v) in constraints.items() if v["foreign_key"]] + for name in fk_constraints: + cursor.execute( + f"ALTER TABLE {table} DROP CONSTRAINT {name};" + ) + + +def clear_model_permissions(apps, schema_editor): + """ + Clear out the old model level permission assignments. + """ + + Group = apps.get_model("galaxy", "Group") + User = apps.get_model(settings.AUTH_USER_MODEL) + + Group.permissions.through.objects.all().delete() + User.user_permissions.through.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("galaxy", "0028_update_synclist_model"), + ] + + operations = [ + migrations.RunPython( + code=migrate_group_permissions_to_roles, reverse_code=migrations.RunPython.noop, + ), + migrations.RunPython( + code=migrate_user_permissions_to_roles, reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=edit_guardian_tables, reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + code=clear_model_permissions, reverse_code=migrations.RunPython.noop + ), + ] \ No newline at end of file diff --git a/galaxy_ng/app/models/auth.py b/galaxy_ng/app/models/auth.py index 89813a9cc1..0933c2b54a 100644 --- a/galaxy_ng/app/models/auth.py +++ b/galaxy_ng/app/models/auth.py @@ -2,6 +2,8 @@ from django.contrib.auth import models as auth_models +from pulpcore.plugin.models import Group as PulpGroup + log = logging.getLogger(__name__) __all__ = ( @@ -36,7 +38,7 @@ def _make_name(scope, name): return f"{scope}:{name}" -class Group(auth_models.Group): +class Group(PulpGroup): objects = GroupManager() class Meta: diff --git a/galaxy_ng/app/models/collectionimport.py b/galaxy_ng/app/models/collectionimport.py index 6d1803eee6..636d6ecdea 100644 --- a/galaxy_ng/app/models/collectionimport.py +++ b/galaxy_ng/app/models/collectionimport.py @@ -2,9 +2,6 @@ from django.urls import reverse from django_lifecycle import LifecycleModel -from pulpcore.plugin.models import ( - AutoDeleteObjPermsMixin, -) from pulp_ansible.app.models import CollectionImport as PulpCollectionImport from .namespace import Namespace @@ -14,7 +11,7 @@ ) -class CollectionImport(LifecycleModel, AutoDeleteObjPermsMixin): +class CollectionImport(LifecycleModel): """ A model representing a mapping between pulp task id and task parameters. diff --git a/galaxy_ng/app/models/namespace.py b/galaxy_ng/app/models/namespace.py index 157708e372..811ac49fb0 100644 --- a/galaxy_ng/app/models/namespace.py +++ b/galaxy_ng/app/models/namespace.py @@ -3,7 +3,6 @@ from django.db import transaction from django.db import IntegrityError from django_lifecycle import LifecycleModel -from pulpcore.plugin.models import AutoDeleteObjPermsMixin from pulp_ansible.app.models import AnsibleRepository, AnsibleDistribution from galaxy_ng.app.access_control import mixins @@ -18,7 +17,7 @@ def create_inbound_repo(name): with contextlib.suppress(IntegrityError): # IntegrityError is suppressed for when the named repo/distro already exists # In that cases the error handling is performed on the caller. - repo = AnsibleRepository.objects.create(name=inbound_name) + repo = AnsibleRepository.objects.create(name=inbound_name, retain_repo_versions=1) AnsibleDistribution.objects.create( name=inbound_name, base_path=inbound_name, @@ -54,7 +53,7 @@ def get_or_create(self, *args, **kwargs): return ns, created -class Namespace(LifecycleModel, mixins.GroupModelPermissionsMixin, AutoDeleteObjPermsMixin): +class Namespace(LifecycleModel, mixins.GroupModelPermissionsMixin): """ A model representing Ansible content namespace. diff --git a/galaxy_ng/app/models/synclist.py b/galaxy_ng/app/models/synclist.py index dbad874dba..19aa44a9f9 100644 --- a/galaxy_ng/app/models/synclist.py +++ b/galaxy_ng/app/models/synclist.py @@ -1,7 +1,6 @@ from django.db import models from django_lifecycle import LifecycleModel from pulp_ansible.app.models import AnsibleDistribution, AnsibleRepository, Collection -from pulpcore.plugin.models import AutoDeleteObjPermsMixin from galaxy_ng.app.access_control.mixins import GroupModelPermissionsMixin @@ -9,7 +8,7 @@ class SyncList( - LifecycleModel, GroupModelPermissionsMixin, AutoDeleteObjPermsMixin + LifecycleModel, GroupModelPermissionsMixin ): POLICY_CHOICES = [ diff --git a/galaxy_ng/app/settings.py b/galaxy_ng/app/settings.py index e8066beb1b..a9e960fed8 100644 --- a/galaxy_ng/app/settings.py +++ b/galaxy_ng/app/settings.py @@ -217,7 +217,6 @@ 'ldap': [ "django_auth_ldap.backend.LDAPBackend", "django.contrib.auth.backends.ModelBackend", - "guardian.backends.ObjectPermissionBackend", "pulpcore.backends.ObjectRolePermissionBackend" ], 'keycloak': [ diff --git a/galaxy_ng/app/tasks/registry_sync.py b/galaxy_ng/app/tasks/registry_sync.py index dd59975286..0a2b055c14 100644 --- a/galaxy_ng/app/tasks/registry_sync.py +++ b/galaxy_ng/app/tasks/registry_sync.py @@ -18,6 +18,7 @@ def launch_container_remote_sync(remote, registry, repository): "remote_pk": str(remote.pk), "repository_pk": str(repository.pk), "mirror": True, + "signed_only": False, }, ) diff --git a/galaxy_ng/app/viewsets.py b/galaxy_ng/app/viewsets.py index f66315aef2..66c32f7cf8 100644 --- a/galaxy_ng/app/viewsets.py +++ b/galaxy_ng/app/viewsets.py @@ -3,7 +3,9 @@ from galaxy_ng.app import models from galaxy_ng.app.access_control import access_policy +from galaxy_ng.app.access_control.statements.roles import LOCKED_ROLES as GALAXY_LOCKED_ROLES from galaxy_ng.app.api.ui import serializers +from galaxy_ng.app.api.v3.serializers import NamespaceSummarySerializer # This file is necesary to prevent the DRF web API browser from breaking on all of the # pulp/api/v3/repositories/ endpoints. @@ -20,19 +22,48 @@ # on the remote field and can't find a viewset name for galaxy's ContainerRegistryRemote model. -class ContainerRegistryRemoteViewSet(pulp_viewsets.NamedModelViewSet, mixins.RetrieveModelMixin): +# Added to keep the DRF forms from breaking +class ContainerRegistryRemoteViewSet( + pulp_viewsets.NamedModelViewSet, + mixins.RetrieveModelMixin, +): queryset = models.ContainerRegistryRemote.objects.all() serializer_class = serializers.ContainerRegistryRemoteSerializer permission_classes = [access_policy.ContainerRegistryRemoteAccessPolicy] - endpoint_name = "execution-environments-registry-detail" + endpoint_name = "galaxy_ng/registry-remote" + LOCKED_ROLES = GALAXY_LOCKED_ROLES +# Added to keep the DRF forms from breaking class ContainerDistributionViewSet( pulp_viewsets.NamedModelViewSet, mixins.RetrieveModelMixin, - mixins.DestroyModelMixin, ): queryset = models.ContainerDistribution.objects.all() serializer_class = serializers.ContainerRepositorySerializer permission_classes = [access_policy.ContainerRepositoryAccessPolicy] - endpoint_name = "container" + endpoint_name = "galaxy_ng/container-distribution-proxy" + + +# added so that object permissions are viewable for namespaces on the roles api endpoint. +class NamespaceViewSet( + pulp_viewsets.NamedModelViewSet, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, +): + queryset = models.Namespace.objects.all() + serializer_class = NamespaceSummarySerializer + permission_classes = [access_policy.NamespaceAccessPolicy] + endpoint_name = "pulp_ansible/namespaces" + + +# This is here because when new objects are created, pulp tries to look up the viewset for +# the model so that it can run creation hooks to assign permissions. This function fails +# if the model isn't registered with a pulp viewset +# https://github.com/pulp/pulpcore/blob/52c8e16997849b9a639aec04945cec98486d54fb/pulpcore +# /app/models/access_policy.py#L73 +class GroupViewset( + pulp_viewsets.NamedModelViewSet, +): + queryset = models.auth.Group.objects.all() + endpoint_name = "galaxy_ng/sgroups" diff --git a/galaxy_ng/tests/functional/api/test_container_repository_tags.py b/galaxy_ng/tests/functional/api/test_container_repository_tags.py index 8c42f39308..5ba38dcb94 100644 --- a/galaxy_ng/tests/functional/api/test_container_repository_tags.py +++ b/galaxy_ng/tests/functional/api/test_container_repository_tags.py @@ -4,7 +4,7 @@ from pulpcore.client.galaxy_ng.exceptions import ApiException from pulp_container.tests.functional.api import rbac_base -from pulp_container.tests.functional.constants import DOCKERHUB_PULP_FIXTURE_1 +from pulp_container.tests.functional.constants import PULP_FIXTURE_1 from pulp_smash import cli from galaxy_ng.tests.functional.utils import TestCaseUsingBindings @@ -37,11 +37,11 @@ def setUpClass(cls): cls.registry_name = urlparse(cls.cfg.get_base_url()).netloc admin_user, admin_password = cls.cfg.pulp_auth cls.user_admin = {"username": admin_user, "password": admin_password} - cls._pull(f"{DOCKERHUB_PULP_FIXTURE_1}:manifest_a") + cls._pull(f"{PULP_FIXTURE_1}:manifest_a") def test_list_container_repository_tags(self): image_name = "foo/bar" - image_path = f"{DOCKERHUB_PULP_FIXTURE_1}:manifest_a" + image_path = f"{PULP_FIXTURE_1}:manifest_a" local_url = "/".join([self.registry_name, f"{image_name}:1.0"]) # expect the Container Repository Tags not to exist yet diff --git a/galaxy_ng/tests/functional/api/test_container_sync_and_manifest_lists.py b/galaxy_ng/tests/functional/api/test_container_sync_and_manifest_lists.py index 3344f0ae58..c7335f36d7 100644 --- a/galaxy_ng/tests/functional/api/test_container_sync_and_manifest_lists.py +++ b/galaxy_ng/tests/functional/api/test_container_sync_and_manifest_lists.py @@ -1,11 +1,3 @@ -import unittest - -from urllib.parse import urlparse - -from pulpcore.client.galaxy_ng.exceptions import ApiException -from pulp_container.tests.functional.api import rbac_base -from pulp_container.tests.functional.constants import DOCKERHUB_PULP_FIXTURE_1 -from pulp_smash import cli from pulp_smash.pulp3.bindings import monitor_task from galaxy_ng.tests.functional.utils import TestCaseUsingBindings @@ -22,7 +14,9 @@ class ContainerSyncandManifestListTestCase(TestCaseUsingBindings): - set up remote container in docker registry for pulp/test-fixture-1 - set tags to: ml_i, manifest_a - perform sync - - verify that the repo has two images with tags ml_i and manifest_a, that manifest_a is a manifest and that ml_i is a manifest list with two manifests in it + - verify that the repo has two images with tags ml_i and manifest_a, + that manifest_a is a manifest and that ml_i is a manifest list with + two manifests in it Test multiple repositories in a registry - set up remote container in docker registry for pulp/test-fixture-1 with just tag manifest_a @@ -36,10 +30,18 @@ def setUp(self): "name": "Docker Hub", "url": "https://registry.hub.docker.com", }) - self.addCleanup(self.smash_client.delete, f"{self.galaxy_api_prefix}/_ui/v1/execution-environments/registries/{self.docker_registry.pk}/") + self.addCleanup( + self.smash_client.delete, + ( + f"{self.galaxy_api_prefix}/_ui/v1/execution-environments/" + f"registries/{self.docker_registry.pk}/" + ) + ) def _delete_remote_repo(self, remote): - self.smash_client.delete(f"{self.galaxy_api_prefix}/_ui/v1/execution-environments/repositories/{remote.name}/") + self.smash_client.delete(( + f"{self.galaxy_api_prefix}/_ui/v1/execution-environments/" + f"repositories/{remote.name}/")) def test_manifests_and_remote_sync(self): remote_repo = self.container_remotes_api.create({ @@ -53,7 +55,9 @@ def test_manifests_and_remote_sync(self): # the galaxy_ng client doesn't seem to return anything with the sync function, so we're # using the api directly instead - self.smash_client.post(f"{self.galaxy_api_prefix}/_ui/v1/execution-environments/repositories/{remote_repo.name}/_content/sync/") + self.smash_client.post(( + f"{self.galaxy_api_prefix}/_ui/v1/execution-environments/" + f"repositories/{remote_repo.name}/_content/sync/")) tags_list = self.container_repo_tags_api.list(remote_repo.name) self.assertEqual(tags_list.meta.count, 2) @@ -64,8 +68,10 @@ def test_manifests_and_remote_sync(self): tagged_ml = tags_list.data[0] self.assertIn("manifest.v2", tagged_ml.tagged_manifest.media_type) - # Note, the manifest_a tag in the pulp test repo points to a manifest that's part of the ml_i manifest list - # so it won't show up in here since exclude_child_manifests filters out any manifest that is part of a manifest list. + # Note, the manifest_a tag in the pulp test repo points to a + # manifest that's part of the ml_i manifest list + # so it won't show up in here since exclude_child_manifests + # filters out any manifest that is part of a manifest list. image_list = self.container_images_api.list(remote_repo.name, exclude_child_manifests=True) self.assertEqual(image_list.meta.count, 1) @@ -77,7 +83,6 @@ def test_manifests_and_remote_sync(self): self.assertIn("manifest.list.v2", manifest_list.media_type) - def test_registry_sync(self): remote_repo1 = self.container_remotes_api.create({ "name": "test-repo1", @@ -96,8 +101,9 @@ def test_registry_sync(self): self.addCleanup(self._delete_remote_repo, remote_repo1) self.addCleanup(self._delete_remote_repo, remote_repo2) - - sync_task = self.smash_client.post(f"{self.galaxy_api_prefix}/_ui/v1/execution-environments/registries/{self.docker_registry.pk}/sync/") + sync_task = self.smash_client.post(( + f"{self.galaxy_api_prefix}/_ui/v1/execution-environments/" + f"registries/{self.docker_registry.pk}/sync/")) for task in sync_task['child_tasks']: monitor_task(task) diff --git a/galaxy_ng/tests/functional/cli/test_collection_upload.py b/galaxy_ng/tests/functional/cli/test_collection_upload.py index 55ec37813e..8ee97c2d63 100644 --- a/galaxy_ng/tests/functional/cli/test_collection_upload.py +++ b/galaxy_ng/tests/functional/cli/test_collection_upload.py @@ -2,13 +2,11 @@ import subprocess import tempfile -from unittest.case import skip from pulp_smash.pulp3.bindings import delete_orphans from pulp_smash.utils import http_get from galaxy_ng.tests.functional.utils import TestCaseUsingBindings -from galaxy_ng.tests.functional.utils import set_up_module as setUpModule # noqa:F401 class UploadCollectionTestCase(TestCaseUsingBindings): @@ -19,8 +17,8 @@ def test_upload_collection(self): delete_orphans() # Create namespace if it doesn't exist - data = str(self.namespace_api.list().data) - if "pulp" not in data: + data = self.namespace_api.list(name="pulp").data + if len(data) == 0: self.namespace_api.create(namespace={"name": "pulp", "groups": []}) # Preapare ansible.cfg for ansible-galaxy CLI @@ -35,7 +33,7 @@ def test_upload_collection(self): cmd = "ansible-galaxy collection publish -vvv -c {}".format(collection_path) - subprocess.run(cmd.split()) + subprocess.check_output(cmd.split()) # Verify that the collection was published collections = self.collections_api.list("published") @@ -51,12 +49,15 @@ def test_upload_collection(self): self.delete_namespace(collection.namespace) def test_uploaded_collection_logged(self): - """Test whether a Collection uploaded via ansible-galaxy is logged correctly in API Access Log.""" + """ + Test whether a Collection uploaded via ansible-galaxy is + logged correctly in API Access Log. + """ delete_orphans() # Create namespace if it doesn't exist - data = str(self.namespace_api.list().data) - if "pulp" not in data: + data = self.namespace_api.list(name="pulp").data + if len(data) == 0: self.namespace_api.create(namespace={"name": "pulp", "groups": []}) # Preapare ansible.cfg for ansible-galaxy CLI @@ -71,11 +72,11 @@ def test_uploaded_collection_logged(self): cmd = "ansible-galaxy collection publish -vvv -c {}".format(collection_path) - subprocess.run(cmd.split()) + subprocess.check_output(cmd.split()) cmd = f"docker cp pulp:/var/log/galaxy_api_access.log {tmp_dir}" - subprocess.run(cmd.split()) + subprocess.check_output(cmd.split()) with open(f"{tmp_dir}/galaxy_api_access.log") as f: log_contents = f.readlines() diff --git a/galaxy_ng/tests/functional/utils.py b/galaxy_ng/tests/functional/utils.py index d8e84ac553..20920f44c2 100644 --- a/galaxy_ng/tests/functional/utils.py +++ b/galaxy_ng/tests/functional/utils.py @@ -1,7 +1,6 @@ """Utilities for tests for the galaxy plugin.""" import os from functools import partial -import random import requests from unittest import SkipTest from tempfile import NamedTemporaryFile @@ -189,11 +188,13 @@ def setUpClass(cls): cls.container_repo_api = ContainerRepositoryApi(cls.client) cls.container_remotes_api = ApiUiV1ExecutionEnvironmentsRemotesApi(cls.client) cls.container_registries_api = ApiUiV1ExecutionEnvironmentsRegistriesApi(cls.client) - cls.container_remote_sync_api = ApiUiV1ExecutionEnvironmentsRepositoriesContentSyncApi(cls.client) + cls.container_remote_sync_api = \ + ApiUiV1ExecutionEnvironmentsRepositoriesContentSyncApi(cls.client) cls.container_registry_sync_api = ApiUiV1ExecutionEnvironmentsRegistriesSyncApi(cls.client) cls.container_images_api = ContainerImagesAPI(cls.client) cls.get_ansible_cfg_before_test() - cls.galaxy_api_prefix = os.getenv("PULP_GALAXY_API_PATH_PREFIX", "/api/galaxy").rstrip("/") + cls.galaxy_api_prefix = os.getenv( + "PULP_GALAXY_API_PATH_PREFIX", "/api/galaxy").rstrip("/") def tearDown(self): """Clean class-wide variable.""" @@ -245,7 +246,6 @@ def sync_repo(self, requirements_file, **kwargs): api_root = os.environ.get("PULP_API_ROOT", "/pulp/") monitor_task(f"{api_root}api/v3/tasks/{response.task}/") - def delete_namespace(self, namespace_name): """Delete a Namespace""" # namespace_api does not support delete, so we can use the smash_client directly diff --git a/galaxy_ng/tests/integration/api/rbac_actions/__init__.py b/galaxy_ng/tests/integration/api/rbac_actions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/galaxy_ng/tests/integration/api/rbac_actions/auth.py b/galaxy_ng/tests/integration/api/rbac_actions/auth.py new file mode 100644 index 0000000000..801dc6b66c --- /dev/null +++ b/galaxy_ng/tests/integration/api/rbac_actions/auth.py @@ -0,0 +1,194 @@ +import requests + +from .utils import ( + API_ROOT, + NAMESPACE, + PULP_API_ROOT, + PASSWORD, + SERVER, + assert_pass, + del_user, + del_role, + del_group, + create_group, + create_user, + create_role, + gen_string +) + + +def view_groups(user, password, expect_pass, extra): + response = requests.get( + f"{API_ROOT}_ui/v1/groups/", + auth=(user['username'], password) + ) + assert_pass(expect_pass, response.status_code, 200, 403) + + +def delete_groups(user, password, expect_pass, extra): + g = create_group(gen_string()) + # delete group + response = requests.delete( + f"{API_ROOT}_ui/v1/groups/{g['id']}/", + auth=(user['username'], password) + ) + + del_group(g['id']) + + assert_pass(expect_pass, response.status_code, 204, 403) + + +def add_groups(user, password, expect_pass, extra): + response = requests.post( + f"{API_ROOT}_ui/v1/groups/", + json={"name": f"{NAMESPACE}_group"}, + auth=(user["username"], password) + ) + + data = response.json() + + if "id" in data: + del_group(data["id"]) + + assert_pass(expect_pass, response.status_code, 201, 403) + return response.json() + + +def change_groups(user, password, expect_pass, extra): + g = create_group(gen_string()) + + response = requests.post( + f"{PULP_API_ROOT}groups/{g['id']}/roles/", + json={ + "content_object": None, + "role": "galaxy.content_admin", + }, + auth=(user["username"], password), + ) + + del_group(g['id']) + + assert_pass(expect_pass, response.status_code, 201, 403) + + +def view_users(user, password, expect_pass, extra): + response = requests.get( + f"{API_ROOT}_ui/v1/users/", + auth=(user["username"], password), + ) + assert_pass(expect_pass, response.status_code, 200, 403) + + +def add_users(user, password, expect_pass, extra): + response = requests.post( + f"{API_ROOT}_ui/v1/users/", + json={ + "username": gen_string(), + "first_name": "", + "last_name": "", + "email": "", + "group": "", + "password": PASSWORD, + "description": "", + }, + auth=(user["username"], password), + ) + + data = response.json() + + if "id" in data: + del_user(data["id"]) + + assert_pass(expect_pass, response.status_code, 201, 403) + + +def change_users(user, password, expect_pass, extra): + new_user = create_user(gen_string(), PASSWORD) + new_user["first_name"] = "foo" + + response = requests.put( + f"{API_ROOT}_ui/v1/users/{new_user['id']}/", + json=new_user, + auth=(user["username"], password), + ) + + del_user(new_user["id"]) + + assert_pass(expect_pass, response.status_code, 200, 403) + + +def delete_users(user, password, expect_pass, extra): + new_user = create_user(gen_string(), PASSWORD) + # Delete user + response = requests.delete( + f"{API_ROOT}_ui/v1/users/{new_user['id']}/", + auth=(user["username"], password), + ) + + del_user(new_user['id']) + + assert_pass(expect_pass, response.status_code, 204, 403) + + +def add_role(user, password, expect_pass, extra): + response = requests.post( + f"{PULP_API_ROOT}roles/", + json={ + "name": gen_string(), + "permissions": [ + "galaxy.add_group", + "galaxy.change_group", + "galaxy.delete_group", + "galaxy.view_group", + ], + }, + auth=(user["username"], password), + ) + + data = response.json() + + if "pulp_href" in data: + del_role(data["pulp_href"]) + + assert_pass(expect_pass, response.status_code, 201, 403) + + +def view_role(user, password, expect_pass, extra): + response = requests.get( + f'{PULP_API_ROOT}roles/', + auth=(user["username"], password), + ) + assert_pass(expect_pass, response.status_code, 200, 403) + + +def delete_role(user, password, expect_pass, extra): + r = create_role(gen_string()) + + response = requests.delete( + f'{SERVER}{r["pulp_href"]}', + auth=(user["username"], password), + ) + + del_role(r["pulp_href"]) + + assert_pass(expect_pass, response.status_code, 204, 403) + + +def change_role(user, password, expect_pass, extra): + role = create_role(gen_string()) + + role["permissions"] = [ + "galaxy.add_user", + "galaxy.change_user", + "galaxy.view_user", + ] + + response = requests.patch( + f'{SERVER}{role["pulp_href"]}', + json=role, + auth=(user["username"], password), + ) + + del_role(role["pulp_href"]) + + assert_pass(expect_pass, response.status_code, 200, 403) diff --git a/galaxy_ng/tests/integration/api/rbac_actions/collections.py b/galaxy_ng/tests/integration/api/rbac_actions/collections.py new file mode 100644 index 0000000000..41ecab5f9b --- /dev/null +++ b/galaxy_ng/tests/integration/api/rbac_actions/collections.py @@ -0,0 +1,200 @@ +import requests +import subprocess + +from .utils import ( + API_ROOT, + ADMIN_USER, + ADMIN_TOKEN, + assert_pass, + del_collection, + del_namespace, + gen_string, + gen_namespace, + reset_remote +) + +from galaxy_ng.tests.integration.utils import build_collection + + +def create_collection_namespace(user, password, expect_pass, extra): + ns = gen_string() + + response = requests.post( + f"{API_ROOT}_ui/v1/namespaces/", + json={ + "name": ns, + "groups": [], + }, + auth=(user['username'], password), + ) + + del_namespace(ns) + + assert_pass(expect_pass, response.status_code, 201, 403) + return response.json() + + +def change_collection_namespace(user, password, expect_pass, extra): + ns = extra['collection'].get_namespace() + + response = requests.put( + f"{API_ROOT}_ui/v1/namespaces/{ns['name']}/", + json={**ns, "description": "foo"}, + auth=(user['username'], password), + ) + assert_pass(expect_pass, response.status_code, 200, 403) + + +def delete_collection_namespace(user, password, expect_pass, extra): + name = gen_string() + + gen_namespace(name) + + response = requests.delete( + f"{API_ROOT}_ui/v1/namespaces/{name}/", + auth=(user['username'], password), + ) + + del_namespace(name) + + assert_pass(expect_pass, response.status_code, 204, 403) + + +def upload_collection_to_namespace(user, password, expect_pass, extra): + + name = gen_string() + + artifact = build_collection( + name=name, + namespace=extra['collection'].get_namespace()["name"] + ) + + # Don't reset the admin user's token, or all the other tests + # will break + if user['username'] == ADMIN_USER: + token = ADMIN_TOKEN + else: + token = requests.post( + f'{API_ROOT}v3/auth/token/', + auth=(user['username'], password), + ).json()['token'] or None + + cmd = [ + "ansible-galaxy", + "collection", + "publish", + "--api-key", + token, + "--server", + API_ROOT, + artifact.filename + ] + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + del_collection(name, extra['collection'].get_namespace()["name"]) + + if expect_pass: + assert proc.returncode == 0 + else: + assert proc.returncode != 0 + + +def delete_collection(user, password, expect_pass, extra): + collection = extra['collection'].get_collection() + + ns = collection['namespace'] + name = collection['name'] + + response = requests.delete( + f"{API_ROOT}v3/plugin/ansible/content/staging/collections/index/{ns}/{name}/", + auth=(user['username'], password), + ) + + assert_pass(expect_pass, response.status_code, 202, 403) + + +def configure_collection_sync(user, password, expect_pass, extra): + remote = reset_remote() + + remote['password'] = "foobar" + + response = requests.put( + f"{API_ROOT}content/{remote['name']}/v3/sync/config/", + json=remote, + auth=(user['username'], password), + ) + assert_pass(expect_pass, response.status_code, 200, 403) + + +def launch_collection_sync(user, password, expect_pass, extra): + # call get_remote to reset object + remote = reset_remote() + + response = requests.post( + f"{API_ROOT}content/{remote['name']}/v3/sync/", + auth=(user['username'], password), + ) + assert_pass(expect_pass, response.status_code, 200, 403) + + +def view_sync_configuration(user, password, expect_pass, extra): + remote = reset_remote() + + response = requests.get( + f"{API_ROOT}content/{remote['name']}/v3/sync/config/", + auth=(user['username'], password), + ) + assert_pass(expect_pass, response.status_code, 200, 403) + + +def approve_collections(user, password, expect_pass, extra): + collection = extra['collection'].get_collection() + response = requests.post( + ( + f"{API_ROOT}v3/collections/{collection['namespace']}" + f"/{collection['name']}/versions/{collection['version']}" + "/move/staging/published/" + ), + auth=(user['username'], password), + ) + assert_pass(expect_pass, response.status_code, 202, 403) + + +def reject_collections(user, password, expect_pass, extra): + collection = extra['collection'].get_collection() + response = requests.post( + ( + f"{API_ROOT}v3/collections/{collection['namespace']}" + f"/{collection['name']}/versions/{collection['version']}" + "/move/staging/rejected/" + ), + auth=(user['username'], password), + ) + assert_pass(expect_pass, response.status_code, 202, 403) + + +def deprecate_collections(user, password, expect_pass, extra): + collection = extra['collection'].get_collection() + response = requests.patch( + ( + f"{API_ROOT}v3/plugin/ansible/content/staging/collections" + f"/index/{collection['namespace']}/{collection['name']}/" + ), + json={"deprecated": True}, + auth=(user['username'], password), + ) + assert_pass(expect_pass, response.status_code, 202, 403) + + +def undeprecate_collections(user, password, expect_pass, extra): + collection = extra['collection'].get_collection() + + response = requests.patch( + ( + f"{API_ROOT}v3/plugin/ansible/content/staging/collections" + f"/index/{collection['namespace']}/{collection['name']}/" + ), + json={"deprecated": False}, + auth=(user['username'], password), + ) + assert_pass(expect_pass, response.status_code, 202, 403) diff --git a/galaxy_ng/tests/integration/api/rbac_actions/exec_env.py b/galaxy_ng/tests/integration/api/rbac_actions/exec_env.py new file mode 100644 index 0000000000..b993f1fec2 --- /dev/null +++ b/galaxy_ng/tests/integration/api/rbac_actions/exec_env.py @@ -0,0 +1,225 @@ +import requests + +from .utils import ( + API_ROOT, + PULP_API_ROOT, + CONTAINER_IMAGE, + assert_pass, + gen_string, + del_registry, + del_container, + gen_registry, + gen_remote_container, + cleanup_test_obj, + podman_push +) + +IMAGE_NAME = CONTAINER_IMAGE[0] + + +# REMOTES +def create_ee_remote(user, password, expect_pass, extra): + registry = extra["registry"].get_registry() + + response = requests.post( + f"{API_ROOT}_ui/v1/execution-environments/remotes/", + json={ + "name": gen_string(), + "upstream_name": "foo", + "registry": registry["pk"], + }, + auth=(user['username'], password), + ) + + cleanup_test_obj(response, "name", del_container) + + assert_pass(expect_pass, response.status_code, 201, 403) + + +def update_ee_remote(user, password, expect_pass, extra): + remote = extra["remote_ee"].get_remote() + remote["include_tags"] = ["latest"] + + response = requests.put( + f"{API_ROOT}_ui/v1/execution-environments/remotes/{remote['pulp_id']}/", + json=remote, + auth=(user['username'], password), + ) + + assert_pass(expect_pass, response.status_code, 200, 403) + + +def sync_remote_ee(user, password, expect_pass, extra): + container = extra["remote_ee"].get_container() + + response = requests.post( + f'{API_ROOT}_ui/v1/execution-environments/repositories/{container["name"]}/_content/sync/', + auth=(user['username'], password) + ) + assert_pass(expect_pass, response.status_code, 202, 403) + + +# REGISTRIES +def delete_ee_registry(user, password, expect_pass, extra): + registry = gen_registry(gen_string()) + + response = requests.delete( + f"{API_ROOT}_ui/v1/execution-environments/registries/{registry['pk']}/", + auth=(user['username'], password), + ) + + del_registry(registry["pk"]) + + assert_pass(expect_pass, response.status_code, 204, 403) + + +def index_ee_registry(user, password, expect_pass, extra): + registry = extra["registry"].get_registry() + + response = requests.post( + f"{API_ROOT}_ui/v1/execution-environments/registries/{registry['pk']}/index/", + auth=(user['username'], password), + ) + assert_pass(expect_pass, response.status_code, 400, 403) + + +def update_ee_registry(user, password, expect_pass, extra): + registry = extra["registry"].get_registry() + + registry['rate_limit'] = 2 + + response = requests.put( + f"{API_ROOT}_ui/v1/execution-environments/registries/{registry['pk']}/", + json=registry, + auth=(user['username'], password), + ) + assert_pass(expect_pass, response.status_code, 200, 403) + + +def create_ee_registry(user, password, expect_pass, extra): + response = requests.post( + f"{API_ROOT}_ui/v1/execution-environments/registries/", + json={ + "name": gen_string(), + "url": "http://example.com", + }, + auth=(user['username'], password), + ) + + cleanup_test_obj(response, "pk", del_registry) + + assert_pass(expect_pass, response.status_code, 201, 403) + + +# EXECUTION ENVIRONMENTS +def delete_ee(user, password, expect_pass, extra): + registry = extra["registry"].get_registry() + + name = gen_string() + gen_remote_container(name, registry["pk"]) + + response = requests.delete( + f"{API_ROOT}_ui/v1/execution-environments/repositories/{name}/", + auth=(user['username'], password), + ) + + del_container(name) + + assert_pass(expect_pass, response.status_code, 202, 403) + + +def change_ee_description(user, password, expect_pass, extra): + container = extra["remote_ee"].get_container() + + response = requests.patch( + f"{PULP_API_ROOT}distributions/container/container/{container['id']}/", + json={ + "description": "hello world", + }, + auth=(user['username'], password), + ) + assert_pass(expect_pass, response.status_code, 202, 403) + + +def change_ee_readme(user, password, expect_pass, extra): + container = extra["remote_ee"].get_container() + + url = ( + f"{API_ROOT}_ui/v1/execution-environments/repositories/" + f"{container['name']}/_content/readme/" + ) + response = requests.put( + url, + json={"text": "Praise the readme!"}, + auth=(user['username'], password), + ) + assert_pass(expect_pass, response.status_code, 200, 403) + + +def change_ee_namespace(user, password, expect_pass, extra): + namespace = extra["remote_ee"].get_namespace() + + response = requests.put( + f"{API_ROOT}_ui/v1/execution-environments/namespaces/{namespace['name']}/", + json=namespace, + auth=(user['username'], password) + ) + + assert_pass(expect_pass, response.status_code, 200, 403) + + +def create_ee_local(user, password, expect_pass, extra): + name = gen_string() + return_code = podman_push(user['username'], password, name) + + if return_code == 0: + del_container(name) + + if expect_pass: + assert return_code == 0 + else: + assert return_code != 0 + + +def create_ee_in_existing_namespace(user, password, expect_pass, extra): + namespace = extra["local_ee"].get_namespace()["name"] + name = f"{namespace}/{gen_string()}" + + return_code = podman_push(user['username'], password, name) + + if return_code == 0: + del_container(name) + + if expect_pass: + assert return_code == 0 + else: + assert return_code != 0 + + +def push_updates_to_existing_ee(user, password, expect_pass, extra): + container = extra["local_ee"].get_container()["name"] + tag = gen_string() + + return_code = podman_push(user['username'], password, container, tag=tag) + + if expect_pass: + assert return_code == 0 + else: + assert return_code != 0 + + +def change_ee_tags(user, password, expect_pass, extra): + manifest = extra["local_ee"].get_manifest() + repo_pk = extra["local_ee"].get_container()["pulp"]["repository"]["pulp_id"] + tag = gen_string() + + response = requests.post( + f'{PULP_API_ROOT}repositories/container/container-push/{repo_pk}/tag/', + json={ + 'digest': manifest['digest'], + 'tag': tag + }, + auth=(user['username'], password) + ) + + assert_pass(expect_pass, response.status_code, 202, 403) diff --git a/galaxy_ng/tests/integration/api/rbac_actions/misc.py b/galaxy_ng/tests/integration/api/rbac_actions/misc.py new file mode 100644 index 0000000000..a6dd532a5f --- /dev/null +++ b/galaxy_ng/tests/integration/api/rbac_actions/misc.py @@ -0,0 +1,11 @@ +import requests +from .utils import PULP_API_ROOT, assert_pass + + +# Tasks +def view_tasks(user, password, expect_pass, extra): + response = requests.get( + f"{PULP_API_ROOT}tasks/", + auth=(user['username'], password) + ) + assert_pass(expect_pass, response.status_code, 200, 403) diff --git a/galaxy_ng/tests/integration/api/rbac_actions/utils.py b/galaxy_ng/tests/integration/api/rbac_actions/utils.py new file mode 100644 index 0000000000..5dc3db48cb --- /dev/null +++ b/galaxy_ng/tests/integration/api/rbac_actions/utils.py @@ -0,0 +1,516 @@ +import random +import requests +import string +import time +import subprocess +from urllib.parse import urljoin +from galaxy_ng.tests.integration.utils import ( + build_collection, + upload_artifact, + get_client, + wait_for_task as wait_for_task_fixtures, + TaskWaitingTimeout +) +from galaxy_ng.tests.integration.conftest import AnsibleConfigFixture + +from ansible.galaxy.api import GalaxyError + +CLIENT_CONFIG = AnsibleConfigFixture("admin") + +API_ROOT = CLIENT_CONFIG["url"] +PULP_API_ROOT = f"{API_ROOT}pulp/api/v3/" +SERVER = API_ROOT.split("/api/")[0] + +ADMIN_USER = CLIENT_CONFIG["username"] +ADMIN_PASSWORD = CLIENT_CONFIG["password"] +ADMIN_TOKEN = CLIENT_CONFIG["token"] + +ADMIN_CREDENTIALS = (ADMIN_USER, ADMIN_PASSWORD) + +NAMESPACE = "rbac_roles_test" +PASSWORD = "p@ssword!" +TLS_VERIFY = "--tls-verify=false" +CONTAINER_IMAGE = ["foo/ubi9-minimal", "foo/ubi8-minimal"] + +REQUIREMENTS_FILE = "collections:\n - name: newswangerd.collection_demo\n" # noqa: 501 + +TEST_CONTAINER = "alpine" + + +class InvalidResponse(Exception): + pass + + +def assert_pass(expect_pass, code, pass_status, deny_status): + if code not in (pass_status, deny_status): + raise InvalidResponse(f'Invalid response status code: {code}') + + if expect_pass: + assert code == pass_status + else: + assert code == deny_status + + +def gen_string(size=10, chars=string.ascii_lowercase): + return ''.join(random.choice(chars) for _ in range(size)) + + +def create_group_with_user_and_role(user, role, content_object=None, group=None): + name = f"{NAMESPACE}_group_{gen_string()}" + + g = create_group(name) + + requests.post( + f"{API_ROOT}_ui/v1/groups/{g['id']}/users/", + json={"username": user["username"]}, + auth=ADMIN_CREDENTIALS + ) + requests.post( + f"{PULP_API_ROOT}groups/{g['id']}/roles/", + json={"role": role, "content_object": content_object}, + auth=ADMIN_CREDENTIALS + ) + return g + + +def create_user(username, password): + response = requests.post( + f"{API_ROOT}_ui/v1/users/", + json={ + "username": username, + "first_name": "", + "last_name": "", + "email": "", + "group": "", + "password": password, + "description": "", + }, + auth=ADMIN_CREDENTIALS, + ) + return response.json() + + +def wait_for_task(resp, path=None, timeout=300): + ready = False + host = SERVER + # Community collection delete wasn't providing path with task pk + if path is not None: + url = urljoin(f"{PULP_API_ROOT}{path}", f'{resp.json()["task"]}/') + else: + url = urljoin(f"{host}", f"{resp.json()['task']}") + wait_until = time.time() + timeout + while not ready: + if wait_until < time.time(): + raise TaskWaitingTimeout() + try: + resp = requests.get(url, auth=ADMIN_CREDENTIALS) + except GalaxyError as e: + if "500" not in str(e): + raise + else: + ready = resp.json()["state"] not in ("running", "waiting") + time.sleep(5) + return resp + + +def ensure_test_container_is_pulled(): + cmd = ["podman", "container", "exists", TEST_CONTAINER] + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if proc.returncode == 1: + cmd = ["podman", "image", "pull", "alpine"] + subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + +def podman_push(username, password, container, tag="latest"): + ensure_test_container_is_pulled() + + new_container = f"localhost:5001/{container}:{tag}" + + tag_cmd = ["podman", "image", "tag", TEST_CONTAINER, new_container] + subprocess.run(tag_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + push_cmd = [ + "podman", + "push", + "--creds", + f"{username}:{password}", + new_container, + "--remove-signatures", + "--tls-verify=false"] + + return subprocess.run(push_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode + + +def del_user(pk): + requests.delete( + f"{API_ROOT}_ui/v1/users/{pk}/", + auth=(ADMIN_CREDENTIALS), + ) + + +def create_group(name): + return requests.post( + f"{API_ROOT}_ui/v1/groups/", + json={"name": name}, + auth=ADMIN_CREDENTIALS + ).json() + + +def del_group(pk): + return requests.delete( + f"{API_ROOT}_ui/v1/groups/{pk}/", + auth=ADMIN_CREDENTIALS + ) + + +def create_role(name): + return requests.post( + f"{PULP_API_ROOT}roles/", + json={ + "name": name, + "permissions": [], + }, + auth=ADMIN_CREDENTIALS, + ).json() + + +def del_role(href): + requests.delete( + f"{SERVER}{href}", + auth=ADMIN_CREDENTIALS + ) + + +def del_namespace(name): + return requests.delete( + f"{API_ROOT}_ui/v1/namespaces/{name}/", + auth=ADMIN_CREDENTIALS, + ) + + +def del_collection(name, namespace, repo="staging"): + requests.delete( + f"{API_ROOT}v3/plugin/ansible/content/{repo}/collections/index/{namespace}/{name}/", + auth=ADMIN_CREDENTIALS, + ) + + +def gen_namespace(name, groups=None): + groups = groups or [] + + return requests.post( + f"{API_ROOT}_ui/v1/namespaces/", + json={ + "name": name, + "groups": groups, + }, + auth=ADMIN_CREDENTIALS, + ).json() + + +def gen_collection(name, namespace): + artifact = build_collection( + name=name, + namespace=namespace + ) + + ansible_config = AnsibleConfigFixture("admin", namespace=artifact.namespace) + + client = get_client(ansible_config) + + wait_for_task_fixtures(client, upload_artifact(ansible_config, client, artifact)) + + resp = requests.get( + f"{PULP_API_ROOT}content/ansible/collection_versions/?name={name}&namespace={namespace}", + auth=ADMIN_CREDENTIALS + ) + + return resp.json()["results"][0] + + +def reset_remote(): + return requests.put( + f"{API_ROOT}content/community/v3/sync/config/", + json={ + "url": "https://example.com/", + "auth_url": None, + "token": None, + "policy": "immediate", + "requirements_file": REQUIREMENTS_FILE, + "username": None, + "password": None, + "tls_validation": False, + "client_key": None, + "client_cert": None, + "download_concurrency": 10, + "proxy_url": None, + "proxy_username": None, + "proxy_password": None, + "rate_limit": 8, + "signed_only": False, + }, + auth=ADMIN_CREDENTIALS, + ).json() + + +def wait_for_all_tasks(timeout=300): + ready = False + wait_until = time.time() + timeout + + while not ready: + if wait_until < time.time(): + raise TaskWaitingTimeout() + running_count = requests.get( + PULP_API_ROOT + "tasks/?state=running", + auth=ADMIN_CREDENTIALS + ).json()["count"] + + waiting_count = requests.get( + PULP_API_ROOT + "tasks/?state=waiting", + auth=ADMIN_CREDENTIALS + ).json()["count"] + + ready = running_count == 0 and waiting_count == 0 + + time.sleep(1) + + +class ReusableCollection: + """ + This provides a reusable namespace and collection so that a new collection + doesn't have to be created for every test that needs to modify one. + """ + + def __init__(self, name, groups=None): + self._namespace_name = f"c_ns_{name}" + self._collection_name = f"col_{name}" + + self._namespace = gen_namespace(self._namespace_name, groups) + + self._published_href = self._get_repo_href("published") + self._rejected_href = self._get_repo_href("rejected") + self._staging_href = self._get_repo_href("staging") + + def _reset_collection_repo(self): + requests.post( + ( + f"{API_ROOT}v3/collections/{self._namespace_name}" + f"/{self._collection_name}/versions/{self._collection['version']}" + "/move/rejected/staging/" + ), + auth=ADMIN_CREDENTIALS, + ) + requests.post( + ( + f"{API_ROOT}v3/collections/{self._namespace_name}" + f"/{self._collection_name}/versions/{self._collection['version']}" + "/move/published/staging/" + ), + auth=ADMIN_CREDENTIALS, + ) + + def _get_repo_href(self, name): + return requests.get( + f"{PULP_API_ROOT}repositories/ansible/ansible/?name={name}", + auth=ADMIN_CREDENTIALS + ).json()["results"][0]["pulp_href"] + + def _reset_collection(self): + resp = requests.get( + ( + f"{PULP_API_ROOT}content/ansible/collection_versions/" + f"?name={self._collection_name}&namespace=" + f"{self._namespace_name}"), + auth=ADMIN_CREDENTIALS + ) + + # Make sure the collection exists + if resp.json()["count"] == 0: + self._collection = gen_collection( + self._collection_name, self._namespace_name) + self._collection_href = self._collection["pulp_href"] + else: + + # If it doesn't, reset it's state. + self._reset_collection_repo() + + wait_for_all_tasks() + + url = ( + f'{API_ROOT}v3/plugin/ansible/content/staging/collections/index/' + f'{self._namespace_name}/{self._collection_name}/' + ) + + requests.patch( + url, + json={"deprecated": False}, + auth=ADMIN_CREDENTIALS, + ) + + def get_namespace(self): + return self._namespace + + def get_collection(self): + self._reset_collection() + return self._collection + + def cleanup(self): + collection = self.get_collection() + namespace = self.get_namespace() + + del_collection(collection['name'], collection['namespace']) + wait_for_all_tasks() + del_namespace(namespace['name']) + + +def cleanup_test_obj(response, pk, del_func): + data = response.json() + + if pk in data: + del_func(pk) + + +def del_container(name): + requests.delete( + f"{API_ROOT}_ui/v1/execution-environments/repositories/{name}/", + auth=ADMIN_CREDENTIALS, + ) + + +def gen_remote_container(name, registry_pk): + return requests.post( + f"{API_ROOT}_ui/v1/execution-environments/remotes/", + json={ + "name": name, + "upstream_name": "foo", + "registry": registry_pk, + }, + auth=ADMIN_CREDENTIALS, + ).json() + + +def del_registry(pk): + requests.delete( + f"{API_ROOT}_ui/v1/execution-environments/registries/{pk}/", + auth=ADMIN_CREDENTIALS, + ) + + +def gen_registry(name): + return requests.post( + f"{API_ROOT}_ui/v1/execution-environments/registries/", + json={ + "name": name, + "url": "http://example.com", + }, + auth=ADMIN_CREDENTIALS, + ).json() + + +class ReusableContainerRegistry: + def __init__(self, name): + self._name = f"ee_ns_{name}" + self._registry = gen_registry(self._name) + + # def _reset(self): + # data = requests.get( + # f'{API_ROOT}_ui/v1/execution-environments/registries/?name={self._name}', + # auth=ADMIN_CREDENTIALS + # ).json() + + # if data['meta']['count'] == 1: + # return data['data'][0] + # else: + # return gen_registry(self._name) + + def get_registry(self): + return self._registry + + def cleanup(self): + del_registry(self._registry["pk"]) + + +class ReusableRemoteContainer: + def __init__(self, name, registry_pk, groups=None): + self._ns_name = f"ee_ns_{name}" + self._name = f"ee_remote_{name}" + self._groups = groups or [] + self._registry_pk = registry_pk + + self._reset() + + def _reset(self): + self._remote = gen_remote_container(f"{self._ns_name}/{self._name}", self._registry_pk) + self._namespace = requests.put( + f"{API_ROOT}_ui/v1/execution-environments/namespaces/{self._ns_name}/", + json={ + "name": self._ns_name, + "groups": self._groups + + }, + auth=ADMIN_CREDENTIALS + ).json() + self._container = requests.get( + f"{API_ROOT}_ui/v1/execution-environments/repositories/{self._ns_name}/{self._name}/", + auth=ADMIN_CREDENTIALS + ).json() + + def get_container(self): + return self._container + + def get_namespace(self): + return self._namespace + + def get_remote(self): + return self._remote + + def cleanup(self): + del_container(f"{self._ns_name}/{self._name}") + + +class ReusableLocalContainer: + def __init__(self, name, groups=None): + self._ns_name = f"ee_ns_{name}" + self._repo_name = f"ee_local_{name}" + self._name = f"{self._ns_name}/{self._repo_name}" + self._groups = groups or [] + + self._reset() + + def _reset(self): + podman_push(ADMIN_USER, ADMIN_PASSWORD, self._name) + + self._namespace = requests.put( + f"{API_ROOT}_ui/v1/execution-environments/namespaces/{self._ns_name}/", + json={ + "name": self._ns_name, + "groups": self._groups + + }, + auth=ADMIN_CREDENTIALS + ).json() + + self._container = requests.get( + f"{API_ROOT}_ui/v1/execution-environments/repositories/{self._name}/", + auth=ADMIN_CREDENTIALS + ).json() + + self._manifest = requests.get( + ( + f"{API_ROOT}_ui/v1/execution-environments/" + f"repositories/{self._name}/_content/images/latest/" + ), + auth=ADMIN_CREDENTIALS + ).json() + + def get_container(self): + return self._container + + def get_namespace(self): + return self._namespace + + def get_manifest(self): + return self._manifest + + def cleanup(self): + del_container(self._name) diff --git a/galaxy_ng/tests/integration/api/test_groups.py b/galaxy_ng/tests/integration/api/test_groups.py new file mode 100644 index 0000000000..9bfe7ee4fd --- /dev/null +++ b/galaxy_ng/tests/integration/api/test_groups.py @@ -0,0 +1,53 @@ +"""test_namespace_management.py - Test related to namespaces. + +See: https://issues.redhat.com/browse/AAH-1303 + +""" +import random +import string +import uuid + +import pytest + +from ..utils import get_client + +pytestmark = pytest.mark.qa # noqa: F821 + + +@pytest.mark.group +@pytest.mark.role +@pytest.mark.standalone_only +def test_group_role_listing(ansible_config): + """Tests ability to list roles assigned to a namespace.""" + + config = ansible_config("admin") + api_client = get_client(config, request_token=True, require_auth=True) + + # Create Group + group_name = str(uuid.uuid4()) + payload = {"name": group_name} + group_response = api_client("/api/automation-hub/_ui/v1/groups/", args=payload, method="POST") + assert group_response["name"] == group_name + + # Create Namespace + ns_name = "".join(random.choices(string.ascii_lowercase, k=10)) + payload = { + "name": ns_name, + "groups": [ + { + "name": f"{group_response['name']}", + "object_roles": ["galaxy.collection_namespace_owner"], + } + ], + } + ns_response = api_client("/api/automation-hub/v3/namespaces/", args=payload, method="POST") + assert ns_response["name"] == ns_name + assert ns_response["groups"][0]["name"] == group_response["name"] + + # List Group's Roles + group_roles_response = api_client( + f'/pulp/api/v3/groups/{group_response["id"]}/roles/', method="GET" + ) + assert group_roles_response["count"] == 1 + assert group_roles_response["results"][0]["role"] == "galaxy.collection_namespace_owner" + assert f'/groups/{group_response["id"]}/' in group_roles_response["results"][0]["pulp_href"] diff --git a/galaxy_ng/tests/integration/api/test_locked_roles.py b/galaxy_ng/tests/integration/api/test_locked_roles.py new file mode 100644 index 0000000000..0dd27ea1db --- /dev/null +++ b/galaxy_ng/tests/integration/api/test_locked_roles.py @@ -0,0 +1,22 @@ +"""test_locked_roles.py - Tests creation of locked roles.""" + +import pytest + +from ..utils import get_client + +pytestmark = pytest.mark.qa # noqa: F821 + + +@pytest.mark.standalone_only +@pytest.mark.role +def test_locked_roles_exist(ansible_config): + config = ansible_config("admin") + api_client = get_client( + config=config, + require_auth=True, + request_token=False + ) + resp = api_client('/pulp/api/v3/roles/?name__startswith=galaxy.', method='GET') + + # verify that the locked roles are getting populated + assert resp["count"] > 0 diff --git a/galaxy_ng/tests/integration/api/test_rbac_roles.py b/galaxy_ng/tests/integration/api/test_rbac_roles.py new file mode 100644 index 0000000000..fbe7241e06 --- /dev/null +++ b/galaxy_ng/tests/integration/api/test_rbac_roles.py @@ -0,0 +1,437 @@ +"""Tests related to RBAC roles. + +See: https://issues.redhat.com/browse/AAH-957 +""" +import logging +import pytest +import requests + +from .rbac_actions.utils import ( + ADMIN_CREDENTIALS, + ADMIN_USER, + ADMIN_PASSWORD, + API_ROOT, + NAMESPACE, + PASSWORD, + ReusableLocalContainer, + create_group_with_user_and_role, + create_user, + gen_string, + del_user, + del_group, + ReusableCollection, + ReusableContainerRegistry, + # ReusableLocalContainer, # waiting on pulp container fix + ReusableRemoteContainer +) + +from .rbac_actions.auth import ( + view_groups, delete_groups, add_groups, change_groups, + view_users, delete_users, add_users, change_users, + view_role, delete_role, add_role, change_role, +) +from .rbac_actions.misc import view_tasks +from .rbac_actions.collections import ( + create_collection_namespace, + change_collection_namespace, + delete_collection_namespace, + upload_collection_to_namespace, + delete_collection, + configure_collection_sync, + launch_collection_sync, + view_sync_configuration, + approve_collections, + reject_collections, + deprecate_collections, + undeprecate_collections, +) +from .rbac_actions.exec_env import ( + # Remotes + create_ee_remote, + update_ee_remote, + sync_remote_ee, + + # Registries + delete_ee_registry, + index_ee_registry, + update_ee_registry, + create_ee_registry, + + # Containers + delete_ee, + change_ee_description, + change_ee_readme, + change_ee_namespace, + create_ee_local, + create_ee_in_existing_namespace, + push_updates_to_existing_ee, + change_ee_tags, +) + +log = logging.getLogger(__name__) + +# Order is important, CRU before D actions +GLOBAL_ACTIONS = [ + + # AUTHENTICATION + add_groups, + view_groups, + change_groups, + delete_groups, + add_users, + change_users, + view_users, + delete_users, + add_role, + change_role, + view_role, + delete_role, + + # COLLECTIONS + create_collection_namespace, + change_collection_namespace, + delete_collection_namespace, + upload_collection_to_namespace, + delete_collection, + configure_collection_sync, + launch_collection_sync, + view_sync_configuration, + approve_collections, + reject_collections, + deprecate_collections, + undeprecate_collections, + + # EEs + # Remotes + create_ee_remote, + update_ee_remote, + sync_remote_ee, + + # Registries + delete_ee_registry, + index_ee_registry, + update_ee_registry, + create_ee_registry, + + # Containers + delete_ee, + change_ee_description, + change_ee_readme, + change_ee_namespace, + create_ee_local, + create_ee_in_existing_namespace, + push_updates_to_existing_ee, + change_ee_tags, + + # MISC + view_tasks, +] + +# TODO: Update object tests to include delete actions +OBJECT_ACTIONS = [ + change_collection_namespace, + upload_collection_to_namespace, + deprecate_collections, + undeprecate_collections, + change_ee_description, + change_ee_readme, + create_ee_in_existing_namespace, + push_updates_to_existing_ee, + change_ee_namespace, + change_ee_tags, + sync_remote_ee, +] + +OBJECT_ROLES_TO_TEST = { + # COLLECTIONS + "galaxy.collection_namespace_owner": { + change_collection_namespace, + upload_collection_to_namespace, + deprecate_collections, + undeprecate_collections, + }, + "galaxy.collection_publisher": { + create_collection_namespace, + change_collection_namespace, + upload_collection_to_namespace, + deprecate_collections, + undeprecate_collections, + }, + + # EEs + "galaxy.execution_environment_publisher": { + create_ee_remote, + update_ee_remote, + sync_remote_ee, + change_ee_description, + change_ee_readme, + change_ee_namespace, + create_ee_local, + create_ee_in_existing_namespace, + push_updates_to_existing_ee, + change_ee_tags, + }, + "galaxy.execution_environment_namespace_owner": { + update_ee_remote, + change_ee_description, + change_ee_readme, + create_ee_in_existing_namespace, + push_updates_to_existing_ee, + change_ee_namespace, + change_ee_tags, + sync_remote_ee, + }, + "galaxy.execution_environment_collaborator": { + update_ee_remote, + change_ee_description, + change_ee_readme, + push_updates_to_existing_ee, + change_ee_tags, + sync_remote_ee, + }, + + +} + +ROLES_TO_TEST = { + "galaxy.content_admin": { + # COLLECTIONS + create_collection_namespace, + change_collection_namespace, + delete_collection_namespace, + upload_collection_to_namespace, + delete_collection, + configure_collection_sync, + launch_collection_sync, + view_sync_configuration, + approve_collections, + reject_collections, + deprecate_collections, + undeprecate_collections, + + # EEs + # Remotes + create_ee_remote, + update_ee_remote, + sync_remote_ee, + + # Registries + delete_ee_registry, + index_ee_registry, + update_ee_registry, + create_ee_registry, + + # Containers + delete_ee, + change_ee_description, + change_ee_readme, + change_ee_namespace, + create_ee_local, + create_ee_in_existing_namespace, + push_updates_to_existing_ee, + change_ee_tags, + }, + "galaxy.collection_admin": { + create_collection_namespace, + change_collection_namespace, + upload_collection_to_namespace, + delete_collection, + delete_collection_namespace, + configure_collection_sync, + launch_collection_sync, + approve_collections, + reject_collections, + deprecate_collections, + undeprecate_collections, + }, + "galaxy.collection_curator": { + configure_collection_sync, + launch_collection_sync, + approve_collections, + reject_collections, + }, + "galaxy.execution_environment_admin": { + # EEs + # Remotes + create_ee_remote, + update_ee_remote, + sync_remote_ee, + + # Registries + delete_ee_registry, + index_ee_registry, + update_ee_registry, + create_ee_registry, + + # Containers + delete_ee, + change_ee_description, + change_ee_readme, + change_ee_namespace, + create_ee_local, + create_ee_in_existing_namespace, + push_updates_to_existing_ee, + change_ee_tags, + }, + "galaxy.group_admin": { + add_groups, + change_groups, + delete_groups, + view_role, + }, + "galaxy.user_admin": { + add_users, + view_users, + change_users, + delete_users, + }, + "galaxy.task_admin": {} +} +ROLES_TO_TEST.update(OBJECT_ROLES_TO_TEST) + +ACTIONS_FOR_ALL_USERS = { + view_sync_configuration, + view_groups, + view_tasks, + view_role, +} + + +@pytest.mark.rbac_roles +@pytest.mark.parametrize("role", ROLES_TO_TEST) +def test_global_role_actions(role): + registry = ReusableContainerRegistry(gen_string()) + registry_pk = registry.get_registry()["pk"] + + extra = { + "collection": ReusableCollection(gen_string()), + "registry": registry, + "remote_ee": ReusableRemoteContainer(gen_string(), registry_pk), + "local_ee": ReusableLocalContainer(gen_string()), + } + + USERNAME = f"{NAMESPACE}_user_{gen_string()}" + + user = create_user(USERNAME, PASSWORD) + group = create_group_with_user_and_role(user, role) + group_id = group['id'] + + expected_allows = ROLES_TO_TEST[role] + + failures = [] + # Test global actions + for action in GLOBAL_ACTIONS: + expect_pass = action in expected_allows or action in ACTIONS_FOR_ALL_USERS + try: + action(user, PASSWORD, expect_pass, extra) + except AssertionError: + failures.append(action.__name__) + + # cleanup user, group + requests.delete(f"{API_ROOT}_ui/v1/users/{user['id']}/", auth=ADMIN_CREDENTIALS) + requests.delete(f"{API_ROOT}_ui/v1/groups/{group_id}/", auth=ADMIN_CREDENTIALS) + + del_user(user['id']) + del_group(group_id) + + extra['collection'].cleanup() + extra['registry'].cleanup() + extra['remote_ee'].cleanup() + extra['local_ee'].cleanup() + + assert failures == [] + + +@pytest.mark.rbac_roles +def test_object_role_actions(): + registry = ReusableContainerRegistry(gen_string()) + registry_pk = registry.get_registry()["pk"] + + users_and_groups = {} + col_groups = [] + ee_groups = [] + + # Create user/group for each role + # Populate collection/ee groups for object assignment + for role in OBJECT_ROLES_TO_TEST: + USERNAME = f"{NAMESPACE}_user_{gen_string()}" + user = create_user(USERNAME, PASSWORD) + group = create_group_with_user_and_role(user, role) + users_and_groups[role] = { + 'user': user, + 'group': group + } + if role in ['galaxy.collection_namespace_owner']: + col_groups.append({ + 'id': users_and_groups[role]['group']['id'], + 'name': users_and_groups[role]['group']['name'], + 'object_roles': [role] + }) + if role in [ + 'galaxy.execution_environment_namespace_owner', + 'galaxy.execution_environment_collaborator' + ]: + ee_groups.append({ + 'id': users_and_groups[role]['group']['id'], + 'name': users_and_groups[role]['group']['name'], + 'object_roles': [role] + }) + + extra = { + "collection": ReusableCollection(gen_string(), groups=col_groups), + "registry": registry, + "remote_ee": ReusableRemoteContainer(gen_string(), registry_pk, groups=ee_groups), + "local_ee": ReusableLocalContainer(gen_string()), + } + + for role in OBJECT_ROLES_TO_TEST: + expected_allows = ROLES_TO_TEST[role] + + failures = [] + # Test object actions + for action in OBJECT_ACTIONS: + expect_pass = action in expected_allows or action in ACTIONS_FOR_ALL_USERS + try: + action(users_and_groups[role]['user'], PASSWORD, expect_pass, extra) + except AssertionError: + failures.append(f'{role}:{action.__name__}') + + # cleanup user, group + for role in OBJECT_ROLES_TO_TEST: + del_user(users_and_groups[role]['user']['id']) + del_group(users_and_groups[role]['group']['id']) + + extra['collection'].cleanup() + extra['registry'].cleanup() + extra['remote_ee'].cleanup() + extra['local_ee'].cleanup() + + assert failures == [] + + +@pytest.mark.rbac_roles +def test_role_actions_for_admin(): + registry = ReusableContainerRegistry(gen_string()) + registry_pk = registry.get_registry()["pk"] + + extra = { + "collection": ReusableCollection(gen_string()), + "registry": registry, + "remote_ee": ReusableRemoteContainer(gen_string(), registry_pk), + "local_ee": ReusableLocalContainer(gen_string()), + } + failures = [] + + # Test global actions + for action in GLOBAL_ACTIONS: + try: + action({'username': ADMIN_USER}, ADMIN_PASSWORD, True, extra) + except AssertionError: + failures.append(action.__name__) + + extra['collection'].cleanup() + extra['registry'].cleanup() + extra['remote_ee'].cleanup() + extra['local_ee'].cleanup() + + assert failures == [] diff --git a/galaxy_ng/tests/integration/conftest.py b/galaxy_ng/tests/integration/conftest.py index ac930c8a9e..6f06df68c2 100644 --- a/galaxy_ng/tests/integration/conftest.py +++ b/galaxy_ng/tests/integration/conftest.py @@ -49,6 +49,9 @@ importer: tests related checks in galaxy-importer pulp_api: tests related to the pulp api endpoints ldap: tests related to the ldap integration +role: Related to RBAC Roles +rbac_roles: Tests checking Role permissions +group: Related to Groups """ diff --git a/galaxy_ng/tests/unit/api/base.py b/galaxy_ng/tests/unit/api/base.py index 2c2ff493cc..2af0a8eb66 100644 --- a/galaxy_ng/tests/unit/api/base.py +++ b/galaxy_ng/tests/unit/api/base.py @@ -8,7 +8,7 @@ from galaxy_ng.app import models from galaxy_ng.app.access_control import access_policy from galaxy_ng.app.models import auth as auth_models -from guardian.shortcuts import assign_perm +from pulpcore.plugin.util import assign_role from galaxy_ng.app import constants @@ -76,13 +76,13 @@ def _create_user(username): return auth_models.User.objects.create(username=username) @staticmethod - def _create_group(scope, name, users=None, perms=[]): + def _create_group(scope, name, users=None, roles=[]): group, _ = auth_models.Group.objects.get_or_create_identity(scope, name) if isinstance(users, auth_models.User): users = [users] group.user_set.add(*users) - for p in perms: - assign_perm(p, group) + for r in roles: + assign_role(r, group) return group @staticmethod @@ -95,51 +95,26 @@ def _create_namespace(name, groups=None): groups_to_add = {} for group in groups: groups_to_add[group] = [ - 'galaxy.upload_to_namespace', - 'galaxy.change_namespace', - 'galaxy.delete_namespace' + 'galaxy.collection_namespace_owner', ] namespace.groups = groups_to_add return namespace @staticmethod def _create_partner_engineer_group(): - pe_perms = [ - # namespaces - 'galaxy.add_namespace', - 'galaxy.change_namespace', - 'galaxy.upload_to_namespace', - 'galaxy.delete_namespace', - - # collections - 'ansible.modify_ansible_repo_content', - 'ansible.delete_collection', - - # users - 'galaxy.view_user', - 'galaxy.delete_user', - 'galaxy.add_user', - 'galaxy.change_user', - - # groups - 'galaxy.view_group', - 'galaxy.delete_group', - 'galaxy.add_group', - 'galaxy.change_group', - - # synclists - 'galaxy.delete_synclist', - 'galaxy.change_synclist', - 'galaxy.view_synclist', - 'galaxy.add_synclist', - - # sync config - 'ansible.change_collectionremote', + # Maintain PE Group consistency with + # galaxy_ng/app/management/commands/maintain-pe-group.py:28 + pe_roles = [ + 'galaxy.collection_namespace_owner', + 'galaxy.collection_admin', + 'galaxy.user_admin', + 'galaxy.group_admin', + 'galaxy.content_admin', ] pe_group = auth_models.Group.objects.create( name='partner-engineers') - for perm in pe_perms: - assign_perm(perm, pe_group) + for role in pe_roles: + assign_role(role, pe_group) return pe_group diff --git a/galaxy_ng/tests/unit/api/synclist_base.py b/galaxy_ng/tests/unit/api/synclist_base.py index cbdf333df2..ffec78d6df 100644 --- a/galaxy_ng/tests/unit/api/synclist_base.py +++ b/galaxy_ng/tests/unit/api/synclist_base.py @@ -2,8 +2,8 @@ from unittest import mock from django.conf import settings -from guardian import shortcuts +from pulpcore.plugin.util import assign_role from pulp_ansible.app import models as pulp_ansible_models from galaxy_ng.app import models as galaxy_models @@ -15,12 +15,7 @@ ACCOUNT_SCOPE = "rh-identity-account" -SYNCLIST_PERMS = [ - "add_synclist", - "view_synclist", - "delete_synclist", - "change_synclist", -] +SYNCLIST_ROLES = ["galaxy.synclist_owner"] log.info("settings.FIXTURE_DIRS(module scope): %s", settings.FIXTURE_DIRS) @@ -28,7 +23,7 @@ class BaseSyncListViewSet(base.BaseTestCase): url_name = "galaxy:api:v3:ui:synclists-list" - default_owner_permissions = SYNCLIST_PERMS + default_owner_roles = SYNCLIST_ROLES def setUp(self): super().setUp() @@ -59,9 +54,8 @@ def _create_group_with_synclist_perms(scope, name, users=None): if isinstance(users, auth_models.User): users = [users] group.user_set.add(*users) - - for perm in SYNCLIST_PERMS: - shortcuts.assign_perm(f"galaxy.{perm}", group) + for role in SYNCLIST_ROLES: + assign_role(role, group) return group def _create_repository(self, name): @@ -87,7 +81,7 @@ def _create_synclist( groups_to_add = {} for group in groups: - groups_to_add[group] = self.default_owner_permissions + groups_to_add[group] = self.default_owner_roles synclist, _ = galaxy_models.SyncList.objects.get_or_create( name=name, repository=repository, upstream_repository=upstream_repository, diff --git a/galaxy_ng/tests/unit/api/test_api_ui_container_remote.py b/galaxy_ng/tests/unit/api/test_api_ui_container_remote.py index e9be7732c7..1ba97679dc 100644 --- a/galaxy_ng/tests/unit/api/test_api_ui_container_remote.py +++ b/galaxy_ng/tests/unit/api/test_api_ui_container_remote.py @@ -17,12 +17,7 @@ class TestContainerRemote(BaseTestCase): def setUp(self): super().setUp() - permissions = [ - # change containers - 'container.namespace_change_containerdistribution', - # create containers - 'container.add_containernamespace', - ] + roles = ['galaxy.execution_environment_admin'] self.container_user = auth_models.User.objects.create(username='container_user') self.admin = auth_models.User.objects.create(username='admin', is_superuser=True) self.regular_user = auth_models.User.objects.create(username='hacker') @@ -43,7 +38,7 @@ def setUp(self): "", "container_group", users=[self.container_user, ], - perms=permissions + roles=roles ) def _create_remote(self, user, name, registry_pk, **kwargs): diff --git a/galaxy_ng/tests/unit/api/test_api_ui_distributions.py b/galaxy_ng/tests/unit/api/test_api_ui_distributions.py index 5e9eb2457d..c95f62b67e 100644 --- a/galaxy_ng/tests/unit/api/test_api_ui_distributions.py +++ b/galaxy_ng/tests/unit/api/test_api_ui_distributions.py @@ -1,7 +1,9 @@ import logging +# from django.contrib.auth import default_app_config from rest_framework import status as http_code +from pulpcore.plugin.util import assign_role from pulp_ansible.app import models as pulp_ansible_models from galaxy_ng.app.constants import DeploymentMode @@ -10,16 +12,14 @@ from .base import BaseTestCase, get_current_ui_url +from .synclist_base import ACCOUNT_SCOPE + log = logging.getLogger(__name__) logging.getLogger().setLevel(logging.DEBUG) class TestUIDistributions(BaseTestCase): - default_owner_permissions = [ - 'change_synclist', - 'view_synclist', - 'delete_synclist' - ] + default_owner_roles = ['galaxy.synclist_owner'] def setUp(self): super().setUp() @@ -27,8 +27,9 @@ def setUp(self): self.distro_url = get_current_ui_url('distributions-list') self.my_distro_url = get_current_ui_url('my-distributions-list') - self.group = auth_models.Group.objects.create(name='test1_group') - self.user.groups.add(self.group) + self.group = self._create_group_with_synclist_perms( + ACCOUNT_SCOPE, "test1_group", users=[self.user] + ) self.synclist_repo = self._create_repository('123-synclist') self.repo2 = self._create_repository('other-repo') @@ -54,6 +55,15 @@ def _create_repository(self, name): repo = pulp_ansible_models.AnsibleRepository.objects.create(name=name) return repo + def _create_group_with_synclist_perms(self, scope, name, users=None): + group, _ = auth_models.Group.objects.get_or_create_identity(scope, name) + if isinstance(users, auth_models.User): + users = [users] + group.user_set.add(*users) + for role in self.default_owner_roles: + assign_role(role, group) + return group + def _create_synclist( self, name, repository, upstream_repository, collections=None, namespaces=None, policy=None, groups=None, @@ -63,7 +73,7 @@ def _create_synclist( if groups: groups_to_add = {} for group in groups: - groups_to_add[group] = self.default_owner_permissions + groups_to_add[group] = self.default_owner_roles synclist.groups = groups_to_add return synclist diff --git a/galaxy_ng/tests/unit/api/test_api_ui_my_synclists.py b/galaxy_ng/tests/unit/api/test_api_ui_my_synclists.py index bc9cc36bdd..be5f8cdce5 100644 --- a/galaxy_ng/tests/unit/api/test_api_ui_my_synclists.py +++ b/galaxy_ng/tests/unit/api/test_api_ui_my_synclists.py @@ -59,7 +59,7 @@ def test_my_synclist_create(self): { "id": self.group.id, "name": self.group.name, - "object_permissions": self.default_owner_permissions, + "object_roles": self.default_owner_roles, }, ], } @@ -89,7 +89,7 @@ def test_my_synclist_update(self): { "id": self.group.id, "name": self.group.name, - "object_permissions": self.default_owner_permissions, + "object_roles": self.default_owner_roles, }, ], } @@ -109,16 +109,16 @@ def test_my_synclist_update(self): self.assertEqual(response.data["name"], self.synclist_name) self.assertEqual(response.data["policy"], "include") - # Sort permission list for comparison - response.data["groups"][0]["object_permissions"].sort() - self.default_owner_permissions.sort() + # Sort role list for comparison + response.data["groups"][0]["object_roles"].sort() + self.default_owner_roles.sort() self.assertEqual( response.data["groups"], [ { "name": self.group.name, "id": self.group.id, - "object_permissions": self.default_owner_permissions, + "object_roles": self.default_owner_roles } ], ) diff --git a/galaxy_ng/tests/unit/api/test_api_ui_sync_config.py b/galaxy_ng/tests/unit/api/test_api_ui_sync_config.py index c6504cbb36..182fbbca04 100644 --- a/galaxy_ng/tests/unit/api/test_api_ui_sync_config.py +++ b/galaxy_ng/tests/unit/api/test_api_ui_sync_config.py @@ -31,8 +31,8 @@ def setUp(self): super().setUp() self.admin_user = self._create_user("admin") - self.pe_group = self._create_partner_engineer_group() - self.admin_user.groups.add(self.pe_group) + self.sync_group = self._create_group( + "", "admins", self.admin_user, ["galaxy.collection_admin"]) self.admin_user.save() # Remotes are created by data migration diff --git a/galaxy_ng/tests/unit/api/test_api_ui_synclists.py b/galaxy_ng/tests/unit/api/test_api_ui_synclists.py index d21c70d463..fb8a863545 100644 --- a/galaxy_ng/tests/unit/api/test_api_ui_synclists.py +++ b/galaxy_ng/tests/unit/api/test_api_ui_synclists.py @@ -5,8 +5,6 @@ from django.conf import settings from rest_framework import status as http_code -from guardian import shortcuts - from galaxy_ng.app.models import auth as auth_models from galaxy_ng.app.constants import DeploymentMode @@ -346,9 +344,6 @@ def setUp(self): self.user.save() self.group.save() - # Remove any group level perms - for perm in self.default_owner_permissions: - shortcuts.remove_perm(f"galaxy.{perm}", self.group) self.group.save() self.synclist_name = "test_synclist" @@ -358,8 +353,6 @@ def setUp(self): upstream_repository=self.default_repo, groups=[self.group], ) - for perm in self.default_owner_permissions: - shortcuts.remove_perm(f"galaxy.{perm}", self.group, self.synclist) self.credentials() self.client.force_authenticate(user=self.user) diff --git a/galaxy_ng/tests/unit/api/test_api_ui_user_viewsets.py b/galaxy_ng/tests/unit/api/test_api_ui_user_viewsets.py index 4cc9e5bead..2d8a989fbd 100644 --- a/galaxy_ng/tests/unit/api/test_api_ui_user_viewsets.py +++ b/galaxy_ng/tests/unit/api/test_api_ui_user_viewsets.py @@ -26,11 +26,8 @@ def setUp(self): def test_super_user(self): with self.settings(GALAXY_DEPLOYMENT_MODE=DeploymentMode.STANDALONE.value): user = auth_models.User.objects.create(username='haxor') - self._create_group('', 'test_group1', users=[user], perms=[ - 'galaxy.view_user', - 'galaxy.delete_user', - 'galaxy.add_user', - 'galaxy.change_user', + self._create_group('', 'test_group1', users=[user], roles=[ + 'galaxy.user_admin', ]) self.client.force_authenticate(user=user) new_user_data = { @@ -72,11 +69,8 @@ def test_super_user(self): def test_user_can_only_create_users_with_their_groups(self): user = auth_models.User.objects.create(username='haxor') - group = self._create_group('', 'test_group1', users=[user], perms=[ - 'galaxy.view_user', - 'galaxy.delete_user', - 'galaxy.add_user', - 'galaxy.change_user', + group = self._create_group('', 'test_group1', users=[user], roles=[ + 'galaxy.user_admin', ]) self.client.force_authenticate(user=user) @@ -109,12 +103,9 @@ def test_user_can_only_create_users_with_their_groups(self): def test_user_can_create_users_with_right_perms(self): user = auth_models.User.objects.create(username='haxor') - self._create_group('', 'test_group1', users=[user], perms=[ - 'galaxy.view_user', - 'galaxy.delete_user', - 'galaxy.add_user', - 'galaxy.change_user', - 'galaxy.change_group' + self._create_group('', 'test_group1', users=[user], roles=[ + 'galaxy.user_admin', + 'galaxy.group_admin', ]) self.client.force_authenticate(user=user) @@ -313,12 +304,9 @@ def test_me_delete(self): group = self._create_group('', 'people_that_can_delete_users', users=[user], - perms=[ - 'galaxy.view_user', - 'galaxy.delete_user', - 'galaxy.add_user', - 'galaxy.change_user', - 'galaxy.change_group' + roles=[ + 'galaxy.user_admin', + 'galaxy.group_admin' ]) self.client.force_authenticate(user=user) @@ -361,12 +349,9 @@ def test_superuser_can_not_be_deleted(self): group = self._create_group('', 'people_that_can_delete_users', users=[user], - perms=[ - 'galaxy.view_user', - 'galaxy.delete_user', - 'galaxy.add_user', - 'galaxy.change_user', - 'galaxy.change_group' + roles=[ + 'galaxy.user_admin', + 'galaxy.group_admin' ]) new_user_data = { diff --git a/galaxy_ng/tests/unit/api/test_api_v3_collections.py b/galaxy_ng/tests/unit/api/test_api_v3_collections.py index e8993649f3..67238a5b5f 100644 --- a/galaxy_ng/tests/unit/api/test_api_v3_collections.py +++ b/galaxy_ng/tests/unit/api/test_api_v3_collections.py @@ -12,7 +12,7 @@ CollectionVersion, ) -from pulp_ansible.app.galaxy.v3.views import get_collection_dependents, get_dependents +from pulp_ansible.app.galaxy.v3.views import get_collection_dependents, get_unique_dependents from rest_framework import status @@ -238,7 +238,8 @@ def test_collections_detail(self): def test_collection_version_delete_dependency_check_positive_match(self): baz_collection = Collection.objects.create(namespace=self.namespace, name="baz") - for counter, dep_version in enumerate(["1.1.1", ">=1", "<2", "~1", "*"]): + # need versions that match 1.1.1, but not 1.1.2 + for counter, dep_version in enumerate(["1.1.1", "<1.1.2", ">0.0.0,<=1.1.1"]): baz_version = _get_create_version_in_repo( self.namespace, baz_collection, @@ -246,7 +247,7 @@ def test_collection_version_delete_dependency_check_positive_match(self): version=counter, dependencies={f"{self.namespace.name}.{self.collection.name}": dep_version}, ) - self.assertIn(baz_version, get_dependents(self.version_1_1_1)) + self.assertIn(baz_version, get_unique_dependents(self.version_1_1_1)) def test_collection_version_delete_dependency_check_negative_match(self): baz_collection = Collection.objects.create(namespace=self.namespace, name="baz") @@ -258,7 +259,7 @@ def test_collection_version_delete_dependency_check_negative_match(self): version=counter, dependencies={f"{self.namespace.name}.{self.collection.name}": dep_version}, ) - self.assertNotIn(baz_version, get_dependents(self.version_1_1_1)) + self.assertNotIn(baz_version, get_unique_dependents(self.version_1_1_1)) def test_collection_delete_dependency_check(self): baz_collection = Collection.objects.create(namespace=self.namespace, name="baz") diff --git a/galaxy_ng/tests/unit/api/test_api_v3_namespace_viewsets.py b/galaxy_ng/tests/unit/api/test_api_v3_namespace_viewsets.py index e1abd2b8f5..c642c38b72 100644 --- a/galaxy_ng/tests/unit/api/test_api_v3_namespace_viewsets.py +++ b/galaxy_ng/tests/unit/api/test_api_v3_namespace_viewsets.py @@ -186,16 +186,15 @@ def test_namespace_api_creates_deletes_inbound_repo(self): { "id": self.pe_group.id, "name": self.pe_group.name, - "object_permissions": [ - 'galaxy.upload_to_namespace', - 'galaxy.change_namespace', - 'galaxy.delete_namespace', + "object_roles": [ + 'galaxy.collection_namespace_owner', ] }, ], }, format='json', ) + print(f"\n\n response: {response} \n\n") self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(1, len(AnsibleRepository.objects.filter(name=repo_name))) self.assertEqual(1, len(AnsibleDistribution.objects.filter(name=repo_name))) diff --git a/galaxy_ng/tests/unit/api/test_api_v3_tasks.py b/galaxy_ng/tests/unit/api/test_api_v3_tasks.py index 23a3ff7223..262a6168f5 100644 --- a/galaxy_ng/tests/unit/api/test_api_v3_tasks.py +++ b/galaxy_ng/tests/unit/api/test_api_v3_tasks.py @@ -15,8 +15,8 @@ def setUp(self): super().setUp() self.admin_user = self._create_user("admin") - self.pe_group = self._create_partner_engineer_group() - self.admin_user.groups.add(self.pe_group) + self.sync_group = self._create_group( + "", "admins", self.admin_user, ["galaxy.collection_admin"]) self.admin_user.save() self.certified_remote = CollectionRemote.objects.get(name='rh-certified') diff --git a/galaxy_ng/tests/unit/app/test_app_auth.py b/galaxy_ng/tests/unit/app/test_app_auth.py index e301ec8b71..a73827aabc 100644 --- a/galaxy_ng/tests/unit/app/test_app_auth.py +++ b/galaxy_ng/tests/unit/app/test_app_auth.py @@ -1,7 +1,9 @@ from unittest.mock import Mock +from django.contrib.contenttypes.models import ContentType from django.test import override_settings from pulp_ansible.app.models import AnsibleDistribution, AnsibleRepository +from pulpcore.plugin.models.role import Role from galaxy_ng.app.auth.auth import RHIdentityAuthentication from galaxy_ng.app.constants import DeploymentMode @@ -35,10 +37,21 @@ def test_authenticate(self): # check objects exist: user, group, synclist, distro User.objects.get(username=username) - Group.objects.get(name=group_name) + group = Group.objects.get(name=group_name) synclist = SyncList.objects.get(name=synclist_name) distro = AnsibleDistribution.objects.get(name=synclist_name) self.assertEqual(synclist.distribution, distro) + # test rbac: check group is linked to expected role + self.assertEqual(group.object_roles.all().count(), 1) + group_role = group.object_roles.all().first() + synclist_owner_role = Role.objects.get(name="galaxy.synclist_owner") + self.assertEqual(group_role.role, synclist_owner_role) + + # test rbac: check group is linked to expected object + self.assertEqual(str(synclist.id), group_role.object_id) + ct = ContentType.objects.get(id=group_role.content_type_id) + self.assertEqual(ct.model, "synclist") + # assert objects do not exist: repo self.assertFalse(AnsibleRepository.objects.filter(name=synclist_name)) diff --git a/galaxy_ng/tests/unit/migrations/test_0029_move_perms_to_roles.py b/galaxy_ng/tests/unit/migrations/test_0029_move_perms_to_roles.py new file mode 100644 index 0000000000..55f57a7d61 --- /dev/null +++ b/galaxy_ng/tests/unit/migrations/test_0029_move_perms_to_roles.py @@ -0,0 +1,425 @@ +import importlib + +from unittest.case import skipIf + +from django.test import TestCase +from django.apps import apps +from django.contrib.contenttypes.models import ContentType +from django.contrib.auth.models import Permission + +from galaxy_ng.app.models import User, Group, Namespace +from pulp_container.app.models import ContainerNamespace +from pulpcore.app.models.role import GroupRole, UserRole, Role +from pulpcore.plugin.util import assign_role + + +# Note, this copied from galaxy_ng.app.access_control.statements.roles instead of +# imported because the roles may change in the future, but the migrations don't +LOCKED_ROLES = { + "galaxy.content_admin": { + "permissions": [ + "galaxy.add_namespace", + "galaxy.change_namespace", + "galaxy.delete_namespace", + "galaxy.upload_to_namespace", + "ansible.delete_collection", + "ansible.change_collectionremote", + "ansible.view_collectionremote", + "ansible.modify_ansible_repo_content", + "container.delete_containerrepository", + "container.namespace_change_containerdistribution", + "container.namespace_modify_content_containerpushrepository", + "container.namespace_push_containerdistribution", + "container.add_containernamespace", + "container.change_containernamespace", + # "container.namespace_add_containerdistribution", + "galaxy.add_containerregistryremote", + "galaxy.change_containerregistryremote", + "galaxy.delete_containerregistryremote", + ], + "description": "Manage all content types." + }, + + # COLLECTIONS + "galaxy.collection_admin": { + "permissions": [ + "galaxy.add_namespace", + "galaxy.change_namespace", + "galaxy.delete_namespace", + "galaxy.upload_to_namespace", + "ansible.delete_collection", + "ansible.change_collectionremote", + "ansible.view_collectionremote", + "ansible.modify_ansible_repo_content", + ], + "description": ( + "Create, delete and change collection namespaces. " + "Upload and delete collections. Sync collections from remotes. " + "Approve and reject collections.") + }, + "galaxy.collection_publisher": { + "permissions": [ + "galaxy.add_namespace", + "galaxy.change_namespace", + "galaxy.upload_to_namespace", + ], + "description": "Upload and modify collections." + }, + "galaxy.collection_curator": { + "permissions": [ + "ansible.change_collectionremote", + "ansible.view_collectionremote", + "ansible.modify_ansible_repo_content", + ], + "description": "Approve, reject and sync collections from remotes.", + }, + "galaxy.collection_namespace_owner": { + "permissions": [ + "galaxy.change_namespace", + "galaxy.upload_to_namespace", + ], + "description": "Change and upload collections to namespaces." + }, + + # EXECUTION ENVIRONMENTS + "galaxy.execution_environment_admin": { + "permissions": [ + "container.delete_containerrepository", + "container.namespace_change_containerdistribution", + "container.namespace_modify_content_containerpushrepository", + "container.namespace_push_containerdistribution", + "container.add_containernamespace", + "container.change_containernamespace", + # "container.namespace_add_containerdistribution", + "galaxy.add_containerregistryremote", + "galaxy.change_containerregistryremote", + "galaxy.delete_containerregistryremote", + ], + "description": ( + "Push, delete, and change execution environments. " + "Create, delete and change remote registries.") + }, + "galaxy.execution_environment_publisher": { + "permissions": [ + "container.namespace_change_containerdistribution", + "container.namespace_modify_content_containerpushrepository", + "container.namespace_push_containerdistribution", + "container.add_containernamespace", + "container.change_containernamespace", + # "container.namespace_add_containerdistribution", + ], + "description": "Push, and change execution environments." + }, + "galaxy.execution_environment_namespace_owner": { + "permissions": [ + "container.change_containernamespace", + "container.namespace_push_containerdistribution", + "container.namespace_change_containerdistribution", + "container.namespace_modify_content_containerpushrepository", + # "container.namespace_add_containerdistribution", + ], + "description": ( + "Create and update execution environments under existing " + "container namespaces.") + }, + "galaxy.execution_environment_collaborator": { + "permissions": [ + "container.namespace_push_containerdistribution", + "container.namespace_change_containerdistribution", + "container.namespace_modify_content_containerpushrepository", + ], + "description": "Change existing execution environments." + }, + + # ADMIN STUFF + "galaxy.group_admin": { + "permissions": [ + "galaxy.view_group", + "galaxy.delete_group", + "galaxy.add_group", + "galaxy.change_group", + ], + "description": "View, add, remove and change groups." + }, + "galaxy.user_admin": { + "permissions": [ + "galaxy.view_user", + "galaxy.delete_user", + "galaxy.add_user", + "galaxy.change_user", + ], + "description": "View, add, remove and change users." + }, + "galaxy.synclist_owner": { + "permissions": [ + "galaxy.add_synclist", + "galaxy.change_synclist", + "galaxy.delete_synclist", + "galaxy.view_synclist", + ], + "description": "View, add, remove and change synclists." + }, + "galaxy.task_admin": { + "permissions": [ + "core.change_task", + "core.delete_task", + "core.view_task" + ], + "description": "View, and cancel any task." + }, +} + + +def is_guardian_installed(): + return importlib.util.find_spec("guardian") is not None + + +class TestMigratingPermissionsToRoles(TestCase): + _assign_perm = None + + def _get_permission(self, permission_name): + app_label = permission_name.split('.')[0] + codename = permission_name.split('.')[1] + return Permission.objects.get( + content_type__app_label=app_label, + codename=codename + ) + + def _run_migrations(self): + migration = importlib.import_module("galaxy_ng.app.migrations.0029_move_perms_to_roles") + migration.migrate_group_permissions_to_roles(apps, None) + migration.migrate_user_permissions_to_roles(apps, None) + migration.edit_guardian_tables(apps, None) + migration.clear_model_permissions(apps, None) + + + def _create_user_and_group_with_permissions(self, name, permissions, obj=None): + user = User.objects.create(username=f"user_{name}") + group = Group.objects.create(name=f"group_{name}") + group.user_set.add(user) + + if obj: + for perm in permissions: + self._get_assign_perm(self._get_permission(perm), group, obj) + else: + for perm in permissions: + group.permissions.add(self._get_permission(perm)) + group.save() + + return (user, group) + + def _has_role(self, group, role, obj=None): + role_obj = Role.objects.get(name=role) + + if obj: + c_type = ContentType.objects.get_for_model(obj) + return GroupRole.objects.filter( + group=group, + role=role_obj, + content_type=c_type, + object_id=obj.pk).exists() + else: + return GroupRole.objects.filter( + group=group, + role=role_obj).exists() + + @property + def _get_assign_perm(self): + if self._assign_perm is None: + from guardian.shortcuts import assign_perm as guardian_assign_perm + self._assign_perm = guardian_assign_perm + + return self._assign_perm + + + def test_group_model_locked_role_mapping(self): + roles = {} + + for role in LOCKED_ROLES: + roles[role] = self._create_user_and_group_with_permissions( + name=role, + permissions=LOCKED_ROLES[role]["permissions"] + ) + + self._run_migrations() + + for role in roles: + permissions = LOCKED_ROLES[role]["permissions"] + user, group = roles[role] + + for perm in permissions: + self.assertTrue(user.has_perm(perm)) + + self.assertEqual(GroupRole.objects.filter(group=group).count(), 1) + self.assertTrue(self._has_role(group, role)) + + + def test_group_model_locked_role_mapping_with_dangling_permissions(self): + permissions_to_add = \ + LOCKED_ROLES["galaxy.collection_admin"]["permissions"] + \ + LOCKED_ROLES["galaxy.execution_environment_admin"]["permissions"] + \ + LOCKED_ROLES["galaxy.collection_namespace_owner"]["permissions"] + \ + ["galaxy.view_user", "core.view_task"] + + user, group = self._create_user_and_group_with_permissions("test", permissions_to_add) + + # Add permissions directly to a group + for perm in permissions_to_add: + group.permissions.add(self._get_permission(perm)) + group.save() + + self._run_migrations() + + for perm in permissions_to_add: + self.assertTrue(user.has_perm(perm)) + + # content_admin contains perms for collection and EE admin + expected_roles = [ + "galaxy.content_admin", + "_permission:galaxy.view_user", + "_permission:core.view_task", + ] + + self.assertEqual(GroupRole.objects.filter(group=group).count(), len(expected_roles)) + + for role in expected_roles: + role_obj = Role.objects.get(name=role) + self.assertEqual(GroupRole.objects.filter(group=group, role=role_obj).count(), 1) + self.assertTrue(self._has_role(group, role)) + + + def test_group_permissions_post_migration(self): + permissions_to_add = LOCKED_ROLES["galaxy.collection_admin"]["permissions"] + + _, group = self._create_user_and_group_with_permissions("test", permissions_to_add) + + # Add permissions directly to a group + for perm in permissions_to_add: + group.permissions.add(self._get_permission(perm)) + group.save() + + self.assertTrue(len(permissions_to_add) > 0) + self.assertEqual(group.permissions.count(), len(permissions_to_add)) + + self._run_migrations() + + expected_role = "galaxy.collection_admin" + role_obj = Role.objects.get(name=expected_role) + self.assertEqual(GroupRole.objects.filter(group=group).count(), 1) + self.assertEqual(GroupRole.objects.filter(group=group, role=role_obj).count(), 1) + self.assertTrue(self._has_role(group, expected_role)) + + # check that group no longer has any permissions associated + group.refresh_from_db() + self.assertEqual(group.permissions.all().count(), 0) + + # check that assigning a role does not alter permissions + perm_count_post_migration = group.permissions.all().count() + new_role = "galaxy.collection_namespace_owner" + assign_role(rolename=new_role, entity=group) + group.refresh_from_db() + self.assertTrue(self._has_role(group, new_role)) + self.assertEqual(group.permissions.all().count(), perm_count_post_migration) + + @skipIf( + not is_guardian_installed(), + "Django guardian is not installed." + ) + def test_group_object_locked_role_mapping(self): + namespace = Namespace.objects.create(name="my_namespace") + container_namespace = ContainerNamespace.objects.create(name="my_container_ns") + + _, namespace_super_group = self._create_user_and_group_with_permissions( + "ns_super_owner", + ["galaxy.change_namespace"], + obj=namespace + ) + + _, container_namespace_super_group = self._create_user_and_group_with_permissions( + "cns_super_owner", + ["container.change_containernamespace", "container.namespace_push_containerdistribution"], + obj=container_namespace + ) + + ns_roles = ["galaxy.collection_namespace_owner"] + c_ns_roles = [ + "galaxy.execution_environment_namespace_owner", + "galaxy.execution_environment_collaborator"] + + ns_users = {} + c_ns_users = {} + + for role in ns_roles: + ns_users[role] = self._create_user_and_group_with_permissions( + role, LOCKED_ROLES[role]["permissions"], obj=namespace) + + for role in c_ns_roles: + c_ns_users[role] = self._create_user_and_group_with_permissions( + role, LOCKED_ROLES[role]["permissions"], obj=container_namespace) + + self._run_migrations() + + # Verify locked role mapping works + for role in ns_users: + permissions = LOCKED_ROLES[role]["permissions"] + user, group = ns_users[role] + + for perm in permissions: + self.assertTrue(user.has_perm(perm, obj=namespace)) + self.assertFalse(user.has_perm(perm)) + + self.assertEqual(GroupRole.objects.filter(group=group).count(), 1) + self.assertTrue(self._has_role(group, role, obj=namespace)) + + for role in c_ns_users: + permissions = LOCKED_ROLES[role]["permissions"] + user, group = c_ns_users[role] + + for perm in permissions: + self.assertTrue(user.has_perm(perm, obj=container_namespace)) + self.assertFalse(user.has_perm(perm)) + + self.assertEqual(GroupRole.objects.filter(group=group).count(), 1) + self.assertTrue(self._has_role(group, role, obj=container_namespace)) + + # Verify super permissions work + self.assertTrue(self._has_role(namespace_super_group, "galaxy.collection_namespace_owner", namespace)) + self.assertTrue( + self._has_role( + container_namespace_super_group, + "galaxy.execution_environment_namespace_owner", + container_namespace) + ) + + @skipIf( + not is_guardian_installed(), + "Django guardian is not installed." + ) + def test_user_role(self): + ns = ContainerNamespace.objects.create(name="my_container_namespace") + user = User.objects.create(username="test") + + perm = self._get_permission("container.change_containernamespace") + self._get_assign_perm(perm, user, obj=ns) + c_type = ContentType.objects.get_for_model(ContainerNamespace) + + self._run_migrations() + + role_obj = Role.objects.get(name="galaxy.execution_environment_namespace_owner") + has_role = UserRole.objects.filter( + user=user, + role=role_obj, + content_type=c_type, + object_id=ns.pk + ).exists() + + self.assertTrue(has_role) + + + def test_empty_groups(self): + user, group = self._create_user_and_group_with_permissions("test", []) + + self._run_migrations() + + self.assertEqual(UserRole.objects.filter(user=user).count(), 0) + self.assertEqual(GroupRole.objects.filter(group=group).count(), 0) diff --git a/lint_requirements.txt b/lint_requirements.txt new file mode 100644 index 0000000000..dad3900d62 --- /dev/null +++ b/lint_requirements.txt @@ -0,0 +1,4 @@ +# python packages handy for developers, but not required by pulp +check-manifest +flake8 + diff --git a/pyproject.toml b/pyproject.toml index a9e8ffffc1..d24d33ae4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,4 +73,5 @@ ignore = [ "build_deploy.sh", "compose", "pr_check.sh", + "lint_requirements.txt", ] diff --git a/requirements/requirements.common.txt b/requirements/requirements.common.txt index 499418b854..7759b5fc51 100644 --- a/requirements/requirements.common.txt +++ b/requirements/requirements.common.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with python 3.10 +# This file is autogenerated by pip-compile with python 3.9 # To update, run: # # pip-compile --output-file=requirements/requirements.common.txt setup.py @@ -16,15 +16,21 @@ aioredis==2.0.1 # via pulpcore aiosignal==1.2.0 # via aiohttp -ansible-builder==1.0.1 +ansible-builder==1.1.0 # via galaxy-importer -ansible-core==2.12.4 - # via galaxy-importer -ansible-lint==5.4.0 - # via galaxy-importer -asgiref==3.5.0 +ansible-compat==2.2.0 + # via ansible-lint +ansible-core==2.13.2 + # via + # ansible-lint + # galaxy-importer +ansible-lint==6.2.2 + # via + # galaxy-importer + # galaxy-ng (setup.py) +asgiref==3.5.2 # via django -async-lru==1.0.2 +async-lru==1.0.3 # via pulp-ansible async-timeout==4.0.2 # via @@ -38,31 +44,32 @@ attrs==21.4.0 # aiohttp # galaxy-importer # jsonschema -backoff==1.11.1 + # pytest +backoff==2.1.2 # via pulpcore -bindep==2.10.2 +bindep==2.11.0 # via ansible-builder bleach==3.3.1 # via galaxy-importer bleach-allowlist==1.0.3 # via galaxy-importer -bracex==2.2.1 +bracex==2.3.post1 # via wcmatch -certifi==2021.10.8 +certifi==2022.6.15 # via requests -cffi==1.15.0 +cffi==1.15.1 # via # cryptography # pycares -charset-normalizer==2.0.12 +charset-normalizer==2.1.0 # via # aiohttp # requests -click==8.1.2 +click==8.1.3 # via pulpcore commonmark==0.9.1 # via rich -cryptography==36.0.2 +cryptography==37.0.4 # via # ansible-core # pulpcore @@ -79,13 +86,12 @@ diff-match-patch==20200713 # via django-import-export distro==1.7.0 # via bindep -django==3.2.14 +django==3.2.15 # via # django-auth-ldap # django-automated-logging # django-currentuser # django-filter - # django-guardian # django-guid # django-import-export # django-lifecycle @@ -100,19 +106,17 @@ django-automated-logging==6.1.3 # via galaxy-ng (setup.py) django-currentuser==0.5.3 # via pulpcore -django-filter==21.1 - # via pulpcore -django-guardian==2.4.0 +django-filter==22.1 # via pulpcore -django-guid==3.2.2 +django-guid==3.3.0 # via pulpcore -django-import-export==2.7.1 +django-import-export==2.8.0 # via pulpcore django-ipware==3.0.7 # via django-automated-logging -django-lifecycle==0.9.6 +django-lifecycle==1.0.0 # via pulpcore -django-picklefield==3.0.1 +django-picklefield==3.1 # via django-automated-logging django-prometheus==2.2.0 # via galaxy-ng (setup.py) @@ -124,11 +128,11 @@ djangorestframework==3.13.1 # pulpcore djangorestframework-queryfields==1.0.0 # via pulpcore -drf-access-policy==1.1.0 +drf-access-policy==1.1.2 # via pulpcore drf-nested-routers==0.93.4 # via pulpcore -drf-spectacular==0.21.2 +drf-spectacular==0.22.1 # via # galaxy-ng (setup.py) # pulpcore @@ -136,7 +140,7 @@ dynaconf==3.1.9 # via # galaxy-ng (setup.py) # pulpcore -ecdsa==0.17.0 +ecdsa==0.18.0 # via pulp-container enrich==1.2.7 # via ansible-lint @@ -144,7 +148,7 @@ et-xmlfile==1.1.0 # via openpyxl flake8==3.9.2 # via galaxy-importer -frozenlist==1.3.0 +frozenlist==1.3.1 # via # aiohttp # aiosignal @@ -164,23 +168,30 @@ idna==3.3 # via # requests # yarl +importlib-metadata==4.12.0 + # via markdown inflection==0.5.1 # via drf-spectacular -jinja2==3.1.1 +iniconfig==1.1.1 + # via pytest +jinja2==3.1.2 # via # ansible-core # pulpcore -jsonschema==4.4.0 +jsonschema==4.6.2 # via + # ansible-compat + # ansible-lint # drf-spectacular # pulp-ansible -markdown==3.3.6 + # pulp-container +markdown==3.4.1 # via galaxy-importer markuppy==1.14 # via tablib markupsafe==2.1.1 # via jinja2 -marshmallow==3.15.0 +marshmallow==3.17.0 # via django-automated-logging mccabe==0.6.1 # via flake8 @@ -188,13 +199,15 @@ multidict==6.0.2 # via # aiohttp # yarl +naya==1.1.1 + # via pulpcore oauthlib==3.2.0 # via # requests-oauthlib # social-auth-core odfpy==1.4.1 # via tablib -openpyxl==3.0.9 +openpyxl==3.0.10 # via tablib packaging==21.3 # via @@ -205,59 +218,68 @@ packaging==21.3 # django-lifecycle # marshmallow # pulp-ansible + # pytest # redis parsley==1.3 # via bindep -pbr==5.8.1 +pathspec==0.9.0 + # via yamllint +pbr==5.9.0 # via bindep +pluggy==1.0.0 + # via pytest prometheus-client==0.14.1 # via django-prometheus psycopg2==2.9.3 # via pulpcore -pulp-ansible==0.13.0 +pulp-ansible==0.14.0 # via galaxy-ng (setup.py) -pulp-container==2.10.3 +pulp-container==2.13.1 # via galaxy-ng (setup.py) -pulpcore==3.18.3 +pulpcore==3.20.0 # via # galaxy-ng (setup.py) # pulp-ansible # pulp-container +py==1.11.0 + # via pytest pyasn1==0.4.8 # via # pyasn1-modules # python-ldap pyasn1-modules==0.2.8 # via python-ldap -pycares==4.1.2 +pycares==4.2.1 # via aiodns pycodestyle==2.7.0 # via flake8 pycparser==2.21 # via cffi -pycryptodomex==3.14.1 +pycryptodomex==3.15.0 # via pyjwkest pyflakes==2.3.1 # via flake8 -pygments==2.11.2 +pygments==2.12.0 # via rich pygtrie==2.4.2 # via pulpcore pyjwkest==1.4.2 # via pulp-container -pyjwt[crypto]==1.7.1 +pyjwt[crypto]==2.4.0 # via # pulp-container # social-auth-core -pyparsing==3.0.8 +pyparsing==3.0.9 # via # drf-access-policy # packaging pyrsistent==0.18.1 # via jsonschema -python-gnupg==0.4.8 +pytest==7.1.2 + # via ansible-lint +python-gnupg==0.4.9 # via pulpcore -python-ldap==3.4.0 +python-ldap==3.4.2 # via django-auth-ldap python3-openid==3.2.0 # via social-auth-core @@ -268,6 +290,7 @@ pytz==2022.1 pyyaml==5.4.1 # via # ansible-builder + # ansible-compat # ansible-core # ansible-lint # drf-spectacular @@ -275,9 +298,10 @@ pyyaml==5.4.1 # pulp-ansible # pulpcore # tablib -redis==4.2.2 + # yamllint +redis==4.3.4 # via pulpcore -requests==2.27.1 +requests==2.28.1 # via # galaxy-importer # pyjwkest @@ -287,9 +311,9 @@ requests-oauthlib==1.3.1 # via social-auth-core requirements-parser==0.5.0 # via ansible-builder -resolvelib==0.5.4 +resolvelib==0.8.1 # via ansible-core -rich==12.2.0 +rich==12.5.1 # via # ansible-lint # enrich @@ -297,7 +321,7 @@ ruamel-yaml==0.17.21 # via ansible-lint ruamel-yaml-clib==0.2.6 # via ruamel-yaml -semantic-version==2.9.0 +semantic-version==2.10.0 # via # galaxy-importer # pulp-ansible @@ -319,40 +343,44 @@ social-auth-core==3.4.0 # social-auth-app-django sqlparse==0.4.2 # via django +subprocess-tee==0.3.5 + # via ansible-compat tablib[html,ods,xls,xlsx,yaml]==3.2.1 # via django-import-export -tenacity==8.0.1 - # via ansible-lint -types-setuptools==57.4.12 +tomli==2.0.1 + # via pytest +types-setuptools==63.4.0 # via requirements-parser -typing-extensions==4.1.1 +typing-extensions==4.3.0 # via aioredis uritemplate==4.1.1 # via drf-spectacular url-normalize==1.4.3 - # via - # pulp-container - # pulpcore -urllib3==1.26.9 + # via pulpcore +urllib3==1.26.11 # via requests urlman==2.0.1 # via django-lifecycle -wcmatch==8.3 +wcmatch==8.4 # via ansible-lint webencodings==0.5.1 # via bleach -whitenoise==6.0.0 +whitenoise==6.2.0 # via pulpcore -wrapt==1.14.0 +wrapt==1.14.1 # via deprecated xlrd==2.0.1 # via tablib xlwt==1.3.0 # via tablib -yarl==1.7.2 +yamllint==1.27.1 + # via ansible-lint +yarl==1.8.1 # via # aiohttp # pulpcore +zipp==3.8.1 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/requirements/requirements.insights.txt b/requirements/requirements.insights.txt index 85243396b8..790ab5b333 100644 --- a/requirements/requirements.insights.txt +++ b/requirements/requirements.insights.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with python 3.10 +# This file is autogenerated by pip-compile with python 3.9 # To update, run: # # pip-compile --output-file=requirements/requirements.insights.txt requirements/requirements.insights.in setup.py @@ -16,17 +16,23 @@ aioredis==2.0.1 # via pulpcore aiosignal==1.2.0 # via aiohttp -ansible-builder==1.0.1 +ansible-builder==1.1.0 # via galaxy-importer -ansible-core==2.12.4 - # via galaxy-importer -ansible-lint==5.4.0 - # via galaxy-importer -app-common-python==0.2.0 +ansible-compat==2.2.0 + # via ansible-lint +ansible-core==2.13.2 + # via + # ansible-lint + # galaxy-importer +ansible-lint==6.2.2 + # via + # galaxy-importer + # galaxy-ng (setup.py) +app-common-python==0.2.3 # via -r requirements/requirements.insights.in -asgiref==3.5.0 +asgiref==3.5.2 # via django -async-lru==1.0.2 +async-lru==1.0.3 # via pulp-ansible async-timeout==4.0.2 # via @@ -40,40 +46,41 @@ attrs==21.4.0 # aiohttp # galaxy-importer # jsonschema -backoff==1.11.1 + # pytest +backoff==2.1.2 # via pulpcore -bindep==2.10.2 +bindep==2.11.0 # via ansible-builder bleach==3.3.1 # via galaxy-importer bleach-allowlist==1.0.3 # via galaxy-importer -boto3==1.21.37 +boto3==1.24.46 # via # -r requirements/requirements.insights.in # django-storages # watchtower -botocore==1.24.37 +botocore==1.27.46 # via # boto3 # s3transfer -bracex==2.2.1 +bracex==2.3.post1 # via wcmatch -certifi==2021.10.8 +certifi==2022.6.15 # via requests -cffi==1.15.0 +cffi==1.15.1 # via # cryptography # pycares -charset-normalizer==2.0.12 +charset-normalizer==2.1.0 # via # aiohttp # requests -click==8.1.2 +click==8.1.3 # via pulpcore commonmark==0.9.1 # via rich -cryptography==36.0.2 +cryptography==37.0.4 # via # ansible-core # pulpcore @@ -90,13 +97,12 @@ diff-match-patch==20200713 # via django-import-export distro==1.7.0 # via bindep -django==3.2.14 +django==3.2.15 # via # django-auth-ldap # django-automated-logging # django-currentuser # django-filter - # django-guardian # django-guid # django-import-export # django-lifecycle @@ -112,23 +118,21 @@ django-automated-logging==6.1.3 # via galaxy-ng (setup.py) django-currentuser==0.5.3 # via pulpcore -django-filter==21.1 - # via pulpcore -django-guardian==2.4.0 +django-filter==22.1 # via pulpcore -django-guid==3.2.2 +django-guid==3.3.0 # via pulpcore -django-import-export==2.7.1 +django-import-export==2.8.0 # via pulpcore django-ipware==3.0.7 # via django-automated-logging -django-lifecycle==0.9.6 +django-lifecycle==1.0.0 # via pulpcore -django-picklefield==3.0.1 +django-picklefield==3.1 # via django-automated-logging django-prometheus==2.2.0 # via galaxy-ng (setup.py) -django-storages[boto3]==1.12.3 +django-storages[boto3]==1.13 # via -r requirements/requirements.insights.in djangorestframework==3.13.1 # via @@ -138,11 +142,11 @@ djangorestframework==3.13.1 # pulpcore djangorestframework-queryfields==1.0.0 # via pulpcore -drf-access-policy==1.1.0 +drf-access-policy==1.1.2 # via pulpcore drf-nested-routers==0.93.4 # via pulpcore -drf-spectacular==0.21.2 +drf-spectacular==0.22.1 # via # galaxy-ng (setup.py) # pulpcore @@ -150,7 +154,7 @@ dynaconf==3.1.9 # via # galaxy-ng (setup.py) # pulpcore -ecdsa==0.17.0 +ecdsa==0.18.0 # via pulp-container enrich==1.2.7 # via ansible-lint @@ -158,7 +162,7 @@ et-xmlfile==1.1.0 # via openpyxl flake8==3.9.2 # via galaxy-importer -frozenlist==1.3.0 +frozenlist==1.3.1 # via # aiohttp # aiosignal @@ -178,29 +182,36 @@ idna==3.3 # via # requests # yarl +importlib-metadata==4.12.0 + # via markdown inflection==0.5.1 # via drf-spectacular -jinja2==3.1.1 +iniconfig==1.1.1 + # via pytest +jinja2==3.1.2 # via # ansible-core # pulpcore -jmespath==1.0.0 +jmespath==1.0.1 # via # boto3 # botocore -jsonschema==4.4.0 +jsonschema==4.6.2 # via + # ansible-compat + # ansible-lint # drf-spectacular # pulp-ansible + # pulp-container logstash-formatter==0.5.17 # via -r requirements/requirements.insights.in -markdown==3.3.6 +markdown==3.4.1 # via galaxy-importer markuppy==1.14 # via tablib markupsafe==2.1.1 # via jinja2 -marshmallow==3.15.0 +marshmallow==3.17.0 # via django-automated-logging mccabe==0.6.1 # via flake8 @@ -208,13 +219,15 @@ multidict==6.0.2 # via # aiohttp # yarl +naya==1.1.1 + # via pulpcore oauthlib==3.2.0 # via # requests-oauthlib # social-auth-core odfpy==1.4.1 # via tablib -openpyxl==3.0.9 +openpyxl==3.0.10 # via tablib packaging==21.3 # via @@ -225,61 +238,70 @@ packaging==21.3 # django-lifecycle # marshmallow # pulp-ansible + # pytest # redis parsley==1.3 # via bindep -pbr==5.8.1 +pathspec==0.9.0 + # via yamllint +pbr==5.9.0 # via bindep +pluggy==1.0.0 + # via pytest prometheus-client==0.14.1 # via django-prometheus psycopg2==2.9.3 # via pulpcore -pulp-ansible==0.13.0 +pulp-ansible==0.14.0 # via galaxy-ng (setup.py) -pulp-container==2.10.3 +pulp-container==2.13.1 # via galaxy-ng (setup.py) -pulpcore==3.18.3 +pulpcore==3.20.0 # via # galaxy-ng (setup.py) # pulp-ansible # pulp-container +py==1.11.0 + # via pytest pyasn1==0.4.8 # via # pyasn1-modules # python-ldap pyasn1-modules==0.2.8 # via python-ldap -pycares==4.1.2 +pycares==4.2.1 # via aiodns pycodestyle==2.7.0 # via flake8 pycparser==2.21 # via cffi -pycryptodomex==3.14.1 +pycryptodomex==3.15.0 # via pyjwkest pyflakes==2.3.1 # via flake8 -pygments==2.11.2 +pygments==2.12.0 # via rich pygtrie==2.4.2 # via pulpcore pyjwkest==1.4.2 # via pulp-container -pyjwt[crypto]==1.7.1 +pyjwt[crypto]==2.4.0 # via # pulp-container # social-auth-core -pyparsing==3.0.8 +pyparsing==3.0.9 # via # drf-access-policy # packaging pyrsistent==0.18.1 # via jsonschema +pytest==7.1.2 + # via ansible-lint python-dateutil==2.8.2 # via botocore -python-gnupg==0.4.8 +python-gnupg==0.4.9 # via pulpcore -python-ldap==3.4.0 +python-ldap==3.4.2 # via django-auth-ldap python3-openid==3.2.0 # via social-auth-core @@ -290,6 +312,7 @@ pytz==2022.1 pyyaml==5.4.1 # via # ansible-builder + # ansible-compat # ansible-core # ansible-lint # drf-spectacular @@ -297,9 +320,10 @@ pyyaml==5.4.1 # pulp-ansible # pulpcore # tablib -redis==4.2.2 + # yamllint +redis==4.3.4 # via pulpcore -requests==2.27.1 +requests==2.28.1 # via # galaxy-importer # pyjwkest @@ -309,9 +333,9 @@ requests-oauthlib==1.3.1 # via social-auth-core requirements-parser==0.5.0 # via ansible-builder -resolvelib==0.5.4 +resolvelib==0.8.1 # via ansible-core -rich==12.2.0 +rich==12.5.1 # via # ansible-lint # enrich @@ -319,9 +343,9 @@ ruamel-yaml==0.17.21 # via ansible-lint ruamel-yaml-clib==0.2.6 # via ruamel-yaml -s3transfer==0.5.2 +s3transfer==0.6.0 # via boto3 -semantic-version==2.9.0 +semantic-version==2.10.0 # via # galaxy-importer # pulp-ansible @@ -344,21 +368,21 @@ social-auth-core==3.4.0 # social-auth-app-django sqlparse==0.4.2 # via django +subprocess-tee==0.3.5 + # via ansible-compat tablib[html,ods,xls,xlsx,yaml]==3.2.1 # via django-import-export -tenacity==8.0.1 - # via ansible-lint -types-setuptools==57.4.12 +tomli==2.0.1 + # via pytest +types-setuptools==63.4.0 # via requirements-parser -typing-extensions==4.1.1 +typing-extensions==4.3.0 # via aioredis uritemplate==4.1.1 # via drf-spectacular url-normalize==1.4.3 - # via - # pulp-container - # pulpcore -urllib3==1.26.9 + # via pulpcore +urllib3==1.26.11 # via # botocore # requests @@ -366,22 +390,26 @@ urlman==2.0.1 # via django-lifecycle watchtower==3.0.0 # via -r requirements/requirements.insights.in -wcmatch==8.3 +wcmatch==8.4 # via ansible-lint webencodings==0.5.1 # via bleach -whitenoise==6.0.0 +whitenoise==6.2.0 # via pulpcore -wrapt==1.14.0 +wrapt==1.14.1 # via deprecated xlrd==2.0.1 # via tablib xlwt==1.3.0 # via tablib -yarl==1.7.2 +yamllint==1.27.1 + # via ansible-lint +yarl==1.8.1 # via # aiohttp # pulpcore +zipp==3.8.1 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/requirements/requirements.standalone.txt b/requirements/requirements.standalone.txt index 01aff4f3ca..e0d43fe4ee 100644 --- a/requirements/requirements.standalone.txt +++ b/requirements/requirements.standalone.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with python 3.10 +# This file is autogenerated by pip-compile with python 3.9 # To update, run: # # pip-compile --output-file=requirements/requirements.standalone.txt requirements/requirements.standalone.in setup.py @@ -16,15 +16,21 @@ aioredis==2.0.1 # via pulpcore aiosignal==1.2.0 # via aiohttp -ansible-builder==1.0.1 +ansible-builder==1.1.0 # via galaxy-importer -ansible-core==2.12.4 - # via galaxy-importer -ansible-lint==5.4.0 - # via galaxy-importer -asgiref==3.5.0 +ansible-compat==2.2.0 + # via ansible-lint +ansible-core==2.13.2 + # via + # ansible-lint + # galaxy-importer +ansible-lint==6.2.2 + # via + # galaxy-importer + # galaxy-ng (setup.py) +asgiref==3.5.2 # via django -async-lru==1.0.2 +async-lru==1.0.3 # via pulp-ansible async-timeout==4.0.2 # via @@ -38,31 +44,32 @@ attrs==21.4.0 # aiohttp # galaxy-importer # jsonschema -backoff==1.11.1 + # pytest +backoff==2.1.2 # via pulpcore -bindep==2.10.2 +bindep==2.11.0 # via ansible-builder bleach==3.3.1 # via galaxy-importer bleach-allowlist==1.0.3 # via galaxy-importer -bracex==2.2.1 +bracex==2.3.post1 # via wcmatch -certifi==2021.10.8 +certifi==2022.6.15 # via requests -cffi==1.15.0 +cffi==1.15.1 # via # cryptography # pycares -charset-normalizer==2.0.12 +charset-normalizer==2.1.0 # via # aiohttp # requests -click==8.1.2 +click==8.1.3 # via pulpcore commonmark==0.9.1 # via rich -cryptography==36.0.2 +cryptography==37.0.4 # via # ansible-core # pulpcore @@ -79,13 +86,12 @@ diff-match-patch==20200713 # via django-import-export distro==1.7.0 # via bindep -django==3.2.14 +django==3.2.15 # via # django-auth-ldap # django-automated-logging # django-currentuser # django-filter - # django-guardian # django-guid # django-import-export # django-lifecycle @@ -100,19 +106,17 @@ django-automated-logging==6.1.3 # via galaxy-ng (setup.py) django-currentuser==0.5.3 # via pulpcore -django-filter==21.1 - # via pulpcore -django-guardian==2.4.0 +django-filter==22.1 # via pulpcore -django-guid==3.2.2 +django-guid==3.3.0 # via pulpcore -django-import-export==2.7.1 +django-import-export==2.8.0 # via pulpcore django-ipware==3.0.7 # via django-automated-logging -django-lifecycle==0.9.6 +django-lifecycle==1.0.0 # via pulpcore -django-picklefield==3.0.1 +django-picklefield==3.1 # via django-automated-logging django-prometheus==2.2.0 # via galaxy-ng (setup.py) @@ -124,11 +128,11 @@ djangorestframework==3.13.1 # pulpcore djangorestframework-queryfields==1.0.0 # via pulpcore -drf-access-policy==1.1.0 +drf-access-policy==1.1.2 # via pulpcore drf-nested-routers==0.93.4 # via pulpcore -drf-spectacular==0.21.2 +drf-spectacular==0.22.1 # via # galaxy-ng (setup.py) # pulpcore @@ -136,7 +140,7 @@ dynaconf==3.1.9 # via # galaxy-ng (setup.py) # pulpcore -ecdsa==0.17.0 +ecdsa==0.18.0 # via pulp-container enrich==1.2.7 # via ansible-lint @@ -144,7 +148,7 @@ et-xmlfile==1.1.0 # via openpyxl flake8==3.9.2 # via galaxy-importer -frozenlist==1.3.0 +frozenlist==1.3.1 # via # aiohttp # aiosignal @@ -164,23 +168,30 @@ idna==3.3 # via # requests # yarl +importlib-metadata==4.12.0 + # via markdown inflection==0.5.1 # via drf-spectacular -jinja2==3.1.1 +iniconfig==1.1.1 + # via pytest +jinja2==3.1.2 # via # ansible-core # pulpcore -jsonschema==4.4.0 +jsonschema==4.6.2 # via + # ansible-compat + # ansible-lint # drf-spectacular # pulp-ansible -markdown==3.3.6 + # pulp-container +markdown==3.4.1 # via galaxy-importer markuppy==1.14 # via tablib markupsafe==2.1.1 # via jinja2 -marshmallow==3.15.0 +marshmallow==3.17.0 # via django-automated-logging mccabe==0.6.1 # via flake8 @@ -188,13 +199,15 @@ multidict==6.0.2 # via # aiohttp # yarl +naya==1.1.1 + # via pulpcore oauthlib==3.2.0 # via # requests-oauthlib # social-auth-core odfpy==1.4.1 # via tablib -openpyxl==3.0.9 +openpyxl==3.0.10 # via tablib packaging==21.3 # via @@ -205,59 +218,68 @@ packaging==21.3 # django-lifecycle # marshmallow # pulp-ansible + # pytest # redis parsley==1.3 # via bindep -pbr==5.8.1 +pathspec==0.9.0 + # via yamllint +pbr==5.9.0 # via bindep +pluggy==1.0.0 + # via pytest prometheus-client==0.14.1 # via django-prometheus psycopg2==2.9.3 # via pulpcore -pulp-ansible==0.13.0 +pulp-ansible==0.14.0 # via galaxy-ng (setup.py) -pulp-container==2.10.3 +pulp-container==2.13.1 # via galaxy-ng (setup.py) -pulpcore==3.18.3 +pulpcore==3.20.0 # via # galaxy-ng (setup.py) # pulp-ansible # pulp-container +py==1.11.0 + # via pytest pyasn1==0.4.8 # via # pyasn1-modules # python-ldap pyasn1-modules==0.2.8 # via python-ldap -pycares==4.1.2 +pycares==4.2.1 # via aiodns pycodestyle==2.7.0 # via flake8 pycparser==2.21 # via cffi -pycryptodomex==3.14.1 +pycryptodomex==3.15.0 # via pyjwkest pyflakes==2.3.1 # via flake8 -pygments==2.11.2 +pygments==2.12.0 # via rich pygtrie==2.4.2 # via pulpcore pyjwkest==1.4.2 # via pulp-container -pyjwt[crypto]==1.7.1 +pyjwt[crypto]==2.4.0 # via # pulp-container # social-auth-core -pyparsing==3.0.8 +pyparsing==3.0.9 # via # drf-access-policy # packaging pyrsistent==0.18.1 # via jsonschema -python-gnupg==0.4.8 +pytest==7.1.2 + # via ansible-lint +python-gnupg==0.4.9 # via pulpcore -python-ldap==3.4.0 +python-ldap==3.4.2 # via django-auth-ldap python3-openid==3.2.0 # via social-auth-core @@ -268,6 +290,7 @@ pytz==2022.1 pyyaml==5.4.1 # via # ansible-builder + # ansible-compat # ansible-core # ansible-lint # drf-spectacular @@ -275,9 +298,10 @@ pyyaml==5.4.1 # pulp-ansible # pulpcore # tablib -redis==4.2.2 + # yamllint +redis==4.3.4 # via pulpcore -requests==2.27.1 +requests==2.28.1 # via # galaxy-importer # pyjwkest @@ -287,9 +311,9 @@ requests-oauthlib==1.3.1 # via social-auth-core requirements-parser==0.5.0 # via ansible-builder -resolvelib==0.5.4 +resolvelib==0.8.1 # via ansible-core -rich==12.2.0 +rich==12.5.1 # via # ansible-lint # enrich @@ -297,7 +321,7 @@ ruamel-yaml==0.17.21 # via ansible-lint ruamel-yaml-clib==0.2.6 # via ruamel-yaml -semantic-version==2.9.0 +semantic-version==2.10.0 # via # galaxy-importer # pulp-ansible @@ -319,40 +343,44 @@ social-auth-core==3.4.0 # social-auth-app-django sqlparse==0.4.2 # via django +subprocess-tee==0.3.5 + # via ansible-compat tablib[html,ods,xls,xlsx,yaml]==3.2.1 # via django-import-export -tenacity==8.0.1 - # via ansible-lint -types-setuptools==57.4.12 +tomli==2.0.1 + # via pytest +types-setuptools==63.4.0 # via requirements-parser -typing-extensions==4.1.1 +typing-extensions==4.3.0 # via aioredis uritemplate==4.1.1 # via drf-spectacular url-normalize==1.4.3 - # via - # pulp-container - # pulpcore -urllib3==1.26.9 + # via pulpcore +urllib3==1.26.11 # via requests urlman==2.0.1 # via django-lifecycle -wcmatch==8.3 +wcmatch==8.4 # via ansible-lint webencodings==0.5.1 # via bleach -whitenoise==6.0.0 +whitenoise==6.2.0 # via pulpcore -wrapt==1.14.0 +wrapt==1.14.1 # via deprecated xlrd==2.0.1 # via tablib xlwt==1.3.0 # via tablib -yarl==1.7.2 +yamllint==1.27.1 + # via ansible-lint +yarl==1.8.1 # via # aiohttp # pulpcore +zipp==3.8.1 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/setup.py b/setup.py index 2a7e18cb87..063c5a5258 100644 --- a/setup.py +++ b/setup.py @@ -86,18 +86,43 @@ def run(self): return super().run() +# FIXME: this currently works for CI and dev env, but pip-tools misses dependencies when +# generating requirements.*.txt files. This needs to be fixed before use in the master branch. +def _format_pulp_requirement(plugin, specifier=None, ref=None, gh_namespace="pulp"): + """ + Formats the pulp plugin requirement. + + The plugin template is VERY picky about the format we use for git refs. This will + help format git refs in a way that won't break CI when we need to pin to development + branches of pulp. + + example: + _format_pulp_requirement("pulpcore", specifier=">=3.18.1,<3.19.0") + _format_pulp_requirement("pulpcore", ref="6e44fb2fe609f92dc1f502b19c67abd08879148f") + """ + if specifier: + return plugin + specifier + else: + repo = plugin.replace("-", "_") + return ( + f"{plugin}@git+https://git@github.com/" + f"{gh_namespace}/{repo}.git@{ref}#egg={plugin}" + ) + + requirements = [ "galaxy-importer==0.4.5", - "pulpcore>=3.18.1,<3.19.0", - "pulp-ansible>=0.13.0,<0.14.0", + "pulpcore>=3.20.0,<3.21.0", + "pulp-ansible>=0.14.0,<0.15.0", "django-prometheus>=2.0.0", "drf-spectacular", - "pulp-container>=2.10.2,<2.11.0", + "pulp-container>=2.13.1,<2.14.0", "django-automated-logging==6.1.3", "social-auth-core>=3.3.1,<4.0.0", "social-auth-app-django>=3.1.0,<4.0.0", "dynaconf>=3.1.9", "django-auth-ldap==4.0.0", + "ansible-lint==6.2.2", # keeping this pinned due to jsonschema dep mismatch with pulp pkgs ] diff --git a/template_config.yml b/template_config.yml index 03e5941074..198df0c817 100644 --- a/template_config.yml +++ b/template_config.yml @@ -1,15 +1,15 @@ # This config represents the latest values used when running the plugin-template. Any settings that # were not present before running plugin-template have been added with their default values. -# generated with plugin_template@2021.08.26-129-gf780fda +# generated with plugin_template@2021.08.26-156-gb5f4d20 additional_repos: - bindings: true - branch: 0.13.0 + branch: 0.14.0 name: pulp_ansible org: pulp - bindings: true - branch: 2.10.2 + branch: 2.13.1 name: pulp_container org: pulp - bindings: false @@ -17,7 +17,7 @@ additional_repos: name: galaxy-importer org: ansible aiohttp_fixtures_origin: 172.18.0.1 -api_root: "/api/galaxy/pulp/" +api_root: /api/galaxy/pulp/ black: false check_commit_message: false check_gettext: true @@ -27,7 +27,7 @@ check_stray_pulpcore_imports: true cherry_pick_automation: false ci_env: GITHUB_USER: ${{ github.event.pull_request.user.login }} -ci_trigger: '{pull_request: {branches: [''*'']}, push: {branches: [''*'']}}' +ci_trigger: '{pull_request: {branches: [''**'']}, push: {branches: [''**'']}}' core_import_allowed: - pulpcore.app.*viewsets - pulpcore\.app.*admin @@ -47,6 +47,7 @@ flake8: true github_org: ansible issue_tracker: null keep_ci_update_since_branch: null +lint_requirements: true noissue_marker: No-Issue parallel_test_workers: 8 plugin_app_label: galaxy @@ -74,7 +75,7 @@ pulp_settings: galaxy_enable_api_access_log: true galaxy_require_content_approval: false rh_entitlement_required: insights -pulpcore_branch: 3.18.1 +pulpcore_branch: 3.20.0 pulpcore_pip_version_specifier: null pulpcore_revision: null pulpprojectdotorg_key_id: null @@ -87,6 +88,10 @@ release_user: ansible run_pulpcore_tests_for_plugins: false single_commit_check: false stable_branch: stable +stalebot: true +stalebot_days_until_close: 30 +stalebot_days_until_stale: 90 +stalebot_limit_to_pulls: true sync_ci: false tasking_allow_async_unsafe: true test_azure: true @@ -103,3 +108,4 @@ travis_notifications: null update_github: false upgrade_range: [] use_issue_template: false +