diff --git a/.compose.env.example b/.compose.env.example index 20308a4e2b..a431e75ff4 100644 --- a/.compose.env.example +++ b/.compose.env.example @@ -39,6 +39,9 @@ LOCK_REQUIREMENTS=1 # where the script is executed in initContainers. WAIT_FOR_MIGRATIONS=1 +# Enable setup of signing service in dev environment. Defaults to `0` for other environments +ENABLE_SIGNING=1 + #### PULP SETTINGS ## Variables prefixed with `PULP_` are added to the `django.conf.settings` for pulp #### diff --git a/.dockerignore b/.dockerignore index 23fe409eb7..d3c7eaf69c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,4 +5,3 @@ venv/ pip-wheel-metadata/ **/__pycache__/ .git/ -dev/ diff --git a/.github/workflows/scripts/post_before_script.sh b/.github/workflows/scripts/post_before_script.sh index 4e1848e613..7bfbebc8da 100644 --- a/.github/workflows/scripts/post_before_script.sh +++ b/.github/workflows/scripts/post_before_script.sh @@ -4,6 +4,17 @@ set -mveuo pipefail source .github/workflows/scripts/utils.sh cmd_prefix bash -c "django-admin compilemessages" + +cmd_stdin_prefix bash -c "cat > /var/lib/pulp/sign-metadata.sh" < "$GITHUB_WORKSPACE"/galaxy_ng/tests/assets/sign-metadata.sh + +cmd_prefix bash -c "curl -L https://github.com/pulp/pulp-fixtures/raw/master/common/GPG-PRIVATE-KEY-pulp-qe | gpg --import" +cmd_prefix bash -c "curl -L https://github.com/pulp/pulp-fixtures/raw/master/common/GPG-KEY-pulp-qe | cat > /tmp/GPG-KEY-pulp-qe" +cmd_prefix chmod a+x /var/lib/pulp/sign-metadata.sh + +KEY_FINGERPRINT="6EDF301256480B9B801EBA3D05A5E6DA269D9D98" +TRUST_LEVEL="6" +echo "$KEY_FINGERPRINT:$TRUST_LEVEL:" | cmd_stdin_prefix gpg --import-ownertrust + echo "machine pulp login admin password password diff --git a/CHANGES/1181.feature b/CHANGES/1181.feature new file mode 100644 index 0000000000..fa7693aa6a --- /dev/null +++ b/CHANGES/1181.feature @@ -0,0 +1 @@ +Add keys, script and signing service to dev env diff --git a/CHANGES/1247.misc b/CHANGES/1247.misc new file mode 100644 index 0000000000..27ea4546b7 --- /dev/null +++ b/CHANGES/1247.misc @@ -0,0 +1 @@ +enable collection signing for ephemeral and other c.rh.c environments diff --git a/Dockerfile.rhel8 b/Dockerfile.rhel8 index 047ce233f6..adbf09179e 100644 --- a/Dockerfile.rhel8 +++ b/Dockerfile.rhel8 @@ -33,6 +33,7 @@ COPY . /app RUN set -ex; \ pip install --no-deps --editable /app && \ + pip install https://github.com/pulp/pulp_ansible/archive/main.zip && \ PULP_CONTENT_ORIGIN=x django-admin collectstatic && \ install -dm 0775 -o galaxy /var/lib/pulp/artifact \ /var/lib/pulp/tmp \ diff --git a/Makefile b/Makefile index e7a3343bcb..5b2106d75f 100644 --- a/Makefile +++ b/Makefile @@ -7,13 +7,13 @@ DJ_MANAGER = $(shell if [ "$(RUNNING)" = "" ]; then echo manage; else echo djang define exec_or_run # Tries to run on existing container if it exists, otherwise starts a new one. - @echo $(1)$(2)$(3)$(4)$(5) + @echo $(1)$(2)$(3)$(4)$(5)$(6) @if [ "$(RUNNING)" != "" ]; then \ echo "Running on existing container $(RUNNING)" 1>&2; \ - ./compose exec $(1) $(2) $(3) $(4) $(5); \ + ./compose exec $(1) $(2) $(3) $(4) $(5) $(6); \ else \ echo "Starting new container" 1>&2; \ - ./compose run --use-aliases --service-ports --rm $(1) $(2) $(3) $(4) $(5); \ + ./compose run --use-aliases --service-ports --rm $(1) $(2) $(3) $(4) $(5) $(6); \ fi endef @@ -96,6 +96,10 @@ docker/makemigrations: ## Run django migrations docker/migrate: ## Run django migrations $(call exec_or_run, api, $(DJ_MANAGER), migrate) +.PHONY: docker/add-signing-service +docker/add-signing-service: ## Add a Signing service using default GPG key + $(call exec_or_run, worker, $(DJ_MANAGER), add-signing-service, ansible-default, /var/lib/pulp/scripts/collection_sign.sh, galaxy3@ansible.com) + .PHONY: docker/resetdb docker/resetdb: ## Cleans database # Databases must be stopped to be able to reset them. @@ -113,6 +117,7 @@ docker/all: ## Build, migrate, loaddata, transl make docker/migrate make docker/loaddata make docker/translations + make docker/add-signing-service # Application management and debugging diff --git a/compose b/compose index 43523596ff..4b6c7b0b9a 100755 --- a/compose +++ b/compose @@ -45,6 +45,7 @@ declare -xr DEV_SOURCE_PATH=${DEV_SOURCE_PATH:-galaxy_ng} declare -xr COMPOSE_CONTEXT=".." declare -xr LOCK_REQUIREMENTS="${LOCK_REQUIREMENTS:-1}" declare -xr COMPOSE_PROFILE="${COMPOSE_PROFILE}" +declare -xr ENABLE_SIGNING="${ENABLE_SIGNING:-1}" declare -xr DEV_IMAGE_SUFFIX="${DEV_IMAGE_SUFFIX:-}" declare -xr DEV_VOLUME_SUFFIX="${DEV_VOLUME_SUFFIX:-${DEV_IMAGE_SUFFIX}}" declare -xr COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-galaxy_ng${DEV_IMAGE_SUFFIX:-}}" diff --git a/dev/Dockerfile.base b/dev/Dockerfile.base index 1b497bbce1..ef2595e6b3 100644 --- a/dev/Dockerfile.base +++ b/dev/Dockerfile.base @@ -38,6 +38,7 @@ RUN set -ex; \ python38-devel \ libpq \ libpq-devel \ + pinentry \ && dnf clean all \ && rm -rf /var/cache/dnf/ \ && rm -f /var/lib/rpm/__db.* \ @@ -54,6 +55,7 @@ ENV PATH="/venv/bin:${PATH}" \ VIRTUAL_ENV="/venv" COPY ./requirements/requirements.common.txt /tmp/requirements.txt +COPY ./dev/common/ansible-sign.key /tmp/ansible-sign.key RUN set -ex; \ pip install --no-cache-dir --upgrade pip \ @@ -83,9 +85,11 @@ RUN set -ex; \ && mkdir --mode=2775 -p \ /var/lib/pulp/artifact \ /var/lib/pulp/tmp \ + /var/lib/pulp/scripts \ /tmp/ansible \ && chown ${USER_NAME}:${USER_GROUP} /var/lib/pulp/artifact \ && chown ${USER_NAME}:${USER_GROUP} /var/lib/pulp/tmp \ + && chown ${USER_NAME}:${USER_GROUP} /var/lib/pulp/scripts \ && chown ${USER_NAME}:${USER_GROUP} \ /tmp/ansible \ /etc/ansible \ @@ -98,7 +102,9 @@ RUN set -ex; \ && chmod 0644 /var/log/galaxy_api_access.log \ && chown galaxy:galaxy /var/log/galaxy_api_access.log \ && mkdir -p /etc/pulp/certs/ \ - && echo "DNmNdwgyZugTax9S64J0FITTr9IHPxbuoF1F1CGPr68=" > /etc/pulp/certs/database_fields.symmetric.key + && echo "DNmNdwgyZugTax9S64J0FITTr9IHPxbuoF1F1CGPr68=" > /etc/pulp/certs/database_fields.symmetric.key \ + && gpg --batch --import /tmp/ansible-sign.key &>/dev/null \ + && (echo trust &echo 5 &echo y &echo quit &echo save) | gpg --batch --command-fd 0 --edit-key galaxy3 &>/dev/null # This symmetric.key is for dev only and should not be used in production # DNmNdwgyZugTax9S64J0FITTr9IHPxbuoF1F1CGPr68= diff --git a/dev/common/ansible-sign-pub.gpg b/dev/common/ansible-sign-pub.gpg new file mode 100644 index 0000000000..000057af24 Binary files /dev/null and b/dev/common/ansible-sign-pub.gpg differ diff --git a/dev/common/ansible-sign-pub.txt b/dev/common/ansible-sign-pub.txt new file mode 100644 index 0000000000..51d32e8868 --- /dev/null +++ b/dev/common/ansible-sign-pub.txt @@ -0,0 +1,41 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +I6KUd9Bv1wf+etJ50jZ0dzt+T1Qs2wKbtf0= +=PHJy +-----END PGP PUBLIC KEY BLOCK----- diff --git a/dev/common/ansible-sign.key b/dev/common/ansible-sign.key new file mode 100644 index 0000000000..0b543fc320 Binary files /dev/null and b/dev/common/ansible-sign.key differ diff --git a/dev/common/collection_sign.sh b/dev/common/collection_sign.sh new file mode 100755 index 0000000000..f180672f47 --- /dev/null +++ b/dev/common/collection_sign.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +FILE_PATH=$1 +SIGNATURE_PATH="$1.asc" + +ADMIN_ID="galaxy3@ansible.com" +PASSWORD="Galaxy2022" + +# Create a detached signature +gpg --quiet --batch --pinentry-mode loopback --yes --passphrase \ + $PASSWORD --homedir ~/.gnupg/ --detach-sign --default-key $ADMIN_ID \ + --armor --output $SIGNATURE_PATH $FILE_PATH + +# Check the exit status +STATUS=$? +if [ $STATUS -eq 0 ]; then + echo {\"file\": \"$FILE_PATH\", \"signature\": \"$SIGNATURE_PATH\"} +else + exit $STATUS +fi diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index ad0184a7b5..6f4d4eba26 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -18,9 +18,11 @@ services: - "LOCK_REQUIREMENTS=${LOCK_REQUIREMENTS}" - "DEV_SOURCE_PATH=${DEV_SOURCE_PATH}" - "COMPOSE_PROFILE=${COMPOSE_PROFILE}" + - "ENABLE_SIGNING=${ENABLE_SIGNING}" entrypoint: "/bin/true" tmpfs: - "/var/lib/pulp/artifact" + - "/var/lib/pulp/scripts" - "/var/lib/pulp/tmp" - "/tmp/ansible" @@ -40,10 +42,12 @@ services: - "LOCK_REQUIREMENTS=${LOCK_REQUIREMENTS}" - "DEV_SOURCE_PATH=${DEV_SOURCE_PATH}" - "COMPOSE_PROFILE=${COMPOSE_PROFILE}" + - "ENABLE_SIGNING=${ENABLE_SIGNING}" env_file: - './common/galaxy_ng.env' volumes: - "./common/settings.py:/etc/pulp/settings.py:z" + - "./common/collection_sign.sh:/var/lib/pulp/scripts/collection_sign.sh:z" - "${COMPOSE_CONTEXT}/..:/src:z" - "pulp:/var/lib/pulp" tmpfs: @@ -61,10 +65,12 @@ services: - "LOCK_REQUIREMENTS=${LOCK_REQUIREMENTS}" - "DEV_SOURCE_PATH=${DEV_SOURCE_PATH}" - "COMPOSE_PROFILE=${COMPOSE_PROFILE}" + - "ENABLE_SIGNING=${ENABLE_SIGNING}" env_file: - './common/galaxy_ng.env' volumes: - "./common/settings.py:/etc/pulp/settings.py:z" + - "./common/collection_sign.sh:/var/lib/pulp/scripts/collection_sign.sh:z" - "${COMPOSE_CONTEXT}/..:/src:z" - "pulp:/var/lib/pulp" tmpfs: @@ -84,10 +90,12 @@ services: - "LOCK_REQUIREMENTS=${LOCK_REQUIREMENTS}" - "DEV_SOURCE_PATH=${DEV_SOURCE_PATH}" - "COMPOSE_PROFILE=${COMPOSE_PROFILE}" + - "ENABLE_SIGNING=${ENABLE_SIGNING}" env_file: - './common/galaxy_ng.env' volumes: - "./common/settings.py:/etc/pulp/settings.py:z" + - "./common/collection_sign.sh:/var/lib/pulp/scripts/collection_sign.sh:z" - "${COMPOSE_CONTEXT}/..:/src:z" - "pulp:/var/lib/pulp" tmpfs: diff --git a/dev/standalone/galaxy_ng.env b/dev/standalone/galaxy_ng.env index b7b42efc6c..d3056e0361 100644 --- a/dev/standalone/galaxy_ng.env +++ b/dev/standalone/galaxy_ng.env @@ -4,6 +4,8 @@ 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 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 diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 96c9c88332..ccd68b8ca2 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -9,13 +9,13 @@ readonly WITH_DEV_INSTALL="${WITH_DEV_INSTALL:-0}" readonly DEV_SOURCE_PATH="${DEV_SOURCE_PATH:-}" readonly LOCK_REQUIREMENTS="${LOCK_REQUIREMENTS:-1}" readonly WAIT_FOR_MIGRATIONS="${WAIT_FOR_MIGRATIONS:-0}" +readonly ENABLE_SIGNING="${ENABLE_SIGNING:-0}" log_message() { echo "$@" >&2 } - # TODO(cutwater): This function should be moved to entrypoint hooks. install_local_deps() { local src_path_list @@ -89,6 +89,10 @@ run_service() { process_init_files /entrypoints.d/* + if [[ "$ENABLE_SIGNING" -eq "1" ]]; then + setup_signing_service + fi + exec "${service_path}" "$@" } @@ -97,9 +101,29 @@ run_manage() { if [[ "$WITH_DEV_INSTALL" -eq "1" ]]; then install_local_deps fi + + if [[ "$ENABLE_SIGNING" -eq "1" ]]; then + setup_signing_service + fi + exec django-admin "$@" } +setup_signing_service() { + log_message "Setting up signing service." + export KEY_FINGERPRINT=$(gpg --show-keys --with-colons --with-fingerprint /tmp/ansible-sign.key | awk -F: '$1 == "fpr" {print $10;}' | head -n1) + export KEY_ID=${KEY_FINGERPRINT: -16} + gpg --batch --import /tmp/ansible-sign.key &>/dev/null + echo "${KEY_FINGERPRINT}:6:" | gpg --import-ownertrust &>/dev/null + + HAS_SIGNING=$(django-admin shell -c 'from pulpcore.app.models import SigningService;print(SigningService.objects.filter(name="ansible-default").count())' 2>/dev/null || true) + if [[ "$HAS_SIGNING" -eq "0" ]]; then + log_message "Creating signing service. using key ${KEY_ID}" + django-admin add-signing-service ansible-default /var/lib/pulp/scripts/collection_sign.sh ${KEY_ID} 2>/dev/null || true + else + log_message "Signing service already exists." + fi +} redis_connection_hack() { redis_host="${PULP_REDIS_HOST:-}" diff --git a/docker/etc/settings.py b/docker/etc/settings.py index 6701f3eddf..ae73167711 100644 --- a/docker/etc/settings.py +++ b/docker/etc/settings.py @@ -15,6 +15,9 @@ GALAXY_PERMISSION_CLASSES = ['rest_framework.permissions.IsAuthenticated', 'galaxy_ng.app.auth.auth.RHEntitlementRequired'] +GALAXY_AUTO_SIGN_COLLECTIONS = "true" +GALAXY_COLLECTION_SIGNING_SERVICE = "ansible-default" + X_PULP_CONTENT_HOST = "pulp-content-app" X_PULP_CONTENT_PORT = 24816 diff --git a/galaxy_ng/app/access_control/access_policy.py b/galaxy_ng/app/access_control/access_policy.py index 053c3af240..2f98f82bbf 100644 --- a/galaxy_ng/app/access_control/access_policy.py +++ b/galaxy_ng/app/access_control/access_policy.py @@ -87,6 +87,26 @@ def can_create_collection(self, request, view, permission): raise NotFound(_('Namespace in filename not found.')) return request.user.has_perm('galaxy.upload_to_namespace', namespace) + def can_sign_collections(self, request, view, permission): + # Repository is required on the CollectionSign payload + # Assumed that if user can modify repo they can sign everything in it + repository = view.get_repository(request) + can_modify_repo = request.user.has_perm('ansible.modify_ansible_repo_content', repository) + + # Payload can optionally specify a namespace to filter its contents + # Assumed that if user has access to modify namespace they can sign its contents. + data = request.data + if namespace := data.get('namespace'): + try: + namespace = models.Namespace.objects.get(name=namespace) + except models.Namespace.DoesNotExist: + raise NotFound(_('Namespace not found.')) + return request.user.has_perm('galaxy.upload_to_namespace', namespace) + + # the other filtering options are content_units and name/version + # and falls on the same permissions as modifying the main repo + return can_modify_repo + def unauthenticated_collection_download_enabled(self, request, view, permission): return settings.GALAXY_ENABLE_UNAUTHENTICATED_COLLECTION_DOWNLOAD diff --git a/galaxy_ng/app/access_control/statements/insights.py b/galaxy_ng/app/access_control/statements/insights.py index afc9fcfe60..62d6ffbae7 100644 --- a/galaxy_ng/app/access_control/statements/insights.py +++ b/galaxy_ng/app/access_control/statements/insights.py @@ -69,6 +69,14 @@ "has_model_perms:ansible.modify_ansible_repo_content", "has_rh_entitlements"] }, + { + "action": "sign", + "principal": "authenticated", + "effect": "allow", + "condition": [ + "can_sign_collections", + "has_rh_entitlements"] + }, { "action": "curate", "principal": "authenticated", diff --git a/galaxy_ng/app/access_control/statements/standalone.py b/galaxy_ng/app/access_control/statements/standalone.py index f8a518c65e..027be44aa5 100644 --- a/galaxy_ng/app/access_control/statements/standalone.py +++ b/galaxy_ng/app/access_control/statements/standalone.py @@ -76,6 +76,12 @@ "principal": "authenticated", "effect": "allow", "condition": "has_model_perms:ansible.modify_ansible_repo_content" + }, + { + "action": "sign", + "principal": "authenticated", + "effect": "allow", + "condition": "can_sign_collections" } ], 'CollectionRemoteViewSet': [ diff --git a/galaxy_ng/app/api/ui/serializers/collection.py b/galaxy_ng/app/api/ui/serializers/collection.py index f1f2a2c89c..dc292d7b7d 100644 --- a/galaxy_ng/app/api/ui/serializers/collection.py +++ b/galaxy_ng/app/api/ui/serializers/collection.py @@ -57,6 +57,19 @@ class CollectionMetadataSerializer(Serializer): authors = serializers.ListField(child=serializers.CharField()) license = serializers.ListField(child=serializers.CharField()) tags = serializers.SerializerMethodField() + signatures = serializers.SerializerMethodField() + + @extend_schema_field(serializers.ListField(child=serializers.DictField())) + def get_signatures(self, obj): + """Returns signature pubkey_fingerprint for each signature.""" + data = [] + for signature in obj.signatures.all(): + sig = {} + sig["signature"] = bytes(signature.data).decode("utf-8") + sig["pubkey_fingerprint"] = signature.pubkey_fingerprint + sig["signing_service"] = signature.signing_service.name + data.append(sig) + return data @extend_schema_field(serializers.ListField) def get_tags(self, collection_version): @@ -67,7 +80,19 @@ def get_tags(self, collection_version): return [tag.name for tag in collection_version.tags.all()] -class CollectionVersionBaseSerializer(Serializer): +class CollectionVersionSignStateMixin: + + @extend_schema_field(serializers.CharField()) + def get_sign_state(self, obj): + """Returns the state of the signature.""" + if obj.signatures.count() == 0: + return "unsigned" + else: + return "signed" + + +class CollectionVersionBaseSerializer(CollectionVersionSignStateMixin, Serializer): + id = serializers.UUIDField(source='pk') namespace = serializers.CharField() name = serializers.CharField() version = serializers.CharField() @@ -75,6 +100,7 @@ class CollectionVersionBaseSerializer(Serializer): created_at = serializers.DateTimeField(source='pulp_created') metadata = CollectionMetadataSerializer(source='*') contents = serializers.ListField(child=ContentSerializer()) + sign_state = serializers.SerializerMethodField() class CollectionVersionSerializer(CollectionVersionBaseSerializer): @@ -101,9 +127,11 @@ class CollectionVersionDetailSerializer(CollectionVersionBaseSerializer): docs_blob = serializers.JSONField() -class CollectionVersionSummarySerializer(Serializer): +class CollectionVersionSummarySerializer(CollectionVersionSignStateMixin, Serializer): + id = serializers.UUIDField(source='pk') version = serializers.CharField() created = serializers.CharField(source='pulp_created') + sign_state = serializers.SerializerMethodField() class _CollectionSerializer(Serializer): @@ -125,6 +153,10 @@ def get_namespace(self, obj): class CollectionListSerializer(_CollectionSerializer): deprecated = serializers.BooleanField() + sign_state = serializers.CharField() + total_versions = serializers.IntegerField(default=0) + signed_versions = serializers.IntegerField(default=0) + unsigned_versions = serializers.IntegerField(default=0) @extend_schema_field(CollectionVersionBaseSerializer) def get_latest_version(self, obj): @@ -133,6 +165,10 @@ def get_latest_version(self, obj): class CollectionDetailSerializer(_CollectionSerializer): all_versions = serializers.SerializerMethodField() + sign_state = serializers.CharField() + total_versions = serializers.IntegerField(default=0) + signed_versions = serializers.IntegerField(default=0) + unsigned_versions = serializers.IntegerField(default=0) # TODO: rename field to "version_details" since with # "version" query param this won't always be the latest version diff --git a/galaxy_ng/app/api/ui/serializers/user.py b/galaxy_ng/app/api/ui/serializers/user.py index 4a660ed01e..c9c79a6b48 100644 --- a/galaxy_ng/app/api/ui/serializers/user.py +++ b/galaxy_ng/app/api/ui/serializers/user.py @@ -149,6 +149,10 @@ class Meta(UserSerializer.Meta): def get_model_permissions(self, obj): permissions = { + # Signing + "sign_collections_on_namespace": obj.has_perm('galaxy.upload_to_namespace'), + "sign_collections_on_repository": obj.has_perm('ansible.modify_ansible_repo_content'), + # Collection Namespace "add_namespace": obj.has_perm('galaxy.add_namespace'), "upload_to_namespace": obj.has_perm('galaxy.upload_to_namespace'), diff --git a/galaxy_ng/app/api/ui/views/settings.py b/galaxy_ng/app/api/ui/views/settings.py index 35f19fd73d..f9eb76e819 100644 --- a/galaxy_ng/app/api/ui/views/settings.py +++ b/galaxy_ng/app/api/ui/views/settings.py @@ -15,6 +15,9 @@ def get(self, request, *args, **kwargs): "GALAXY_ENABLE_UNAUTHENTICATED_COLLECTION_DOWNLOAD", "GALAXY_FEATURE_FLAGS", "GALAXY_TOKEN_EXPIRATION", + "GALAXY_REQUIRE_CONTENT_APPROVAL", + "GALAXY_COLLECTION_SIGNING_SERVICE", + "GALAXY_AUTO_SIGN_COLLECTIONS", ] data = {key: settings.as_dict().get(key, None) for key in keyset} return Response(data) diff --git a/galaxy_ng/app/api/ui/viewsets/collection.py b/galaxy_ng/app/api/ui/viewsets/collection.py index cd09daac38..7bfabb2e04 100644 --- a/galaxy_ng/app/api/ui/viewsets/collection.py +++ b/galaxy_ng/app/api/ui/viewsets/collection.py @@ -1,4 +1,4 @@ -from django.db.models import Exists, OuterRef, Q +from django.db.models import Exists, OuterRef, Q, When, Case, Value, Subquery, F, Func from django.core.exceptions import ObjectDoesNotExist from django.http import Http404 from django.shortcuts import get_object_or_404 @@ -32,6 +32,17 @@ class CollectionByCollectionVersionFilter(pulp_ansible_viewsets.CollectionVersio versioning_class = versioning.UIVersioning keywords = filters.CharFilter(field_name="keywords", method="filter_by_q") deprecated = filters.BooleanFilter() + sign_state = filters.CharFilter(method="filter_by_sign_state") + + def filter_by_sign_state(self, qs, name, value): + """ + Filter queryset qs by list of sign_state. + """ + query_params = Q() + states = value.split(",") + for state in states: + query_params |= Q(sign_state=state.strip()) + return qs.filter(query_params) class CollectionViewSet( @@ -50,6 +61,43 @@ class CollectionViewSet( filterset_class = CollectionByCollectionVersionFilter permission_classes = [access_policy.CollectionAccessPolicy] + def build_signing_annotations(self, base_total_qs): + """Builds a dict with queries for annotation.""" + total_versions_query = Subquery( + base_total_qs.annotate( + total=Func(F("pk"), function="count") + ).values('total') + ) + + signed_versions_query = Subquery( + base_total_qs.filter( + signatures__isnull=False, + ).annotate( + total=Func(F("pk"), function="count") + ).values('total') + ) + + unsigned_versions_query = Subquery( + base_total_qs.filter( + signatures__isnull=True, + ).annotate( + total=Func(F("pk"), function="count") + ).values('total') + ) + + sign_state_query = Case( + When(signed_versions=F("total_versions"), then=Value("signed")), + When(unsigned_versions=F("total_versions"), then=Value("unsigned")), + When(signed_versions__lt=F("total_versions"), then=Value("partial")), + ) + + return { + "total_versions": total_versions_query, + "signed_versions": signed_versions_query, + "unsigned_versions": unsigned_versions_query, + "sign_state": sign_state_query, + } + def get_queryset(self): """Returns a CollectionVersions queryset for specified distribution.""" if getattr(self, "swagger_fake_view", False): @@ -59,7 +107,8 @@ def get_queryset(self): if path is None: raise Http404(_("Distribution base path is required")) - versions = CollectionVersion.objects.filter(pk__in=self._distro_content).values_list( + base_versions_query = CollectionVersion.objects.filter(pk__in=self._distro_content) + versions = base_versions_query.values_list( "collection_id", "version", ) @@ -78,9 +127,10 @@ def get_queryset(self): if not collection_versions.items(): return CollectionVersion.objects.none().annotate( - # AAH-122: annotated fields must exist in all the returned querysets + # AAH-122: annotated filterable fields must exist in all the returned querysets # in order for filters to work. - deprecated=Exists(deprecated_query) + deprecated=Exists(deprecated_query), + sign_state=Value("unsigned"), ) query_params = Q() @@ -88,7 +138,16 @@ def get_queryset(self): query_params |= Q(collection_id=collection_id, version=version) version_qs = CollectionVersion.objects.select_related("collection").filter(query_params) - version_qs = version_qs.annotate(deprecated=Exists(deprecated_query)) + + base_total_qs = base_versions_query.filter( + namespace=OuterRef("namespace"), name=OuterRef("name") + ) + + version_qs = version_qs.annotate( + deprecated=Exists(deprecated_query), + **self.build_signing_annotations(base_total_qs) + ) + return version_qs def get_object(self): @@ -104,11 +163,13 @@ def get_object(self): queryset, namespace=self.kwargs["namespace"], name=self.kwargs["name"] ) - return get_object_or_404( - CollectionVersion.objects.all(), + base_qs = CollectionVersion.objects.filter( pk__in=self._distro_content, namespace=self.kwargs["namespace"], name=self.kwargs["name"], + ) + return get_object_or_404( + base_qs.annotate(**self.build_signing_annotations(base_qs)), version=version, ) diff --git a/galaxy_ng/app/api/ui/viewsets/execution_environment.py b/galaxy_ng/app/api/ui/viewsets/execution_environment.py index 0e0b1e5f0a..5967cd3042 100644 --- a/galaxy_ng/app/api/ui/viewsets/execution_environment.py +++ b/galaxy_ng/app/api/ui/viewsets/execution_environment.py @@ -157,7 +157,7 @@ def destroy(self, request, *args, **kwargs): async_result = dispatch( delete_container_distribution, args=(ids_for_multi_delete,), - exclusive_resources=reservations + exclusive_resources=reservations, ) return OperationPostponedResponse(async_result, request) diff --git a/galaxy_ng/app/api/ui/viewsets/my_synclist.py b/galaxy_ng/app/api/ui/viewsets/my_synclist.py index b6a9bf7a0d..07e4f4f46d 100644 --- a/galaxy_ng/app/api/ui/viewsets/my_synclist.py +++ b/galaxy_ng/app/api/ui/viewsets/my_synclist.py @@ -40,8 +40,8 @@ def curate(self, request, pk): synclist = get_object_or_404(models.SyncList, pk=pk) synclist_task = dispatch( curate_synclist_repository, + args=(pk, ), exclusive_resources=[synclist.repository], - args=(pk, ) ) log.debug("synclist_task: %s", synclist_task) diff --git a/galaxy_ng/app/api/v3/serializers/collection.py b/galaxy_ng/app/api/v3/serializers/collection.py index d28b10227f..2eb683798e 100644 --- a/galaxy_ng/app/api/v3/serializers/collection.py +++ b/galaxy_ng/app/api/v3/serializers/collection.py @@ -112,9 +112,13 @@ def get_href(self, obj) -> str: class CollectionVersionSerializer(_CollectionVersionSerializer, HrefNamespaceMixin): collection = CollectionRefSerializer(read_only=True) + id = serializers.CharField(source="pk") class Meta(_CollectionVersionSerializer.Meta): ref_name = "CollectionVersionWithDownloadUrlSerializer" + fields = ( + "id", + ) + _CollectionVersionSerializer.Meta.fields def get_download_url(self, obj) -> str: return self._get_download_url(obj) diff --git a/galaxy_ng/app/api/v3/serializers/sync.py b/galaxy_ng/app/api/v3/serializers/sync.py index 3110883071..65ae9feb6e 100644 --- a/galaxy_ng/app/api/v3/serializers/sync.py +++ b/galaxy_ng/app/api/v3/serializers/sync.py @@ -124,7 +124,8 @@ class Meta: 'proxy_username', 'proxy_password', 'write_only_fields', - 'rate_limit' + 'rate_limit', + 'signed_only', ) extra_kwargs = { 'name': {'read_only': True}, diff --git a/galaxy_ng/app/api/v3/urls.py b/galaxy_ng/app/api/v3/urls.py index b357cda728..e919b38fd4 100644 --- a/galaxy_ng/app/api/v3/urls.py +++ b/galaxy_ng/app/api/v3/urls.py @@ -92,4 +92,9 @@ path("tasks/", viewsets.TaskViewSet.as_view({"get": "list"}), name="tasks-list"), path("tasks//", viewsets.TaskViewSet.as_view({"get": "retrieve"}), name="tasks-detail"), path("excludes/", views.ExcludesView.as_view(), name="excludes-file"), + path( + "sign/collections/", + viewsets.CollectionSignViewSet.as_view({"post": "sign"}), + name="collection-sign" + ), ] diff --git a/galaxy_ng/app/api/v3/viewsets/__init__.py b/galaxy_ng/app/api/v3/viewsets/__init__.py index 1612f24623..df7a98fb64 100644 --- a/galaxy_ng/app/api/v3/viewsets/__init__.py +++ b/galaxy_ng/app/api/v3/viewsets/__init__.py @@ -6,6 +6,7 @@ CollectionVersionViewSet, CollectionVersionDocsViewSet, CollectionVersionMoveViewSet, + CollectionSignViewSet, UnpaginatedCollectionViewSet, UnpaginatedCollectionVersionViewSet, RepoMetadataViewSet, @@ -30,6 +31,7 @@ 'CollectionVersionViewSet', 'CollectionVersionDocsViewSet', 'CollectionVersionMoveViewSet', + 'CollectionSignViewSet', 'NamespaceViewSet', 'SyncConfigViewSet', 'TaskViewSet', diff --git a/galaxy_ng/app/api/v3/viewsets/collection.py b/galaxy_ng/app/api/v3/viewsets/collection.py index 780baa76d9..5999a35aaf 100644 --- a/galaxy_ng/app/api/v3/viewsets/collection.py +++ b/galaxy_ng/app/api/v3/viewsets/collection.py @@ -12,6 +12,7 @@ from pulp_ansible.app.models import AnsibleDistribution from pulp_ansible.app.models import CollectionImport as PulpCollectionImport from pulp_ansible.app.models import CollectionVersion +from pulpcore.plugin.models import SigningService from pulpcore.plugin.models import Task from pulpcore.plugin.serializers import AsyncOperationResponseSerializer from pulpcore.plugin.tasking import dispatch @@ -36,14 +37,14 @@ from galaxy_ng.app.common.parsers import AnsibleGalaxy29MultiPartParser from galaxy_ng.app.constants import INBOUND_REPO_NAME_FORMAT, DeploymentMode from galaxy_ng.app.tasks import ( - call_copy_task, - call_remove_task, curate_all_synclist_repository, delete_collection, delete_collection_version, import_and_auto_approve, import_and_move_to_staging, ) +from galaxy_ng.app.tasks.signing import call_sign_and_move_task, call_sign_task +from galaxy_ng.app.tasks.promotion import call_move_content_task log = logging.getLogger(__name__) @@ -411,6 +412,119 @@ def get(self, request, *args, **kwargs): return redirect(distribution.content_guard.cast().preauthenticate_url(url)) +class CollectionSignViewSet(api_base.ViewSet): + permission_classes = [access_policy.CollectionAccessPolicy] + + def sign(self, request, *args, **kwargs): + """Creates a signature for the content units specified in the request. + + The request body should contain a JSON object with the following keys: + + # Required + - signing_service: The name of the signing service to use + - repository: The name of the repository to add the signatures + + # Optional + - content_units: A list of content units UUIDS to be signed. + (if content_units is ["*"], all units under the repo will be signed) + OR + - namespace: Namespace name + (if only namespace is specified, all collections under that namespace will be signed) + + # Optional (one or more) + - collection: Collection name + (if collection name is added, all versions under that collection will be signed) + - version: The version of the collection to sign + (if version is specified, only that version will be signed) + """ + + signing_service = self.get_signing_service(request) + repository = self.get_repository(request) + content_units = self.get_content_units_to_sign(request, repository) + + sign_task = call_sign_task( + signing_service, + repository, + content_units, + ) + + return Response( + data={"task_id": sign_task.pk}, + status=status.HTTP_202_ACCEPTED + ) + + def get_content_units_to_sign(self, request, repository): + """ + Returns a list of content units to sign. + + If `content_units` is specified in the request, it will be used. + Otherwise, will use the filtering options specified in the request. + + namespace, collection, version can be used to filter the content units. + """ + if request.data.get('content_units'): + return request.data['content_units'] + else: + try: + namespace = request.data['namespace'] + except KeyError: + raise ValidationError( + _('Missing required field: namespace') + ) + + query_params = { + "pulp_type": "ansible.collection_version", + "ansible_collectionversion__namespace": namespace, + } + + if request.data.get('collection'): + query_params['ansible_collectionversion__name'] = request.data['collection'] + if request.data.get('version'): + query_params['ansible_collectionversion__version'] = request.data['version'] + + content_units = repository.content.filter(**query_params).values_list('pk', flat=True) + if not content_units: + raise ValidationError( + _('No content units found for: %s') % query_params + ) + + return [str(item) for item in content_units] + + def get_repository(self, request): + """ + Retrieves the repository object from the request. + + :param request: the request object + :return: the repository object + """ + try: + return AnsibleDistribution.objects.get( + base_path=request.data["repository"] + ).repository + except KeyError: + raise ValidationError( + _("repository field is required.") + ) + except ObjectDoesNotExist: + raise ValidationError( + _("Repository %s does not exist.") % request.data["repository"] + ) + + def get_signing_service(self, request): + try: + return SigningService.objects.get( + name=request.data['signing_service'] + ) + except KeyError: + raise ValidationError( + _('signing_service field is required.') + ) + except ObjectDoesNotExist: + raise ValidationError( + _('Signing service "%s" does not exist.') % request.data['signing_service'] + ) + + class CollectionVersionMoveViewSet(api_base.ViewSet): permission_classes = [access_policy.CollectionAccessPolicy] @@ -447,10 +561,37 @@ def move_content(self, request, *args, **kwargs): if collection_version in dest_versions: raise NotFound(_('Collection %s already found in destination repo') % version_str) - copy_task = call_copy_task(collection_version, src_repo, dest_repo) - remove_task = call_remove_task(collection_version, src_repo) + response_data = {} + + published_path = settings.GALAXY_API_DEFAULT_DISTRIBUTION_BASE_PATH + auto_sign = settings.get("GALAXY_AUTO_SIGN_COLLECTIONS", False) + + if auto_sign and dest_repo.name == published_path: + # Assumed that if user has access to modify the repo, they can also sign the content + # so we don't need to check access policies here. + signing_service_name = settings.get( + "GALAXY_COLLECTION_SIGNING_SERVICE", "ansible-default" + ) + try: + signing_service = SigningService.objects.get(name=signing_service_name) + except ObjectDoesNotExist: + raise NotFound(_('Signing %s service not found') % signing_service_name) + + move_task = call_sign_and_move_task( + signing_service, + collection_version, + src_repo, + dest_repo, + ) + else: + move_task = call_move_content_task( + collection_version, + src_repo, + dest_repo, + ) + + response_data['copy_task_id'] = response_data['remove_task_id'] = move_task.pk - curate_task_id = None if settings.GALAXY_DEPLOYMENT_MODE == DeploymentMode.INSIGHTS.value: golden_repo = AnsibleDistribution.objects.get( base_path=settings.GALAXY_API_DEFAULT_DISTRIBUTION_BASE_PATH @@ -466,15 +607,8 @@ def move_content(self, request, *args, **kwargs): curate_all_synclist_repository, exclusive_resources=locks, args=task_args, - kwargs=task_kwargs + kwargs=task_kwargs, ) - curate_task_id = curate_task.pk + response_data['curate_all_synclist_repository_task_id'] = curate_task.pk - return Response( - data={ - 'copy_task_id': copy_task.pk, - 'remove_task_id': remove_task.pk, - "curate_all_synclist_repository_task_id": curate_task_id, - }, - status='202' - ) + return Response(data=response_data, status='202') diff --git a/galaxy_ng/app/dynaconf_hooks.py b/galaxy_ng/app/dynaconf_hooks.py index d3f89fd1d2..60b0dde115 100644 --- a/galaxy_ng/app/dynaconf_hooks.py +++ b/galaxy_ng/app/dynaconf_hooks.py @@ -19,6 +19,7 @@ def post(settings: Dynaconf) -> Dict[str, Any]: data.update(configure_logging(settings)) data.update(configure_keycloak(settings)) data.update(configure_cors(settings)) + data.update(configure_feature_flags(settings)) return data @@ -268,3 +269,13 @@ def configure_cors(settings: Dynaconf) -> Dict[str, Any]: corsmiddleware = ["galaxy_ng.app.common.openapi.AllowCorsMiddleware"] data["MIDDLEWARE"] = corsmiddleware + settings.get("MIDDLEWARE", []) return data + + +def configure_feature_flags(settings: Dynaconf) -> Dict[str, Any]: + """Adds conditional feature flags""" + data = {} + data["GALAXY_FEATURE_FLAGS__collection_signing"] = settings.get( + "GALAXY_COLLECTION_SIGNING_SERVICE") is not None + data["GALAXY_FEATURE_FLAGS__collection_auto_sign"] = settings.get( + "GALAXY_AUTO_SIGN_COLLECTIONS", False) + return data diff --git a/galaxy_ng/app/tasks/__init__.py b/galaxy_ng/app/tasks/__init__.py index 3c41fa64af..672196f9c3 100644 --- a/galaxy_ng/app/tasks/__init__.py +++ b/galaxy_ng/app/tasks/__init__.py @@ -1,8 +1,9 @@ from .registry_sync import launch_container_remote_sync, sync_all_repos_in_registry # noqa: F401 from .deletion import delete_collection, delete_collection_version # noqa: F401 -from .promotion import call_copy_task, call_remove_task # noqa: F401 +from .promotion import call_move_content_task # noqa: F401 from .publishing import import_and_auto_approve, import_and_move_to_staging # noqa: F401 from .synclist import curate_all_synclist_repository, curate_synclist_repository # noqa: F401 +from .signing import call_sign_and_move_task, call_sign_task # noqa: F401 from .index_registry import index_execution_environments_from_redhat_registry # noqa: F401 # from .synchronizing import synchronize # noqa diff --git a/galaxy_ng/app/tasks/promotion.py b/galaxy_ng/app/tasks/promotion.py index 0023e65cd9..56bea550cb 100644 --- a/galaxy_ng/app/tasks/promotion.py +++ b/galaxy_ng/app/tasks/promotion.py @@ -1,43 +1,66 @@ from pulpcore.plugin.tasking import dispatch -from pulp_ansible.app.models import AnsibleRepository, CollectionVersion +from pulp_ansible.app.models import ( + AnsibleRepository, + CollectionVersion, + CollectionVersionSignature +) from pulp_ansible.app.tasks.copy import copy_content -def call_copy_task(collection_version, source_repo, dest_repo): - """Calls pulp_ansible task to copy content from source to destination repo.""" - locks = [source_repo, dest_repo] +def move_content(collection_version_pk, source_repo_pk, dest_repo_pk): + """Move collection version from one repository to another""" + # Copy to the destination repo including the content signatures + source_repo = AnsibleRepository.objects.get(pk=source_repo_pk) + signatures = CollectionVersionSignature.objects.filter( + signed_collection=collection_version_pk, + pk__in=source_repo.content.values_list("pk", flat=True) + ).values_list("pk", flat=True) + content = [collection_version_pk] + if signatures: + content += signatures + config = [{ 'source_repo_version': source_repo.latest_version().pk, - 'dest_repo': dest_repo.pk, - 'content': [collection_version.pk], + 'dest_repo': dest_repo_pk, + 'content': content, }] - return dispatch( - copy_content, - args=[config], - kwargs={}, - exclusive_resources=locks, - ) + copy_content(config) + + # remove old content from source repo + _remove_content_from_repository(collection_version_pk, source_repo_pk, signatures) -def call_remove_task(collection_version, repository): - """Calls task to remove content from repo.""" - remove_task_args = (collection_version.pk, repository.pk) + +def call_move_content_task(collection_version, source_repo, dest_repo): + """Dispatches the move content task + + This is a wrapper to group copy_content and remove_content tasks + because those 2 must run in sequence ensuring the same locks. + + """ return dispatch( - _remove_content_from_repository, - args=remove_task_args, - kwargs={}, - exclusive_resources=[repository], + move_content, + exclusive_resources=[source_repo, dest_repo], + kwargs=dict( + collection_version_pk=collection_version.pk, + source_repo_pk=source_repo.pk, + dest_repo_pk=dest_repo.pk + ) ) -def _remove_content_from_repository(collection_version_pk, repository_pk): +def _remove_content_from_repository(collection_version_pk, repository_pk, signatures_pk=None): """ Remove a CollectionVersion from a repository. Args: collection_version_pk: The pk of the CollectionVersion to remove from repository. repository_pk: The pk of the AnsibleRepository to remove the CollectionVersion from. + signatures_pk: A list of pks of the CollectionVersionSignatures to remove from the repo. """ repository = AnsibleRepository.objects.get(pk=repository_pk) qs = CollectionVersion.objects.filter(pk=collection_version_pk) with repository.new_version() as new_version: new_version.remove_content(qs) + if signatures_pk: + sigs = CollectionVersionSignature.objects.filter(pk__in=signatures_pk) + new_version.remove_content(sigs) diff --git a/galaxy_ng/app/tasks/publishing.py b/galaxy_ng/app/tasks/publishing.py index ac7f8eeeba..ac082f15d5 100644 --- a/galaxy_ng/app/tasks/publishing.py +++ b/galaxy_ng/app/tasks/publishing.py @@ -6,13 +6,17 @@ from pulp_ansible.app.models import AnsibleDistribution, AnsibleRepository, CollectionVersion from pulp_ansible.app.tasks.collections import import_collection from pulpcore.plugin.models import Task +from pulpcore.plugin.models import SigningService -from .promotion import call_copy_task, call_remove_task +from .promotion import call_move_content_task +from .signing import call_sign_and_move_task log = logging.getLogger(__name__) GOLDEN_NAME = settings.GALAXY_API_DEFAULT_DISTRIBUTION_BASE_PATH STAGING_NAME = settings.GALAXY_API_STAGING_DISTRIBUTION_BASE_PATH +AUTO_SIGN = settings.get("GALAXY_AUTO_SIGN_COLLECTIONS", False) +SIGNING_SERVICE_NAME = settings.get("GALAXY_COLLECTION_SIGNING_SERVICE", "ansible-default") def get_created_collection_versions(): @@ -57,8 +61,7 @@ def import_and_move_to_staging(temp_file_pk, **kwargs): created_collection_versions = get_created_collection_versions() for collection_version in created_collection_versions: - call_copy_task(collection_version, inbound_repo, staging_repo) - call_remove_task(collection_version, inbound_repo) + call_move_content_task(collection_version, inbound_repo, staging_repo) if settings.GALAXY_ENABLE_API_ACCESS_LOG: _log_collection_upload( @@ -94,22 +97,43 @@ def import_and_auto_approve(temp_file_pk, **kwargs): created_collection_versions = get_created_collection_versions() - for collection_version in created_collection_versions: - call_copy_task(collection_version, inbound_repo, golden_repo) - call_remove_task(collection_version, inbound_repo) + if AUTO_SIGN: - log.info('Imported and auto approved collection artifact %s to repository %s', - collection_version.relative_path, - golden_repo.latest_version()) + try: + signing_service = SigningService.objects.get(name=SIGNING_SERVICE_NAME) + except SigningService.DoesNotExist: + raise RuntimeError(_('Signing %s service not found') % SIGNING_SERVICE_NAME) - if settings.GALAXY_ENABLE_API_ACCESS_LOG: - _log_collection_upload( - kwargs["username"], - kwargs["expected_namespace"], - kwargs["expected_name"], - kwargs["expected_version"] + for collection_version in created_collection_versions: + call_sign_and_move_task( + signing_service, + collection_version, + inbound_repo, + golden_repo, + ) + else: + + for collection_version in created_collection_versions: + call_move_content_task( + collection_version, + inbound_repo, + golden_repo ) + log.info( + 'Imported and auto approved collection artifact %s to repository %s', + collection_version.relative_path, + golden_repo.latest_version() + ) + + if settings.GALAXY_ENABLE_API_ACCESS_LOG: + _log_collection_upload( + kwargs["username"], + kwargs["expected_namespace"], + kwargs["expected_name"], + kwargs["expected_version"] + ) + def _log_collection_upload(username, namespace, name, version): api_access_log = logging.getLogger("automated_logging") diff --git a/galaxy_ng/app/tasks/signing.py b/galaxy_ng/app/tasks/signing.py new file mode 100644 index 0000000000..ec47ee4035 --- /dev/null +++ b/galaxy_ng/app/tasks/signing.py @@ -0,0 +1,93 @@ + +import logging +from pulpcore.plugin.tasking import dispatch +from pulp_ansible.app.tasks.signature import sign +from pulp_ansible.app.tasks.copy import copy_content +from pulp_ansible.app.models import AnsibleRepository, CollectionVersionSignature + +from .promotion import _remove_content_from_repository + +log = logging.getLogger(__name__) + + +def sign_and_move(signing_service_pk, collection_version_pk, source_repo_pk, dest_repo_pk): + """Sign collection version and then copy to the destination repo""" + # Sign while in the source repository + sign( + repository_href=source_repo_pk, + content_hrefs=[collection_version_pk], + signing_service_href=signing_service_pk + ) + + # Then copy to the destination repo including the new signature + source_repo = AnsibleRepository.objects.get(pk=source_repo_pk) + signatures = CollectionVersionSignature.objects.filter( + signed_collection=collection_version_pk, + pk__in=source_repo.content.values_list("pk", flat=True) + ).values_list("pk", flat=True) + content = [collection_version_pk] + if signatures: + content += signatures + config = [{ + 'source_repo_version': source_repo.latest_version().pk, + 'dest_repo': dest_repo_pk, + 'content': content, + }] + copy_content(config) + + # remove old content from source repo + _remove_content_from_repository(collection_version_pk, source_repo_pk, signatures) + + +def call_sign_and_move_task(signing_service, collection_version, source_repo, dest_repo): + """Dispatches sign and move task + + + This is a wrapper to group sign, copy_content and remove_content tasks + because those 3 must run in sequence ensuring the same locks. + """ + log.info( + 'Signing with `%s` and moving collection version `%s` from `%s` to `%s`', + signing_service.name, + collection_version.pk, + source_repo.name, + dest_repo.name + ) + + return dispatch( + sign_and_move, + exclusive_resources=[source_repo, dest_repo], + kwargs=dict( + signing_service_pk=signing_service.pk, + collection_version_pk=collection_version.pk, + source_repo_pk=source_repo.pk, + dest_repo_pk=dest_repo.pk + ) + ) + + +def call_sign_task(signing_service, repository, content_units): + """Calls task to sign collection content. + + signing_service: Instance of SigningService + repository: Instance of AnsibleRepository + content_units: List of content units UUIDS to sign or '*' + to sign all content units under repo + + """ + log.info( + 'Signing on-demand with `%s` on repository `%s` content `%s`', + signing_service.name, + repository.name, + content_units + ) + + return dispatch( + sign, + exclusive_resources=[repository], + kwargs=dict( + repository_href=repository.pk, + content_hrefs=content_units, + signing_service_href=signing_service.pk + ) + ) diff --git a/galaxy_ng/tests/assets/sign-metadata.sh b/galaxy_ng/tests/assets/sign-metadata.sh new file mode 100644 index 0000000000..f8ba8d2646 --- /dev/null +++ b/galaxy_ng/tests/assets/sign-metadata.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +FILE_PATH=$1 +SIGNATURE_PATH="$1.asc" + +GPG_KEY_ID="Pulp QE" + +# Create a detached signature +gpg --quiet --batch --homedir ~/.gnupg/ --detach-sign --local-user "${GPG_KEY_ID}" \ + --armor --output ${SIGNATURE_PATH} ${FILE_PATH} + +# Check the exit status +STATUS=$? +if [[ ${STATUS} -eq 0 ]]; then + echo {\"file\": \"${FILE_PATH}\", \"signature\": \"${SIGNATURE_PATH}\"} +else + exit ${STATUS} +fi \ No newline at end of file diff --git a/galaxy_ng/tests/functional/api/test_collection_signatures.py b/galaxy_ng/tests/functional/api/test_collection_signatures.py new file mode 100644 index 0000000000..5e2e2b630f --- /dev/null +++ b/galaxy_ng/tests/functional/api/test_collection_signatures.py @@ -0,0 +1,129 @@ +"""Tests functionality around Collection-Version Signatures.""" +from pulp_smash.pulp3.bindings import delete_orphans, monitor_task +from pulp_ansible.tests.functional.utils import ( + create_signing_service, + delete_signing_service, + gen_repo, + gen_ansible_remote, + get_content, + SyncHelpersMixin, + TestCaseUsingBindings, + skip_if, +) +from pulp_ansible.tests.functional.constants import TEST_COLLECTION_CONFIGS +from orionutils.generator import build_collection +from pulpcore.client.pulp_ansible import AnsibleCollectionsApi, ContentCollectionSignaturesApi +from pulp_ansible.tests.functional.utils import set_up_module as setUpModule # noqa:F401 + + +class CRUDCollectionVersionSignatures(TestCaseUsingBindings, SyncHelpersMixin): + """ + CRUD CollectionVersionSignatures + + This test targets the following issues: + + * `Pulp #757 `_ + * `Pulp #758 `_ + """ + + @classmethod + def setUpClass(cls): + """Sets up signing service used for creating signatures.""" + super().setUpClass() + delete_orphans() + cls.signing_service = create_signing_service() + cls.collections = [] + cls.signed_collections = [] + cls.repo = {} + cls.sig_api = ContentCollectionSignaturesApi(cls.client) + col_api = AnsibleCollectionsApi(cls.client) + for i in range(4): + collection = build_collection("skeleton", config=TEST_COLLECTION_CONFIGS[i]) + response = col_api.upload_collection(collection.filename) + task = monitor_task(response.task) + cls.collections.append(task.created_resources[0]) + + @classmethod + def tearDownClass(cls): + """Deletes repository and removes any content and signatures.""" + if cls.repo: + monitor_task(cls.repo_api.delete(cls.repo["pulp_href"]).task) + delete_signing_service(cls.signing_service.name) + delete_orphans() + + # def test_01_create_signed_collections(self): + # """Test collection signatures can be created through the sign task.""" + # repo = self.repo_api.create(gen_repo()) + # body = {"add_content_units": self.collections} + # monitor_task(self.repo_api.modify(repo.pulp_href, body).task) + + # body = {"content_units": self.collections, "signing_service": self.signing_service.pulp_href} + # monitor_task(self.repo_api.sign(repo.pulp_href, body).task) + # repo = self.repo_api.read(repo.pulp_href) + # self.repo.update(repo.to_dict()) + + # self.assertEqual(int(repo.latest_version_href[-2]), 2) + # content_response = get_content(self.repo) + # self.assertIn("ansible.collection_signature", content_response) + # self.assertEqual(len(content_response["ansible.collection_signature"]), 4) + # self.signed_collections.extend(content_response["ansible.collection_signature"]) + + @skip_if(bool, "signed_collections", False) + def test_02_read_signed_collection(self): + """Test that a collection's signature can be read.""" + signature = self.sig_api.read(self.signed_collections[0]["pulp_href"]) + self.assertIn(signature.signed_collection, self.collections) + self.assertEqual(signature.signing_service, self.signing_service.pulp_href) + + @skip_if(bool, "signed_collections", False) + def test_03_read_signed_collections(self): + """Test that collection signatures can be listed.""" + signatures = self.sig_api.list(repository_version=self.repo["latest_version_href"]) + self.assertEqual(signatures.count, len(self.signed_collections)) + signature_set = set([s.pulp_href for s in signatures.results]) + self.assertEqual(signature_set, {s["pulp_href"] for s in self.signed_collections}) + + @skip_if(bool, "signed_collections", False) + def test_04_partially_update(self): + """Attempt to update a content unit using HTTP PATCH. + + This HTTP method is not supported and a HTTP exception is expected. + """ + attrs = {"pubkey_fingerprint": "testing"} + with self.assertRaises(AttributeError) as exc: + self.sig_api.partial_update(self.signed_collections[0], attrs) + msg = "object has no attribute 'partial_update'" + self.assertIn(msg, exc.exception.args[0]) + + @skip_if(bool, "signed_collections", False) + def test_05_fully_update(self): + """Attempt to update a content unit using HTTP PUT. + + This HTTP method is not supported and a HTTP exception is expected. + """ + attrs = {"pubkey_fingerprint": "testing"} + with self.assertRaises(AttributeError) as exc: + self.sig_api.update(self.signed_collections[0]["pulp_href"], attrs) + msg = "object has no attribute 'update'" + self.assertIn(msg, exc.exception.args[0]) + + @skip_if(bool, "signed_collections", False) + def test_06_delete(self): + """Attempt to delete a content unit using HTTP DELETE. + + This HTTP method is not supported and a HTTP exception is expected. + """ + with self.assertRaises(AttributeError) as exc: + self.sig_api.delete(self.signed_collections[0]["pulp_href"]) + msg = "object has no attribute 'delete'" + self.assertIn(msg, exc.exception.args[0]) + + @skip_if(bool, "signed_collections", False) + def test_07_duplicate(self): + """Attempt to create a signature duplicate""" + body = {"content_units": self.collections, "signing_service": self.signing_service.pulp_href} + result = monitor_task(self.repo_api.sign(self.repo["pulp_href"], body).task) + repo = self.repo_api.read(self.repo["pulp_href"]) + + self.assertEqual(repo.latest_version_href, self.repo["latest_version_href"]) + self.assertEqual(len(result.created_resources), 0) \ No newline at end of file diff --git a/galaxy_ng/tests/unit/app/test_tasks.py b/galaxy_ng/tests/unit/app/test_tasks.py index c7a6d39ad4..d6f56850b4 100644 --- a/galaxy_ng/tests/unit/app/test_tasks.py +++ b/galaxy_ng/tests/unit/app/test_tasks.py @@ -121,7 +121,7 @@ def test_import_and_auto_approve(self, mocked_dispatch, mocked_import, mocked_ge ) self.assertTrue(mocked_import.call_count == 1) - self.assertTrue(mocked_dispatch.call_count == 2) + self.assertTrue(mocked_dispatch.call_count == 1) # test cannot find golden repo golden_repo.name = 'a_different_name_for_golden' @@ -161,7 +161,7 @@ def test_import_and_move_to_staging(self, mocked_dispatch, mocked_import, mocked ) self.assertTrue(mocked_import.call_count == 1) - self.assertTrue(mocked_dispatch.call_count == 2) + self.assertTrue(mocked_dispatch.call_count == 1) # test cannot find staging repo staging_repo.name = 'a_different_name_for_staging' diff --git a/openshift/clowder/clowd-app.yaml b/openshift/clowder/clowd-app.yaml index 7ac20fccba..6a378c628e 100644 --- a/openshift/clowder/clowd-app.yaml +++ b/openshift/clowder/clowd-app.yaml @@ -13,6 +13,23 @@ objects: database_fields.symmetric.key: | RE5tTmR3Z3ladWdUYXg5UzY0SjBGSVRUcjlJSFB4YnVvRjFGMUNHUHI2OD0= +- apiVersion: v1 + kind: Secret + metadata: + name: signing-gpg-key + namespace: automation-hub + data: + ansible-sign.key: | + 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 +- apiVersion: v1 + kind: Secret + metadata: + name: signing-script + namespace: automation-hub + data: + collection_sign.sh: | + IyEvYmluL2Jhc2gKCkZJTEVfUEFUSD0kMQpTSUdOQVRVUkVfUEFUSD0iJDEuYXNjIgoKQURNSU5fSUQ9ImdhbGF4eTNAYW5zaWJsZS5jb20iClBBU1NXT1JEPSJHYWxheHkyMDIyIgoKIyBDcmVhdGUgYSBkZXRhY2hlZCBzaWduYXR1cmUKZ3BnIC0tcXVpZXQgLS1iYXRjaCAtLXBpbmVudHJ5LW1vZGUgbG9vcGJhY2sgLS15ZXMgLS1wYXNzcGhyYXNlICAgJFBBU1NXT1JEIC0taG9tZWRpciAvdG1wL2Fuc2libGUvLmdudXBnIC0tZGV0YWNoLXNpZ24gLS1kZWZhdWx0LWtleSAkQURNSU5fSUQgICAtLWFybW9yIC0tb3V0cHV0ICRTSUdOQVRVUkVfUEFUSCAkRklMRV9QQVRICgojIENoZWNrIHRoZSBleGl0IHN0YXR1cwpTVEFUVVM9JD8KaWYgWyAkU1RBVFVTIC1lcSAwIF07IHRoZW4KICBlY2hvIHtcImZpbGVcIjogXCIkRklMRV9QQVRIXCIsIFwic2lnbmF0dXJlXCI6IFwiJFNJR05BVFVSRV9QQVRIXCJ9CmVsc2UKICBleGl0ICRTVEFUVVMKZmkK + - apiVersion: v1 kind: ConfigMap metadata: @@ -99,14 +116,32 @@ objects: value: ${REDIS_SSL} - name: prometheus_multiproc_dir value: ${PROMETHEUS_DIR} + - name: ENABLE_SIGNING + value: "1" + - name: GNUPGHOME + value: '/tmp/ansible/.gnupg' volumeMounts: - name: pulp-key mountPath: /etc/pulp/certs readOnly: true + - name: signing-gpg-key + mountPath: /tmp/ansible-sign.key + subPath: ansible-sign.key + readOnly: true + - name: signing-script + mountPath: /var/lib/pulp/scripts + readOnly: true volumes: - name: pulp-key secret: secretName: pulp-key + - name: signing-gpg-key + secret: + secretName: signing-gpg-key + - name: signing-script + secret: + defaultMode: 0555 + secretName: signing-script webServices: public: enabled: true @@ -135,14 +170,32 @@ objects: value: '10000' - name: PULP_REDIS_SSL value: ${REDIS_SSL} + - name: ENABLE_SIGNING + value: "1" + - name: GNUPGHOME + value: '/tmp/ansible/.gnupg' volumeMounts: - name: pulp-key mountPath: /etc/pulp/certs readOnly: true + - name: signing-gpg-key + mountPath: /tmp/ansible-sign.key + subPath: ansible-sign.key + readOnly: true + - name: signing-script + mountPath: /var/lib/pulp/scripts + readOnly: true volumes: - name: pulp-key secret: secretName: pulp-key + - name: signing-gpg-key + secret: + secretName: signing-gpg-key + - name: signing-script + secret: + defaultMode: 0555 + secretName: signing-script webServices: private: enabled: true @@ -184,12 +237,23 @@ objects: value: 1000m - name: PULP_REDIS_SSL value: ${REDIS_SSL} + - name: ENABLE_SIGNING + value: "1" + - name: GNUPGHOME + value: '/tmp/ansible/.gnupg' volumeMounts: - name: importer-config mountPath: /etc/galaxy-importer - name: pulp-key mountPath: /etc/pulp/certs readOnly: true + - name: signing-gpg-key + mountPath: /tmp/ansible-sign.key + subPath: ansible-sign.key + readOnly: true + - name: signing-script + mountPath: /var/lib/pulp/scripts + readOnly: true volumes: - name: importer-config configMap: @@ -197,6 +261,13 @@ objects: - name: pulp-key secret: secretName: pulp-key + - name: signing-gpg-key + secret: + secretName: signing-gpg-key + - name: signing-script + secret: + defaultMode: 0555 + secretName: signing-script # Creates a database if local mode, or uses RDS in production database: diff --git a/setup.py b/setup.py index 6d7a448f48..fa02c8f35b 100644 --- a/setup.py +++ b/setup.py @@ -90,7 +90,8 @@ def run(self): requirements = [ "galaxy-importer==0.4.2", "pulpcore>=3.17.3,<3.18.0", - "pulp-ansible>=0.11.1,<0.12.0", + # "pulp-ansible>=0.11.1,<0.12.0", + "pulp_ansible", "django-prometheus>=2.0.0", "drf-spectacular", "pulp-container>=2.10.0,<2.11.0",