diff --git a/CHANGES/1278.misc b/CHANGES/1278.misc new file mode 100644 index 0000000000..b2fb444362 --- /dev/null +++ b/CHANGES/1278.misc @@ -0,0 +1 @@ +Add landing page endpoint for the console.redhat diff --git a/galaxy_ng/app/access_control/access_policy.py b/galaxy_ng/app/access_control/access_policy.py index 053c3af240..2f3f1bd0d8 100644 --- a/galaxy_ng/app/access_control/access_policy.py +++ b/galaxy_ng/app/access_control/access_policy.py @@ -25,9 +25,10 @@ def galaxy_statements(self): # import here to avoid working outside django/dynaconf settings context from galaxy_ng.app.access_control.statements import STANDALONE_STATEMENTS # noqa from galaxy_ng.app.access_control.statements import INSIGHTS_STATEMENTS # noqa + self._STATEMENTS = { - 'insights': INSIGHTS_STATEMENTS, - 'standalone': STANDALONE_STATEMENTS + "insights": INSIGHTS_STATEMENTS, + "standalone": STANDALONE_STATEMENTS, } return self._STATEMENTS @@ -43,7 +44,7 @@ def _get_rh_identity(self, request): log.debug("No request rh_identity request.auth found for request %s", request) return False - x_rh_identity = request.auth.get('rh_identity') + x_rh_identity = request.auth.get("rh_identity") if not x_rh_identity: return False @@ -55,21 +56,24 @@ def has_rh_entitlements(self, request, view, permission): x_rh_identity = self._get_rh_identity(request) if not x_rh_identity: - log.debug("No x_rh_identity found when check entitlements for request %s for view %s", - request, view) + log.debug( + "No x_rh_identity found when check entitlements for request %s for view %s", + request, + view, + ) return False - entitlements = x_rh_identity.get('entitlements', {}) + entitlements = x_rh_identity.get("entitlements", {}) entitlement = entitlements.get(settings.RH_ENTITLEMENT_REQUIRED, {}) - return entitlement.get('is_entitled', False) + return entitlement.get("is_entitled", False) class NamespaceAccessPolicy(UnauthenticatedCollectionAccessMixin, AccessPolicyBase): - NAME = 'NamespaceViewSet' + NAME = "NamespaceViewSet" class CollectionAccessPolicy(UnauthenticatedCollectionAccessMixin, AccessPolicyBase): - NAME = 'CollectionViewSet' + NAME = "CollectionViewSet" def can_update_collection(self, request, view, permission): if getattr(self, "swagger_fake_view", False): @@ -77,26 +81,26 @@ 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 request.user.has_perm("galaxy.upload_to_namespace", namespace) def can_create_collection(self, request, view, permission): data = view._get_data(request) try: - namespace = models.Namespace.objects.get(name=data['filename'].namespace) + 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) + raise NotFound(_("Namespace in filename not found.")) + return request.user.has_perm("galaxy.upload_to_namespace", namespace) def unauthenticated_collection_download_enabled(self, request, view, permission): return settings.GALAXY_ENABLE_UNAUTHENTICATED_COLLECTION_DOWNLOAD class CollectionRemoteAccessPolicy(AccessPolicyBase): - NAME = 'CollectionRemoteViewSet' + NAME = "CollectionRemoteViewSet" class UserAccessPolicy(AccessPolicyBase): - NAME = 'UserViewSet' + NAME = "UserViewSet" def user_is_superuser(self, request, view, action): if getattr(self, "swagger_fake_view", False): @@ -113,88 +117,88 @@ def is_current_user(self, request, view, action): class MyUserAccessPolicy(UnauthenticatedCollectionAccessMixin, AccessPolicyBase): - NAME = 'MyUserViewSet' + NAME = "MyUserViewSet" def is_current_user(self, request, view, action): return request.user == view.get_object() class SyncListAccessPolicy(AccessPolicyBase): - NAME = 'SyncListViewSet' + NAME = "SyncListViewSet" class MySyncListAccessPolicy(AccessPolicyBase): - NAME = 'MySyncListViewSet' + NAME = "MySyncListViewSet" def is_org_admin(self, request, view, permission): """Check the rhn_entitlement data to see if user is an org admin""" x_rh_identity = self._get_rh_identity(request) if not x_rh_identity: - log.debug("No x_rh_identity found for request %s for view %s", - request, view) + log.debug("No x_rh_identity found for request %s for view %s", request, view) return False - identity = x_rh_identity['identity'] - user = identity['user'] - return user.get('is_org_admin', False) + identity = x_rh_identity["identity"] + user = identity["user"] + return user.get("is_org_admin", False) class TagsAccessPolicy(AccessPolicyBase): - NAME = 'TagViewSet' + NAME = "TagViewSet" class TaskAccessPolicy(AccessPolicyBase): - NAME = 'TaskViewSet' + NAME = "TaskViewSet" class LoginAccessPolicy(AccessPolicyBase): - NAME = 'LoginView' + NAME = "LoginView" class LogoutAccessPolicy(AccessPolicyBase): - NAME = 'LogoutView' + NAME = "LogoutView" class TokenAccessPolicy(AccessPolicyBase): - NAME = 'TokenView' + NAME = "TokenView" class GroupAccessPolicy(AccessPolicyBase): - NAME = 'GroupViewSet' + NAME = "GroupViewSet" class DistributionAccessPolicy(AccessPolicyBase): - NAME = 'DistributionViewSet' + NAME = "DistributionViewSet" class MyDistributionAccessPolicy(AccessPolicyBase): - NAME = 'MyDistributionViewSet' + NAME = "MyDistributionViewSet" class ContainerRepositoryAccessPolicy(AccessPolicyBase): - NAME = 'ContainerRepositoryViewSet' + NAME = "ContainerRepositoryViewSet" class ContainerReadmeAccessPolicy(AccessPolicyBase): - NAME = 'ContainerReadmeViewset' + NAME = "ContainerReadmeViewset" 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 request.user.has_perm(permission) or request.user.has_perm( + permission, readme.container.namespace + ) class ContainerNamespaceAccessPolicy(AccessPolicyBase): - NAME = 'ContainerNamespaceViewset' + NAME = "ContainerNamespaceViewset" class ContainerRegistryRemoteAccessPolicy(AccessPolicyBase): - NAME = 'ContainerRegistryRemoteViewSet' + NAME = "ContainerRegistryRemoteViewSet" class ContainerRemoteAccessPolicy(AccessPolicyBase, NamespacedAccessPolicyMixin): - NAME = 'ContainerRemoteViewSet' + NAME = "ContainerRemoteViewSet" def has_distro_permission(self, request, view, action, permission): class FakeView: @@ -218,3 +222,7 @@ def get_object(self): return True return False + + +class LandingPageAccessPolicy(AccessPolicyBase): + NAME = "LandingPageViewSet" diff --git a/galaxy_ng/app/access_control/statements/insights.py b/galaxy_ng/app/access_control/statements/insights.py index afc9fcfe60..ebba6d57ce 100644 --- a/galaxy_ng/app/access_control/statements/insights.py +++ b/galaxy_ng/app/access_control/statements/insights.py @@ -1,5 +1,5 @@ INSIGHTS_STATEMENTS = { - 'NamespaceViewSet': [ + "NamespaceViewSet": [ { "action": ["list", "retrieve"], "principal": "authenticated", @@ -10,24 +10,22 @@ "action": "create", "principal": "authenticated", "effect": "allow", - "condition": ["has_model_perms:galaxy.add_namespace", "has_rh_entitlements"] + "condition": ["has_model_perms:galaxy.add_namespace", "has_rh_entitlements"], }, { "action": "destroy", "principal": "authenticated", "effect": "allow", - "condition": ["has_model_or_obj_perms:galaxy.delete_namespace", "has_rh_entitlements"] + "condition": ["has_model_or_obj_perms:galaxy.delete_namespace", "has_rh_entitlements"], }, { "action": "update", "principal": "authenticated", "effect": "allow", - "condition": [ - "has_model_or_obj_perms:galaxy.change_namespace", - "has_rh_entitlements"] + "condition": ["has_model_or_obj_perms:galaxy.change_namespace", "has_rh_entitlements"], }, ], - 'CollectionViewSet': [ + "CollectionViewSet": [ { "action": ["list", "retrieve"], "principal": "authenticated", @@ -45,7 +43,7 @@ }, { "action": ["download"], - "principal": 'authenticated', + "principal": "authenticated", "effect": "allow", "condition": "has_rh_entitlements", }, @@ -53,13 +51,13 @@ "action": "create", "principal": "authenticated", "effect": "allow", - "condition": ["can_create_collection", "has_rh_entitlements"] + "condition": ["can_create_collection", "has_rh_entitlements"], }, { "action": "update", "principal": "authenticated", "effect": "allow", - "condition": "can_update_collection" + "condition": "can_update_collection", }, { "action": "move_content", @@ -67,7 +65,8 @@ "effect": "allow", "condition": [ "has_model_perms:ansible.modify_ansible_repo_content", - "has_rh_entitlements"] + "has_rh_entitlements", + ], }, { "action": "curate", @@ -75,103 +74,89 @@ "effect": "allow", "condition": [ "has_model_perms:ansible.modify_ansible_repo_content", - "has_rh_entitlements"] - } - ], - 'CollectionRemoteViewSet': [ - { - "action": ["list", "retrieve"], - "principal": "authenticated", - "effect": "allow" + "has_rh_entitlements", + ], }, + ], + "CollectionRemoteViewSet": [ + {"action": ["list", "retrieve"], "principal": "authenticated", "effect": "allow"}, { "action": ["sync", "update", "partial_update"], "principal": "authenticated", "effect": "allow", - "condition": "has_model_perms:ansible.change_collectionremote" - } + "condition": "has_model_perms:ansible.change_collectionremote", + }, ], - 'UserViewSet': [ + "UserViewSet": [ { "action": ["*"], "principal": "authenticated", "effect": "deny", }, ], - 'MyUserViewSet': [ + "MyUserViewSet": [ { "action": ["retrieve"], "principal": "authenticated", "effect": "allow", - "condition": "is_current_user" + "condition": "is_current_user", }, ], - 'SyncListViewSet': [ + "SyncListViewSet": [ { "action": ["list"], "principal": "authenticated", "effect": "allow", - "condition": ["has_model_perms:galaxy.view_synclist", - "has_rh_entitlements"] + "condition": ["has_model_perms:galaxy.view_synclist", "has_rh_entitlements"], }, { "action": ["retrieve"], "principal": "authenticated", "effect": "allow", - "condition": [ - "has_model_or_obj_perms:galaxy.view_synclist", - "has_rh_entitlements"] + "condition": ["has_model_or_obj_perms:galaxy.view_synclist", "has_rh_entitlements"], }, { "action": ["destroy"], "principal": "authenticated", "effect": "allow", - "condition": [ - "has_model_perms:galaxy.delete_synclist", - "has_rh_entitlements"] + "condition": ["has_model_perms:galaxy.delete_synclist", "has_rh_entitlements"], }, { "action": ["create"], "principal": "authenticated", "effect": "allow", - "condition": [ - "has_model_perms:galaxy.add_synclist", - "has_rh_entitlements"] + "condition": ["has_model_perms:galaxy.add_synclist", "has_rh_entitlements"], }, { "action": ["update", "partial_update"], "principal": "authenticated", "effect": "allow", - "condition": [ - "has_model_perms:galaxy.change_synclist", - "has_rh_entitlements"] + "condition": ["has_model_perms:galaxy.change_synclist", "has_rh_entitlements"], }, ], - 'MySyncListViewSet': [ + "MySyncListViewSet": [ { "action": ["retrieve", "list", "update", "partial_update", "curate"], "principal": "authenticated", "effect": "allow", - "condition": [ - "has_rh_entitlements", - "is_org_admin"] + "condition": ["has_rh_entitlements", "is_org_admin"], }, ], - 'TaskViewSet': [ + "TaskViewSet": [ { "action": ["list", "retrieve"], "principal": "authenticated", "effect": "allow", }, ], - 'TagViewSet': [ + "TagViewSet": [ { "action": ["list", "retrieve"], "principal": "authenticated", "effect": "allow", }, ], - 'GroupViewSet': [ + "GroupViewSet": [ { "action": ["list", "retrieve"], "principal": "authenticated", @@ -179,14 +164,14 @@ "condition": "has_rh_entitlements", }, ], - 'DistributionViewSet': [ + "DistributionViewSet": [ { "action": ["*"], "principal": "authenticated", "effect": "deny", }, ], - 'MyDistributionViewSet': [ + "MyDistributionViewSet": [ { "action": ["list", "retrieve"], "principal": "authenticated", @@ -194,11 +179,19 @@ "condition": "has_rh_entitlements", }, ], - 'ContainerRepositoryViewSet': [ + "ContainerRepositoryViewSet": [ { "action": ["*"], "principal": "*", "effect": "deny", }, ], + "LandingPageViewSet": [ + { + "action": ["retrieve"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_rh_entitlements", + }, + ], } diff --git a/galaxy_ng/app/api/ui/urls.py b/galaxy_ng/app/api/ui/urls.py index 54d555335b..1e25e5ee63 100644 --- a/galaxy_ng/app/api/ui/urls.py +++ b/galaxy_ng/app/api/ui/urls.py @@ -152,6 +152,7 @@ path('auth/', include(auth_views)), path("settings/", views.SettingsView.as_view(), name="settings"), + path("landing-page/", views.LandingPageView.as_view(), name="landing-page"), path('feature-flags/', views.FeatureFlagsView.as_view(), name='feature-flags'), path('controllers/', views.ControllerListView.as_view(), name='controllers'), path('groups/', include(group_paths)), diff --git a/galaxy_ng/app/api/ui/views/__init__.py b/galaxy_ng/app/api/ui/views/__init__.py index 11482dc3ba..57c4251ab7 100644 --- a/galaxy_ng/app/api/ui/views/__init__.py +++ b/galaxy_ng/app/api/ui/views/__init__.py @@ -7,6 +7,7 @@ from .controller import ControllerListView from .settings import SettingsView +from .landing_page import LandingPageView from .sync import ContainerSyncRemoteView, ContainerSyncRegistryView from .index_execution_environments import IndexRegistryEEView @@ -25,6 +26,9 @@ # settings "SettingsView", + # landing_page + "LandingPageView", + # sync "ContainerSyncRemoteView", "ContainerSyncRegistryView", diff --git a/galaxy_ng/app/api/ui/views/landing_page.py b/galaxy_ng/app/api/ui/views/landing_page.py new file mode 100644 index 0000000000..ccd6e17ba2 --- /dev/null +++ b/galaxy_ng/app/api/ui/views/landing_page.py @@ -0,0 +1,216 @@ +from galaxy_ng.app.access_control import access_policy +from random import sample +from rest_framework.response import Response +from pulp_ansible.app.models import CollectionVersion, AnsibleDistribution +from galaxy_ng.app.models import Namespace +from galaxy_ng.app import settings +from galaxy_ng.app.api import base as api_base + + +class LandingPageView(api_base.APIView): + permission_classes = [access_policy.LandingPageAccessPolicy] + action = "retrieve" + + def get(self, request, *args, **kwargs): + golden_name = settings.GALAXY_API_DEFAULT_DISTRIBUTION_BASE_PATH + + distro = AnsibleDistribution.objects.get(base_path=golden_name) + repository_version = distro.repository.latest_version() + collection_count = CollectionVersion.objects.filter( + pk__in=repository_version.content, is_highest=True + ).count() + + partner_count = Namespace.objects.count() + + # If there are no partners dont show the recommendation for it + recommendations = {} + if partner_count > 0: + namespace = sample(list(Namespace.objects.all()), 1)[0] + recommendations = { + "recs": [ + { + "id": "ansible-partner", + "icon": "bulb", + "action": { + "title": "Check out our partner %s" % namespace.company, + "href": "./ansible/automation-hub/partners/%s" % namespace.name, + }, + "description": "Discover automation from our partners.", + "permissions": [ + { + "method": "isEntitled", + "args": ["ansible"], + } + ] + } + ] + } + + data = { + "estate": { + "items": [ + { + "id": "ansible-collections", + "count": collection_count, + "shape": { + "title": "Collections", + "href": "./ansible/automation-hub/", + }, + "permissions": [ + { + "method": "isEntitled", + "args": ["ansible"], + } + ], + }, + { + "id": "ansible-partners", + "count": partner_count, + "shape": { + "title": "Partners", + "href": "./ansible/automation-hub/partners/", + }, + "permissions": [ + { + "method": "isEntitled", + "args": ["ansible"], + } + ], + }, + ], + }, + "recommendations": recommendations, + "configTryLearn": { + "configure": [ + { + "permissions": [ + { + "method": "isEntitled", + "args": ["ansible"], + } + ], + "shape": { + "title": "Sync Red Hat certified collections", + "description": ( + "Configure access to sync collections ", + "to Private Automation Hub.", + ), + "link": { + "title": "Get started", + "href": "./ansible/automation-hub/token", + }, + }, + } + ], + "try": [ + { + "permissions": [ + { + "method": "isEntitled", + "args": ["ansible"], + } + ], + "shape": { + "title": "Install Private Automation Hub", + "link": { + "title": "Get started", + "external": True, + "href": ( + "https://access.redhat.com/documentation/en-us/" + "red_hat_ansible_automation_platform/2.1/html/" + "red_hat_ansible_automation_platform_installation_guide/index" + ), + }, + }, + }, + { + "permissions": [ + { + "method": "isEntitled", + "args": ["ansible"], + } + ], + "shape": { + "title": "Manage repositories in Private Automation Hub", + "description": ( + "Add community and privately developed collections " + "to your Private Automation Hub." + ), + "link": { + "title": "Get started", + "external": True, + "href": ( + "https://access.redhat.com/documentation/en-us/" + "red_hat_ansible_automation_platform/2.1/html/" + "publishing_proprietary_content_collections_in_" + "automation_hub/index" + ), + }, + }, + }, + ], + "learn": [ + { + "permissions": [ + { + "method": "isEntitled", + "args": ["ansible"], + } + ], + "shape": { + "title": "Connect Automation Hub to your automation infrastructure", + "link": { + "title": "Get started", + "external": True, + "href": ( + "https://docs.ansible.com/ansible-tower/latest/html/userguide/" + "projects.html?extIdCarryOver=true&" + "sc_cid=701f2000001Css5AAC#using-collections-in-tower" + ), + }, + }, + }, + { + "permissions": [ + { + "method": "isEntitled", + "args": ["ansible"], + } + ], + "shape": { + "title": "Learn about namespaces", + "description": ( + "Organize collections content into " "namespaces users can access." + ), + "link": { + "title": "Learn more", + "external": True, + "href": ( + "https://access.redhat.com/documentation" + "/en-us/red_hat_ansible_automation_platform/2.1/html" + "/curating_collections_using_namespaces_in_automation_hub/index" + ), + }, + }, + }, + { + "permissions": [ + { + "method": "isEntitled", + "args": ["ansible"], + } + ], + "shape": { + "title": "Explore Red Hat certified collections", + "link": { + "title": "Learn more", + "external": True, + "href": "https://www.ansible.com/partners", + }, + }, + }, + ], + }, + } + + return Response(data) diff --git a/galaxy_ng/tests/integration/api/test_landing_page.py b/galaxy_ng/tests/integration/api/test_landing_page.py new file mode 100644 index 0000000000..640d181b62 --- /dev/null +++ b/galaxy_ng/tests/integration/api/test_landing_page.py @@ -0,0 +1,36 @@ +"""test_landing_page.py - Test related to landing page endpoint. +""" +import pytest + +from ..utils import get_client + + +@pytest.mark.cloud_only +def test_landing_page(ansible_config): + """Tests whether the landing page returns the expected fields and numbers.""" + + api_client = get_client( + config=ansible_config("ansible_insights"), request_token=True, require_auth=True + ) + + resultsDict = api_client("/api/automation-hub/_ui/v1/landing-page") + + assert resultsDict["estate"]["items"][0] + assert resultsDict["estate"]["items"][0]["shape"]["title"] == "Collections" + assert resultsDict["estate"]["items"][0]["count"] == 0 + + assert resultsDict["estate"]["items"][1] + assert resultsDict["estate"]["items"][1]["shape"]["title"] == "Partners" + assert resultsDict["estate"]["items"][1]["count"] == 2 + + assert resultsDict["recommendations"]['recs'] + assert len(resultsDict["recommendations"]['recs']) == 1 + + assert resultsDict["configTryLearn"]["configure"] + assert len(resultsDict["configTryLearn"]["configure"]) == 1 + + assert resultsDict["configTryLearn"]["try"] + assert len(resultsDict["configTryLearn"]["try"]) == 2 + + assert resultsDict["configTryLearn"]["learn"] + assert len(resultsDict["configTryLearn"]["learn"]) == 3