Skip to content

Commit

Permalink
Merge 62d1fa7 into a37e9dc
Browse files Browse the repository at this point in the history
  • Loading branch information
EliasBoulharts committed Feb 8, 2024
2 parents a37e9dc + 62d1fa7 commit 941e9c1
Show file tree
Hide file tree
Showing 57 changed files with 850 additions and 179 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# 2.5.1 2024-02-08

## Fix

- CSS issue in documentation rendering for <pre></pre> 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
Expand Down
9 changes: 6 additions & 3 deletions Squest/utils/squest_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Squest/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version__ = "2.5.0"
__version__ = "2.5.1"
VERSION = __version__
3 changes: 3 additions & 0 deletions Squest/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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[
Expand Down
4 changes: 4 additions & 0 deletions docs/manual/service_catalog/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 26 additions & 0 deletions plugins/field_validators/is_json.py
Original file line number Diff line number Diff line change
@@ -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")
49 changes: 49 additions & 0 deletions plugins/field_validators/is_public_ssh_key.py
Original file line number Diff line number Diff line change
@@ -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)
22 changes: 22 additions & 0 deletions profiles/models/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def get_queryset(self):
def expand(self):
return self.get_queryset().expand()


class Scope(AbstractScope):
class Meta:
permissions = [
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions profiles/tables/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
from .team_table import *
from .user_table import *
from .permission_table import *
from .approval_workflow import *
19 changes: 19 additions & 0 deletions profiles/tables/approval_workflow.py
Original file line number Diff line number Diff line change
@@ -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'<a title={record} href="{record.get_absolute_url()}">{record}</a>')
6 changes: 5 additions & 1 deletion profiles/views/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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'])

Expand Down
6 changes: 6 additions & 0 deletions profiles/views/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion project-static/squest/css/squest-dark.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 4 additions & 0 deletions project-static/squest/css/squest.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,7 @@ input.form-control[disabled] {
.popover {
max-width: 100%;
}

.callout a{
color: unset;
}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 Marcq <nicolas.marcq@hpe.com>", "Elias Boulharts <elias.boulharts@hpe.com", "Anthony Belhadj <abelhadj@hpe.com>"]
license = "MIT"
Expand Down
9 changes: 9 additions & 0 deletions service_catalog/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,14 @@ class DocAdmin(admin.ModelAdmin):
models.TextField: {'widget': AdminMartorWidget},
}

list_filter = ['services', 'operations']
list_display = ['title', 'linked_services', 'linked_operations']

def linked_services(self, obj):
return ", ".join([str(service) for service in obj.services.all()])

def linked_operations(self, obj):
return ", ".join([str(operation) for operation in obj.operations.all()])


admin.site.register(Doc, DocAdmin)
4 changes: 1 addition & 3 deletions service_catalog/filters/doc_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ def __init__(self, *args, **kwargs):


class DocFilter(SquestFilter):
services = ServiceFilter(widget=SelectMultiple(attrs={'data-live-search': "true"}))

class Meta:
model = Doc
fields = ['title', 'services']
fields = ['title']
10 changes: 9 additions & 1 deletion service_catalog/forms/approval_workflow_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ class Meta:
fields = ['name', 'operation', 'scopes', 'enabled']

def clean(self):
cleaned_data = super().clean()
cleaned_data = super(ApprovalWorkflowForm, self).clean()
operation = cleaned_data.get("operation")
scopes = cleaned_data.get("scopes")
self.custom_clean_scopes(operation, scopes)

def custom_clean_scopes(self, operation, scopes):
# check that selected scopes are not already in use by another approval workflow for the selected operation
exclude_id = self.instance.id if self.instance else None
if not scopes.exists():
Expand All @@ -29,3 +32,8 @@ class ApprovalWorkflowFormEdit(ApprovalWorkflowForm):
class Meta:
model = ApprovalWorkflow
fields = ['name', 'scopes', 'enabled']

def clean(self):
cleaned_data = super(ApprovalWorkflowForm, self).clean()
scopes = cleaned_data.get("scopes")
self.custom_clean_scopes(self.instance.operation, scopes)
2 changes: 1 addition & 1 deletion service_catalog/forms/operation_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def __init__(self, *args, **kwargs):
if self.instance is not None:
if self.instance.validators is not None:
# Converting comma separated string to python list
instance_validator_as_list = self.instance.validators.split(",")
instance_validator_as_list = self.instance.validators_name
# set the current value
self.initial["validators"] = instance_validator_as_list

Expand Down
8 changes: 4 additions & 4 deletions service_catalog/models/approval_step_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ def reset_to_pending(self):
def get_scopes(self):
return self.approval_workflow_state.get_scopes()

def who_can_approve(self):
def who_can_approve(self, exclude_superuser=False):
return self.approval_workflow_state.request.instance.quota_scope.who_has_perm(
self.approval_step.permission.permission_str)
self.approval_step.permission.permission_str, exclude_superuser=exclude_superuser)

def who_can_accept(self):
return self.who_can_approve()
def who_can_accept(self, exclude_superuser=False):
return self.who_can_approve(exclude_superuser)

@classmethod
def get_q_filter(cls, user, perm):
Expand Down
4 changes: 2 additions & 2 deletions service_catalog/models/approval_workflow_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def first_step(self):
return first_step.first()
return None

def who_can_approve(self):
def who_can_approve(self, exclude_superuser=False):
if self.current_step is not None:
return self.current_step.who_can_approve()
return self.current_step.who_can_approve(exclude_superuser=exclude_superuser)
return User.objects.none()
12 changes: 7 additions & 5 deletions service_catalog/models/documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,16 @@ def render(self, instance=None):
return self.content
try:
template = Template(self.content)
context = {
"instance": instance
}
return template.render(context)
except UndefinedError as e:
logger.warning(f"Error: {e.message}, instance: {instance}, doc: {self}")
raise TemplateError(e)
except TemplateSyntaxError as e:
logger.warning(f"Error: {e.message}, instance: {instance}, doc: {self}")
raise TemplateError(e)

context = {
"instance": instance
}
return template.render(context)
except TypeError as e:
logger.warning(f"Error: {e}, instance: {instance}, doc: {self}")
raise TemplateError(e)
6 changes: 5 additions & 1 deletion service_catalog/models/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,14 @@ class Operation(SquestModel):
help_text="Jinja supported. Job template type")
validators = CharField(null=True, blank=True, max_length=200, verbose_name="Survey validators")

@property
def validators_name(self):
return self.validators.split(",") if self.validators else None

def get_validators(self):
validators = list()
if self.validators is not None:
all_validators = self.validators.split(",")
all_validators = self.validators_name
all_validators.sort()
for validator_file in all_validators:
validator = PluginController.get_survey_validator_def(validator_file)
Expand Down

0 comments on commit 941e9c1

Please sign in to comment.