Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
c496d10c1a | |||
3993cc96a5 | |||
a4b37b34a9 | |||
2f55024f0b | |||
213644a51a | |||
281d839801 | |||
4fd157a785 | |||
968b3a0f92 | |||
f5ba608ed1 | |||
289668bb7f | |||
9e28722dba | |||
9b673f4a07 | |||
3a9e4b29b3 | |||
8d59462561 | |||
098e41e6a1 | |||
fc3f0b39e2 | |||
de53948cea | |||
823ebc0eb5 | |||
41414438d1 | |||
5704560beb | |||
8a48902b64 | |||
61fe059513 | |||
94576cc733 | |||
3a32c62119 | |||
9ea4fe1adc | |||
0798a672c2 | |||
f4e68529ba | |||
92a411baec | |||
034857d088 | |||
e5ce86a9bb | |||
5188b3d52e | |||
61b9435d1f | |||
8244676530 | |||
ec1e7cca85 | |||
72ab9253d7 | |||
4f89255c4f | |||
8d6d1d0d56 | |||
2d0c3a660a | |||
974a208869 | |||
7f225784c2 | |||
a3be95013c | |||
adefbf3960 | |||
9a1ca7a104 | |||
e84e80cd8f | |||
ebc266010a | |||
519277e18b | |||
a5a5874211 |
2
.cz.yaml
2
.cz.yaml
@ -4,5 +4,5 @@ commitizen:
|
||||
prerelease_offset: 1
|
||||
tag_format: $version
|
||||
update_changelog_on_bump: false
|
||||
version: 1.0.0-a2
|
||||
version: 1.0.0-b5
|
||||
version_scheme: semver
|
||||
|
@ -15,6 +15,9 @@ variables:
|
||||
DOCKER_IMAGE_PUBLISH_REGISTRY: docker.io/nofusscomputing
|
||||
DOCKER_IMAGE_PUBLISH_URL: https://hub.docker.com/r/nofusscomputing/$DOCKER_IMAGE_PUBLISH_NAME
|
||||
|
||||
# Extra release commands
|
||||
MY_COMMAND: ./.gitlab/additional_actions_bump.sh
|
||||
|
||||
# Docs NFC
|
||||
PAGES_ENVIRONMENT_PATH: projects/centurion_erp/
|
||||
|
||||
@ -129,7 +132,7 @@ Docker Container:
|
||||
- mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/$CI_JOB_NAME"
|
||||
- mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/tests"
|
||||
- apk update
|
||||
- apk add git
|
||||
- apk add git curl
|
||||
- apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python
|
||||
- python -m ensurepip && ln -sf pip3 /usr/bin/pip
|
||||
- pip install --upgrade pip
|
||||
@ -146,18 +149,42 @@ Docker Container:
|
||||
- git push --set-upstream origin development
|
||||
- RELEASE_VERSION_CURRENT=$(cz version --project)
|
||||
script:
|
||||
- "$MY_COMMAND"
|
||||
- if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease alpha); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi
|
||||
- if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease beta); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi
|
||||
- RELEASE_VERSION_NEW=$(cz version --project)
|
||||
- RELEASE_TAG=$RELEASE_VERSION_NEW
|
||||
- export RELEASE_TAG=$RELEASE_VERSION_NEW
|
||||
- echo "[DEBUG] RELEASE_VERSION_CURRENT[$RELEASE_VERSION_CURRENT]"
|
||||
- echo "[DEBUG] RELEASE_CHANGELOG[$RELEASE_CHANGELOG]"
|
||||
- echo "[DEBUG] RELEASE_VERSION_NEW[$RELEASE_VERSION_NEW]"
|
||||
- echo "[DEBUG] RELEASE_TAG[$RELEASE_TAG]"
|
||||
- RELEASE_TAG_SHA1=$(git log -n1 --format=format:"%H")
|
||||
- echo "[DEBUG] RELEASE_TAG_SHA1[$RELEASE_TAG_SHA1]"
|
||||
|
||||
- |
|
||||
if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then
|
||||
|
||||
echo "[DEBUG] not running extra actions, no new version";
|
||||
|
||||
else
|
||||
|
||||
echo "[DEBUG] Creating new Version Label";
|
||||
|
||||
echo "----------------------------";
|
||||
|
||||
echo ${MY_COMMAND};
|
||||
|
||||
echo "----------------------------";
|
||||
|
||||
cat ${MY_COMMAND};
|
||||
|
||||
echo "----------------------------";
|
||||
|
||||
${MY_COMMAND};
|
||||
|
||||
echo "----------------------------";
|
||||
fi
|
||||
|
||||
- if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No tag to delete, version was not bumped"; else git tag -d $RELEASE_TAG; fi
|
||||
|
||||
|
||||
- if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No push will be conducted, version was not bumped"; else git push; fi
|
||||
- if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No release will be created, version was not bumped"; else release-cli create --name "Release $RELEASE_TAG" --tag-name "$RELEASE_TAG" --tag-message "$RELEASE_CHANGELOG" --ref "$RELEASE_TAG_SHA1" --description "$RELEASE_CHANGELOG"; fi
|
||||
- if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git checkout master; fi
|
||||
|
7
.gitlab/additional_actions_bump.sh
Executable file
7
.gitlab/additional_actions_bump.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Create Version label wtihn repo
|
||||
curl \
|
||||
--data "name=v${RELEASE_TAG}&color=#eee600&description=Version%20that%20is%20affected" \
|
||||
--header "PRIVATE-TOKEN: $GIT_COMMIT_TOKEN" \
|
||||
"https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/labels"
|
@ -24,6 +24,8 @@
|
||||
|
||||
_Breaking Change must also be notated in the commit that introduces it and in [Conventional Commit Format](https://www.conventionalcommits.org/en/v1.0.0/)._
|
||||
|
||||
- [ ] Release notes updated
|
||||
|
||||
- [ ] ~Documentation Documentation written
|
||||
|
||||
_All features to be documented within the correct section(s). Administration, Development and/or User_
|
||||
|
66
CHANGELOG.md
66
CHANGELOG.md
@ -1,3 +1,69 @@
|
||||
## 1.0.0-b5 (2024-07-31)
|
||||
|
||||
### Feat
|
||||
|
||||
- **api**: Add device config groups to devices
|
||||
- **api**: Ability to fetch configgroups from api along with config
|
||||
|
||||
### Fix
|
||||
|
||||
- **api**: Ensure device groups is read only
|
||||
|
||||
## 1.0.0-b4 (2024-07-29)
|
||||
|
||||
### Feat
|
||||
|
||||
- **swagger**: remove `{format}` suffixed doc entries
|
||||
|
||||
### Fix
|
||||
|
||||
- **api**: cleanup team post/get
|
||||
- **api**: confirm HTTP method is allowed before permission check
|
||||
- **api**: Ensure that organizations can't be created via the API
|
||||
- **access**: Team model class inheritance order corrected
|
||||
|
||||
## 1.0.0-b3 (2024-07-21)
|
||||
|
||||
### Fix
|
||||
|
||||
- **itam**: Limit os version count to devices user has access to
|
||||
|
||||
## 1.0.0-b2 (2024-07-19)
|
||||
|
||||
### Fix
|
||||
|
||||
- **itam**: only show os version once
|
||||
|
||||
## 1.0.0-b1 (2024-07-19)
|
||||
|
||||
### Fix
|
||||
|
||||
- **itam**: ensure installed operating system count is limited to users organizations
|
||||
- **itam**: ensure installed software count is limited to users organizations
|
||||
|
||||
## 1.0.0-a4 (2024-07-18)
|
||||
|
||||
### Feat
|
||||
|
||||
- **api**: When processing uploaded inventory and name does not match, update name to one within inventory file
|
||||
- **config_management**: Group name to be entire breadcrumb
|
||||
|
||||
## 1.0.0-a3 (2024-07-18)
|
||||
|
||||
### Feat
|
||||
|
||||
- **config_management**: Prevent a config group from being able to change organization
|
||||
- **itam**: On device organization change remove config groups
|
||||
|
||||
### Fix
|
||||
|
||||
- **config_management**: dont attempt to do action during save if group being created
|
||||
- **itam**: remove org filter for device so that user can see installations
|
||||
- **itam**: remove org filter for operating systems so that user can see installations
|
||||
- **itam**: remove org filter for software so that user can see installations
|
||||
- **itam**: Device related items should not be global.
|
||||
- **itam**: When changing device organization move related items too.
|
||||
|
||||
## 1.0.0-a2 (2024-07-17)
|
||||
|
||||
### Feat
|
||||
|
7
Release-Notes.md
Normal file
7
Release-Notes.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Version 1.0.0
|
||||
|
||||
Initial Release of Centurion ERP.
|
||||
|
||||
## Breaking changes
|
||||
|
||||
- Nil
|
@ -190,7 +190,7 @@ class TenancyObject(SaveHistory):
|
||||
|
||||
|
||||
|
||||
class Team(Group, TenancyObject, SaveHistory):
|
||||
class Team(Group, TenancyObject):
|
||||
class Meta:
|
||||
# proxy = True
|
||||
verbose_name_plural = "Teams"
|
||||
|
@ -5,7 +5,7 @@ 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
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
@ -24,7 +24,7 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie
|
||||
|
||||
url_name = '_api_organization'
|
||||
|
||||
url_list = 'device-list'
|
||||
url_list = '_api_orgs'
|
||||
|
||||
change_data = {'name': 'device'}
|
||||
|
||||
@ -124,6 +124,8 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie
|
||||
delete_team.permissions.set([delete_permissions])
|
||||
|
||||
|
||||
self.super_user = User.objects.create_user(username="super_user", password="password", is_superuser=True)
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
@ -171,3 +173,67 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
||||
|
||||
|
||||
def test_add_is_prohibited_anon_user(self):
|
||||
""" Ensure Organization cant be created
|
||||
|
||||
Attempt to create organization as anon user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
# client.force_login(self.add_user)
|
||||
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_add_is_prohibited_diff_org_user(self):
|
||||
""" Ensure Organization cant be created
|
||||
|
||||
Attempt to create organization as user with different org permissions.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
|
||||
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
def test_add_is_prohibited_super_user(self):
|
||||
""" Ensure Organization cant be created
|
||||
|
||||
Attempt to create organization as user who is super user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
client.force_login(self.super_user)
|
||||
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
|
||||
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
def test_add_is_prohibited_user_same_org(self):
|
||||
""" Ensure Organization cant be created
|
||||
|
||||
Attempt to create organization as user with permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
|
||||
|
||||
assert response.status_code == 405
|
||||
|
@ -63,4 +63,8 @@ class TeamModel(
|
||||
|
||||
@pytest.mark.skip(reason="uses Django group manager")
|
||||
def test_attribute_is_type_objects(self):
|
||||
pass
|
||||
|
||||
@pytest.mark.skip(reason="uses Django group manager")
|
||||
def test_model_class_tenancy_manager_function_get_queryset_called(self):
|
||||
pass
|
@ -14,9 +14,8 @@ class TeamSerializerBase(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = (
|
||||
"id",
|
||||
"team_name",
|
||||
'organization',
|
||||
'team_name',
|
||||
'permissions',
|
||||
'url',
|
||||
)
|
||||
|
||||
@ -29,9 +28,18 @@ class TeamSerializerBase(serializers.ModelSerializer):
|
||||
|
||||
|
||||
|
||||
class TeamPermissionSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Permission
|
||||
depth = 1
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class TeamSerializer(TeamSerializerBase):
|
||||
|
||||
permissions = serializers.SerializerMethodField('get_url')
|
||||
permissions_url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
def get_url(self, obj):
|
||||
|
||||
@ -63,16 +71,18 @@ class TeamSerializer(TeamSerializerBase):
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
depth = 1
|
||||
depth = 2
|
||||
fields = (
|
||||
"id",
|
||||
"team_name",
|
||||
'organization',
|
||||
'permissions',
|
||||
'permissions_url',
|
||||
'url',
|
||||
)
|
||||
read_only_fields = [
|
||||
'permissions',
|
||||
'id',
|
||||
'organization',
|
||||
'permissions_url',
|
||||
'url'
|
||||
]
|
||||
|
||||
@ -111,7 +121,7 @@ class OrganizationSerializer(serializers.ModelSerializer):
|
||||
|
||||
return request.build_absolute_uri(reverse('API:_api_organization_teams', args=[obj.id]))
|
||||
|
||||
teams = TeamSerializerBase(source='team_set', many=True, read_only=False)
|
||||
teams = TeamSerializer(source='team_set', many=True, read_only=False)
|
||||
|
||||
view_name="API:_api_organization"
|
||||
|
||||
|
86
app/api/serializers/config.py
Normal file
86
app/api/serializers/config.py
Normal file
@ -0,0 +1,86 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from config_management.models.groups import ConfigGroups
|
||||
|
||||
|
||||
|
||||
class ParentGroupSerializer(serializers.ModelSerializer):
|
||||
|
||||
url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
|
||||
class Meta:
|
||||
model = ConfigGroups
|
||||
fields = [
|
||||
'id',
|
||||
'name',
|
||||
'url',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def get_url(self, obj):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
|
||||
|
||||
|
||||
|
||||
class ConfigGroupsSerializerBase(serializers.ModelSerializer):
|
||||
|
||||
parent = ParentGroupSerializer(read_only=True)
|
||||
url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
|
||||
class Meta:
|
||||
model = ConfigGroups
|
||||
fields = [
|
||||
'id',
|
||||
'parent',
|
||||
'name',
|
||||
'config',
|
||||
'url',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'config',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def get_url(self, obj):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
|
||||
|
||||
|
||||
|
||||
class ConfigGroupsSerializer(ConfigGroupsSerializerBase):
|
||||
|
||||
|
||||
class Meta:
|
||||
model = ConfigGroups
|
||||
depth = 1
|
||||
fields = [
|
||||
'id',
|
||||
'parent',
|
||||
'name',
|
||||
'config',
|
||||
'url',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'parent',
|
||||
'name',
|
||||
'config',
|
||||
'url',
|
||||
]
|
||||
|
@ -1,9 +1,38 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from itam.models.device import Device
|
||||
from rest_framework import serializers
|
||||
|
||||
from api.serializers.config import ParentGroupSerializer
|
||||
|
||||
from config_management.models.groups import ConfigGroupHosts
|
||||
|
||||
from itam.models.device import Device
|
||||
|
||||
|
||||
|
||||
class DeviceConfigGroupsSerializer(serializers.ModelSerializer):
|
||||
|
||||
name = serializers.CharField(source='group.name', read_only=True)
|
||||
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="API:_api_config_group", format="html"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ConfigGroupHosts
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'name',
|
||||
'url',
|
||||
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
class DeviceSerializer(serializers.ModelSerializer):
|
||||
@ -13,7 +42,9 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
config = serializers.SerializerMethodField('get_device_config')
|
||||
|
||||
|
||||
groups = DeviceConfigGroupsSerializer(source='configgrouphosts_set', many=True, read_only=True)
|
||||
|
||||
def get_device_config(self, device):
|
||||
|
||||
request = self.context.get('request')
|
||||
@ -22,11 +53,29 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = '__all__'
|
||||
|
||||
read_only_fields = [
|
||||
'inventorydate',
|
||||
depth = 1
|
||||
fields = [
|
||||
'id',
|
||||
'is_global',
|
||||
'slug',
|
||||
'name',
|
||||
'config',
|
||||
'serial_number',
|
||||
'uuid',
|
||||
'inventorydate',
|
||||
'created',
|
||||
'modified',
|
||||
'groups',
|
||||
'organization',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'config',
|
||||
'inventorydate',
|
||||
'created',
|
||||
'modified',
|
||||
'groups',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
@ -119,19 +119,35 @@ def process_inventory(self, data, organization: int):
|
||||
|
||||
logger.info(f"Device: {device.name}, Serial: {device.serial_number}, UUID: {device.uuid}")
|
||||
|
||||
device_edited = False
|
||||
|
||||
|
||||
if not device.uuid and device_uuid:
|
||||
|
||||
device.uuid = device_uuid
|
||||
|
||||
device.save()
|
||||
device_edited = True
|
||||
|
||||
|
||||
if not device.serial_number and device_serial_number:
|
||||
|
||||
device.serial_number = data.details.serial_number
|
||||
|
||||
device_edited = True
|
||||
|
||||
|
||||
if str(device.name).lower() != str(data.details.name).lower(): # Update device Name
|
||||
|
||||
device.name = data.details.name
|
||||
|
||||
device_edited = True
|
||||
|
||||
|
||||
if device_edited:
|
||||
|
||||
device.save()
|
||||
|
||||
|
||||
operating_system = OperatingSystem.objects.filter(
|
||||
name=data.operating_system.name,
|
||||
is_global = True
|
||||
|
@ -255,6 +255,20 @@ class InventoryAPI(TestCase):
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_uuid_match(self):
|
||||
""" Device uuid match """
|
||||
|
||||
assert self.device.uuid == self.inventory['details']['uuid']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_serial_number_match(self):
|
||||
""" Device SN match """
|
||||
|
||||
assert self.device.serial_number == self.inventory['details']['serial_number']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_added(self):
|
||||
""" Operating System is created """
|
||||
|
||||
@ -424,3 +438,552 @@ class InventoryAPI(TestCase):
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class InventoryAPIDifferentNameSerialNumberMatch(TestCase):
|
||||
""" Test inventory upload with different name
|
||||
|
||||
should match by serial number
|
||||
"""
|
||||
|
||||
model = Device
|
||||
|
||||
model_name = 'device'
|
||||
app_label = 'itam'
|
||||
|
||||
inventory = {
|
||||
"details": {
|
||||
"name": "device_name",
|
||||
"serial_number": "serial_number_123",
|
||||
"uuid": "string"
|
||||
},
|
||||
"os": {
|
||||
"name": "os_name",
|
||||
"version_major": "12",
|
||||
"version": "12.1"
|
||||
},
|
||||
"software": [
|
||||
{
|
||||
"name": "software_name",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
{
|
||||
"name": "software_name_not_semver",
|
||||
"category": "category_name",
|
||||
"version": "2024.4"
|
||||
},
|
||||
{
|
||||
"name": "software_name_semver_contained",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3-rc1"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user
|
||||
2. Create a team for user with correct permissions
|
||||
3. add user to the teeam
|
||||
4. upload the inventory
|
||||
5. conduct queries for tests
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
Device.objects.create(
|
||||
name='random device name',
|
||||
serial_number='serial_number_123'
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
# upload the inventory
|
||||
process_inventory(json.dumps(self.inventory), organization.id)
|
||||
|
||||
|
||||
self.device = Device.objects.get(name=self.inventory['details']['name'])
|
||||
|
||||
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
|
||||
|
||||
self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major'])
|
||||
|
||||
self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version'])
|
||||
|
||||
self.software = Software.objects.get(name=self.inventory['software'][0]['name'])
|
||||
|
||||
self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category'])
|
||||
|
||||
self.software_version = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][0]['version'],
|
||||
software = self.software,
|
||||
)
|
||||
|
||||
self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name'])
|
||||
|
||||
self.software_version_not_semver = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][1]['version'],
|
||||
software = self.software_not_semver
|
||||
)
|
||||
|
||||
self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name'])
|
||||
|
||||
self.software_version_is_semver = SoftwareVersion.objects.get(
|
||||
software = self.software_is_semver
|
||||
)
|
||||
|
||||
self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software)
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_added(self):
|
||||
""" Device is created """
|
||||
|
||||
assert self.device.name == self.inventory['details']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_uuid_match(self):
|
||||
""" Device uuid match """
|
||||
|
||||
assert self.device.uuid == self.inventory['details']['uuid']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_serial_number_match(self):
|
||||
""" Device SN match """
|
||||
|
||||
assert self.device.serial_number == self.inventory['details']['serial_number']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_added(self):
|
||||
""" Operating System is created """
|
||||
|
||||
assert self.operating_system.name == self.inventory['os']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_version_added(self):
|
||||
""" Operating System version is created """
|
||||
|
||||
assert self.operating_system_version.name == self.inventory['os']['version_major']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_operating_system_added(self):
|
||||
""" Operating System version linked to device """
|
||||
|
||||
assert self.device_operating_system.version == self.inventory['os']['version']
|
||||
|
||||
|
||||
|
||||
@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(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
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_added(self):
|
||||
""" Software category exists """
|
||||
|
||||
assert self.software_category.name == self.inventory['software'][0]['category']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_added(self):
|
||||
""" Test software exists """
|
||||
|
||||
assert self.software.name == self.inventory['software'][0]['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_linked_to_software(self):
|
||||
""" Software category linked to software """
|
||||
|
||||
assert self.software.category == self.software_category
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_added(self):
|
||||
""" Test software version exists """
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_semver(self):
|
||||
""" Software Version from inventory returns semver if within version string """
|
||||
|
||||
assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0]
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_original_version(self):
|
||||
""" Software Version from inventory returns inventoried version if no semver found """
|
||||
|
||||
assert self.software_version_not_semver.name == self.inventory['software'][1]['version']
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_linked_to_software(self):
|
||||
""" Test software version linked to software it belongs too """
|
||||
|
||||
assert self.software_version.software == self.software
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_software_version(self):
|
||||
""" Inventoried software is linked to device and it's the corret one"""
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_has_installed_date(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert self.device_software.installed is not None
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_installed_date_type(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert type(self.device_software.installed) is datetime.datetime
|
||||
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class InventoryAPIDifferentNameUUIDMatch(TestCase):
|
||||
""" Test inventory upload with different name
|
||||
|
||||
should match by uuid
|
||||
"""
|
||||
|
||||
model = Device
|
||||
|
||||
model_name = 'device'
|
||||
app_label = 'itam'
|
||||
|
||||
inventory = {
|
||||
"details": {
|
||||
"name": "device_name",
|
||||
"serial_number": "serial_number_123",
|
||||
"uuid": "123-456-789"
|
||||
},
|
||||
"os": {
|
||||
"name": "os_name",
|
||||
"version_major": "12",
|
||||
"version": "12.1"
|
||||
},
|
||||
"software": [
|
||||
{
|
||||
"name": "software_name",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
{
|
||||
"name": "software_name_not_semver",
|
||||
"category": "category_name",
|
||||
"version": "2024.4"
|
||||
},
|
||||
{
|
||||
"name": "software_name_semver_contained",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3-rc1"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user
|
||||
2. Create a team for user with correct permissions
|
||||
3. add user to the teeam
|
||||
4. upload the inventory
|
||||
5. conduct queries for tests
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
Device.objects.create(
|
||||
name='random device name',
|
||||
uuid='123-456-789'
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
# upload the inventory
|
||||
process_inventory(json.dumps(self.inventory), organization.id)
|
||||
|
||||
|
||||
self.device = Device.objects.get(name=self.inventory['details']['name'])
|
||||
|
||||
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
|
||||
|
||||
self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major'])
|
||||
|
||||
self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version'])
|
||||
|
||||
self.software = Software.objects.get(name=self.inventory['software'][0]['name'])
|
||||
|
||||
self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category'])
|
||||
|
||||
self.software_version = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][0]['version'],
|
||||
software = self.software,
|
||||
)
|
||||
|
||||
self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name'])
|
||||
|
||||
self.software_version_not_semver = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][1]['version'],
|
||||
software = self.software_not_semver
|
||||
)
|
||||
|
||||
self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name'])
|
||||
|
||||
self.software_version_is_semver = SoftwareVersion.objects.get(
|
||||
software = self.software_is_semver
|
||||
)
|
||||
|
||||
self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software)
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_added(self):
|
||||
""" Device is created """
|
||||
|
||||
assert self.device.name == self.inventory['details']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_uuid_match(self):
|
||||
""" Device uuid match """
|
||||
|
||||
assert self.device.uuid == self.inventory['details']['uuid']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_serial_number_match(self):
|
||||
""" Device SN match """
|
||||
|
||||
assert self.device.serial_number == self.inventory['details']['serial_number']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_added(self):
|
||||
""" Operating System is created """
|
||||
|
||||
assert self.operating_system.name == self.inventory['os']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_version_added(self):
|
||||
""" Operating System version is created """
|
||||
|
||||
assert self.operating_system_version.name == self.inventory['os']['version_major']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_operating_system_added(self):
|
||||
""" Operating System version linked to device """
|
||||
|
||||
assert self.device_operating_system.version == self.inventory['os']['version']
|
||||
|
||||
|
||||
|
||||
@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(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
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_added(self):
|
||||
""" Software category exists """
|
||||
|
||||
assert self.software_category.name == self.inventory['software'][0]['category']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_added(self):
|
||||
""" Test software exists """
|
||||
|
||||
assert self.software.name == self.inventory['software'][0]['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_linked_to_software(self):
|
||||
""" Software category linked to software """
|
||||
|
||||
assert self.software.category == self.software_category
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_added(self):
|
||||
""" Test software version exists """
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_semver(self):
|
||||
""" Software Version from inventory returns semver if within version string """
|
||||
|
||||
assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0]
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_original_version(self):
|
||||
""" Software Version from inventory returns inventoried version if no semver found """
|
||||
|
||||
assert self.software_version_not_semver.name == self.inventory['software'][1]['version']
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_linked_to_software(self):
|
||||
""" Test software version linked to software it belongs too """
|
||||
|
||||
assert self.software_version.software == self.software
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_software_version(self):
|
||||
""" Inventoried software is linked to device and it's the corret one"""
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_has_installed_date(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert self.device_software.installed is not None
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_installed_date_type(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert type(self.device_software.installed) is datetime.datetime
|
||||
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
@ -3,7 +3,7 @@ from django.urls import path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
|
||||
from .views import access, index
|
||||
from .views import access, config, index
|
||||
|
||||
from .views.itam import software, config as itam_config
|
||||
from .views.itam.device import DeviceViewSet
|
||||
@ -24,6 +24,9 @@ router.register('software', software.SoftwareViewSet, basename='software')
|
||||
urlpatterns = [
|
||||
path("config/<slug:slug>/", itam_config.View.as_view(), name="_api_device_config"),
|
||||
|
||||
path("configuration/", config.ConfigGroupsList.as_view(), name='_api_config_groups'),
|
||||
path("configuration/<int:pk>", config.ConfigGroupsDetail.as_view(), name='_api_config_group'),
|
||||
|
||||
path("device/inventory", inventory.Collect.as_view(), name="_api_device_inventory"),
|
||||
|
||||
path("organization/", access.OrganizationList.as_view(), name='_api_orgs'),
|
||||
|
@ -1,5 +1,7 @@
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
|
||||
|
||||
from rest_framework import generics, routers, serializers, views
|
||||
from rest_framework.permissions import DjangoObjectPermissions
|
||||
from rest_framework.response import Response
|
||||
@ -7,12 +9,17 @@ from rest_framework.response import Response
|
||||
from access.mixin import OrganizationMixin
|
||||
from access.models import Organization, Team
|
||||
|
||||
from api.serializers.access import OrganizationSerializer, OrganizationListSerializer, TeamSerializer
|
||||
from api.serializers.access import OrganizationSerializer, OrganizationListSerializer, TeamSerializer, TeamPermissionSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
|
||||
|
||||
class OrganizationList(generics.ListCreateAPIView):
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Fetch Organizations",
|
||||
description="Returns a list of organizations."
|
||||
),
|
||||
)
|
||||
class OrganizationList(generics.ListAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
@ -28,7 +35,18 @@ class OrganizationList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
|
||||
class OrganizationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Get An Organization",
|
||||
),
|
||||
patch=extend_schema(
|
||||
summary = "Update an organization",
|
||||
),
|
||||
put=extend_schema(
|
||||
summary = "Update an organization",
|
||||
),
|
||||
)
|
||||
class OrganizationDetail(generics.RetrieveUpdateAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
@ -44,6 +62,20 @@ class OrganizationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(
|
||||
summary = "Create a Team",
|
||||
description = """Create a team within the defined organization.""",
|
||||
tags = ['team',],
|
||||
request = TeamSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
create=extend_schema(exclude=True),
|
||||
)
|
||||
class TeamList(generics.ListCreateAPIView):
|
||||
|
||||
permission_classes = [
|
||||
@ -66,6 +98,45 @@ class TeamList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Fetch a Team",
|
||||
description = """Fetch a team within the defined organization.
|
||||
""",
|
||||
methods=["GET"],
|
||||
tags = ['team',],
|
||||
request = TeamSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
patch=extend_schema(
|
||||
summary = "Update a Team",
|
||||
description = """Update a team within the defined organization.
|
||||
""",
|
||||
methods=["Patch"],
|
||||
tags = ['team',],
|
||||
request = TeamSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
put = extend_schema(
|
||||
summary = "Amend a team",
|
||||
tags = ['team',],
|
||||
),
|
||||
delete=extend_schema(
|
||||
summary = "Delete a Team",
|
||||
tags = ['team',],
|
||||
),
|
||||
post = extend_schema(
|
||||
exclude = True,
|
||||
)
|
||||
)
|
||||
class TeamDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
permission_classes = [
|
||||
@ -79,12 +150,66 @@ class TeamDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
|
||||
|
||||
class TeamPermissionDetail(routers.APIRootView):
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Fetch a teams permissions",
|
||||
tags = ['team',],
|
||||
),
|
||||
post=extend_schema(
|
||||
summary = "Replace team Permissions",
|
||||
description = """Replace the teams permissions with the permissions supplied.
|
||||
|
||||
# temp disabled until permission checker updated
|
||||
# permission_classes = [
|
||||
# OrganizationPermissionAPI
|
||||
# ]
|
||||
Teams Permissions will be replaced with the permissions supplied. **ALL** existing permissions will be
|
||||
removed.
|
||||
|
||||
permissions are required to be in format `<module name>_<permission>_<table name>`
|
||||
""",
|
||||
|
||||
methods=["POST"],
|
||||
tags = ['team',],
|
||||
request = TeamPermissionSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
delete=extend_schema(
|
||||
summary = "Delete permissions",
|
||||
tags = ['team',],
|
||||
),
|
||||
patch = extend_schema(
|
||||
summary = "Amend team Permissions",
|
||||
description = """Amend the teams permissions with the permissions supplied.
|
||||
|
||||
Teams permissions will include the existing permissions along with the ones supplied.
|
||||
permissions are required to be in format `<module name>_<permission>_<table name>`
|
||||
""",
|
||||
|
||||
methods=["PATCH"],
|
||||
parameters = None,
|
||||
tags = ['team',],
|
||||
request = TeamPermissionSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
put = extend_schema(
|
||||
summary = "Amend team Permissions",
|
||||
tags = ['team',],
|
||||
)
|
||||
)
|
||||
class TeamPermissionDetail(views.APIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = Team.objects.all()
|
||||
|
||||
serializer_class = TeamPermissionSerializer
|
||||
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
54
app/api/views/config.py
Normal file
54
app/api/views/config.py
Normal file
@ -0,0 +1,54 @@
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
|
||||
from rest_framework import generics
|
||||
|
||||
from api.serializers.config import ConfigGroupsSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
from config_management.models.groups import ConfigGroups
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Fetch Config groups",
|
||||
description="Returns a list of Config Groups."
|
||||
),
|
||||
)
|
||||
class ConfigGroupsList(generics.ListAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = ConfigGroups.objects.all()
|
||||
lookup_field = 'pk'
|
||||
serializer_class = ConfigGroupsSerializer
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Config Groups"
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Get A Config Group",
|
||||
# responses = {}
|
||||
),
|
||||
)
|
||||
class ConfigGroupsDetail(generics.RetrieveAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = ConfigGroups.objects.all()
|
||||
lookup_field = 'pk'
|
||||
serializer_class = ConfigGroupsSerializer
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Config Group"
|
||||
|
||||
|
@ -27,6 +27,7 @@ class Index(viewsets.ViewSet):
|
||||
{
|
||||
# "teams": reverse("_api_teams", request=request),
|
||||
"devices": reverse("API:device-list", request=request),
|
||||
"config_groups": reverse("API:_api_config_groups", request=request),
|
||||
"organizations": reverse("API:_api_orgs", request=request),
|
||||
"software": reverse("API:software-list", request=request),
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.forms import ValidationError
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.permissions import DjangoObjectPermissions
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
@ -28,12 +29,16 @@ class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin):
|
||||
|
||||
self.request = request
|
||||
|
||||
method = self.request._request.method.lower()
|
||||
|
||||
if method.upper() not in view.allowed_methods:
|
||||
|
||||
view.http_method_not_allowed(request._request)
|
||||
|
||||
if hasattr(view, 'queryset'):
|
||||
if view.queryset.model._meta:
|
||||
self.obj = view.queryset.model
|
||||
|
||||
method = self.request._request.method.lower()
|
||||
|
||||
object_organization = None
|
||||
|
||||
if method == 'get':
|
||||
|
@ -307,6 +307,9 @@ curl:
|
||||
'SWAGGER_UI_DIST': 'SIDECAR',
|
||||
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
|
||||
'REDOC_DIST': 'SIDECAR',
|
||||
'PREPROCESSING_HOOKS': [
|
||||
'drf_spectacular.hooks.preprocess_exclude_path_format'
|
||||
],
|
||||
}
|
||||
|
||||
DATETIME_FORMAT = 'j N Y H:i:s'
|
||||
|
@ -7,6 +7,7 @@ from access.tests.abstract.tenancy_object import TenancyObject as TenancyObjectT
|
||||
from app.tests.abstract.views import AddView, ChangeView, DeleteView, DisplayView, IndexView
|
||||
|
||||
from core.mixin.history_save import SaveHistory
|
||||
from core.tests.abstract.models import Models
|
||||
|
||||
|
||||
|
||||
@ -30,7 +31,8 @@ class BaseModel:
|
||||
|
||||
class TenancyModel(
|
||||
BaseModel,
|
||||
TenancyObjectTestCases
|
||||
TenancyObjectTestCases,
|
||||
Models
|
||||
):
|
||||
""" Test cases for tenancy models"""
|
||||
|
||||
|
@ -186,6 +186,15 @@ class ConfigGroups(GroupsCommonFields, SaveHistory):
|
||||
if self.parent:
|
||||
self.organization = ConfigGroups.objects.get(id=self.parent.id).organization
|
||||
|
||||
if self.pk:
|
||||
|
||||
obj = ConfigGroups.objects.get(
|
||||
id = self.id,
|
||||
)
|
||||
|
||||
# Prevent organization change. ToDo: add feature so that config can change organizations
|
||||
self.organization = obj.organization
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
@ -193,7 +202,7 @@ class ConfigGroups(GroupsCommonFields, SaveHistory):
|
||||
|
||||
if self.parent:
|
||||
|
||||
return f'{self.parent.name} > {self.name}'
|
||||
return f'{self.parent} > {self.name}'
|
||||
|
||||
return self.name
|
||||
|
||||
|
@ -0,0 +1,224 @@
|
||||
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 Client, TestCase
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from app.tests.abstract.models import TenancyModel
|
||||
|
||||
from config_management.models.groups import ConfigGroups
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class ConfigGroupsAPI(
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = ConfigGroups
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
2. Create an item
|
||||
|
||||
"""
|
||||
|
||||
self.organization = Organization.objects.create(name='test_org')
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'one',
|
||||
config = dict({"key": "one", "existing": "dont_over_write"})
|
||||
)
|
||||
|
||||
self.second_item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'one_two',
|
||||
config = dict({"key": "two"}),
|
||||
parent = self.item
|
||||
)
|
||||
|
||||
self.url_view_kwargs = {'pk': self.second_item.id}
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = self.organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_config_group', kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
self.api_data = response.data
|
||||
|
||||
|
||||
def test_api_field_exists_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_parent(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
parent field must exist
|
||||
"""
|
||||
|
||||
assert 'parent' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_parent(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
parent field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['parent']) is dict
|
||||
|
||||
|
||||
def test_api_field_exists_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_config(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
config field must exist
|
||||
"""
|
||||
|
||||
assert 'config' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_config(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
config field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['config']) is dict
|
||||
|
||||
|
||||
def test_api_field_exists_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
url field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['url']) is str
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_parent_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
parent.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['parent']
|
||||
|
||||
|
||||
def test_api_field_type_parent_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
parent.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['parent']['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_parent_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
parent.name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data['parent']
|
||||
|
||||
|
||||
def test_api_field_type_parent_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
parent.name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['parent']['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_parent_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
parent.url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data['parent']
|
||||
|
||||
|
||||
def test_api_field_type_parent_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
parent.url field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['parent']['url']) is str
|
@ -1,7 +1,6 @@
|
||||
import json
|
||||
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.db.models import Count, Q
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
@ -47,13 +46,7 @@ class GroupIndexView(IndexView):
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
|
||||
return self.model.objects.filter(parent=None).order_by('name')
|
||||
|
||||
else:
|
||||
|
||||
return self.model.objects.filter(Q(parent=None, organization__in=self.user_organizations()) | Q(parent=None, is_global = True)).order_by('name')
|
||||
return self.model.objects.filter(parent=None).order_by('name')
|
||||
|
||||
|
||||
|
||||
|
@ -3,21 +3,38 @@ import unittest
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from access.models import TenancyManager
|
||||
|
||||
|
||||
class Models:
|
||||
""" Test cases for Model Abstract Classes """
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="write test")
|
||||
|
||||
def test_model_class_tenancy_manager_function_get_queryset(self):
|
||||
""" Function Check
|
||||
|
||||
function `get_queryset()` must exist
|
||||
"""
|
||||
|
||||
pass
|
||||
assert hasattr(self.model.objects, 'get_queryset')
|
||||
|
||||
assert callable(self.model.objects.get_queryset)
|
||||
|
||||
|
||||
@patch.object(TenancyManager, 'get_queryset')
|
||||
def test_model_class_tenancy_manager_function_get_queryset_called(self, get_queryset):
|
||||
""" Function Check
|
||||
|
||||
function `access.models.TenancyManager.get_queryset()` within the Tenancy manager must
|
||||
be called as this function limits queries to the current users organizations.
|
||||
"""
|
||||
|
||||
self.model.objects.filter()
|
||||
|
||||
assert get_queryset.called
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="write test")
|
||||
|
@ -76,7 +76,6 @@ class Device(DeviceCommonFieldsName, SaveHistory):
|
||||
null = True,
|
||||
blank= True,
|
||||
help_text = 'Type of device.',
|
||||
|
||||
)
|
||||
|
||||
|
||||
@ -86,6 +85,43 @@ class Device(DeviceCommonFieldsName, SaveHistory):
|
||||
blank = True,
|
||||
)
|
||||
|
||||
def save(
|
||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||
):
|
||||
""" Save Device Model
|
||||
|
||||
After saving the device update the related items so that they are a part
|
||||
of the same organization as the device.
|
||||
"""
|
||||
|
||||
super().save(
|
||||
force_insert=False, force_update=False, using=None, update_fields=None
|
||||
)
|
||||
|
||||
models_to_update =[
|
||||
DeviceSoftware,
|
||||
DeviceOperatingSystem
|
||||
]
|
||||
|
||||
for update_model in models_to_update:
|
||||
|
||||
obj = update_model.objects.filter(
|
||||
device = self.id,
|
||||
)
|
||||
|
||||
if obj.exists():
|
||||
|
||||
obj.update(
|
||||
is_global = False,
|
||||
organization = self.organization,
|
||||
)
|
||||
|
||||
from config_management.models.groups import ConfigGroupHosts
|
||||
|
||||
ConfigGroupHosts.objects.filter(
|
||||
host = self.id,
|
||||
).delete()
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@ -258,6 +294,16 @@ class DeviceSoftware(DeviceCommonFields, SaveHistory):
|
||||
return self.device
|
||||
|
||||
|
||||
def save(
|
||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||
):
|
||||
|
||||
self.is_global = False
|
||||
|
||||
super().save(
|
||||
force_insert=False, force_update=False, using=None, update_fields=None
|
||||
)
|
||||
|
||||
|
||||
class DeviceOperatingSystem(DeviceCommonFields, SaveHistory):
|
||||
|
||||
@ -300,3 +346,14 @@ class DeviceOperatingSystem(DeviceCommonFields, SaveHistory):
|
||||
""" Fetch the parent object """
|
||||
|
||||
return self.device
|
||||
|
||||
|
||||
def save(
|
||||
self, force_insert=False, force_update=False, using=None, update_fields=None
|
||||
):
|
||||
|
||||
self.is_global = False
|
||||
|
||||
super().save(
|
||||
force_insert=False, force_update=False, using=None, update_fields=None
|
||||
)
|
||||
|
@ -38,6 +38,16 @@ class Device(
|
||||
# name = 'deviceone'
|
||||
# )
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_move_organization(user):
|
||||
"""Move Organization test
|
||||
|
||||
When a device moves organization, devicesoftware and devicesoftware table data
|
||||
must also move organizations
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_device_software_action(user):
|
||||
"""Ensure only software that is from the same organization or is global can be added to the device
|
||||
|
522
app/itam/tests/unit/device/test_device_api.py
Normal file
522
app/itam/tests/unit/device/test_device_api.py
Normal file
@ -0,0 +1,522 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from api.tests.abstract.api_permissions import APIPermissions
|
||||
|
||||
from config_management.models.groups import ConfigGroups, ConfigGroupHosts
|
||||
|
||||
from itam.models.device import Device
|
||||
|
||||
|
||||
class DeviceAPI(TestCase):
|
||||
|
||||
|
||||
model = Device
|
||||
|
||||
|
||||
@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
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization=organization,
|
||||
name = 'deviceone',
|
||||
uuid = 'val',
|
||||
serial_number = 'another val'
|
||||
)
|
||||
|
||||
config_group = ConfigGroups.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'one',
|
||||
config = dict({"key": "one", "existing": "dont_over_write"})
|
||||
)
|
||||
|
||||
config_group_second_item = ConfigGroups.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'one_two',
|
||||
config = dict({"key": "two"}),
|
||||
parent = config_group
|
||||
)
|
||||
|
||||
config_group_hosts = ConfigGroupHosts.objects.create(
|
||||
organization = organization,
|
||||
host = self.item,
|
||||
group = config_group,
|
||||
)
|
||||
|
||||
|
||||
config_group_hosts_two = ConfigGroupHosts.objects.create(
|
||||
organization = organization,
|
||||
host = self.item,
|
||||
group = config_group_second_item,
|
||||
)
|
||||
|
||||
|
||||
# self.url_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.url_view_kwargs = {'pk': self.item.id}
|
||||
|
||||
# self.add_data = {'name': 'device', 'organization': self.organization.id}
|
||||
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
|
||||
# add_permissions = Permission.objects.get(
|
||||
# codename = 'add_' + self.model._meta.model_name,
|
||||
# content_type = ContentType.objects.get(
|
||||
# app_label = self.model._meta.app_label,
|
||||
# model = self.model._meta.model_name,
|
||||
# )
|
||||
# )
|
||||
|
||||
# add_team = Team.objects.create(
|
||||
# team_name = 'add_team',
|
||||
# organization = organization,
|
||||
# )
|
||||
|
||||
# add_team.permissions.set([add_permissions])
|
||||
|
||||
|
||||
|
||||
# change_permissions = Permission.objects.get(
|
||||
# codename = 'change_' + self.model._meta.model_name,
|
||||
# content_type = ContentType.objects.get(
|
||||
# app_label = self.model._meta.app_label,
|
||||
# model = self.model._meta.model_name,
|
||||
# )
|
||||
# )
|
||||
|
||||
# change_team = Team.objects.create(
|
||||
# team_name = 'change_team',
|
||||
# organization = organization,
|
||||
# )
|
||||
|
||||
# change_team.permissions.set([change_permissions])
|
||||
|
||||
|
||||
|
||||
# delete_permissions = Permission.objects.get(
|
||||
# codename = 'delete_' + self.model._meta.model_name,
|
||||
# content_type = ContentType.objects.get(
|
||||
# app_label = self.model._meta.app_label,
|
||||
# model = self.model._meta.model_name,
|
||||
# )
|
||||
# )
|
||||
|
||||
# delete_team = Team.objects.create(
|
||||
# team_name = 'delete_team',
|
||||
# organization = organization,
|
||||
# )
|
||||
|
||||
# delete_team.permissions.set([delete_permissions])
|
||||
|
||||
|
||||
# self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
# self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
# teamuser = TeamUsers.objects.create(
|
||||
# team = add_team,
|
||||
# user = self.add_user
|
||||
# )
|
||||
|
||||
# self.change_user = User.objects.create_user(username="test_user_change", password="password")
|
||||
# teamuser = TeamUsers.objects.create(
|
||||
# team = change_team,
|
||||
# user = self.change_user
|
||||
# )
|
||||
|
||||
# self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
|
||||
# teamuser = TeamUsers.objects.create(
|
||||
# team = delete_team,
|
||||
# user = self.delete_user
|
||||
# )
|
||||
|
||||
|
||||
# self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
# different_organization_team = Team.objects.create(
|
||||
# team_name = 'different_organization_team',
|
||||
# organization = different_organization,
|
||||
# )
|
||||
|
||||
# different_organization_team.permissions.set([
|
||||
# view_permissions,
|
||||
# add_permissions,
|
||||
# change_permissions,
|
||||
# delete_permissions,
|
||||
# ])
|
||||
|
||||
# TeamUsers.objects.create(
|
||||
# team = different_organization_team,
|
||||
# user = self.different_organization_user
|
||||
# )
|
||||
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:device-detail', kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
self.api_data = response.data
|
||||
|
||||
|
||||
def test_api_field_exists_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_is_global(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
is_global field must exist
|
||||
"""
|
||||
|
||||
assert 'is_global' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_is_global(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
is_global field must be boolean
|
||||
"""
|
||||
|
||||
assert type(self.api_data['is_global']) is bool
|
||||
|
||||
|
||||
def test_api_field_exists_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_config(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
config field must exist
|
||||
"""
|
||||
|
||||
assert 'config' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_config(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
config field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['config']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_serial_number(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
serial_number field must exist
|
||||
"""
|
||||
|
||||
assert 'serial_number' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_serial_number(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
serial_number field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['serial_number']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_uuid(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
uuid field must exist
|
||||
"""
|
||||
|
||||
assert 'uuid' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_uuid(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
uuid field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['uuid']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_inventorydate(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
inventorydate field must exist
|
||||
"""
|
||||
|
||||
assert 'inventorydate' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_inventorydate(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
inventorydate field must be str
|
||||
"""
|
||||
|
||||
assert (
|
||||
type(self.api_data['inventorydate']) is str
|
||||
or
|
||||
self.api_data['inventorydate'] is None
|
||||
)
|
||||
|
||||
|
||||
def test_api_field_exists_created(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
created field must exist
|
||||
"""
|
||||
|
||||
assert 'created' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_created(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
created field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['created']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_modified(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
modified field must exist
|
||||
"""
|
||||
|
||||
assert 'modified' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_modified(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
modified field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['modified']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_groups(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
groups field must exist
|
||||
"""
|
||||
|
||||
assert 'groups' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_groups(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
groups field must be list
|
||||
"""
|
||||
|
||||
assert type(self.api_data['groups']) is list
|
||||
|
||||
|
||||
def test_api_field_exists_organization(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
organization field must exist
|
||||
"""
|
||||
|
||||
assert 'organization' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_organization(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
organization field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['organization']) is dict
|
||||
|
||||
|
||||
def test_api_field_exists_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
url field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['url']) is Hyperlink
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_organization_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
organization.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['organization']
|
||||
|
||||
|
||||
def test_api_field_type_organization_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
organization.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['organization']['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_organization_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
organization.name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data['organization']
|
||||
|
||||
|
||||
def test_api_field_type_organization_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
organization.name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['organization']['name']) is str
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_groups_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
groups.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['groups'][0]
|
||||
|
||||
|
||||
def test_api_field_type_groups_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
groups.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['groups'][0]['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_groups_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
groups.name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data['groups'][0]
|
||||
|
||||
|
||||
def test_api_field_type_groups_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
groups.name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['groups'][0]['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_groups_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
groups.url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data['groups'][0]
|
||||
|
||||
|
||||
def test_api_field_type_groups_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
groups.url field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['groups'][0]['url']) is Hyperlink
|
@ -3,7 +3,6 @@ import markdown
|
||||
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
@ -61,7 +60,7 @@ class IndexView(IndexView):
|
||||
|
||||
else:
|
||||
|
||||
return Device.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
|
||||
return Device.objects.filter().order_by('name')
|
||||
|
||||
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.db.models import Q, Count
|
||||
from django.db.models import Count, Q
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
@ -40,7 +40,7 @@ class IndexView(IndexView):
|
||||
|
||||
else:
|
||||
|
||||
return OperatingSystem.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
|
||||
return OperatingSystem.objects.filter().order_by('name')
|
||||
|
||||
|
||||
|
||||
@ -62,7 +62,21 @@ class View(ChangeView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
operating_system_versions = OperatingSystemVersion.objects.filter(operating_system=self.kwargs['pk']).order_by('name').annotate(installs=Count("deviceoperatingsystem"))
|
||||
operating_system_versions = OperatingSystemVersion.objects.filter(
|
||||
operating_system=self.kwargs['pk']
|
||||
).order_by(
|
||||
'name'
|
||||
).annotate(
|
||||
installs=Count(
|
||||
"deviceoperatingsystem",
|
||||
filter=Q(deviceoperatingsystem__device__organization__in = self.user_organizations())
|
||||
),
|
||||
# filter=Q(deviceoperatingsystem__operating_system_version__organization__in = self.user_organizations())
|
||||
# filter=Q(deviceoperatingsystem__operating_system_version__deviceoperatingsystem__device__organization__in = self.user_organizations()),
|
||||
filter=Q(deviceoperatingsystem__operating_system_version__organization__in = self.user_organizations()),
|
||||
|
||||
)
|
||||
|
||||
context['operating_system_versions'] = operating_system_versions
|
||||
|
||||
installs = DeviceOperatingSystem.objects.filter(operating_system_version__operating_system_id=self.kwargs['pk'])
|
||||
|
@ -47,7 +47,7 @@ class IndexView(IndexView):
|
||||
|
||||
else:
|
||||
|
||||
return Software.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
|
||||
return Software.objects.filter().order_by('name')
|
||||
|
||||
|
||||
|
||||
@ -71,9 +71,12 @@ class View(ChangeView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
software_versions = SoftwareVersion.objects.filter(
|
||||
software=self.kwargs['pk']
|
||||
software=self.kwargs['pk'],
|
||||
).annotate(
|
||||
installs=Count("installedversion")
|
||||
installs=Count(
|
||||
"installedversion",
|
||||
filter=Q(installedversion__organization__in = self.user_organizations())
|
||||
)
|
||||
)
|
||||
|
||||
context['software_versions'] = software_versions
|
||||
@ -98,9 +101,9 @@ class View(ChangeView):
|
||||
)
|
||||
|
||||
elif not self.request.user.is_superuser:
|
||||
|
||||
context['device_software'] = DeviceSoftware.objects.filter(
|
||||
Q(device__in=self.user_organizations(),
|
||||
software=self.kwargs['pk'])
|
||||
software=self.kwargs['pk']
|
||||
).order_by(
|
||||
'device',
|
||||
'organization'
|
||||
|
@ -1,5 +1,3 @@
|
||||
from django.db.models import Q
|
||||
|
||||
from core.views.common import IndexView
|
||||
|
||||
from itam.models.device_models import DeviceModel
|
||||
@ -23,13 +21,8 @@ class Index(IndexView):
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
return self.model.objects.filter().order_by('name')
|
||||
|
||||
return self.model.objects.filter().order_by('name')
|
||||
|
||||
else:
|
||||
|
||||
return self.model.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -1,5 +1,3 @@
|
||||
from django.db.models import Q
|
||||
|
||||
from core.views.common import IndexView
|
||||
|
||||
from itam.models.device import DeviceType
|
||||
@ -23,13 +21,7 @@ class Index(IndexView):
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
|
||||
return self.model.objects.filter().order_by('name')
|
||||
|
||||
else:
|
||||
|
||||
return self.model.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
|
||||
return self.model.objects.filter().order_by('name')
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -1,5 +1,4 @@
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import generic
|
||||
|
@ -22,13 +22,7 @@ class Index(IndexView):
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
|
||||
return self.model.objects.filter().order_by('name')
|
||||
|
||||
else:
|
||||
|
||||
return self.model.objects.filter(organization__in=self.user_organizations()).order_by('name')
|
||||
return self.model.objects.filter().order_by('name')
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Authentication
|
||||
description: Authentication administration documentation for Centurion ERP by No Fuss Computing
|
||||
date: 2024-07-19
|
||||
template: project.html
|
||||
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp
|
||||
---
|
||||
|
||||
Centurion ERP requires that a user be authenticated to access the ERP features of the application. The built in authentication system as well as Single Sign on (SSO) via an identity broker is available.
|
11
docs/projects/centurion_erp/administration/backup.md
Normal file
11
docs/projects/centurion_erp/administration/backup.md
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
title: Backup
|
||||
description: Backup documentation for Centurion ERP by No Fuss Computing
|
||||
date: 2024-07-19
|
||||
template: project.html
|
||||
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp
|
||||
---
|
||||
|
||||
Most Data within Centurion ERP resides within the database. This simplifies the backup/restoration process as only the database the application uses needs to be backed up.
|
||||
|
||||
Tasks that have been sent to the RabbitMQ server will remain within the task queue, if Centurion ERP has not processed them. Should you wish not to loose tasks you should also backup the rabbitMQ server.
|
@ -9,24 +9,15 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen
|
||||
This documentation is targeted towards those whom administer the applications deployment.
|
||||
|
||||
|
||||
## Installation
|
||||
## Contents
|
||||
|
||||
To install this application you must have a container engine installed, both docker and kubernetes are supported. The container image is available on [Docker Hub](https://hub.docker.com/r/nofusscomputing/centurion-erp) and can be pulled with `docker pull nofusscomputing/centurion-erp:latest`.
|
||||
- [Authentication](./authentication.md)
|
||||
|
||||
Settings for the application are stored within a docker volume at path `/etc/itsm/`, with the settings living in `.py` files. A database is also required for the application to store it's settings. SQLLite and MariaDB/MySQL are supported.
|
||||
- [Backup](./backup.md)
|
||||
|
||||
- [Installation](./installation.md)
|
||||
|
||||
|
||||
### Background workers
|
||||
## Ansible Automation Platform / AWX
|
||||
|
||||
This application requires that you deploy at least one background worker. The background worker requires access to a RabbitMQ message broker for the queueing and routing of messages (background jobs). If you are using our docker container to deploy this application, launch an additional container with `celery -A app worker -l INFO` as the entrypoint/command. Configuration for the worker resides in directory `/etc/itsm/` within the container. see below for the `CELERY_` configuration.
|
||||
|
||||
|
||||
### Settings file
|
||||
|
||||
The settings file is a python file `.py` and must remain a valid python file for the application to work.
|
||||
|
||||
``` py title="settings.py"
|
||||
|
||||
--8<-- "includes/etc/itsm/settings.py"
|
||||
|
||||
```
|
||||
We have built an [Ansible Collection](../../ansible/collections/centurion/index.md) for Centurion ERP that you could consider the bridge between the config within Centurion and the end device. This collection can be directly added to AAP / AWX as a project which enables accessing the features the collection has to offer. Please refer to the [collections documentation](../../ansible/collections/centurion/index.md) for further information.
|
||||
|
78
docs/projects/centurion_erp/administration/installation.md
Normal file
78
docs/projects/centurion_erp/administration/installation.md
Normal file
@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Installation
|
||||
description: Installation documentation for Centurion ERP by No Fuss Computing
|
||||
date: 2024-07-19
|
||||
template: project.html
|
||||
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp
|
||||
---
|
||||
|
||||
Centurion ERP is a simple application to deploy with the only additional requirements being that you have already deployed a database server and a RabbitMQ server. Centurion ERP is container based and is deployable via Docker or upon Kubernetes. Our images are available on [Docker Hub](https://hub.docker.com/r/nofusscomputing/centurion-erp).
|
||||
|
||||
!!! note "TL;DR"
|
||||
`docker pull nofusscomputing/centurion-erp:latest`.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Basic installation steps are as follows:
|
||||
|
||||
1. Deploy a Database Server
|
||||
|
||||
1. Deploy a RabbitMQ Server
|
||||
|
||||
1. Deploy a Web container for Centurion ERP
|
||||
|
||||
1. Deploy a Worker container for Centurion ERP
|
||||
|
||||
1. Add settings file to path `/etc/itsm/settings.py` for both Centurion ERP containers.
|
||||
|
||||
1. Run migrations
|
||||
|
||||
- Docker `docker exec -ti <container name or id> -- python manage.py migrate`
|
||||
|
||||
- Kubernetes `kubectl exec -ti -n <namespace> deploy/<deployment-name> -- python manage.py migrate`
|
||||
|
||||
|
||||
### Database Server
|
||||
|
||||
As Centurion ERP is uses the Django Framework, Theoretically Every Django supported database is available. The reality is however, that we have only used PostgreSQL Server with Centurion ERP. By default if no database is configured a SQLite database will be used. This allows [tests](../development/testing.md) to function and to quickly spin up a deployment for testing.
|
||||
|
||||
|
||||
### RabbitMQ Server
|
||||
|
||||
Centurion ERP uses RabbitMQ as for its worker queue. As tasks are created when using Centurion ERP, they are added to the RabbitMQ server for the background worker to pickup. When the background worker picks up the task, it does it's business, clears the task from the RabbitMQ server and saves the [results](../user/core/index.md#background-worker) within the Database.
|
||||
|
||||
|
||||
### Web Container
|
||||
|
||||
The [web container](https://hub.docker.com/r/nofusscomputing/centurion-erp) is the guts of Centurion ERP. It provides the interface and endpoints for interacting with Centurion ERP. This container is scalable with the only additional requirement being that a load-balancer be placed in front of all web containers for traffic routing. If deploying to Kubernetes the service load-balancer is sufficient and setting the deployment `replicas` to the number of desired containers is the simplest method to scale.
|
||||
|
||||
|
||||
### Background Worker Container
|
||||
|
||||
The [Background Worker container](https://hub.docker.com/r/nofusscomputing/centurion-erp) is a worker that waits for tasks sent to the RabbitMQ server. The worker is based upon [Celery](https://docs.celeryq.dev/en/stable/index.html). On the worker not being busy, it'll pickup and run the task. This container is scalable with nil additional requirements for launching additional workers. If deploying to Kubernetes the setting the deployment `replicas` to the number of desired containers is the simplest method to scale. The container start command will need to be set to `celery -A app worker -l INFO` so that the worker is started on container startup.
|
||||
|
||||
Configuration for the worker resides in directory `/etc/itsm/` within the container. see below for the `CELERY_` configuration.
|
||||
|
||||
|
||||
### Settings file
|
||||
|
||||
The settings file is a python file `.py` and must remain a valid python file for the application to work. Settings for the application are stored within a docker volume at path `/etc/itsm/`, with the settings living in `.py` files. A database is also required for the application to store it's settings. SQLLite and MariaDB/MySQL are supported.
|
||||
|
||||
``` py title="settings.py"
|
||||
|
||||
--8<-- "includes/etc/itsm/settings.py"
|
||||
|
||||
```
|
||||
|
||||
|
||||
### Migrations
|
||||
|
||||
Migrations serve the purpose of setting up the database. On initial deployment of Centurion ERP migrations must be run as must they be on any upgrade.
|
||||
|
||||
|
||||
## Updating
|
||||
|
||||
We use [semver](https://semver.org/) versioning for Centurion ERP. Using this method of versioning enables us to clearly show what versions will have breaking changes. You can rest assured that every version whose `Major` version number remains the same will not break your deployment. [Release notes](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/blob/master/Release-Notes.md) are available within the repository root and are a running document for the current `Major` release. To locate the release notes for your particular version please select the release tag from the branches drop-down. We will use the release notes to denote **Any** Breaking changes.
|
||||
|
||||
Updating to a newer version of Centurion ERP is as simple as [backing up your database](./backup.md) and RabbitMQ server, then updating the deployed image to the desired version and running the database migrations.
|
@ -27,6 +27,8 @@ Whilst there are many Enterprise Rescource Planning (ERP) applications, Centurio
|
||||
|
||||
Centurion ERP contains the following modules:
|
||||
|
||||
- [Companion Ansible Collection](../ansible/collections/centurion/index.md)
|
||||
|
||||
- [Configuration Management](./user/config_management/index.md)
|
||||
|
||||
- [IT Asset Management (ITAM)](./user/itam/index.md)
|
||||
|
@ -41,6 +41,9 @@ To add a new model navigate to `settings -> ITAM -> Device Models`
|
||||
|
||||
Operating System is also visible on this tab with the version `name` as intended to be full [semver](https://semver.org/).
|
||||
|
||||
!!! note
|
||||
If you change the devices organization the config groups the device is a part of will be removed.
|
||||
|
||||
|
||||
### Software
|
||||
|
||||
|
@ -56,6 +56,12 @@ nav:
|
||||
|
||||
- projects/centurion_erp/administration/index.md
|
||||
|
||||
- projects/centurion_erp/administration/authentication.md
|
||||
|
||||
- projects/centurion_erp/administration/backup.md
|
||||
|
||||
- projects/centurion_erp/administration/installation.md
|
||||
|
||||
- Development:
|
||||
|
||||
- projects/centurion_erp/development/index.md
|
||||
|
Reference in New Issue
Block a user