diff --git a/CHANGELOG.md b/CHANGELOG.md index ba12b7b34..da34a8acb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# 2.5.1 2024-02-08 + +## Fix + +- CSS issue in documentation rendering for
block in dark theme +- Template issue on documentation when applying jinja filters on None +- Remove superusers from "list_approvers". They still can approve but are not displayed in the list. +- When editing an ApprovalWorkflow, it was possible to use a scope already assigned to another workflow. + +## Enhancement + +- List related docs in OperationDetails and ServiceDetails view +- Add PROCESSING requests in the dashboard of the main page +- Display only docs that are not linked to services or operations in ListDoc + +## Feature + +- ApprovalWorkflow preview. +- Provided field validators (json and public ssh key). + # 2.5.0 2024-01-09 ## Fix diff --git a/Squest/utils/squest_model.py b/Squest/utils/squest_model.py index 8b2baf402..9d676b9a0 100644 --- a/Squest/utils/squest_model.py +++ b/Squest/utils/squest_model.py @@ -111,8 +111,7 @@ def get_scopes(self): squest_scope = GlobalScope.load() return squest_scope.get_scopes() - - def who_has_perm(self, permission_str): + def who_has_perm(self, permission_str, exclude_superuser=False): app_label, codename = permission_str.split(".") # Global Perm permission for all users @@ -134,7 +133,11 @@ def who_has_perm(self, permission_str): ) rbacs = rbac0 | rbac1 - return User.objects.filter(Q(groups__in=rbacs) | Q(is_superuser=True)).distinct() + if exclude_superuser is True: + return User.objects.filter(Q(groups__in=rbacs)).distinct() + else: + return User.objects.filter(Q(groups__in=rbacs) | Q(is_superuser=True)).distinct() + class SquestChangelog(Model): class Meta: diff --git a/Squest/version.py b/Squest/version.py index c3c61c86f..037904b86 100644 --- a/Squest/version.py +++ b/Squest/version.py @@ -1,2 +1,2 @@ -__version__ = "2.5.0" +__version__ = "2.5.1" VERSION = __version__ diff --git a/Squest/views.py b/Squest/views.py index 70dd48491..a172eab6f 100644 --- a/Squest/views.py +++ b/Squest/views.py @@ -80,6 +80,9 @@ def home(request): service_dict["hold_requests"] = sum([x["count"] for x in all_requests if x["state"] == RequestState.ON_HOLD and x[ "instance__service"] == service.id]) + service_dict["processing_requests"] = sum([x["count"] for x in all_requests if + x["state"] == RequestState.PROCESSING and x[ + "instance__service"] == service.id]) service_dict["opened_supports"] = sum([x["count"] for x in all_supports if x["state"] == SupportState.OPENED and x[ diff --git a/docs/manual/service_catalog/docs.md b/docs/manual/service_catalog/docs.md index 7461c9758..fa13b59d6 100644 --- a/docs/manual/service_catalog/docs.md +++ b/docs/manual/service_catalog/docs.md @@ -4,6 +4,10 @@ Docs section allow administrators to create and link documentation to Squest ser Documentation are writen with Markdown syntax. +!!!note + + Docs linked to a service or an operation are not listed in the global doc list from the sidebar menu. + ## Linked to services When linked to one or more service, the documentation is shown in each "instance detail" page that correspond to the type of selected services. diff --git a/plugins/field_validators/is_json.py b/plugins/field_validators/is_json.py new file mode 100644 index 000000000..0ddaf80dd --- /dev/null +++ b/plugins/field_validators/is_json.py @@ -0,0 +1,26 @@ +import json + +# For testing +try: + from django.core.exceptions import ValidationError as UIValidationError + from rest_framework.serializers import ValidationError as APIValidationError +except ImportError: + pass + + +def is_json(json_str): + try: + json.loads(json_str) + except ValueError as e: + return False + return True + + +def validate_api(value): + if not is_json(value): + raise APIValidationError("is not JSON") + + +def validate_ui(value): + if not is_json(value): + raise UIValidationError("is not JSON") diff --git a/plugins/field_validators/is_public_ssh_key.py b/plugins/field_validators/is_public_ssh_key.py new file mode 100644 index 000000000..8fb2a61c2 --- /dev/null +++ b/plugins/field_validators/is_public_ssh_key.py @@ -0,0 +1,49 @@ +import base64 +import binascii +import struct + +# For testing +try: + from django.core.exceptions import ValidationError as UIValidationError + from rest_framework.serializers import ValidationError as APIValidationError +except ImportError: + pass + +ERROR_MESSAGE = "Is not a valid public ssh key" + +def is_public_ssh_key(ssh_key): + array = ssh_key.split() + # Each rsa-ssh key has 3 different strings in it, first one being + # type_of_key second one being keystring third one being username. + if len(array) not in [2, 3]: + return False + type_of_key = array[0] + ssh_key_str = array[1] + + # must have only valid rsa-ssh key characters ie binascii characters + try: + data = base64.decodebytes(bytes(ssh_key_str, 'utf-8')) + except binascii.Error: + return False + a = 4 + # unpack the contents of ssh_key, from ssh_key[:4] , it must be equal to 7 , property of ssh key . + try: + str_len = struct.unpack('>I', data[:a])[0] + except struct.error: + return False + # ssh_key[4:11] must have string which matches with the type_of_key , another ssh key property. + print(str_len) + if data[a:a + str_len].decode(encoding='utf-8') == type_of_key: + return True + else: + return False + + +def validate_api(value): + if not is_public_ssh_key(value): + raise APIValidationError(ERROR_MESSAGE) + + +def validate_ui(value): + if not is_public_ssh_key(value): + raise UIValidationError(ERROR_MESSAGE) diff --git a/profiles/models/scope.py b/profiles/models/scope.py index 47ccfe3ff..82d3bc33e 100644 --- a/profiles/models/scope.py +++ b/profiles/models/scope.py @@ -123,6 +123,7 @@ def get_queryset(self): def expand(self): return self.get_queryset().expand() + class Scope(AbstractScope): class Meta: permissions = [ @@ -178,3 +179,24 @@ def get_q_filter(cls, user, perm): def get_absolute_url(self): return self.get_object().get_absolute_url() + + def get_workflows(self): + from service_catalog.models import Operation, ApprovalWorkflow + operations = Operation.objects.filter(enabled=True) + + # Teams + approval_workflow = ApprovalWorkflow.objects.filter(scopes__id=self.id, operation__in=operations, enabled=True) + operations = operations.exclude(id__in=approval_workflow.values_list('operation__id', flat=True)) + + # Org + if self.is_team: + approval_workflow = approval_workflow | ApprovalWorkflow.objects.filter(scopes__id=self.get_object().org.id, + operation__in=operations, + enabled=True) + operations = operations.exclude(id__in=approval_workflow.values_list('operation__id', flat=True)) + + # Default + approval_workflow = approval_workflow | ApprovalWorkflow.objects.filter(scopes__isnull=True, + operation__in=operations, + enabled=True) + return approval_workflow diff --git a/profiles/tables/__init__.py b/profiles/tables/__init__.py index a247497b4..7b3680b44 100644 --- a/profiles/tables/__init__.py +++ b/profiles/tables/__init__.py @@ -4,3 +4,4 @@ from .team_table import * from .user_table import * from .permission_table import * +from .approval_workflow import * diff --git a/profiles/tables/approval_workflow.py b/profiles/tables/approval_workflow.py new file mode 100644 index 000000000..fa5b574d9 --- /dev/null +++ b/profiles/tables/approval_workflow.py @@ -0,0 +1,19 @@ +from django.utils.html import format_html +from django_tables2 import LinkColumn, TemplateColumn + +from Squest.utils.squest_table import SquestTable +from profiles.models import Scope + + +class ApprovalWorkflowPreviewTable(SquestTable): + + name = LinkColumn() + preview = TemplateColumn(template_name='profiles/custom_columns/preview_workflow.html', orderable=False) + + class Meta: + model = Scope + attrs = {"id": "role_table", "class": "table squest-pagination-tables"} + fields = ("name", "preview") + + def render_name(self, value, record): + return format_html(f'{record}') diff --git a/profiles/views/organization.py b/profiles/views/organization.py index 283991122..0c030b4be 100644 --- a/profiles/views/organization.py +++ b/profiles/views/organization.py @@ -7,6 +7,7 @@ from profiles.models import Organization, Team from profiles.tables import OrganizationTable, ScopeRoleTable, TeamTable, UserRoleTable from profiles.tables.quota_table import QuotaTable +from service_catalog.tables.approval_workflow_table import ApprovalWorkflowPreviewTable class OrganizationListView(SquestListView): @@ -38,7 +39,10 @@ def get_context_data(self, **kwargs): hide_fields=('org',), prefix="team-" ) config.configure(context['teams']) - + if self.request.user.has_perm("service_catalog.view_approvalworkflow"): + context["workflows"] = ApprovalWorkflowPreviewTable(self.get_object().get_workflows(), prefix="workflow-", + hide_fields=["enabled", "actions", "scopes"]) + config.configure(context["workflows"]) context['roles'] = ScopeRoleTable(self.object.roles.distinct()) config.configure(context['roles']) diff --git a/profiles/views/team.py b/profiles/views/team.py index 79e2f2386..22b448d81 100644 --- a/profiles/views/team.py +++ b/profiles/views/team.py @@ -7,6 +7,7 @@ from profiles.models.team import Team from profiles.tables import UserRoleTable, ScopeRoleTable, TeamTable from profiles.tables.quota_table import QuotaTable +from service_catalog.tables.approval_workflow_table import ApprovalWorkflowPreviewTable def get_organization_breadcrumbs(team): @@ -44,6 +45,11 @@ def get_context_data(self, **kwargs): context['roles'] = ScopeRoleTable(self.object.roles.distinct(), prefix="role-") config.configure(context['roles']) + if self.request.user.has_perm("service_catalog.view_approvalworkflow"): + context["workflows"] = ApprovalWorkflowPreviewTable(self.get_object().get_workflows(), prefix="workflow-", + hide_fields=["enabled", "actions", "scopes"]) + config.configure(context["workflows"]) + return context diff --git a/project-static/squest/css/squest-dark.css b/project-static/squest/css/squest-dark.css index ac7e826d2..86dad5aa4 100644 --- a/project-static/squest/css/squest-dark.css +++ b/project-static/squest/css/squest-dark.css @@ -7,7 +7,7 @@ footer{ color: #fff !important; } -.bg-dark table tr,.bg-dark blockquote,.bg-dark pre { +.bg-dark table tr,.bg-dark blockquote,details.bg-dark pre { background-color: #343a40 !important; color: #fff !important; } diff --git a/project-static/squest/css/squest.css b/project-static/squest/css/squest.css index 20a93b71f..6aeca8588 100644 --- a/project-static/squest/css/squest.css +++ b/project-static/squest/css/squest.css @@ -93,3 +93,7 @@ input.form-control[disabled] { .popover { max-width: 100%; } + +.callout a{ + color: unset; +} diff --git a/pyproject.toml b/pyproject.toml index 0ea49faee..f8994539c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "squest" -version = "2.5.0" +version = "2.5.1" description = "Service catalog on top of Red Hat Ansible Automation Platform(RHAAP)/AWX (formerly known as Ansible Tower)" authors = ["Nicolas MarcqAvailable instances
@@ -140,7 +148,7 @@Opened supports
diff --git a/templates/profiles/custom_columns/preview_workflow.html b/templates/profiles/custom_columns/preview_workflow.html new file mode 100644 index 000000000..d80b52560 --- /dev/null +++ b/templates/profiles/custom_columns/preview_workflow.html @@ -0,0 +1,3 @@ + + Preview + diff --git a/templates/profiles/organization_detail.html b/templates/profiles/organization_detail.html index 38135c962..c940b677a 100644 --- a/templates/profiles/organization_detail.html +++ b/templates/profiles/organization_detail.html @@ -87,6 +87,11 @@Drag and drop steps to reorganize the order
-Drag and drop steps to reorganize the order