From 505f4cfdd9d6738d0f634508983412a1b736d3e3 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 11 Jun 2024 22:17:53 +0930 Subject: [PATCH 1/4] fix(api): ensure proper permission checking !24 fixes #55 --- app/access/mixin.py | 2 +- app/api/views/itam/inventory.py | 59 +++++++++++++++++++++------------ app/api/views/mixin.py | 14 +++++--- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/app/access/mixin.py b/app/access/mixin.py index 030d0e3c..88b31988 100644 --- a/app/access/mixin.py +++ b/app/access/mixin.py @@ -150,7 +150,7 @@ def user_organizations(self) -> list(): # ToDo: Ensure that the group has access to item - def has_organization_permission(self, organization=None) -> bool: + def has_organization_permission(self, organization: int=None) -> bool: has_permission = False diff --git a/app/api/views/itam/inventory.py b/app/api/views/itam/inventory.py index e4337c3e..756a9618 100644 --- a/app/api/views/itam/inventory.py +++ b/app/api/views/itam/inventory.py @@ -2,7 +2,7 @@ import json import re -from django.http import JsonResponse +from django.http import Http404, JsonResponse from django.utils import timezone from rest_framework import generics, views @@ -36,44 +36,57 @@ def permission_check(self, request, view, obj=None) -> bool: -class Collect(OrganizationMixin, views.APIView): +class Collect(OrganizationPermissionAPI, views.APIView): - permission_classes = [ - InventoryPermissions - ] + # permission_classes = [ + # InventoryPermissions + # ] queryset = Device.objects.all() def post(self, request, *args, **kwargs): + data = json.loads(request.body) - status = Http.Status.BAD_REQUEST - + # data = self.request.data + device = None + + self.default_organization = UserSettings.objects.get(user=request.user).default_organization + + if Device.objects.filter(slug=str(data['details']['name']).lower()).exists(): + + self.obj = Device.objects.get(slug=str(data['details']['name']).lower()) + + device = self.obj + + + if not self.permission_check(request=request, view=self, obj=device): + + raise Http404 + + + + status = Http.Status.BAD_REQUEST + device_operating_system = None operating_system = None operating_system_version = None try: - default_organization = UserSettings.objects.get(user=request.user).default_organization - app_settings = AppSettings.objects.get(owner_organization = None) - if Device.objects.filter(name=data['details']['name']).exists(): - - device = Device.objects.get(name=data['details']['name']) - - else: # Create the device + if not device: # Create the device device = Device.objects.create( name = data['details']['name'], device_type = None, serial_number = data['details']['serial_number'], uuid = data['details']['uuid'], - organization = default_organization, + organization = self.default_organization, ) status = Http.Status.CREATED @@ -87,7 +100,7 @@ def post(self, request, *args, **kwargs): operating_system = OperatingSystem.objects.create( name = data['os']['name'], - organization = default_organization, + organization = self.default_organization, is_global = True ) @@ -95,7 +108,7 @@ def post(self, request, *args, **kwargs): if OperatingSystemVersion.objects.filter( name=data['os']['version_major'], operating_system=operating_system ).exists(): operating_system_version = OperatingSystemVersion.objects.get( - organization = default_organization, + organization = self.default_organization, is_global = True, name = data['os']['version_major'], operating_system = operating_system @@ -104,7 +117,7 @@ def post(self, request, *args, **kwargs): else: # Create Operating System Version operating_system_version = OperatingSystemVersion.objects.create( - organization = default_organization, + organization = self.default_organization, is_global = True, name = data['os']['version_major'], operating_system = operating_system, @@ -128,7 +141,7 @@ def post(self, request, *args, **kwargs): else: # Create Operating System Version device_operating_system = DeviceOperatingSystem.objects.create( - organization = default_organization, + organization = self.default_organization, device=device, version = data['os']['version'], operating_system_version = operating_system_version, @@ -223,7 +236,7 @@ def post(self, request, *args, **kwargs): else: # Create Software Category software_version = SoftwareVersion.objects.create( - organization = default_organization, + organization = self.default_organization, is_global = True, name = semver, software = software, @@ -240,7 +253,7 @@ def post(self, request, *args, **kwargs): else: # Create Software device_software = DeviceSoftware.objects.create( - organization = default_organization, + organization = self.default_organization, is_global = True, installedversion = software_version, software = software, @@ -284,7 +297,9 @@ def post(self, request, *args, **kwargs): device.save() - status = Http.Status.OK + if status != Http.Status.CREATED: + + status = Http.Status.OK except Exception as e: diff --git a/app/api/views/mixin.py b/app/api/views/mixin.py index 680c26c7..5f205db4 100644 --- a/app/api/views/mixin.py +++ b/app/api/views/mixin.py @@ -1,4 +1,4 @@ - +from django.core.exceptions import PermissionDenied from django.forms import ValidationError from rest_framework.permissions import DjangoObjectPermissions @@ -50,7 +50,6 @@ def permission_check(self, request, view, obj=None) -> bool: raise ValidationError('you must provide an organization') object_organization = int(request.data['organization']) - elif method == 'patch': action = 'change' @@ -126,12 +125,17 @@ def permission_check(self, request, view, obj=None) -> bool: return True + if hasattr(self, 'default_organization'): + object_organization = self.default_organization + + if method == 'post' and hasattr(self, 'default_organization'): - if object_organization is None: + if self.default_organization: - raise Exception("unable to determine object organization") + object_organization = self.default_organization.id if not self.has_organization_permission(object_organization) and not request.user.is_superuser: - return False + + raise PermissionDenied('You are not part of this organization') return True From 65c6065ba1c4ddfe324323d2f6eaffce7c3f6fa5 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 11 Jun 2024 22:19:06 +0930 Subject: [PATCH 2/4] test(api): Inventory upload permission checks !24 #55 --- app/api/tests/inventory/test_api_inventory.py | 280 ++++++++++++------ .../test_inventory_permission_api.py | 33 ++- 2 files changed, 208 insertions(+), 105 deletions(-) diff --git a/app/api/tests/inventory/test_api_inventory.py b/app/api/tests/inventory/test_api_inventory.py index 95f5c3a2..5ff31219 100644 --- a/app/api/tests/inventory/test_api_inventory.py +++ b/app/api/tests/inventory/test_api_inventory.py @@ -1,151 +1,251 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType from django.shortcuts import reverse from django.test import TestCase, Client -import pytest -import unittest +from unittest.mock import patch + +from access.models import Organization, Team, TeamUsers, Permission + +from api.views.mixin import OrganizationPermissionAPI + +from itam.models.device import Device + +from settings.models.user_settings import UserSettings + + + +class InventoryAPI(TestCase): + + model = Device + + model_name = 'device' + app_label = 'itam' + + inventory = { + "details": { + "name": "device_name", + "serial_number": "a serial number", + "uuid": "string" + }, + "os": { + "name": "os_name", + "version_major": "12", + "version": "12.1" + }, + "software": [ + { + "name": "software_name", + "category": "category_name", + "version": "1.2.3" + } + ] + } + + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a device + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model_name, + content_type = ContentType.objects.get( + app_label = self.app_label, + model = self.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + + add_user_settings = UserSettings.objects.get(user=self.add_user) + + add_user_settings.default_organization = organization + + add_user_settings.save() + + + + @patch.object(OrganizationPermissionAPI, 'permission_check') + def test_inventory_function_called_permission_check(self, permission_check): + """ Inventory Upload checks permissions + + Function 'permission_check' is the function that checks permissions + + As the non-established way of authentication an API permission is being done + confimation that the permissions are still checked is required. + """ + + client = Client() + url = reverse('API:_api_device_inventory') + + client.force_login(self.add_user) + response = client.post(url, data=self.inventory, content_type='application/json') + assert permission_check.called -@pytest.mark.skip(reason="to be written") -def test_api_inventory_device_added(): - """ Device is created """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_device_added(self): + """ Device is created """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_operating_system_added(): - """ Operating System is created """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_operating_system_added(self): + """ Operating System is created """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_operating_system_version_added(): - """ Operating System version is created """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_operating_system_version_added(self): + """ Operating System version is created """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_device_has_operating_system_added(): - """ Operating System version linked to device """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_device_has_operating_system_added(self): + """ Operating System version linked to device """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_device_operating_system_version_is_semver(): - """ Operating System version is full semver - - Operating system versions name is the major version number of semver. - The device version is to be full semver - """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_device_operating_system_version_is_semver(self): + """ Operating System version is full semver + + Operating system versions name is the major version number of semver. + The device version is to be full semver + """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_software_no_version_cleaned(): - """ Check softare cleaned up - - As part of the inventory upload the software versions of software found on the device is set to null - and before the processing is completed, the version=null software is supposed to be cleaned up. - """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_software_no_version_cleaned(self): + """ Check softare cleaned up + + As part of the inventory upload the software versions of software found on the device is set to null + and before the processing is completed, the version=null software is supposed to be cleaned up. + """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_software_category_added(): - """ Software category exists """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_software_category_added(self): + """ Software category exists """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_software_added(): - """ Test software exists """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_software_added(self): + """ Test software exists """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_software_category_linked_to_software(): - """ Software category linked to software """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_software_category_linked_to_software(self): + """ Software category linked to software """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_software_version_added(): - """ Test software version exists """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_software_version_added(self): + """ Test software version exists """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_software_version_returns_semver(): - """ Software Version from inventory returns semver if within version string """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_software_version_returns_semver(self): + """ Software Version from inventory returns semver if within version string """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_software_version_returns_original_version(): - """ Software Version from inventory returns inventoried version if no semver found """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_software_version_returns_original_version(self): + """ Software Version from inventory returns inventoried version if no semver found """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_software_version_linked_to_software(): - """ Test software version linked to software it belongs too """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_software_version_linked_to_software(self): + """ Test software version linked to software it belongs too """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_device_has_software_version(): - """ Inventoried software is linked to device and it's the corret one""" - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_device_has_software_version(self): + """ Inventoried software is linked to device and it's the corret one""" + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_device_software_has_installed_date(): - """ Inventoried software version has install date """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_device_software_has_installed_date(self): + """ Inventoried software version has install date """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_device_software_blank_installed_date_is_updated(): - """ A blank installed date of software is updated if the software was already attached to the device """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_device_software_blank_installed_date_is_updated(self): + """ A blank installed date of software is updated if the software was already attached to the device """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_valid_status_created(): - """ Successful inventory upload returns 201 """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_valid_status_created(self): + """ Successful inventory upload returns 201 """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_invalid_status_bad_request(): - """ Incorrectly formated inventory upload returns 400 """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_invalid_status_bad_request(self): + """ Incorrectly formated inventory upload returns 400 """ + pass -@pytest.mark.skip(reason="to be written") -def test_api_inventory_exeception_status_sever_error(): - """ if the method throws an exception 500 must be returned. - - idea to test: add a random key to the report that is not documented - and perform some action against it that will cause a python exception. - """ - pass + @pytest.mark.skip(reason="to be written") + def test_api_inventory_exeception_status_sever_error(self): + """ if the method throws an exception 500 must be returned. + + idea to test: add a random key to the report that is not documented + and perform some action against it that will cause a python exception. + """ + pass diff --git a/app/api/tests/inventory/test_inventory_permission_api.py b/app/api/tests/inventory/test_inventory_permission_api.py index 3d530195..c96c35a5 100644 --- a/app/api/tests/inventory/test_inventory_permission_api.py +++ b/app/api/tests/inventory/test_inventory_permission_api.py @@ -1,17 +1,18 @@ -# from django.conf import settings +import pytest +import unittest +import requests + from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser, User from django.contrib.contenttypes.models import ContentType from django.shortcuts import reverse from django.test import TestCase, Client -import pytest -import unittest -import requests - from access.models import Organization, Team, TeamUsers, Permission + from itam.models.device import Device +from settings.models.user_settings import UserSettings class InventoryPermissionsAPI(TestCase): @@ -140,6 +141,13 @@ def setUpTestData(self): ) self.add_user = User.objects.create_user(username="test_user_add", password="password") + + add_user_settings = UserSettings.objects.get(user=self.add_user) + + add_user_settings.default_organization = organization + + add_user_settings.save() + teamuser = TeamUsers.objects.create( team = add_team, user = self.add_user @@ -180,7 +188,6 @@ def setUpTestData(self): - @pytest.mark.skip(reason="test to be written") def test_device_auth_add_user_anon_denied(self): """ Check correct permission for add @@ -191,12 +198,11 @@ def test_device_auth_add_user_anon_denied(self): url = reverse('API:_api_device_inventory') - response = client.put(url, data=self.inventory) + response = client.put(url, data=self.inventory, content_type='application/json') assert response.status_code == 401 - @pytest.mark.skip(reason="test to be written") def test_device_auth_add_no_permission_denied(self): """ Check correct permission for add @@ -208,12 +214,11 @@ def test_device_auth_add_no_permission_denied(self): client.force_login(self.no_permissions_user) - response = client.post(url, data=self.inventory) + response = client.post(url, data=self.inventory, content_type='application/json') assert response.status_code == 403 - @pytest.mark.skip(reason="test to be written") def test_device_auth_add_different_organization_denied(self): """ Check correct permission for add @@ -225,12 +230,11 @@ def test_device_auth_add_different_organization_denied(self): client.force_login(self.different_organization_user) - response = client.post(url, data=self.inventory) + response = client.post(url, data=self.inventory, content_type='application/json') assert response.status_code == 403 - @pytest.mark.skip(reason="test to be written") def test_device_auth_add_permission_view_denied(self): """ Check correct permission for add @@ -242,12 +246,11 @@ def test_device_auth_add_permission_view_denied(self): client.force_login(self.view_user) - response = client.post(url, data=self.inventory) + response = client.post(url, data=self.inventory, content_type='application/json') assert response.status_code == 403 - @pytest.mark.skip(reason="test to be written") def test_device_auth_add_has_permission(self): """ Check correct permission for add @@ -259,7 +262,7 @@ def test_device_auth_add_has_permission(self): client.force_login(self.add_user) - response = client.post(url, data=self.inventory) + response = client.post(url, data=self.inventory, content_type='application/json') assert response.status_code == 201 From 36fa364d04473c7a9de71890fed4eebf843954ed Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 11 Jun 2024 22:21:54 +0930 Subject: [PATCH 3/4] docs(api): notate inventory permission !24 #55 --- README.md | 2 ++ docs/projects/django-template/api.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 00ecbe6e..40abd045 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,5 @@ ![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/django-template?style=plastic&logo=docker&color=0db7ed) +![Gitlab Code Coverage](https://img.shields.io/gitlab/pipeline-coverage/nofusscomputing%2Fprojects%2Fdjango_template?branch=master&style=plastic&logo=gitlab&label=Test%20Coverage) + diff --git a/docs/projects/django-template/api.md b/docs/projects/django-template/api.md index 85187524..93524d4d 100644 --- a/docs/projects/django-template/api.md +++ b/docs/projects/django-template/api.md @@ -30,6 +30,8 @@ curl -X GET http://127.0.0.1:8000/api/ -H 'Authorization: Token ' - content `application/json` +- permission `itam.add_device` + Passing a valid inventory report to this endpoint will update the device within the app. If the device doesn't exist it will be created. Report Format From 2eb50311b485094304f99cdd7d5fc1513896fabd Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 12 Jun 2024 01:48:30 +0930 Subject: [PATCH 4/4] docs(api): document the inventory endpoint !24 #55 --- app/api/serializers/itam/inventory.py | 73 ++++++++++++++++++++ app/api/views/itam/inventory.py | 39 +++++++++-- app/app/settings.py | 29 +++++++- docs/projects/django-template/api.md | 53 ++------------ docs/projects/django-template/itam/device.md | 27 ++++++++ 5 files changed, 166 insertions(+), 55 deletions(-) create mode 100644 app/api/serializers/itam/inventory.py diff --git a/app/api/serializers/itam/inventory.py b/app/api/serializers/itam/inventory.py new file mode 100644 index 00000000..66c35dd3 --- /dev/null +++ b/app/api/serializers/itam/inventory.py @@ -0,0 +1,73 @@ +from django.urls import reverse + +from itam.models.device import Device +from rest_framework import serializers + + + + +class InventorySerializer(serializers.Serializer): + """ Serializer for Inventory Upload """ + + + class DetailsSerializer(serializers.Serializer): + + name = serializers.CharField( + help_text = 'Host name', + required = True + ) + + serial_number = serializers.CharField( + help_text = 'Devices serial number', + required = True + ) + + uuid = serializers.CharField( + help_text = 'Device system UUID', + required = True + ) + + + class OperatingSystemSerializer(serializers.Serializer): + + name = serializers.CharField( + help_text='Name of the operating system installed on the device', + required = True, + ) + + version_major = serializers.IntegerField( + help_text='Major semver version number of the OS version', + required = True, + ) + + version = serializers.CharField( + help_text='semver version number of the OS', + required = True + ) + + + class SoftwareSerializer(serializers.Serializer): + + name = serializers.CharField( + help_text='Name of the software', + required = True + ) + + category = serializers.CharField( + help_text='Category of the software', + default = None, + required = False + ) + + version = serializers.CharField( + default = None, + help_text='semver version number of the software', + required = False + ) + + + details = DetailsSerializer() + + os = OperatingSystemSerializer() + + software = SoftwareSerializer(many = True) diff --git a/app/api/views/itam/inventory.py b/app/api/views/itam/inventory.py index 756a9618..c3281005 100644 --- a/app/api/views/itam/inventory.py +++ b/app/api/views/itam/inventory.py @@ -5,6 +5,8 @@ from django.http import Http404, JsonResponse from django.utils import timezone +from drf_spectacular.utils import extend_schema, OpenApiExample, OpenApiTypes, OpenApiResponse, OpenApiParameter + from rest_framework import generics, views from rest_framework.response import Response @@ -12,6 +14,7 @@ from access.models import Organization from api.views.mixin import OrganizationPermissionAPI +from api.serializers.itam.inventory import InventorySerializer from core.http.common import Http @@ -38,20 +41,44 @@ def permission_check(self, request, view, obj=None) -> bool: class Collect(OrganizationPermissionAPI, views.APIView): - # permission_classes = [ - # InventoryPermissions - # ] - queryset = Device.objects.all() + @extend_schema( + summary = "Upload a device's inventory", + description = """After inventorying a device, it's inventory file, `.json` is uploaded to this endpoint. +If the device does not exist, it will be created. If the device does exist the existing +device will be updated with the information within the inventory. + +matching for an existing device is by slug which is the hostname converted to lower case +letters. This conversion is automagic. + +**NOTE:** _for device creation, the API user must have user setting 'Default Organization'. Without +this setting populated, no device will be created and the endpoint will return HTTP/403_ + +## Permissions + +- `itam.add_device` Required to upload inventory + """, + + methods=["POST"], + parameters = None, + tags = ['device', 'inventory',], + request = InventorySerializer, + responses = { + 200: OpenApiResponse(description='Inventory updated an existing device'), + 201: OpenApiResponse(description='Inventory created a new device'), + 400: OpenApiResponse(description='Inventory is invalid'), + 401: OpenApiResponse(description='User Not logged in'), + 403: OpenApiResponse(description='User is missing permission or in different organization'), + 500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'), + } + ) def post(self, request, *args, **kwargs): data = json.loads(request.body) - # data = self.request.data - device = None self.default_organization = UserSettings.objects.get(user=request.user).default_organization diff --git a/app/app/settings.py b/app/app/settings.py index 94ea60f5..01579786 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -223,8 +223,33 @@ } SPECTACULAR_SETTINGS = { - 'TITLE': 'Your Project API', - 'DESCRIPTION': 'Your project description', + 'TITLE': 'ITSM API', + 'DESCRIPTION': """This UI is intended to serve as the API documentation. + +## Authentication + +Authentication with the api is via Token. The token is placed in header `Authorization` with a value of `Token `. + +## Token Generation + +To generate a token, run `python3 manage.py drf_create_token ` from the CLI. + +## Examples + +curl: +- Simple API Request: `curl -X GET /api/ -H 'Authorization: Token '` + +- Post an Inventory File: + + ``` bash + curl --header "Content-Type: application/json" \\ + --header "Authorization: Token " \\ + --request POST \\ + --data @/.json \\ + /api/device/inventory + ``` + + """, 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, diff --git a/docs/projects/django-template/api.md b/docs/projects/django-template/api.md index 93524d4d..b512d8b6 100644 --- a/docs/projects/django-template/api.md +++ b/docs/projects/django-template/api.md @@ -6,65 +6,24 @@ template: project.html about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/django_app --- -to access the api, it can be done with the following command: - -``` bash - -curl -X GET http://127.0.0.1:8000/api/ -H 'Authorization: Token ' - -``` +An api is available for this application and can be viewed at endpoint `/api/`. Documentation specific to each of the endpoints can be found within the swagger UI at endpoint `api/swagger/`. ## Features -- Inventory Report Collection +- Device Inventory upload - Swagger UI -## Inventory Reports - -- url `/api/device/inventory` - -- method `POST` - -- content `application/json` - -- permission `itam.add_device` - -Passing a valid inventory report to this endpoint will update the device within the app. If the device doesn't exist it will be created. - -Report Format - -``` json - -{ - "details": { - "name": "string", - "serial_number": "string", - "uuid": "string" - }, - "os": { - "name": "name of os", - "version_major": "major version number", - "version": "as reported" - }, - "software": [ - { - "name": "string", - "category": "string", - "version": "string" - } - ] -} - +## Device Inventory -``` +You can [inventory](itam/device.md#inventory) your devices and upload them to the inventory endpoint. -## User Token +## Swagger UI -To generate a user token to access the api, use command `python3 manage.py drf_create_token ` +The swagger UI is included within this application and can be found at endpoint `/api/swagger` on your server. This UI has been used to document the API. ## Organizations diff --git a/docs/projects/django-template/itam/device.md b/docs/projects/django-template/itam/device.md index 2fc9e24f..7e5f53c1 100644 --- a/docs/projects/django-template/itam/device.md +++ b/docs/projects/django-template/itam/device.md @@ -97,3 +97,30 @@ The report can contain the following information: !!! info When the software is added to the inventory, a regex search is done to return the [semver](https://semver.org/) of the software. if no semver is found, the version number provided is used. + +Example Report + +``` json + +{ + "details": { + "name": "string", + "serial_number": "string", + "uuid": "string" + }, + "os": { + "name": "name of os", + "version_major": "major version number", + "version": "as reported" + }, + "software": [ + { + "name": "string", + "category": "string", + "version": "string" + } + ] +} + + +```