Skip to content

Commit

Permalink
First commit for library upgrade
Browse files Browse the repository at this point in the history
  • Loading branch information
monsieurswag committed May 17, 2024
1 parent 50a231f commit 65d9753
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 6 deletions.
150 changes: 149 additions & 1 deletion backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from django.urls import reverse
from datetime import date, datetime
from typing import Union, Self
from typing import Union, Dict, Set, Tuple, Type, Self
from django.utils.html import format_html

from structlog import get_logger
Expand Down Expand Up @@ -203,11 +203,159 @@ def load(self) -> Union[str, None]:
return error_msg


class LibraryUpgrader:
def __init__(self,old_library: Type["LoadedLibrary"],new_library: StoredLibrary) :
self.old_library = old_library
self.old_objects = [
*old_library.threats.all(),
*old_library.reference_controls.all(),
*old_library.threats.all(),
*old_library.risk_matrices.all()
]
self.new_library = new_library
library_content = json.loads(self.new_library.content)
self.new_framework = library_content.get("framework")
self.new_matrices = library_content.get("risk_matrix")
self.threats = library_content.get("threats",[])
self.reference_controls = library_content.get("reference_controls",[])
self.new_objects = {obj["urn"]: obj for obj in self.threats}
self.new_objects.update({obj["urn"]: obj for obj in self.reference_controls})
if self.new_framework :
self.new_object[self.new_framework["urn"]] = self.new_framework
if self.new_matrices :
for matrix in self.new_matrices :
self.new_objects[matrix["urn"]] = matrix

# We should create a LibraryVerifier class in the future that check if the libary is valid and use it for a better error handling.
def upgrade_library(self) -> Union[str,None] :
old_urns = set(obj.urn for obj in self.old_objects)
new_urns = set(self.new_objects.keys())

if not new_urns.issuperset(old_urns) :
return "This library upgrade is deleting existing objects."

for key, value in [
("name", self.new_library.name),
("version", self.new_library.version),
("provider", self.new_library.provider),
("packager", self.new_library.packager), # A user can fake a builtin library in this case because he can upgrade a builtin library by adding its own libary with the same URN as a builtin libary.
("ref_id", self.new_library.ref_id), # Should we even update the ref_id ?
("description", self.new_library.description),
("annotation", self.new_library.annotation),
("copyright", self.new_library.copyright),
("objects_meta", self.new_library.objects_meta)
] :
setattr(self.old_library,key,value)
self.old_library.save()

objects_created = {} # URN => Object Created
referential_object_dict = {
"locale": self.old_library.locale,
"default_locale": self.old_library.default_locale,
"provider": self.new_library.provider,
"is_published": True
}

for threat in self.threats :
new_threat, _ = Threat.objects.update_or_create(
urn=threat["urn"],
defaults=threat,
create_defaults={
**referential_object_dict,
**threat
}
)
objects_created[threat["urn"]] = new_threat

for reference_control in self.reference_controls :
new_reference_control, _ = ReferenceControl.objects.update_or_create(
urn=reference_control["urn"],
defaults=reference_control,
create_defaults={
**referential_object_dict,
**reference_control
}
)
objects_created[reference_control["urn"]] = new_reference_control

if self.new_framework is not None :
framework_dict = {**self.new_framework}
del framework_dict["requirement_nodes"]

new_framework, _ = Framework.objects.update_or_create(
library=self,
defaults=framework_dict,
create_defaults={
**referential_object_dict,
**framework_dict
}
)

for requirement_node in self.new_framework["requirement_nodes"] :
# I can simplify this code and do like i did for the framework update/creation process !

requirement_node_dict = {**requirement_node}
for key in ["maturity","reference_controls","threats"] :
del requirement_node_dict[key]

new_requirement_node, _ = RequirementNode.objects.update_or_create(
urn=requirement_node["urn"],
defaults=requirement_node_dict,
create_defaults={
**referential_object_dict,
**requirement_node_dict,
"framework": new_framework,
}
)

for threat_urn in requirement_node :
new_requirement_node.threats.add(objects_created[threat_urn])

for reference_control_urn in requirement_node :
new_requirement_node.reference_controls.add(objects_created[reference_control_urn])

if self.new_matrices is not None :
for matrix in self.new_matrices :
json_definition_keys = {"grid","probability","impact","risk"} # Store this as a constant somewhere (as a static attribute of the class)
other_keys = set(matrix.keys()) - json_definition_keys
matrix_dict = {
key: matrix["key"]
for key in other_keys
}
matrix_dict["json_definition"] = {}
for key in json_definition_keys :
if key in matrix : # If all keys are mandatory this condition is useless
matrix_dict["json_definition"][key] = matrix[key]

RiskMatrix.objects.update_or_create(
urn=matrix["urn"],
defaults=matrix_dict,
create_defaults={
**referential_object_dict
**matrix_dict,
}
)


class LoadedLibrary(LibraryMixin):
dependencies = models.ManyToManyField(
"self", blank=True, verbose_name=_("Dependencies"), symmetrical=False
)

def upgrade(self) :
# What happens if we delete a loaded library while it's being upgraded ?
# Should we make some kind of mutex for this ?

new_libraries = [*StoredLibrary.objects.filter(urn=self.urn,locale=self.locale,version__gt=self.version)]
if not new_libraries :
return None # We should raise an error there in the future to inform that no new library has been found.

# Dependencies updates are not working yet

new_library = max(new_libraries,key=lambda lib: lib.version)
library_upgrader = LibraryUpgrader(self,new_library)
return library_upgrader.upgrade_library()

@property
def _objects(self):
res = {}
Expand Down
2 changes: 1 addition & 1 deletion backend/library/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ def import_requirement_node(self, framework_object: Framework):
)

for threat in self.requirement_data.get("threats", []):
requirement_node.threats.add(Threat.objects.get(urn=threat.lower()))
requirement_node.threats.add(Threat.objects.get(urn=threat.lower())) # URN are not case insensitive in the whole codebase yet, we should fix that and make sure URNs are always transformed into lowercase before being used.

for reference_control in self.requirement_data.get("reference_controls", []):
requirement_node.reference_controls.add(
Expand Down
20 changes: 19 additions & 1 deletion backend/library/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ def upload_library(self, request):
status=HTTP_400_BAD_REQUEST,
)


class LoadedLibraryViewSet(viewsets.ModelViewSet):
# serializer_class = LoadedLibrarySerializer
# parser_classes = [FileUploadParser]
Expand Down Expand Up @@ -285,3 +284,22 @@ def tree(
framework = lib.frameworks.first()
requirement_nodes = framework.requirement_nodes.all()
return Response(get_sorted_requirement_nodes(requirement_nodes, None))

@action(detail=True, methods=["get"], url_path="upgrade")
def upgrade(self, request, pk) :
if not RoleAssignment.is_access_allowed(
user=request.user,
perm=Permission.objects.get(codename="add_loadedlibrary"), # We should use either this permission or making a new permission "upgrade_loadedlibrary"
folder=Folder.get_root_folder(),
):
return Response(status=HTTP_403_FORBIDDEN)
try :
key = "urn" if pk.startswith("urn:") else "id"
library = LoadedLibrary.objects.get(**{key: pk})
except Exception as e :
return Response(data="Library not found.", status=HTTP_404_NOT_FOUND) # Error messages could be returned as JSON instead

error_msg = library.upgrade()
if error_msg is None :
return Response(status=HTTP_204_NO_CONTENT)
return Response(error_msg,status=HTTP_422_UNPROCESSABLE_ENTITY) # We must make at least one error message
45 changes: 44 additions & 1 deletion frontend/src/lib/components/ModelTable/LibraryActions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
<span class="hover:text-primary-500">
<form
method="post"
action="/libraries/{library.urn}"
action="/libraries/{library.urn}?/load"
use:enhance={() => {
loading.form = true;
loading.library = library.urn;
Expand All @@ -71,3 +71,46 @@
</span>
{/if}
{/if}
<!-- This condition must check that the libary is a LoadedLibrary object and that there is an available upgrade for it -->
<!-- Should we put a is_upgradable BooleanField directly into the LoadedLibrary model or query the database everytime we load the loaded libraries menu to check if there is an upgrade available or not among the stored liaries ? -->
{#if false && loading.form && loading.library === library.urn} <!-- Il faut créer une variable pour identifier quelle bouton doit être remplacé par un loader pour que les 2 loaders ne s activent pas ne même temps -->
<div class="flex items-center cursor-progress" role="status">
<svg
aria-hidden="true"
class="w-5 h-5 text-gray-200 animate-spin dark:text-gray-600 fill-primary-500"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
{:else}
<span class="hover:text-primary-500">
<form
method="post"
action="/libraries/{library.urn}?/upgrade"
use:enhance={() => {
loading.form = true;
loading.library = library.urn;
return async ({ update }) => {
loading.form = false;
loading.library = '';
update();
};
}}
on:submit={handleSubmit}
>
<button on:click={e => e.stopPropagation()}>
<i class="fa-solid fa-circle-up" />
</button>
</form>
</span>
{/if}
18 changes: 16 additions & 2 deletions frontend/src/routes/(app)/libraries/[id=urn]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { setFlash } from 'sveltekit-flash-message/server';
import * as m from '$paraglide/messages';

export const actions: Actions = {
default: async (event) => {
load: async (event) => {
const endpoint = `${BASE_API_URL}/stored-libraries/${event.params.id}/import`;
const res = await event.fetch(endpoint);
const res = await event.fetch(endpoint); // We will have to make this a POST later (we should use POST when creating a new object)
if (!res.ok) {
const response = await res.json();
console.error('server response:', response);
Expand All @@ -20,5 +20,19 @@ export const actions: Actions = {
},
event
);
},
upgrade: async(event) => {
const endpoint = `${BASE_API_URL}/loaded-libraries/${event.params.id}/upgrade/`;
const res = await event.fetch(endpoint); // We will have to make this a PATCH later (we should use PATCH when modifying an object)

console.log(`Response: ${res.status} ${res.statusText}`);

setFlash(
{
type: 'success',
message: "Request successfully processed" // Temporary message for debugging purposes
},
event
);
}
};

0 comments on commit 65d9753

Please sign in to comment.